From f171eb5e29753c624641fe4c8de5088f0ce704d2 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 5 Jun 2023 10:40:41 +0000 Subject: [PATCH 01/27] chore(release): Bumped to Version 1.17.1 ## [1.17.1](https://github.com/frappe/ecommerce_integrations/compare/v1.17.0...v1.17.1) (2023-06-05) ### Bug Fixes * dont run invoice hooks if unicommerce isn't enabled ([cb5853f](https://github.com/frappe/ecommerce_integrations/commit/cb5853fd49bfc189e288f10fd839170605da54a5)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 30244104..c7bbe496 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.17.0" +__version__ = "1.17.1" From b59a1ed415c1d2f0b4e203a1bbd0b5134fb63cdb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 5 Jun 2023 16:13:24 +0530 Subject: [PATCH 02/27] chore: ignore if unicommerce is not enabled --- ecommerce_integrations/unicommerce/invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ecommerce_integrations/unicommerce/invoice.py b/ecommerce_integrations/unicommerce/invoice.py index 5d61ce67..8f99a520 100644 --- a/ecommerce_integrations/unicommerce/invoice.py +++ b/ecommerce_integrations/unicommerce/invoice.py @@ -615,6 +615,10 @@ def on_submit(self, method=None): def on_cancel(self, method=None): + settings = frappe.get_cached_doc(SETTINGS_DOCTYPE) + if not settings.is_enabled(): + return + results = frappe.db.get_all( "Pick List Sales Order Details", filters={"sales_invoice": self.name, "docstatus": 1} ) From 394a2e62602ab7e245948026bab825fc3b37e176 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 22 Jun 2023 12:22:54 +0000 Subject: [PATCH 03/27] chore(release): Bumped to Version 1.17.2 ## [1.17.2](https://github.com/frappe/ecommerce_integrations/compare/v1.17.1...v1.17.2) (2023-06-22) ### Bug Fixes * Correctly filter existing return order ([2fe3d07](https://github.com/frappe/ecommerce_integrations/commit/2fe3d07cd4dd4d2b7eda733910a28b13ba5c50b3)) * ignore picklist validation if not enabled ([1fee343](https://github.com/frappe/ecommerce_integrations/commit/1fee343b2ac5d3e3d5bde9dc548f28ba28e6efa8)) * Use UTC timestamp for filtering recent orders ([6ab22a0](https://github.com/frappe/ecommerce_integrations/commit/6ab22a04a3db197dabece0c1fbbb6eb14b6f57bc)) * Warning about recomputed taxes ([1bb9198](https://github.com/frappe/ecommerce_integrations/commit/1bb9198e92a39df741c1a376ed0556c6c70e5a3d)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index c7bbe496..0daeee7b 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.17.1" +__version__ = "1.17.2" From f346383e6ce13fc366563f945c992792e103f6e9 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 29 Jun 2023 14:48:34 +0000 Subject: [PATCH 04/27] chore(release): Bumped to Version 1.17.3 ## [1.17.3](https://github.com/frappe/ecommerce_integrations/compare/v1.17.2...v1.17.3) (2023-06-29) ### Bug Fixes * delete logs in single query before uninstall ([8b8251a](https://github.com/frappe/ecommerce_integrations/commit/8b8251a5a7b120adfbbe749a6e0e60604424b95c)) ### Performance Improvements * use cached shopify settings ([80ec3a2](https://github.com/frappe/ecommerce_integrations/commit/80ec3a2783285164dc44fc5e9eea5d9938465109)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 0daeee7b..66942597 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.17.2" +__version__ = "1.17.3" From 4c4b0772cf4f5c4cd245b305e6e6c68fd938fd7b Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 7 Jul 2023 07:47:48 +0000 Subject: [PATCH 05/27] chore(release): Bumped to Version 1.17.4 ## [1.17.4](https://github.com/frappe/ecommerce_integrations/compare/v1.17.3...v1.17.4) (2023-07-07) ### Bug Fixes * bump shopify API version ([#254](https://github.com/frappe/ecommerce_integrations/issues/254)) ([d0fb890](https://github.com/frappe/ecommerce_integrations/commit/d0fb890fba4d9d8902e813196d2ea38b133ba404)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 66942597..81205689 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.17.3" +__version__ = "1.17.4" From ee9def8d52b716baed32c7fb8597faa9c958485e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 2 Aug 2023 08:41:09 +0000 Subject: [PATCH 06/27] chore(release): Bumped to Version 1.18.0 # [1.18.0](https://github.com/frappe/ecommerce_integrations/compare/v1.17.4...v1.18.0) (2023-08-02) ### Bug Fixes * add filters for `Warehouse` and `Account Group` ([b7b9f4d](https://github.com/frappe/ecommerce_integrations/commit/b7b9f4da3c62e14cb8ba6783f9a215b8b694abe6)) * add validation for `Amazon Fields Map` ([bab7702](https://github.com/frappe/ecommerce_integrations/commit/bab7702a1aaf70f48cf024d70fcf999798e23d4d)) * make `state` title case ([d0b2da0](https://github.com/frappe/ecommerce_integrations/commit/d0b2da00ac7b5ba636548198e0ef317bbf67320a)) * set default field-map onload ([60c936a](https://github.com/frappe/ecommerce_integrations/commit/60c936ab608b324d208a77273b64541b42ca73a8)) * tests ([212127b](https://github.com/frappe/ecommerce_integrations/commit/212127b1346c406b700c6cbc7d0db044570e8cfc)) * **ux:** don't show Sync buttons in local doc ([e525f9e](https://github.com/frappe/ecommerce_integrations/commit/e525f9ec5e9945dab7d5a186918f4c7bab252997)) ### Features * add table `Amazon Fields Map` in `Amazon SP API Settings` ([45d48ac](https://github.com/frappe/ecommerce_integrations/commit/45d48acc45af7ec6df06374d3bacc6bf2bed1252)) * new child doctype `Amazon Fields Map` ([e3d1bf2](https://github.com/frappe/ecommerce_integrations/commit/e3d1bf202aad3b8c726147923aa7cdd3054ff1b8)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 81205689..6cea18d8 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.17.4" +__version__ = "1.18.0" From 34cfb56e95f838e7ee4c56cd5322a6971e71f274 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 3 Aug 2023 09:33:35 +0000 Subject: [PATCH 07/27] chore(release): Bumped to Version 1.18.1 ## [1.18.1](https://github.com/frappe/ecommerce_integrations/compare/v1.18.0...v1.18.1) (2023-08-03) ### Bug Fixes * unable to save `Amazon SP API Settings` ([#262](https://github.com/frappe/ecommerce_integrations/issues/262)) ([ffb7c97](https://github.com/frappe/ecommerce_integrations/commit/ffb7c97ad79d5888b0f9358c212356932b6f2db6)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 6cea18d8..4a7bff54 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.18.0" +__version__ = "1.18.1" From b0feb7be0ff79674d2816d18eee736d41e106fad Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 3 Aug 2023 16:41:59 +0000 Subject: [PATCH 08/27] chore(release): Bumped to Version 1.18.2 ## [1.18.2](https://github.com/frappe/ecommerce_integrations/compare/v1.18.1...v1.18.2) (2023-08-03) ### Bug Fixes * **patch:** `set_default_amazon_item_fields_map` ([#263](https://github.com/frappe/ecommerce_integrations/issues/263)) ([27f777b](https://github.com/frappe/ecommerce_integrations/commit/27f777bb15e0762b4893f6bcbcb4f27ceb9282fd)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 4a7bff54..4bd6f7be 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.18.1" +__version__ = "1.18.2" From 187ffdb65e21abb8b796a786b9f46f4e05724a66 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 9 Oct 2023 10:34:05 +0530 Subject: [PATCH 09/27] fix: shopify default customer (#270) --- ecommerce_integrations/shopify/order.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py index c282b4eb..03585fda 100644 --- a/ecommerce_integrations/shopify/order.py +++ b/ecommerce_integrations/shopify/order.py @@ -68,9 +68,10 @@ def create_order(order, setting, company=None): def create_sales_order(shopify_order, setting, company=None): - customer = frappe.db.get_value( - "Customer", {CUSTOMER_ID_FIELD: shopify_order.get("customer", {}).get("id")}, "name", - ) + customer = setting.default_customer + if customer_id := shopify_order.get("customer", {}).get("id"): + customer = frappe.db.get_value("Customer", {CUSTOMER_ID_FIELD: customer_id}, "name") + so = frappe.db.get_value("Sales Order", {ORDER_ID_FIELD: shopify_order.get("id")}, "name") if not so: @@ -100,7 +101,7 @@ def create_sales_order(shopify_order, setting, company=None): "naming_series": setting.sales_order_series or "SO-Shopify-", ORDER_ID_FIELD: str(shopify_order.get("id")), ORDER_NUMBER_FIELD: shopify_order.get("name"), - "customer": customer or setting.default_customer, + "customer": customer, "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(), "delivery_date": getdate(shopify_order.get("created_at")) or nowdate(), "company": setting.company, From 0047478eb34e4c33d300af051268f4ae92c29b34 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 9 Oct 2023 05:19:03 +0000 Subject: [PATCH 10/27] chore(release): Bumped to Version 1.18.3 ## [1.18.3](https://github.com/frappe/ecommerce_integrations/compare/v1.18.2...v1.18.3) (2023-10-09) ### Bug Fixes * shopify default customer ([#270](https://github.com/frappe/ecommerce_integrations/issues/270)) ([187ffdb](https://github.com/frappe/ecommerce_integrations/commit/187ffdb65e21abb8b796a786b9f46f4e05724a66)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 4bd6f7be..14e8848a 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.18.2" +__version__ = "1.18.3" From 3a00eb931334637448fb36379cb0bae88d67d49e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 17 Oct 2023 15:40:14 +0000 Subject: [PATCH 11/27] chore(release): Bumped to Version 1.18.4 ## [1.18.4](https://github.com/frappe/ecommerce_integrations/compare/v1.18.3...v1.18.4) (2023-10-17) ### Bug Fixes * prioritize user input in tax description ([83c5299](https://github.com/frappe/ecommerce_integrations/commit/83c52994a2c1720a7b9bce3101dbb003dd7856ce)) * shopify default customer ([#270](https://github.com/frappe/ecommerce_integrations/issues/270)) ([882207f](https://github.com/frappe/ecommerce_integrations/commit/882207f931fedfea0ff43d012c63c2c005b62b3f)) * use RQ job to query active jobs ([f7100d2](https://github.com/frappe/ecommerce_integrations/commit/f7100d2060678e384adadf18881be455fe80c94a)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 14e8848a..e2f88eea 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.18.3" +__version__ = "1.18.4" From e58487272fea2082cc64338b7c971441259db543 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 24 Oct 2023 06:11:46 +0000 Subject: [PATCH 12/27] chore(release): Bumped to Version 1.19.0 # [1.19.0](https://github.com/frappe/ecommerce_integrations/compare/v1.18.4...v1.19.0) (2023-10-24) ### Bug Fixes * avoid duplicate logs for old order sync ([9f90628](https://github.com/frappe/ecommerce_integrations/commit/9f9062885440d00edde522e46fae96d280ae3508)) ### Features * Default sales tax account in shopify ([7c18889](https://github.com/frappe/ecommerce_integrations/commit/7c18889986d54b2bc39d4b47c3f4c8ff43f7b886)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index e2f88eea..d84d79d4 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.18.4" +__version__ = "1.19.0" From 5197835bc0f3a07bf76774541429ce594c6bd55a Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Oct 2023 12:32:01 +0000 Subject: [PATCH 13/27] chore(release): Bumped to Version 1.19.1 ## [1.19.1](https://github.com/frappe/ecommerce_integrations/compare/v1.19.0...v1.19.1) (2023-10-31) ### Bug Fixes * Correctly wrap function ([b1dc35c](https://github.com/frappe/ecommerce_integrations/commit/b1dc35cfd7cfd73e38dcdade7450e8ac05bc277d)) * product sync ([3acebaa](https://github.com/frappe/ecommerce_integrations/commit/3acebaa1254e964d72639774f2c18497840e3602)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index d84d79d4..90fa0968 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.0" +__version__ = "1.19.1" From c62e360bcb6c13db536d075ae22c1a882f29a06e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 8 Nov 2023 11:57:11 +0000 Subject: [PATCH 14/27] chore(release): Bumped to Version 1.19.2 ## [1.19.2](https://github.com/frappe/ecommerce_integrations/compare/v1.19.1...v1.19.2) (2023-11-08) ### Bug Fixes * iterate and fetch all locations ([af4c57c](https://github.com/frappe/ecommerce_integrations/commit/af4c57cc61cd21bbdf740e530b2c7ba3fc1b6b32)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 90fa0968..d388c7ab 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.1" +__version__ = "1.19.2" From f9a9dd15e153794d8c94d48c820c99d3c2fd9d59 Mon Sep 17 00:00:00 2001 From: Muhammad Saad <72243425+saadsafda@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:15:04 +0500 Subject: [PATCH 15/27] fix: error when there is no billing address is shopify order (#283) * fix: error when there is no billing address is shopify order * fix: error when there is no billing address is shopify order * chore: unnecessary default --------- Co-authored-by: Ankush Menat --- ecommerce_integrations/shopify/order.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py index 39cb4d67..74284cdb 100644 --- a/ecommerce_integrations/shopify/order.py +++ b/ecommerce_integrations/shopify/order.py @@ -38,9 +38,9 @@ def sync_sales_order(payload, request_id=None): create_shopify_log(status="Invalid", message="Sales order already exists, not synced") return try: - shopify_customer = order.get("customer", {}) - shopify_customer["billing_address"] = order.get("billing_address") - shopify_customer["shipping_address"] = order.get("shipping_address") + shopify_customer = order.get("customer") if order.get("customer") is not None else {} + shopify_customer["billing_address"] = order.get("billing_address", "") + shopify_customer["shipping_address"] = order.get("shipping_address", "") customer_id = shopify_customer.get("id") if customer_id: customer = ShopifyCustomer(customer_id=customer_id) From 02a2e7fa9fdb8c30fb979e6372ba1cdc263baa39 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 20 Nov 2023 11:15:54 +0000 Subject: [PATCH 16/27] chore(release): Bumped to Version 1.19.3 ## [1.19.3](https://github.com/frappe/ecommerce_integrations/compare/v1.19.2...v1.19.3) (2023-11-20) ### Bug Fixes * error when there is no billing address is shopify order ([#283](https://github.com/frappe/ecommerce_integrations/issues/283)) ([f9a9dd1](https://github.com/frappe/ecommerce_integrations/commit/f9a9dd15e153794d8c94d48c820c99d3c2fd9d59)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index d388c7ab..f27cba7c 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.2" +__version__ = "1.19.3" From c5ab362e810972a7b75b4e65edd11a76b9f992e3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 29 Nov 2023 11:56:09 +0530 Subject: [PATCH 17/27] chore: bump `boto3` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b312446e..027e65b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ ShopifyAPI==12.3.0 # update after resolving pyjwt conflict in frappe -boto3~=1.18.65 +boto3~=1.28.10 From 2e06e87264974566e6535a3958e51df1aa2990ae Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 4 Dec 2023 07:11:53 +0000 Subject: [PATCH 18/27] chore(release): Bumped to Version 1.19.4 ## [1.19.4](https://github.com/frappe/ecommerce_integrations/compare/v1.19.3...v1.19.4) (2023-12-04) ### Bug Fixes * error when there is no billing address is shopify order ([#283](https://github.com/frappe/ecommerce_integrations/issues/283)) ([d1e7354](https://github.com/frappe/ecommerce_integrations/commit/d1e735480e379263e2e4ed6b86e3535093adfb4e)) * Give low priority to SKU ([#287](https://github.com/frappe/ecommerce_integrations/issues/287)) ([042c88a](https://github.com/frappe/ecommerce_integrations/commit/042c88a8837749728d68d5e67bbff26decaf014c)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index f27cba7c..ad7ca551 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.3" +__version__ = "1.19.4" From 67907707e08a819769aa9d2aad501c3cbbaf9112 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 5 Dec 2023 13:46:59 +0000 Subject: [PATCH 19/27] chore(release): Bumped to Version 1.19.5 ## [1.19.5](https://github.com/frappe/ecommerce_integrations/compare/v1.19.4...v1.19.5) (2023-12-05) ### Bug Fixes * clear old integration logs automatically ([4452fea](https://github.com/frappe/ecommerce_integrations/commit/4452feae5250ce66c2d0247f6ba850399d1e4944)) * **UX:** Correct URL in shopify webhooks ([71d5a3f](https://github.com/frappe/ecommerce_integrations/commit/71d5a3fc0214bb60bd42c609f6379204109d4f3c)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index ad7ca551..a0d493d9 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.4" +__version__ = "1.19.5" From 1480e7af3dee9f4bda5cd0d5941d18f3b86db0d7 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 7 Dec 2023 06:02:35 +0000 Subject: [PATCH 20/27] chore(release): Bumped to Version 1.19.6 ## [1.19.6](https://github.com/frappe/ecommerce_integrations/compare/v1.19.5...v1.19.6) (2023-12-07) ### Bug Fixes * **shopify:** handle multiple instance of same item in delivery ([7d663c1](https://github.com/frappe/ecommerce_integrations/commit/7d663c1a95d49b7d7b433c10152eabdd479fa996)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index a0d493d9..bdc444fd 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.5" +__version__ = "1.19.6" From 8d4176852bd7fb622179caaea62bd4f1606786d1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 19 Jan 2024 06:18:30 +0530 Subject: [PATCH 21/27] fix: shopify sync issue without customer --- ecommerce_integrations/shopify/order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py index 74284cdb..431c431c 100644 --- a/ecommerce_integrations/shopify/order.py +++ b/ecommerce_integrations/shopify/order.py @@ -75,8 +75,9 @@ def create_order(order, setting, company=None): def create_sales_order(shopify_order, setting, company=None): customer = setting.default_customer - if customer_id := shopify_order.get("customer", {}).get("id"): - customer = frappe.db.get_value("Customer", {CUSTOMER_ID_FIELD: customer_id}, "name") + if shopify_order.get("customer", {}): + if customer_id := shopify_order.get("customer", {}).get("id"): + customer = frappe.db.get_value("Customer", {CUSTOMER_ID_FIELD: customer_id}, "name") so = frappe.db.get_value("Sales Order", {ORDER_ID_FIELD: shopify_order.get("id")}, "name") From 1ea1f732fc32492808ea8679f3a648ba5ee6b9a7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 7 Feb 2024 15:23:44 +0530 Subject: [PATCH 22/27] ci: use node 20 for release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 152f5bdf..cfe7b4bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Node.js v14 uses: actions/setup-node@v2 with: - node-version: 18 + node-version: 20 - name: Setup dependencies run: | npm install @semantic-release/git @semantic-release/exec --no-save From c4db7c0df2ebdd071e3762fbee9b71c7b0a9e246 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 7 Feb 2024 09:54:35 +0000 Subject: [PATCH 23/27] chore(release): Bumped to Version 1.20.0 # [1.20.0](https://github.com/frappe/ecommerce_integrations/compare/v1.19.6...v1.20.0) (2024-02-07) ### Bug Fixes * shopify sync issue without customer ([8d41768](https://github.com/frappe/ecommerce_integrations/commit/8d4176852bd7fb622179caaea62bd4f1606786d1)) * shopify sync issue without customer ([3763ab5](https://github.com/frappe/ecommerce_integrations/commit/3763ab5d113ca29d477f6d12f1672c302958672d)) ### Features * bulk retry failed jobs ([2f9bb71](https://github.com/frappe/ecommerce_integrations/commit/2f9bb7137adfe3d9f41a4773d1897821dffb234a)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index bdc444fd..0dcddbc8 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.19.6" +__version__ = "1.20.0" From c12600d6521e3466022d57b37001fa3ad58fe525 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 27 Mar 2024 14:53:13 +0000 Subject: [PATCH 24/27] chore(release): Bumped to Version 1.20.1 ## [1.20.1](https://github.com/frappe/ecommerce_integrations/compare/v1.20.0...v1.20.1) (2024-03-27) ### Bug Fixes * Bump shopify version ([#310](https://github.com/frappe/ecommerce_integrations/issues/310)) ([0eeef5e](https://github.com/frappe/ecommerce_integrations/commit/0eeef5edbb49e506f63efb033de295928818fd87)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 0dcddbc8..3e49871f 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.20.0" +__version__ = "1.20.1" From 80f30bc8699563d423db3f8a8784b37ca07c3c0a Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor Date: Mon, 11 Aug 2025 10:28:40 +0530 Subject: [PATCH 25/27] fix: Add required_apps hooks for erpnext (cherry picked from commit 3a57547cd419549812c8bfbf9d3b2f01164d5470) --- ecommerce_integrations/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ecommerce_integrations/hooks.py b/ecommerce_integrations/hooks.py index face21c3..84bc60a9 100644 --- a/ecommerce_integrations/hooks.py +++ b/ecommerce_integrations/hooks.py @@ -8,6 +8,7 @@ app_color = "grey" app_email = "developers@frappe.io" app_license = "GNU GPL v3.0" +required_apps = ["frappe/erpnext"] # Includes in # ------------------ From 16cd5c15bd7e05088a635a69bf74a43731b0f8ae Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 23 Sep 2025 08:14:35 +0000 Subject: [PATCH 26/27] chore(release): Bumped to Version 1.20.2 ## [1.20.2](https://github.com/frappe/ecommerce_integrations/compare/v1.20.1...v1.20.2) (2025-09-23) ### Bug Fixes * Add required_apps hooks for erpnext ([80f30bc](https://github.com/frappe/ecommerce_integrations/commit/80f30bc8699563d423db3f8a8784b37ca07c3c0a)) --- ecommerce_integrations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py index 3e49871f..67d42d31 100644 --- a/ecommerce_integrations/__init__.py +++ b/ecommerce_integrations/__init__.py @@ -1 +1 @@ -__version__ = "1.20.1" +__version__ = "1.20.2" From 7840d5213bf53a32a131b78f1897ed4681f9d818 Mon Sep 17 00:00:00 2001 From: connect4systems Date: Fri, 7 Nov 2025 13:21:01 +0200 Subject: [PATCH 27/27] Skyway: Shopify one-way stock push + default-warehouse hook + recreate-on-404 + image upload --- .../shopify_setting/shopify_setting.json | 13 +- ecommerce_integrations/shopify/inventory.py | 220 +++- ecommerce_integrations/shopify/product.py | 1098 +++++++++-------- 3 files changed, 777 insertions(+), 554 deletions(-) diff --git a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json index 01722169..73b9dfbe 100644 --- a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json +++ b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.json @@ -392,7 +392,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-24 10:38:49.247431", + "modified": "2025-09-28 19:36:38.323699", "modified_by": "Administrator", "module": "shopify", "name": "Shopify Setting", @@ -407,8 +407,19 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Website Manager", + "share": 1, + "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/ecommerce_integrations/shopify/inventory.py b/ecommerce_integrations/shopify/inventory.py index 526107dd..3063ae77 100644 --- a/ecommerce_integrations/shopify/inventory.py +++ b/ecommerce_integrations/shopify/inventory.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from collections import Counter import frappe @@ -6,8 +7,8 @@ from shopify.resources import InventoryLevel, Variant from ecommerce_integrations.controllers.inventory import ( - get_inventory_levels, - update_inventory_sync_status, + get_inventory_levels, + update_inventory_sync_status, ) from ecommerce_integrations.controllers.scheduling import need_to_run from ecommerce_integrations.shopify.connection import temp_shopify_session @@ -15,79 +16,180 @@ from ecommerce_integrations.shopify.utils import create_shopify_log -def update_inventory_on_shopify() -> None: - """Upload stock levels from ERPNext to Shopify. - - Called by scheduler on configured interval. - """ - setting = frappe.get_doc(SETTING_DOCTYPE) +def _log(status: str, message: str, data=None, method: str = "update_inventory_on_shopify"): + create_shopify_log(status=status, message=message, request_data=data or {}, method=method) - if not setting.is_enabled() or not setting.update_erpnext_stock_levels_to_shopify: - return - if not need_to_run(SETTING_DOCTYPE, "inventory_sync_frequency", "last_inventory_sync"): - return +def _filter_to_flagged_items(inventory_levels): + """Keep rows for Items with custom_sync_to_shopify = 1 and not disabled.""" + if not inventory_levels: + return [] - warehous_map = setting.get_erpnext_to_integration_wh_mapping() - inventory_levels = get_inventory_levels(tuple(warehous_map.keys()), MODULE_NAME) + ecom_names = [d.ecom_item for d in inventory_levels if getattr(d, "ecom_item", None)] + if not ecom_names: + return [] - if inventory_levels: - upload_inventory_data_to_shopify(inventory_levels, warehous_map) + ecom_rows = frappe.get_all( + "Ecommerce Item", + filters={"name": ["in", ecom_names], "integration": MODULE_NAME}, + fields=["name", "erpnext_item_code"], + ) + ecom_to_item = {r["name"]: r["erpnext_item_code"] for r in ecom_rows if r.get("erpnext_item_code")} + allowed_items = set( + frappe.get_all( + "Item", + filters={"name": ["in", list(ecom_to_item.values())], "custom_sync_to_shopify": 1, "disabled": 0}, + pluck="name", + ) + ) -@temp_shopify_session -def upload_inventory_data_to_shopify(inventory_levels, warehous_map) -> None: - synced_on = now() + filtered = [] + for d in inventory_levels: + erp_item = ecom_to_item.get(getattr(d, "ecom_item", "")) + if erp_item in allowed_items: + filtered.append(d) - for inventory_sync_batch in create_batch(inventory_levels, 50): - for d in inventory_sync_batch: - d.shopify_location_id = warehous_map[d.warehouse] + return filtered - try: - variant = Variant.find(d.variant_id) - inventory_id = variant.inventory_item_id - InventoryLevel.set( - location_id=d.shopify_location_id, - inventory_item_id=inventory_id, - # shopify doesn't support fractional quantity - available=cint(d.actual_qty) - cint(d.reserved_qty), - ) - update_inventory_sync_status(d.ecom_item, time=synced_on) - d.status = "Success" - except ResourceNotFound: - # Variant or location is deleted, mark as last synced and ignore. - update_inventory_sync_status(d.ecom_item, time=synced_on) - d.status = "Not Found" - except Exception as e: - d.status = "Failed" - d.failure_reason = str(e) +def _commit_row_and_continue(d, synced_on): + try: + if getattr(d, "ecom_item", None): + update_inventory_sync_status(getattr(d, "ecom_item"), time=synced_on) + except Exception: + pass + frappe.db.commit() - frappe.db.commit() - _log_inventory_update_status(inventory_sync_batch) +def _log_batch_status(inventory_levels) -> None: + """Per-row status summary in Shopify Log.""" + log_message = "variant_id,location_id,status,failure_reason\n" + log_message += "\n".join( + f"{getattr(d,'variant_id','')},{getattr(d,'shopify_location_id','')}," + f"{getattr(d,'status','')},{getattr(d,'failure_reason','') or ''}" + for d in inventory_levels + ) + statuses = [getattr(d, "status", "Failed") for d in inventory_levels] + stats = Counter(statuses) + total = max(len(inventory_levels), 1) + pct = stats.get("Success", 0) / total -def _log_inventory_update_status(inventory_levels) -> None: - """Create log of inventory update.""" - log_message = "variant_id,location_id,status,failure_reason\n" + status = "Success" if pct == 1 else ("Partial Success" if pct > 0 else "Failed") + _log(status, f"Updated {pct * 100}% items\n\n{log_message}") - log_message += "\n".join( - f"{d.variant_id},{d.shopify_location_id},{d.status},{d.failure_reason or ''}" - for d in inventory_levels - ) - stats = Counter([d.status for d in inventory_levels]) +@temp_shopify_session +def upload_inventory_data_to_shopify(inventory_levels, warehouse_map) -> None: + """Push inventory to Shopify for each row (Default Warehouse only).""" + synced_on = now() + + for batch in create_batch(inventory_levels, 50): + for d in batch: + # force single mapped location for Default Warehouse + try: + d.shopify_location_id = int(warehouse_map[d.warehouse]) + except Exception: + d.status = "Failed" + d.failure_reason = f"No numeric Shopify Location for ERP Warehouse: {d.warehouse}" + _commit_row_and_continue(d, synced_on) + continue + + try: + if not getattr(d, "variant_id", None): + d.status = "Failed" + d.failure_reason = "Missing variant_id in Ecommerce Item mapping." + _commit_row_and_continue(d, synced_on) + continue + + variant = Variant.find(d.variant_id) + inventory_item_id = getattr(variant, "inventory_item_id", None) + if not inventory_item_id: + d.status = "Failed" + d.failure_reason = f"Shopify variant {d.variant_id} has no inventory_item_id." + _commit_row_and_continue(d, synced_on) + continue + + available = cint(d.actual_qty) - cint(d.reserved_qty) # Shopify wants integers + InventoryLevel.set( + location_id=d.shopify_location_id, + inventory_item_id=inventory_item_id, + available=int(available), + ) + update_inventory_sync_status(d.ecom_item, time=synced_on) + d.status = "Success" + + except ResourceNotFound: + update_inventory_sync_status(d.ecom_item, time=synced_on) + d.status = "Not Found" + d.failure_reason = ( + f"Variant or Location not found. variant_id={getattr(d,'variant_id',None)} " + f"loc={getattr(d,'shopify_location_id',None)}" + ) + except Exception as e: + d.status = "Failed" + d.failure_reason = str(e) + + frappe.db.commit() + + _log_batch_status(batch) + + +def _get_numeric_location_id(setting, default_wh: str) -> int: + erp_to_shop = setting.get_erpnext_to_integration_wh_mapping() or {} + if not default_wh or default_wh not in erp_to_shop: + raise Exception(f"No Shopify Location mapping for Default Warehouse: {default_wh}") + try: + return int(erp_to_shop[default_wh]) + except Exception: + raise Exception( + f"Shopify Location ID must be numeric. Got '{erp_to_shop[default_wh]}' for '{default_wh}'." + ) + + +def _run_push_for_default_warehouse(setting) -> None: + """Core one-way push using ONLY the Default Warehouse on Shopify Setting.""" + if not setting.is_enabled() or not setting.update_erpnext_stock_levels_to_shopify: + return + + default_wh = getattr(setting, "warehouse", None) + if not default_wh: + _log("Error", "Default Warehouse is not set on Shopify Setting (field: warehouse).") + return + + location_id = None + try: + location_id = _get_numeric_location_id(setting, default_wh) + except Exception as e: + _log("Error", str(e)) + return + + # inventory rows for only the Default Warehouse + inventory_levels = get_inventory_levels((default_wh,), MODULE_NAME) + if not inventory_levels: + _log("Success", f"No inventory rows found for Default Warehouse: {default_wh}") + return + + # keep only flagged items + inventory_levels = _filter_to_flagged_items(inventory_levels) + if not inventory_levels: + _log("Success", "No flagged items to sync (custom_sync_to_shopify=1 & not disabled).") + return + + # push + upload_inventory_data_to_shopify(inventory_levels, {default_wh: location_id}) - percent_successful = stats["Success"] / len(inventory_levels) - if percent_successful == 0: - status = "Failed" - elif percent_successful < 1: - status = "Partial Success" - else: - status = "Success" +def update_inventory_on_shopify() -> None: + """Scheduled job entrypoint – one-way push from ERPNext to Shopify.""" + setting = frappe.get_doc(SETTING_DOCTYPE) + if not need_to_run(SETTING_DOCTYPE, "inventory_sync_frequency", "last_inventory_sync"): + return + _run_push_for_default_warehouse(setting) - log_message = f"Updated {percent_successful * 100}% items\n\n" + log_message - create_shopify_log(method="update_inventory_on_shopify", status=status, message=log_message) +def update_inventory_on_shopify_now() -> None: + """Immediate manual push (can be called from console or other code).""" + setting = frappe.get_doc(SETTING_DOCTYPE) + _run_push_for_default_warehouse(setting) diff --git a/ecommerce_integrations/shopify/product.py b/ecommerce_integrations/shopify/product.py index ffae3204..1c7c34cb 100644 --- a/ecommerce_integrations/shopify/product.py +++ b/ecommerce_integrations/shopify/product.py @@ -2,557 +2,667 @@ import frappe from frappe import _, msgprint -from frappe.utils import cint, cstr +from frappe.utils import cint, cstr, get_url from frappe.utils.nestedset import get_root_of +from pyactiveresource.connection import ResourceNotFound from shopify.resources import Product, Variant from ecommerce_integrations.ecommerce_integrations.doctype.ecommerce_item import ecommerce_item from ecommerce_integrations.shopify.connection import temp_shopify_session from ecommerce_integrations.shopify.constants import ( - ITEM_SELLING_RATE_FIELD, - MODULE_NAME, - SETTING_DOCTYPE, - SHOPIFY_VARIANTS_ATTR_LIST, - SUPPLIER_ID_FIELD, - WEIGHT_TO_ERPNEXT_UOM_MAP, + ITEM_SELLING_RATE_FIELD, + MODULE_NAME, + SETTING_DOCTYPE, + SHOPIFY_VARIANTS_ATTR_LIST, + SUPPLIER_ID_FIELD, + WEIGHT_TO_ERPNEXT_UOM_MAP, ) from ecommerce_integrations.shopify.utils import create_shopify_log class ShopifyProduct: - def __init__( - self, - product_id: str, - variant_id: Optional[str] = None, - sku: Optional[str] = None, - has_variants: Optional[int] = 0, - ): - self.product_id = str(product_id) - self.variant_id = str(variant_id) if variant_id else None - self.sku = str(sku) if sku else None - self.has_variants = has_variants - self.setting = frappe.get_doc(SETTING_DOCTYPE) - - if not self.setting.is_enabled(): - frappe.throw(_("Can not create Shopify product when integration is disabled.")) - - def is_synced(self) -> bool: - return ecommerce_item.is_synced( - MODULE_NAME, integration_item_code=self.product_id, variant_id=self.variant_id, sku=self.sku, - ) - - def get_erpnext_item(self): - return ecommerce_item.get_erpnext_item( - MODULE_NAME, - integration_item_code=self.product_id, - variant_id=self.variant_id, - sku=self.sku, - has_variants=self.has_variants, - ) - - @temp_shopify_session - def sync_product(self): - if not self.is_synced(): - shopify_product = Product.find(self.product_id) - product_dict = shopify_product.to_dict() - self._make_item(product_dict) - - def _make_item(self, product_dict): - _add_weight_details(product_dict) - - warehouse = self.setting.warehouse - - if _has_variants(product_dict): - self.has_variants = 1 - attributes = self._create_attribute(product_dict) - self._create_item(product_dict, warehouse, 1, attributes) - self._create_item_variants(product_dict, warehouse, attributes) - - else: - product_dict["variant_id"] = product_dict["variants"][0]["id"] - self._create_item(product_dict, warehouse) - - def _create_attribute(self, product_dict): - attribute = [] - for attr in product_dict.get("options"): - if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"): - frappe.get_doc( - { - "doctype": "Item Attribute", - "attribute_name": attr.get("name"), - "item_attribute_values": [ - {"attribute_value": attr_value, "abbr": attr_value} for attr_value in attr.get("values") - ], - } - ).insert() - attribute.append({"attribute": attr.get("name")}) - - else: - # check for attribute values - item_attr = frappe.get_doc("Item Attribute", attr.get("name")) - if not item_attr.numeric_values: - self._set_new_attribute_values(item_attr, attr.get("values")) - item_attr.save() - attribute.append({"attribute": attr.get("name")}) - - else: - attribute.append( - { - "attribute": attr.get("name"), - "from_range": item_attr.get("from_range"), - "to_range": item_attr.get("to_range"), - "increment": item_attr.get("increment"), - "numeric_values": item_attr.get("numeric_values"), - } - ) - - return attribute - - def _set_new_attribute_values(self, item_attr, values): - for attr_value in values: - if not any( - (d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower()) - for d in item_attr.item_attribute_values - ): - item_attr.append("item_attribute_values", {"attribute_value": attr_value, "abbr": attr_value}) - - def _create_item(self, product_dict, warehouse, has_variant=0, attributes=None, variant_of=None): - item_dict = { - "variant_of": variant_of, - "is_stock_item": 1, - "item_code": cstr(product_dict.get("item_code")) or cstr(product_dict.get("id")), - "item_name": product_dict.get("title", "").strip(), - "description": product_dict.get("body_html") or product_dict.get("title"), - "item_group": self._get_item_group(product_dict.get("product_type")), - "has_variants": has_variant, - "attributes": attributes or [], - "stock_uom": product_dict.get("uom") or _("Nos"), - "sku": product_dict.get("sku") or _get_sku(product_dict), - "default_warehouse": warehouse, - "image": _get_item_image(product_dict), - "weight_uom": WEIGHT_TO_ERPNEXT_UOM_MAP[product_dict.get("weight_unit")], - "weight_per_unit": product_dict.get("weight"), - "default_supplier": self._get_supplier(product_dict), - } - - integration_item_code = product_dict["id"] # shopify product_id - variant_id = product_dict.get("variant_id", "") # shopify variant_id if has variants - sku = item_dict["sku"] - - if not _match_sku_and_link_item( - item_dict, integration_item_code, variant_id, variant_of=variant_of, has_variant=has_variant - ): - ecommerce_item.create_ecommerce_item( - MODULE_NAME, - integration_item_code, - item_dict, - variant_id=variant_id, - sku=sku, - variant_of=variant_of, - has_variants=has_variant, - ) - - def _create_item_variants(self, product_dict, warehouse, attributes): - template_item = ecommerce_item.get_erpnext_item( - MODULE_NAME, integration_item_code=product_dict.get("id"), has_variants=1 - ) - - if template_item: - for variant in product_dict.get("variants"): - shopify_item_variant = { - "id": product_dict.get("id"), - "variant_id": variant.get("id"), - "item_code": variant.get("id"), - "title": product_dict.get("title", "").strip() + "-" + variant.get("title"), - "product_type": product_dict.get("product_type"), - "sku": variant.get("sku"), - "uom": template_item.stock_uom or _("Nos"), - "item_price": variant.get("price"), - "weight_unit": variant.get("weight_unit"), - "weight": variant.get("weight"), - } - - for i, variant_attr in enumerate(SHOPIFY_VARIANTS_ATTR_LIST): - if variant.get(variant_attr): - attributes[i].update( - {"attribute_value": self._get_attribute_value(variant.get(variant_attr), attributes[i])} - ) - self._create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name) - - def _get_attribute_value(self, variant_attr_val, attribute): - attribute_value = frappe.db.sql( - """select attribute_value from `tabItem Attribute Value` - where parent = %s and (abbr = %s or attribute_value = %s)""", - (attribute["attribute"], variant_attr_val, variant_attr_val), - as_list=1, - ) - return attribute_value[0][0] if len(attribute_value) > 0 else cint(variant_attr_val) - - def _get_item_group(self, product_type=None): - parent_item_group = get_root_of("Item Group") - - if not product_type: - return parent_item_group - - if frappe.db.get_value("Item Group", product_type, "name"): - return product_type - item_group = frappe.get_doc( - { - "doctype": "Item Group", - "item_group_name": product_type, - "parent_item_group": parent_item_group, - "is_group": "No", - } - ).insert() - return item_group.name - - def _get_supplier(self, product_dict): - if product_dict.get("vendor"): - supplier = frappe.db.sql( - f"""select name from tabSupplier - where name = %s or {SUPPLIER_ID_FIELD} = %s """, - (product_dict.get("vendor"), product_dict.get("vendor").lower()), - as_list=1, - ) - - if supplier: - return product_dict.get("vendor") - supplier = frappe.get_doc( - { - "doctype": "Supplier", - "supplier_name": product_dict.get("vendor"), - SUPPLIER_ID_FIELD: product_dict.get("vendor").lower(), - "supplier_group": self._get_supplier_group(), - } - ).insert() - return supplier.name - else: - return "" - - def _get_supplier_group(self): - supplier_group = frappe.db.get_value("Supplier Group", _("Shopify Supplier")) - if not supplier_group: - supplier_group = frappe.get_doc( - {"doctype": "Supplier Group", "supplier_group_name": _("Shopify Supplier")} - ).insert() - return supplier_group.name - return supplier_group + def __init__( + self, + product_id: str, + variant_id: Optional[str] = None, + sku: Optional[str] = None, + has_variants: Optional[int] = 0, + ): + self.product_id = str(product_id) + self.variant_id = str(variant_id) if variant_id else None + self.sku = str(sku) if sku else None + self.has_variants = has_variants + self.setting = frappe.get_doc(SETTING_DOCTYPE) + + if not self.setting.is_enabled(): + frappe.throw(_("Can not create Shopify product when integration is disabled.")) + + def is_synced(self) -> bool: + return ecommerce_item.is_synced( + MODULE_NAME, integration_item_code=self.product_id, variant_id=self.variant_id, sku=self.sku, + ) + + def get_erpnext_item(self): + return ecommerce_item.get_erpnext_item( + MODULE_NAME, + integration_item_code=self.product_id, + variant_id=self.variant_id, + sku=self.sku, + has_variants=self.has_variants, + ) + + @temp_shopify_session + def sync_product(self): + if not self.is_synced(): + shopify_product = Product.find(self.product_id) + product_dict = shopify_product.to_dict() + self._make_item(product_dict) + + def _make_item(self, product_dict): + _add_weight_details(product_dict) + + warehouse = self.setting.warehouse + + if _has_variants(product_dict): + self.has_variants = 1 + attributes = self._create_attribute(product_dict) + self._create_item(product_dict, warehouse, 1, attributes) + self._create_item_variants(product_dict, warehouse, attributes) + + else: + product_dict["variant_id"] = product_dict["variants"][0]["id"] + self._create_item(product_dict, warehouse) + + def _create_attribute(self, product_dict): + attribute = [] + for attr in product_dict.get("options"): + if not frappe.db.get_value("Item Attribute", attr.get("name"), "name"): + frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": attr.get("name"), + "item_attribute_values": [ + {"attribute_value": attr_value, "abbr": attr_value} for attr_value in attr.get("values") + ], + } + ).insert() + attribute.append({"attribute": attr.get("name")}) + + else: + # check for attribute values + item_attr = frappe.get_doc("Item Attribute", attr.get("name")) + if not item_attr.numeric_values: + self._set_new_attribute_values(item_attr, attr.get("values")) + item_attr.save() + attribute.append({"attribute": attr.get("name")}) + + else: + attribute.append( + { + "attribute": attr.get("name"), + "from_range": item_attr.get("from_range"), + "to_range": item_attr.get("to_range"), + "increment": item_attr.get("increment"), + "numeric_values": item_attr.get("numeric_values"), + } + ) + + return attribute + + def _set_new_attribute_values(self, item_attr, values): + for attr_value in values: + if not any( + (d.abbr.lower() == attr_value.lower() or d.attribute_value.lower() == attr_value.lower()) + for d in item_attr.item_attribute_values + ): + item_attr.append("item_attribute_values", {"attribute_value": attr_value, "abbr": attr_value}) + + def _create_item(self, product_dict, warehouse, has_variant=0, attributes=None, variant_of=None): + item_dict = { + "variant_of": variant_of, + "is_stock_item": 1, + "item_code": cstr(product_dict.get("item_code")) or cstr(product_dict.get("id")), + "item_name": product_dict.get("title", "").strip(), + "description": product_dict.get("body_html") or product_dict.get("title"), + "item_group": self._get_item_group(product_dict.get("product_type")), + "has_variants": has_variant, + "attributes": attributes or [], + "stock_uom": product_dict.get("uom") or _("Nos"), + "sku": product_dict.get("sku") or _get_sku(product_dict), + "default_warehouse": warehouse, + "image": _get_item_image(product_dict), + "weight_uom": WEIGHT_TO_ERPNEXT_UOM_MAP[product_dict.get("weight_unit")], + "weight_per_unit": product_dict.get("weight"), + "default_supplier": self._get_supplier(product_dict), + } + + integration_item_code = product_dict["id"] # shopify product_id + variant_id = product_dict.get("variant_id", "") # shopify variant_id if has variants + sku = item_dict["sku"] + + if not _match_sku_and_link_item( + item_dict, integration_item_code, variant_id, variant_of=variant_of, has_variant=has_variant + ): + ecommerce_item.create_ecommerce_item( + MODULE_NAME, + integration_item_code, + item_dict, + variant_id=variant_id, + sku=sku, + variant_of=variant_of, + has_variants=has_variant, + ) + + def _create_item_variants(self, product_dict, warehouse, attributes): + template_item = ecommerce_item.get_erpnext_item( + MODULE_NAME, integration_item_code=product_dict.get("id"), has_variants=1 + ) + + if template_item: + for variant in product_dict.get("variants"): + shopify_item_variant = { + "id": product_dict.get("id"), + "variant_id": variant.get("id"), + "item_code": variant.get("id"), + "title": product_dict.get("title", "").strip() + "-" + variant.get("title"), + "product_type": product_dict.get("product_type"), + "sku": variant.get("sku"), + "uom": template_item.stock_uom or _("Nos"), + "item_price": variant.get("price"), + "weight_unit": variant.get("weight_unit"), + "weight": variant.get("weight"), + } + + for i, variant_attr in enumerate(SHOPIFY_VARIANTS_ATTR_LIST): + if variant.get(variant_attr): + attributes[i].update( + {"attribute_value": self._get_attribute_value(variant.get(variant_attr), attributes[i])} + ) + self._create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name) + + def _get_attribute_value(self, variant_attr_val, attribute): + attribute_value = frappe.db.sql( + """select attribute_value from `tabItem Attribute Value` + where parent = %s and (abbr = %s or attribute_value = %s)""", + (attribute["attribute"], variant_attr_val, variant_attr_val), + as_list=1, + ) + return attribute_value[0][0] if len(attribute_value) > 0 else cint(variant_attr_val) + + def _get_item_group(self, product_type=None): + parent_item_group = get_root_of("Item Group") + + if not product_type: + return parent_item_group + + if frappe.db.get_value("Item Group", product_type, "name"): + return product_type + item_group = frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": product_type, + "parent_item_group": parent_item_group, + "is_group": "No", + } + ).insert() + return item_group.name + + def _get_supplier(self, product_dict): + if product_dict.get("vendor"): + supplier = frappe.db.sql( + f"""select name from tabSupplier + where name = %s or {SUPPLIER_ID_FIELD} = %s """, + (product_dict.get("vendor"), product_dict.get("vendor").lower()), + as_list=1, + ) + + if supplier: + return product_dict.get("vendor") + supplier = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": product_dict.get("vendor"), + SUPPLIER_ID_FIELD: product_dict.get("vendor").lower(), + "supplier_group": self._get_supplier_group(), + } + ).insert() + return supplier.name + else: + return "" + + def _get_supplier_group(self): + supplier_group = frappe.db.get_value("Supplier Group", _("Shopify Supplier")) + if not supplier_group: + supplier_group = frappe.get_doc( + {"doctype": "Supplier Group", "supplier_group_name": _("Shopify Supplier")} + ).insert() + return supplier_group.name + return supplier_group def _add_weight_details(product_dict): - variants = product_dict.get("variants") - if variants: - product_dict["weight"] = variants[0]["weight"] - product_dict["weight_unit"] = variants[0]["weight_unit"] + variants = product_dict.get("variants") + if variants: + product_dict["weight"] = variants[0]["weight"] + product_dict["weight_unit"] = variants[0]["weight_unit"] def _has_variants(product_dict) -> bool: - options = product_dict.get("options") - return bool(options and "Default Title" not in options[0]["values"]) + options = product_dict.get("options") + return bool(options and "Default Title" not in options[0]["values"]) def _get_sku(product_dict): - if product_dict.get("variants"): - return product_dict.get("variants")[0].get("sku") - return "" + if product_dict.get("variants"): + return product_dict.get("variants")[0].get("sku") + return "" def _get_item_image(product_dict): - if product_dict.get("image"): - return product_dict.get("image").get("src") - return None + if product_dict.get("image"): + return product_dict.get("image").get("src") + return None def _match_sku_and_link_item( - item_dict, product_id, variant_id, variant_of=None, has_variant=False + item_dict, product_id, variant_id, variant_of=None, has_variant=False ) -> bool: - """Tries to match new item with existing item using Shopify SKU == item_code. - - Returns true if matched and linked. - """ - sku = item_dict["sku"] - if not sku or variant_of or has_variant: - return False - - item_name = frappe.db.get_value("Item", {"item_code": sku}) - if item_name: - try: - ecommerce_item = frappe.get_doc( - { - "doctype": "Ecommerce Item", - "integration": MODULE_NAME, - "erpnext_item_code": item_name, - "integration_item_code": product_id, - "has_variants": 0, - "variant_id": cstr(variant_id), - "sku": sku, - } - ) - - ecommerce_item.insert() - return True - except Exception: - return False - - + """Tries to match new item with existing item using Shopify SKU == item_code. + + Returns true if matched and linked. + """ + sku = item_dict["sku"] + if not sku or variant_of or has_variant: + return False + + item_name = frappe.db.get_value("Item", {"item_code": sku}) + if item_name: + try: + ecommerce_item = frappe.get_doc( + { + "doctype": "Ecommerce Item", + "integration": MODULE_NAME, + "erpnext_item_code": item_name, + "integration_item_code": product_id, + "has_variants": 0, + "variant_id": cstr(variant_id), + "sku": sku, + } + ) + + ecommerce_item.insert() + return True + except Exception: + return False + + +# ---------------- ONE-WAY CREATION ENFORCEMENT ---------------- def create_items_if_not_exist(order): - """Using shopify order, sync all items that are not already synced.""" - for item in order.get("line_items", []): - - product_id = item["product_id"] - variant_id = item.get("variant_id") - sku = item.get("sku") - product = ShopifyProduct(product_id, variant_id=variant_id, sku=sku) - - if not product.is_synced(): - product.sync_product() + """Disabled to enforce one-way item creation (ERPNext ➜ Shopify only).""" + return +# -------------------------------------------------------------- def get_item_code(shopify_item): - """Get item code using shopify_item dict. + """Get item code using shopify_item dict. - Item should contain both product_id and variant_id.""" + Item should contain both product_id and variant_id.""" - item = ecommerce_item.get_erpnext_item( - integration=MODULE_NAME, - integration_item_code=shopify_item.get("product_id"), - variant_id=shopify_item.get("variant_id"), - sku=shopify_item.get("sku"), - ) - if item: - return item.item_code + item = ecommerce_item.get_erpnext_item( + integration=MODULE_NAME, + integration_item_code=shopify_item.get("product_id"), + variant_id=shopify_item.get("variant_id"), + sku=shopify_item.get("sku"), + ) + if item: + return item.item_code @temp_shopify_session def upload_erpnext_item(doc, method=None): - """This hook is called when inserting new or updating existing `Item`. - - New items are pushed to shopify and changes to existing items are - updated depending on what is configured in "Shopify Setting" doctype. - """ - template_item = item = doc # alias for readability - # a new item recieved from ecommerce_integrations is being inserted - if item.flags.from_integration: - return - - setting = frappe.get_doc(SETTING_DOCTYPE) - - if not setting.is_enabled() or not setting.upload_erpnext_items: - return - - if frappe.flags.in_import: - return - - if item.has_variants: - return - - if len(item.attributes) > 3: - msgprint(_("Template items/Items with 4 or more attributes can not be uploaded to Shopify.")) - return - - if doc.variant_of and not setting.upload_variants_as_items: - msgprint(_("Enable variant sync in setting to upload item to Shopify.")) - return - - if item.variant_of: - template_item = frappe.get_doc("Item", item.variant_of) - - product_id = frappe.db.get_value( - "Ecommerce Item", - {"erpnext_item_code": template_item.name, "integration": MODULE_NAME}, - "integration_item_code", - ) - is_new_product = not bool(product_id) - - if is_new_product: - product = Product() - product.published = False - product.status = "active" if setting.sync_new_item_as_active else "draft" - - map_erpnext_item_to_shopify(shopify_product=product, erpnext_item=template_item) - is_successful = product.save() - - if is_successful: - update_default_variant_properties( - product, - sku=template_item.item_code, - price=template_item.get(ITEM_SELLING_RATE_FIELD), - is_stock_item=template_item.is_stock_item, - ) - if item.variant_of: - product.options = [] - product.variants = [] - variant_attributes = { - "title": template_item.item_name, - "sku": item.item_code, - "price": item.get(ITEM_SELLING_RATE_FIELD), - } - max_index_range = min(3, len(template_item.attributes)) - for i in range(0, max_index_range): - attr = template_item.attributes[i] - product.options.append( - { - "name": attr.attribute, - "values": frappe.db.get_all( - "Item Attribute Value", {"parent": attr.attribute}, pluck="attribute_value" - ), - } - ) - try: - variant_attributes[f"option{i+1}"] = item.attributes[i].attribute_value - except IndexError: - frappe.throw(_("Shopify Error: Missing value for attribute {}").format(attr.attribute)) - product.variants.append(Variant(variant_attributes)) - - product.save() # push variant - - ecom_items = list(set([item, template_item])) - for d in ecom_items: - ecom_item = frappe.get_doc( - { - "doctype": "Ecommerce Item", - "erpnext_item_code": d.name, - "integration": MODULE_NAME, - "integration_item_code": str(product.id), - "variant_id": "" if d.has_variants else str(product.variants[0].id), - "sku": "" if d.has_variants else str(product.variants[0].sku), - "has_variants": d.has_variants, - "variant_of": d.variant_of, - } - ) - ecom_item.insert() - - write_upload_log(status=is_successful, product=product, item=item) - elif setting.update_shopify_item_on_update: - product = Product.find(product_id) - if product: - map_erpnext_item_to_shopify(shopify_product=product, erpnext_item=template_item) - if not item.variant_of: - update_default_variant_properties( - product, is_stock_item=template_item.is_stock_item, price=item.get(ITEM_SELLING_RATE_FIELD) - ) - else: - variant_attributes = {"sku": item.item_code, "price": item.get(ITEM_SELLING_RATE_FIELD)} - product.options = [] - max_index_range = min(3, len(template_item.attributes)) - for i in range(0, max_index_range): - attr = template_item.attributes[i] - product.options.append( - { - "name": attr.attribute, - "values": frappe.db.get_all( - "Item Attribute Value", {"parent": attr.attribute}, pluck="attribute_value" - ), - } - ) - try: - variant_attributes[f"option{i+1}"] = item.attributes[i].attribute_value - except IndexError: - frappe.throw(_("Shopify Error: Missing value for attribute {}").format(attr.attribute)) - product.variants.append(Variant(variant_attributes)) - - is_successful = product.save() - if is_successful and item.variant_of: - map_erpnext_variant_to_shopify_variant(product, item, variant_attributes) - - write_upload_log(status=is_successful, product=product, item=item, action="Updated") + """This hook is called when inserting new or updating existing `Item`. + + New items are pushed to shopify and changes to existing items are + updated depending on what is configured in "Shopify Setting" doctype. + """ + template_item = item = doc # alias for readability + # a new item recieved from ecommerce_integrations is being inserted + if item.flags.from_integration: + return + + setting = frappe.get_doc(SETTING_DOCTYPE) + + if not setting.is_enabled() or not setting.upload_erpnext_items: + return + + # Only push if explicitly selected + if not cint(getattr(item, "custom_sync_to_shopify", 0)): + return + + if frappe.flags.in_import: + return + + if item.has_variants: + return + + if len(item.attributes) > 3: + msgprint(_("Template items/Items with 4 or more attributes can not be uploaded to Shopify.")) + return + + if doc.variant_of and not setting.upload_variants_as_items: + msgprint(_("Enable variant sync in setting to upload item to Shopify.")) + return + + if item.variant_of: + template_item = frappe.get_doc("Item", item.variant_of) + + product_id = frappe.db.get_value( + "Ecommerce Item", + {"erpnext_item_code": template_item.name, "integration": MODULE_NAME}, + "integration_item_code", + ) + is_new_product = not bool(product_id) + + if is_new_product: + product = Product() + product.published = False + product.status = "active" if setting.sync_new_item_as_active else "draft" + + map_erpnext_item_to_shopify(shopify_product=product, erpnext_item=template_item) + # ensure image on create + _apply_erp_image_to_shopify_product(product, template_item) + + is_successful = product.save() + + if is_successful: + update_default_variant_properties( + product, + sku=template_item.item_code, + price=template_item.get(ITEM_SELLING_RATE_FIELD), + is_stock_item=template_item.is_stock_item, + ) + if item.variant_of: + product.options = [] + product.variants = [] + variant_attributes = { + "title": template_item.item_name, + "sku": item.item_code, + "price": item.get(ITEM_SELLING_RATE_FIELD), + } + max_index_range = min(3, len(template_item.attributes)) + for i in range(0, max_index_range): + attr = template_item.attributes[i] + product.options.append( + { + "name": attr.attribute, + "values": frappe.db.get_all( + "Item Attribute Value", {"parent": attr.attribute}, pluck="attribute_value" + ), + } + ) + try: + variant_attributes[f"option{i+1}"] = item.attributes[i].attribute_value + except IndexError: + frappe.throw(_("Shopify Error: Missing value for attribute {}").format(attr.attribute)) + product.variants.append(Variant(variant_attributes)) + + product.save() # push variant + + ecom_items = list(set([item, template_item])) + for d in ecom_items: + ecom_item = frappe.get_doc( + { + "doctype": "Ecommerce Item", + "erpnext_item_code": d.name, + "integration": MODULE_NAME, + "integration_item_code": str(product.id), + "variant_id": "" if d.has_variants else str(product.variants[0].id), + "sku": "" if d.has_variants else str(product.variants[0].sku), + "has_variants": d.has_variants, + "variant_of": d.variant_of, + } + ) + ecom_item.insert() + + write_upload_log(status=is_successful, product=product, item=item) + + elif setting.update_shopify_item_on_update: + try: + product = Product.find(product_id) + if product: + # normal update path + map_erpnext_item_to_shopify(shopify_product=product, erpnext_item=template_item) + _apply_erp_image_to_shopify_product(product, template_item) + + if not item.variant_of: + update_default_variant_properties( + product, is_stock_item=template_item.is_stock_item, price=item.get(ITEM_SELLING_RATE_FIELD) + ) + else: + variant_attributes = {"sku": item.item_code, "price": item.get(ITEM_SELLING_RATE_FIELD)} + product.options = [] + max_index_range = min(3, len(template_item.attributes)) + for i in range(0, max_index_range): + attr = template_item.attributes[i] + product.options.append( + { + "name": attr.attribute, + "values": frappe.db.get_all( + "Item Attribute Value", {"parent": attr.attribute}, pluck="attribute_value" + ), + } + ) + try: + variant_attributes[f"option{i+1}"] = item.attributes[i].attribute_value + except IndexError: + frappe.throw(_("Shopify Error: Missing value for attribute {}").format(attr.attribute)) + product.variants.append(Variant(variant_attributes)) + + is_successful = product.save() + if is_successful and item.variant_of: + map_erpnext_variant_to_shopify_variant(product, item, variant_attributes) + + write_upload_log(status=is_successful, product=product, item=item, action="Updated") + + except ResourceNotFound: + # Mapping exists but Shopify product is gone. Clean mapping & create a fresh product. + frappe.db.delete("Ecommerce Item", { + "erpnext_item_code": template_item.name, + "integration": MODULE_NAME + }) + frappe.db.commit() + + # Re-enter the create branch + product = Product() + product.published = False + product.status = "active" if setting.sync_new_item_as_active else "draft" + + map_erpnext_item_to_shopify(shopify_product=product, erpnext_item=template_item) + _apply_erp_image_to_shopify_product(product, template_item) + + is_successful = product.save() + + if is_successful: + update_default_variant_properties( + product, + sku=template_item.item_code, + price=template_item.get(ITEM_SELLING_RATE_FIELD), + is_stock_item=template_item.is_stock_item, + ) + if item.variant_of: + product.options = [] + product.variants = [] + variant_attributes = { + "title": template_item.item_name, + "sku": item.item_code, + "price": item.get(ITEM_SELLING_RATE_FIELD), + } + max_index_range = min(3, len(template_item.attributes)) + for i in range(0, max_index_range): + attr = template_item.attributes[i] + product.options.append( + { + "name": attr.attribute, + "values": frappe.db.get_all( + "Item Attribute Value", {"parent": attr.attribute}, pluck="attribute_value" + ), + } + ) + try: + variant_attributes[f"option{i+1}"] = item.attributes[i].attribute_value + except IndexError: + frappe.throw(_("Shopify Error: Missing value for attribute {}").format(attr.attribute)) + product.variants.append(Variant(variant_attributes)) + + product.save() + + # Recreate mapping rows for template and item as needed + ecom_items = list(set([item, template_item])) + for d in ecom_items: + ecom_item = frappe.get_doc( + { + "doctype": "Ecommerce Item", + "erpnext_item_code": d.name, + "integration": MODULE_NAME, + "integration_item_code": str(product.id), + "variant_id": "" if d.has_variants else str(product.variants[0].id), + "sku": "" if d.has_variants else str(product.variants[0].sku), + "has_variants": d.has_variants, + "variant_of": d.variant_of, + } + ) + ecom_item.insert() + + write_upload_log(status=True, product=product, item=item, action="Recreated") + + +def _apply_erp_image_to_shopify_product(shopify_product: Product, erpnext_item) -> None: + """Ensure Shopify product has the same primary image as ERPNext Item.image.""" + try: + img_src = _get_erp_item_image_url(erpnext_item) + if not img_src: + return + if getattr(shopify_product, "images", None): + try: + shopify_product.images[0]["src"] = img_src + except Exception: + try: + shopify_product.images[0].src = img_src + except Exception: + shopify_product.images = [{"src": img_src}] + else: + shopify_product.images = [{"src": img_src}] + except Exception: + # Never block the sync on image issues. + pass + + +def _get_erp_item_image_url(erpnext_item) -> Optional[str]: + """Return absolute URL for ERPNext Item.image if present.""" + path = getattr(erpnext_item, "image", None) + if not path: + return None + try: + return get_url(path) + except Exception: + return None def map_erpnext_variant_to_shopify_variant( - shopify_product: Product, erpnext_item, variant_attributes + shopify_product: Product, erpnext_item, variant_attributes ): - variant_product_id = frappe.db.get_value( - "Ecommerce Item", - {"erpnext_item_code": erpnext_item.name, "integration": MODULE_NAME}, - "integration_item_code", - ) - if not variant_product_id: - for variant in shopify_product.variants: - if ( - variant.option1 == variant_attributes.get("option1") - and variant.option2 == variant_attributes.get("option2") - and variant.option3 == variant_attributes.get("option3") - ): - variant_product_id = str(variant.id) - if not frappe.flags.in_test: - frappe.get_doc( - { - "doctype": "Ecommerce Item", - "erpnext_item_code": erpnext_item.name, - "integration": MODULE_NAME, - "integration_item_code": str(shopify_product.id), - "variant_id": variant_product_id, - "sku": str(variant.sku), - "variant_of": erpnext_item.variant_of, - } - ).insert() - break - if not variant_product_id: - msgprint(_("Shopify: Couldn't sync item variant.")) - return variant_product_id + variant_product_id = frappe.db.get_value( + "Ecommerce Item", + {"erpnext_item_code": erpnext_item.name, "integration": MODULE_NAME}, + "integration_item_code", + ) + if not variant_product_id: + for variant in shopify_product.variants: + if ( + variant.option1 == variant_attributes.get("option1") + and variant.option2 == variant_attributes.get("option2") + and variant.option3 == variant_attributes.get("option3") + ): + variant_product_id = str(variant.id) + if not frappe.flags.in_test: + frappe.get_doc( + { + "doctype": "Ecommerce Item", + "erpnext_item_code": erpnext_item.name, + "integration": MODULE_NAME, + "integration_item_code": str(shopify_product.id), + "variant_id": variant_product_id, + "sku": str(variant.sku), + "variant_of": erpnext_item.variant_of, + } + ).insert() + break + if not variant_product_id: + msgprint(_("Shopify: Couldn't sync item variant.")) + return variant_product_id def map_erpnext_item_to_shopify(shopify_product: Product, erpnext_item): - """Map erpnext fields to shopify, called both when updating and creating new products.""" + """Map erpnext fields to shopify, called both when updating and creating new products.""" - shopify_product.title = erpnext_item.item_name - shopify_product.body_html = erpnext_item.description - shopify_product.product_type = erpnext_item.item_group + shopify_product.title = erpnext_item.item_name + shopify_product.body_html = erpnext_item.description + shopify_product.product_type = erpnext_item.item_group - if erpnext_item.weight_uom in WEIGHT_TO_ERPNEXT_UOM_MAP.values(): - # reverse lookup for key - uom = get_shopify_weight_uom(erpnext_weight_uom=erpnext_item.weight_uom) - shopify_product.weight = erpnext_item.weight_per_unit - shopify_product.weight_unit = uom + if erpnext_item.weight_uom in WEIGHT_TO_ERPNEXT_UOM_MAP.values(): + # reverse lookup for key + uom = get_shopify_weight_uom(erpnext_weight_uom=erpnext_item.weight_uom) + shopify_product.weight = erpnext_item.weight_per_unit + shopify_product.weight_unit = uom - if erpnext_item.disabled: - shopify_product.status = "draft" - shopify_product.published = False - msgprint(_("Status of linked Shopify product is changed to Draft.")) + if erpnext_item.disabled: + shopify_product.status = "draft" + shopify_product.published = False + msgprint(_("Status of linked Shopify product is changed to Draft.")) def get_shopify_weight_uom(erpnext_weight_uom: str) -> str: - for shopify_uom, erpnext_uom in WEIGHT_TO_ERPNEXT_UOM_MAP.items(): - if erpnext_uom == erpnext_weight_uom: - return shopify_uom + for shopify_uom, erpnext_uom in WEIGHT_TO_ERPNEXT_UOM_MAP.items(): + if erpnext_uom == erpnext_weight_uom: + return shopify_uom def update_default_variant_properties( - shopify_product: Product, - is_stock_item: bool, - sku: Optional[str] = None, - price: Optional[float] = None, + shopify_product: Product, + is_stock_item: bool, + sku: Optional[str] = None, + price: Optional[float] = None, ): - """Shopify creates default variant upon saving the product. + """Shopify creates default variant upon saving the product. - Some item properties are supposed to be updated on the default variant. - Input: saved shopify_product, sku and price - """ - default_variant: Variant = shopify_product.variants[0] + Some item properties are supposed to be updated on the default variant. + Input: saved shopify_product, sku and price + """ + default_variant: Variant = shopify_product.variants[0] - # this will create Inventory item and qty will be updated by scheduled job. - if is_stock_item: - default_variant.inventory_management = "shopify" + # this will create Inventory item and qty will be updated by scheduled job. + if is_stock_item: + default_variant.inventory_management = "shopify" - if price is not None: - default_variant.price = price - if sku is not None: - default_variant.sku = sku + if price is not None: + default_variant.price = price + if sku is not None: + default_variant.sku = sku def write_upload_log(status: bool, product: Product, item, action="Created") -> None: - if not status: - msg = _("Failed to upload item to Shopify") + "
" - msg += _("Shopify reported errors:") + " " + ", ".join(product.errors.full_messages()) - msgprint(msg, title="Note", indicator="orange") - - create_shopify_log( - status="Error", request_data=product.to_dict(), message=msg, method="upload_erpnext_item", - ) - else: - create_shopify_log( - status="Success", - request_data=product.to_dict(), - message=f"{action} Item: {item.name}, shopify product: {product.id}", - method="upload_erpnext_item", - ) + if not status: + msg = _("Failed to upload item to Shopify") + "
" + msg += _("Shopify reported errors:") + " " + ", ".join(product.errors.full_messages()) + msgprint(msg, title="Note", indicator="orange") + + create_shopify_log( + status="Error", request_data=product.to_dict(), message=msg, method="upload_erpnext_item", + ) + else: + create_shopify_log( + status="Success", + request_data=product.to_dict(), + message=f"{action} Item: {item.name}, shopify product: {product.id}", + method="upload_erpnext_item", + )