Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ jobs:
grep -rn "def test" > /dev/null

- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.14'

- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 24
check-latest: true

- name: Cache pip
Expand Down Expand Up @@ -93,11 +93,8 @@ jobs:
bench get-app ls_shop $GITHUB_WORKSPACE
bench setup requirements --dev
bench get-app erpnext
bench get-app payments
bench get-app webshop
bench new-site --db-root-password root --admin-password admin test_site
bench --site test_site install-app erpnext
bench --site test_site install-app webshop
bench --site test_site install-app ls_shop
bench build
env:
Expand Down
2 changes: 1 addition & 1 deletion ls_shop/api/checkout.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import frappe
from webshop.webshop.shopping_cart.cart import _get_cart_quotation, get_cart_quotation

from ls_shop.api.payments import set_charges
from ls_shop.core import _get_cart_quotation
from ls_shop.utils import get_delivery_configuration


Expand Down
5 changes: 1 addition & 4 deletions ls_shop/api/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
from erpnext.selling.doctype.quotation.quotation import _make_sales_order
from frappe.utils import getdate
from webshop.webshop.shopping_cart.cart import (
_get_cart_quotation,
get_cart_quotation,
)

from ls_shop.core import _get_cart_quotation
from ls_shop.utils import get_cod_configuration


Expand Down
148 changes: 148 additions & 0 deletions ls_shop/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import os

import frappe
import frappe.defaults
from frappe import _
from frappe.contacts.doctype.address.address import get_address_display
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.utils import get_fullname
from frappe.utils.nestedset import get_root_of


def generate_otp():
Expand All @@ -20,3 +26,145 @@ def send_otp(email):
return

frappe.sendmail(recipients=email, subject="Your OTP", message=f"Your OTP: {otp}", now=True)


def _get_default_territory() -> str:
return frappe.db.get_single_value("Selling Settings", "territory") or get_root_of("Territory")


def _create_party_for_user(user: str):
fullname = get_fullname(user) or user
customer = frappe.new_doc("Customer")
customer_group = frappe.db.get_single_value("Lifestyle Settings", "Lifestyle Settings", "customer_group")
customer.update(
{
"customer_name": fullname,
"customer_type": "Individual",
"customer_group": customer_group,
"territory": _get_default_territory(),
}
)
customer.append("portal_users", {"user": user})
customer.flags.ignore_mandatory = True
customer.insert(ignore_permissions=True)

contact = frappe.new_doc("Contact")
contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]})
contact.append("links", {"link_doctype": "Customer", "link_name": customer.name})
contact.flags.ignore_mandatory = True
contact.insert(ignore_permissions=True)

return customer


def get_party(user=None):
if not user:
user = frappe.session.user

if user == "Guest":
raise frappe.PermissionError

contact_name = get_contact_name(user)
if contact_name:
contact = frappe.get_cached_doc("Contact", contact_name)
link = next(
(l for l in contact.links if l.link_doctype in {"Customer", "Supplier"}),
None,
)
if link:
party_doc = frappe.get_cached_doc(link.link_doctype, link.link_name)
if not frappe.db.exists("Portal User", {"parent": party_doc.name, "user": user}):
party_doc.append("portal_users", {"user": user})
party_doc.flags.ignore_permissions = True
party_doc.flags.ignore_mandatory = True
party_doc.save()
return party_doc

if portal_party := frappe.db.get_value("Portal User", {"user": user}, "parent"):
if frappe.db.exists("Customer", portal_party):
return frappe.get_cached_doc("Customer", portal_party)

return _create_party_for_user(user)


def _get_cart_quotation(party=None):
if not party:
party = get_party()

quotation = frappe.get_all(
"Quotation",
fields=["name"],
filters={
"party_name": party.name,
"contact_email": frappe.session.user,
"order_type": "Shopping Cart",
"docstatus": 0,
},
order_by="modified desc",
limit_page_length=1,
pluck="name",
)

if quotation:
return frappe.get_cached_doc("Quotation", quotation[0])
lifestyle_settings = frappe.get_cached_doc("Lifestyle Settings")
company = lifestyle_settings.get("company") or frappe.get_cached_value(
"Global Defaults", "Global Defaults", "default_company"
)
quotation_doc = frappe.new_doc("Quotation")
quotation_doc.quotation_to = party.doctype
quotation_doc.company = company
quotation_doc.order_type = "Shopping Cart"
quotation_doc.party_name = party.name
quotation_doc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
quotation_doc.contact_email = frappe.session.user
quotation_doc.flags.ignore_permissions = True
quotation_doc.run_method("set_missing_values")
if sale_price_list := frappe.get_cached_value(
"Lifestyle Settings", "Lifestyle Settings", "sale_price_list"
):
quotation_doc.selling_price_list = sale_price_list
return quotation_doc


def get_address_docs(party=None):
if not party:
party = get_party()
if not party:
return []

address_names = frappe.get_all(
"Dynamic Link",
filters={
"parenttype": "Address",
"link_doctype": party.doctype,
"link_name": party.name,
},
pluck="parent",
)

if not address_names:
return []

addresses = frappe.get_all(
"Address",
filters={"name": ("in", address_names)},
fields=[
"name",
"address_title",
"address_type",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
"phone",
"email_id",
],
)

for address in addresses:
address["display"] = get_address_display(address)

return addresses
2 changes: 1 addition & 1 deletion ls_shop/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
app_email = "rahul@buildwithhussain.com"
app_license = "agpl-3.0"

required_apps = ["webshop"]

website_redirects = [
{"source": "/products", "target": "/en/products"},
Expand Down Expand Up @@ -87,6 +86,7 @@
}

after_install = "ls_shop.migrate.after_install"
after_migrate = "ls_shop.migrate.after_migrate"


doc_events = {
Expand Down
20 changes: 0 additions & 20 deletions ls_shop/lifestyle_shop_ecommerce/custom/sales_order.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,26 +407,6 @@
"parentfield": "links",
"parenttype": "Customize Form",
"table_fieldname": null
},
{
"creation": "2025-06-09 10:26:21.097257",
"custom": 1,
"docstatus": 0,
"group": null,
"hidden": 0,
"idx": 0,
"is_child_table": 0,
"link_doctype": "Tabby Payment Request",
"link_fieldname": "ref_docname",
"modified": "2025-06-09 10:26:21.097257",
"modified_by": "Administrator",
"name": "vh3cpo4afa",
"owner": "Administrator",
"parent": "Sales Order",
"parent_doctype": null,
"parentfield": "links",
"parenttype": "Customize Form",
"table_fieldname": null
}
],
"property_setters": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"field_order": [
"branding_section",
"store_name",
"company",
"column_break_brand",
"brand_logo",
"footer_logo",
Expand Down Expand Up @@ -646,12 +647,19 @@
"fieldname": "publish_all_items",
"fieldtype": "Button",
"label": "Publish All Items to Website"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-10-07 15:34:41.977684",
"modified": "2026-04-29 12:00:00.000000",
"modified_by": "Administrator",
"module": "Lifestyle Shop Ecommerce",
"name": "Lifestyle Settings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class LifestyleSettings(Document):
cod_charge: DF.Currency
cod_charge_applicable_below: DF.Currency
cod_enabled: DF.Check
company: DF.Link
contact_email: DF.Data | None
contact_phone: DF.Data | None
copyright_text: DF.Data | None
Expand Down Expand Up @@ -86,18 +87,10 @@ def validate(self):
frappe.throw(frappe._("At least one payment method (Telr, Tabby, or COD) must be enabled."))

def get_default_price_list(self):
return (
self.default_price_list
if self.default_price_list
else frappe.get_cached_value("Webshop Settings", "Webshop Settings", "price_list")
)
return self.default_price_list

def get_sale_price_list(self):
return (
self.sale_price_list
if self.sale_price_list
else frappe.get_cached_value("Webshop Settings", "Webshop Settings", "price_list")
)
return self.sale_price_list

@frappe.whitelist()
def enqueue_publish_all_variants(self, attribute: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,40 +59,52 @@ def get_data():
PaymentEntry = DocType("Payment Entry")
PaymentEntryReference = DocType("Payment Entry Reference")
Telr_payment_request = DocType("Telr Payment Request")
tabby_payment_request = DocType("Tabby Payment Request")
tabby_installed = bool(frappe.db.exists("DocType", "Tabby Payment Request"))

orphaned_payments = (
query = (
qb.from_(PaymentEntry)
.left_join(PaymentEntryReference)
.on(PaymentEntryReference.parent == PaymentEntry.name)
.left_join(Telr_payment_request)
.on(Telr_payment_request.telr_order_ref == PaymentEntry.reference_no)
.left_join(tabby_payment_request)
.on(tabby_payment_request.tabby_order_ref == PaymentEntry.reference_no)
.select(
PaymentEntry.name,
PaymentEntry.paid_amount,
PaymentEntry.mode_of_payment,
PaymentEntry.posting_date,
PaymentEntry.docstatus,
Telr_payment_request.status.as_("telr_status"),
tabby_payment_request.status.as_("tabby_status"),
)
.where(
)

if tabby_installed:
tabby_payment_request = DocType("Tabby Payment Request")
query = (
query.left_join(tabby_payment_request)
.on(tabby_payment_request.tabby_order_ref == PaymentEntry.reference_no)
.select(tabby_payment_request.status.as_("tabby_status"))
.where(
((PaymentEntry.docstatus == 1) & (PaymentEntryReference.name.isnull()))
| ((PaymentEntry.docstatus == 2) & (Telr_payment_request.status != "Refunded"))
| ((PaymentEntry.docstatus == 2) & (tabby_payment_request.status != "REFUND"))
)
)
else:
query = query.where(
((PaymentEntry.docstatus == 1) & (PaymentEntryReference.name.isnull()))
| ((PaymentEntry.docstatus == 2) & (Telr_payment_request.status != "Refunded"))
| ((PaymentEntry.docstatus == 2) & (tabby_payment_request.status != "REFUND"))
)
).run(as_dict=True)
for payment in orphaned_payments:

for payment in query.run(as_dict=True):
tabby_status = payment.get("tabby_status") if tabby_installed else None
data.append(
{
"payment_entry": payment.name,
"paid_amount": payment.paid_amount,
"payment_mode": payment.mode_of_payment,
"posting_date": payment.posting_date,
"cancelled": payment.docstatus == 2,
"refunded": (payment.telr_status == "Refunded" or payment.tabby_status == "REFUND"),
"refunded": (payment.telr_status == "Refunded" or tabby_status == "REFUND"),
}
)
return data
Loading
Loading