From 77e3fb08f838582c5efb494100ceb3a43ab2577b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 29 Apr 2026 16:04:56 +0530 Subject: [PATCH 1/2] feat: remove webshop dependency --- .github/workflows/ci.yml | 3 - ls_shop/api/checkout.py | 2 +- ls_shop/api/payments.py | 5 +- ls_shop/core.py | 148 ++++++++++++++++++ ls_shop/hooks.py | 2 +- .../custom/sales_order.json | 20 --- .../lifestyle_settings.json | 10 +- .../lifestyle_settings/lifestyle_settings.py | 13 +- .../orphaned_payments/orphaned_payments.py | 32 ++-- ls_shop/migrate.py | 37 +++++ ls_shop/public/css/tailwind.css | 90 +---------- ls_shop/utils.py | 6 +- ls_shop/www/cart/checkout.py | 20 ++- 13 files changed, 248 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 791f285..c397ba6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/ls_shop/api/checkout.py b/ls_shop/api/checkout.py index 23f2f1d..8ad435c 100644 --- a/ls_shop/api/checkout.py +++ b/ls_shop/api/checkout.py @@ -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 diff --git a/ls_shop/api/payments.py b/ls_shop/api/payments.py index 6e5e5a9..0c53d4c 100644 --- a/ls_shop/api/payments.py +++ b/ls_shop/api/payments.py @@ -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 diff --git a/ls_shop/core.py b/ls_shop/core.py index 610f075..d37688e 100644 --- a/ls_shop/core.py +++ b/ls_shop/core.py @@ -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(): @@ -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 diff --git a/ls_shop/hooks.py b/ls_shop/hooks.py index fe8a872..e9696d3 100644 --- a/ls_shop/hooks.py +++ b/ls_shop/hooks.py @@ -5,7 +5,6 @@ app_email = "rahul@buildwithhussain.com" app_license = "agpl-3.0" -required_apps = ["webshop"] website_redirects = [ {"source": "/products", "target": "/en/products"}, @@ -87,6 +86,7 @@ } after_install = "ls_shop.migrate.after_install" +after_migrate = "ls_shop.migrate.after_migrate" doc_events = { diff --git a/ls_shop/lifestyle_shop_ecommerce/custom/sales_order.json b/ls_shop/lifestyle_shop_ecommerce/custom/sales_order.json index a44977c..5d59a1c 100644 --- a/ls_shop/lifestyle_shop_ecommerce/custom/sales_order.json +++ b/ls_shop/lifestyle_shop_ecommerce/custom/sales_order.json @@ -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": [ diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json index bc22fd5..47ddeef 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json @@ -7,6 +7,7 @@ "field_order": [ "branding_section", "store_name", + "company", "column_break_brand", "brand_logo", "footer_logo", @@ -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", diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py index fe94d73..6058a4a 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py @@ -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 @@ -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): diff --git a/ls_shop/lifestyle_shop_ecommerce/report/orphaned_payments/orphaned_payments.py b/ls_shop/lifestyle_shop_ecommerce/report/orphaned_payments/orphaned_payments.py index 08c2a72..4085635 100644 --- a/ls_shop/lifestyle_shop_ecommerce/report/orphaned_payments/orphaned_payments.py +++ b/ls_shop/lifestyle_shop_ecommerce/report/orphaned_payments/orphaned_payments.py @@ -59,16 +59,14 @@ 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, @@ -76,15 +74,29 @@ def get_data(): 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, @@ -92,7 +104,7 @@ def get_data(): "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 diff --git a/ls_shop/migrate.py b/ls_shop/migrate.py index 2739d71..9bd5aa4 100644 --- a/ls_shop/migrate.py +++ b/ls_shop/migrate.py @@ -13,6 +13,43 @@ def after_install(): frappe.errprint(error_msg) frappe.errprint(traceback.format_exc()) + register_optional_doctype_links() + + +def after_migrate(): + register_optional_doctype_links() + + +def register_optional_doctype_links(): + """Add Customize Form connections for optional integrations whose doctypes are + provided by tabby_frappe. + """ + add_sales_order_link_if_doctype_exists("Tabby Payment Request", "ref_docname") + + +def add_sales_order_link_if_doctype_exists(link_doctype: str, link_fieldname: str): + if not frappe.db.exists("DocType", link_doctype): + return + + customize_form = frappe.get_doc({"doctype": "Customize Form", "doc_type": "Sales Order"}) + customize_form.run_method("fetch_to_customize") + if any(row.link_doctype == link_doctype for row in (customize_form.get("links") or [])): + return + + customize_form.append( + "links", + { + "link_doctype": link_doctype, + "link_fieldname": link_fieldname, + }, + ) + try: + customize_form.save() + except Exception: + import traceback + + frappe.log_error(traceback.format_exc(), f"ls_shop: optional link for {link_doctype}") + def create_payment_modes(): modes = {"Telr"} diff --git a/ls_shop/public/css/tailwind.css b/ls_shop/public/css/tailwind.css index 7859bba..70faa1b 100644 --- a/ls_shop/public/css/tailwind.css +++ b/ls_shop/public/css/tailwind.css @@ -9,7 +9,6 @@ --color-red-400: oklch(70.4% 0.191 22.216); --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-700: oklch(50.5% 0.213 27.518); --color-red-800: #ac0005; --color-red-900: #991b1f; --color-orange-400: oklch(75% 0.183 55.934); @@ -386,9 +385,6 @@ .order-3 { order: 3; } - .order-4 { - order: 4; - } .order-5 { order: 5; } @@ -687,6 +683,9 @@ .h-12 { height: calc(var(--spacing) * 12); } + .h-16 { + height: calc(var(--spacing) * 16); + } .h-20 { height: calc(var(--spacing) * 20); } @@ -795,6 +794,9 @@ .w-28 { width: calc(var(--spacing) * 28); } + .w-32 { + width: calc(var(--spacing) * 32); + } .w-34 { width: calc(var(--spacing) * 34); } @@ -1280,15 +1282,9 @@ .border-red-600 { border-color: var(--color-red-600); } - .border-red-700 { - border-color: var(--color-red-700); - } .border-red-800 { border-color: var(--color-red-800); } - .border-red-900 { - border-color: var(--color-red-900); - } .border-yellow-600 { border-color: var(--color-yellow-600); } @@ -1364,9 +1360,6 @@ .bg-red-800 { background-color: var(--color-red-800); } - .bg-red-900 { - background-color: var(--color-red-900); - } .bg-slate-800 { background-color: var(--color-slate-800); } @@ -1726,15 +1719,6 @@ .text-red-600 { color: var(--color-red-600); } - .text-red-700 { - color: var(--color-red-700); - } - .text-red-800 { - color: var(--color-red-800); - } - .text-red-900 { - color: var(--color-red-900); - } .text-white { color: var(--color-white); } @@ -1756,9 +1740,6 @@ .decoration-gray-400 { text-decoration-color: var(--color-gray-400); } - .decoration-red-900 { - text-decoration-color: var(--color-red-900); - } .decoration-1 { text-decoration-thickness: 1px; } @@ -1781,9 +1762,6 @@ .accent-red-800 { accent-color: var(--color-red-800); } - .accent-red-900 { - accent-color: var(--color-red-900); - } .opacity-0 { opacity: 0%; } @@ -1839,9 +1817,6 @@ outline-style: var(--tw-outline-style); outline-width: 1px; } - .outline-red-900 { - outline-color: var(--color-red-900); - } .invert { --tw-invert: invert(100%); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); @@ -2004,13 +1979,6 @@ } } } - .hover\:bg-red-800 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-800); - } - } - } .hover\:bg-red-900 { &:hover { @media (hover: hover) { @@ -2060,27 +2028,6 @@ } } } - .hover\:text-red-600 { - &:hover { - @media (hover: hover) { - color: var(--color-red-600); - } - } - } - .hover\:text-red-800 { - &:hover { - @media (hover: hover) { - color: var(--color-red-800); - } - } - } - .hover\:text-red-900 { - &:hover { - @media (hover: hover) { - color: var(--color-red-900); - } - } - } .hover\:text-white { &:hover { @media (hover: hover) { @@ -2102,11 +2049,6 @@ } } } - .focus\:border-red-900 { - &:focus { - border-color: var(--color-red-900); - } - } .focus\:ring-2 { &:focus { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -2133,11 +2075,6 @@ --tw-ring-color: var(--color-gray-700); } } - .focus\:ring-red-900 { - &:focus { - --tw-ring-color: var(--color-red-900); - } - } .focus\:outline-hidden { &:focus { --tw-outline-style: none; @@ -2440,21 +2377,6 @@ order: 2; } } - .md\:order-3 { - @media (width >= 48rem) { - order: 3; - } - } - .md\:order-4 { - @media (width >= 48rem) { - order: 4; - } - } - .md\:order-5 { - @media (width >= 48rem) { - order: 5; - } - } .md\:col-span-1 { @media (width >= 48rem) { grid-column: span 1 / span 1; diff --git a/ls_shop/utils.py b/ls_shop/utils.py index 45260b1..9587168 100644 --- a/ls_shop/utils.py +++ b/ls_shop/utils.py @@ -9,10 +9,8 @@ from frappe.query_builder.functions import Count, Min, Sum from frappe.utils import add_days, flt, get_datetime, now_datetime from pypika import Order -from webshop.webshop.shopping_cart.cart import ( - get_address_docs, - get_party, -) + +from ls_shop.core import get_address_docs, get_party def get_complete_nested_links(parent_group): diff --git a/ls_shop/www/cart/checkout.py b/ls_shop/www/cart/checkout.py index 70c40b1..e9a6dd9 100644 --- a/ls_shop/www/cart/checkout.py +++ b/ls_shop/www/cart/checkout.py @@ -1,8 +1,8 @@ import frappe from frappe.query_builder import DocType from frappe.utils.caching import site_cache -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 ( format_addresses, get_addresses, @@ -134,7 +134,23 @@ def get_store_pickup_addresses(): # Map address to warehouse address_to_warehouse = {link["parent"]: link["link_name"] for link in links} address_names = list(address_to_warehouse.keys()) - addresses = frappe.get_all("Address", filters={"name": ["in", address_names]}, fields=["*"]) + 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", + ], + ) # Format and attach warehouse info formatted_addresses = format_addresses(addresses, address_type="Shop") for addr in formatted_addresses: From ea45ae2e4e4ef8a94269f66cab1cd949438cc58b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Wed, 29 Apr 2026 16:34:01 +0530 Subject: [PATCH 2/2] ci: bump Python to 3.14 and Node to 24 Frappe's develop branch now requires Python 3.14 (uses PEP 695 type statement syntax) and Node 24, so the CI was failing during bench init. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c397ba6..036c4dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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