diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index ce3401804..c3ebc403a 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -6,11 +6,11 @@ cd ~ || exit
sudo apt-get update
sudo apt-get -y remove mysql-server mysql-client
-sudo apt-get -y install redis-server libcups2-dev mariadb-client-10.6 -qq
+sudo apt-get -y install redis-server libcups2-dev mariadb-client -qq
pip install frappe-bench
-git clone https://github.com/frappe/frappe --branch develop --depth 1
+git clone https://github.com/frappe/frappe --branch version-15 --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
@@ -33,11 +33,9 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments --branch develop
-bench get-app erpnext --branch develop
+bench get-app erpnext --branch version-15
+bench get-app ecommerce_integrations "${GITHUB_WORKSPACE}"
bench start &
bench --site test_site reinstall --yes
-
-bench get-app ecommerce_integrations "${GITHUB_WORKSPACE}"
-bench --site test_site install-app ecommerce_integrations
bench setup requirements --dev
diff --git a/.github/helper/site_config.json b/.github/helper/site_config.json
index 8c86f7372..77945c7f5 100644
--- a/.github/helper/site_config.json
+++ b/.github/helper/site_config.json
@@ -11,6 +11,6 @@
"root_login": "root",
"root_password": "root",
"host_name": "http://test_site:8000",
- "install_apps": ["payments", "erpnext"],
+ "install_apps": ["payments", "erpnext", "ecommerce_integrations"],
"throttle_user_limit": 100
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 366751e31..ca62cf088 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,20 +3,35 @@ name: CI
on:
push:
branches:
- - develop
+ - main
+ - version-15
pull_request:
branches:
- - develop
- main
+ - version-15
+ paths-ignore:
+ - "**.css"
+ - "**.js"
+ - "**.md"
+ - "**.html"
+ - "**.csv"
+ schedule:
+ # Run everyday at midnight UTC / 5:30 IST
+ - cron: "0 0 * * *"
+
+env:
+ ECOMMERCE_BRANCH: ${{ github.base_ref || github.ref_name }}
concurrency:
- group: develop-${{ github.event.number }}
+ group: ci-${{ github.event.number || github.ref_name }}
cancel-in-progress: true
jobs:
tests:
runs-on: ubuntu-latest
timeout-minutes: 20
+ env:
+ WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
@@ -30,14 +45,14 @@ jobs:
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
- options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+ options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
@@ -51,15 +66,16 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt') }}
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
+
- name: Cache node modules
- uses: actions/cache@v2
+ uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -74,7 +90,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- - uses: actions/cache@v2
+ - uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -85,16 +101,36 @@ jobs:
- name: Install
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ BRANCH_TO_CLONE: ${{ env.ECOMMERCE_BRANCH }}
- name: Run Tests
- run: cd ~/frappe-bench/ && bench --site test_site run-tests --app ecommerce_integrations --coverage
+ run: cd ~/frappe-bench/ && bench --site test_site run-tests --app ecommerce_integrations ${{ env.WITH_COVERAGE == 'true' && '--coverage' || '' }}
env:
TYPE: server
+ - name: Upload coverage data
+ uses: actions/upload-artifact@v4
+ if: github.event_name != 'pull_request'
+ with:
+ name: coverage
+ path: /home/runner/frappe-bench/sites/coverage.xml
+
+ coverage:
+ name: Coverage Wrap Up
+ needs: tests
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name != 'pull_request' }}
+ steps:
+ - name: Clone
+ uses: actions/checkout@v4
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+
- name: Upload coverage data
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
- files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index ebb88c9ed..dbf763266 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -1,31 +1,31 @@
name: Linters
on:
- pull_request: { }
+ pull_request:
+ branches:
+ - main
+ - version-15
jobs:
-
linters:
name: linters
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - name: Set up Python 3.8
- uses: actions/setup-python@v2
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
with:
- python-version: 3.8
+ python-version: '3.10'
+ cache: pip
- name: Install and Run Pre-commit
- uses: pre-commit/action@v2.0.3
+ uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- - uses: returntocorp/semgrep-action@v1
- env:
- SEMGREP_TIMEOUT: 120
- with:
- config: >-
- r/python.lang.correctness
- ./frappe-semgrep-rules/rules
+ - name: Run Semgrep rules
+ run: |
+ pip install semgrep==1.90.0
+ semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cfe7b4bf1..05b969756 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,11 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup Node.js v14
- uses: actions/setup-node@v2
+ persist-credentials: false
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup dependencies
@@ -21,7 +22,8 @@ jobs:
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6c5ded60d..1c321fc47 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,37 +1,34 @@
exclude: 'node_modules|.git'
-default_stages: [commit]
-fail_fast: true
+default_stages: [pre-commit]
+fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.0.1
+ rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "ecommerce_integrations.*"
+ exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: end-of-file-fixer
files: "ecommerce_integrations.*"
exclude: ".*json$|.*txt$"
- id: check-yaml
+ - id: check-merge-conflict
+ - id: check-ast
+ - id: check-json
+ - id: check-toml
+ - id: debug-statements
- - repo: https://github.com/adityahase/black
- rev: 364d1ddcf58eb6bad2e0b757329f06f40ea83044
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.2.0
hooks:
- - id: black
- exclude: ".*setup.py$"
- additional_dependencies: ['click==8.0.4']
-
- - repo: https://github.com/PyCQA/isort
- rev: 5.12.0
- hooks:
- - id: isort
- exclude: ".*setup.py$"
-
- - repo: https://github.com/PyCQA/flake8
- rev: 5.0.4
- hooks:
- - id: flake8
- additional_dependencies: [flake8-isort]
- exclude: ".*setup.py$"
+ - id: ruff
+ name: "Run ruff import sorter"
+ args: ["--select=I", "--fix"]
+ - id: ruff
+ name: "Run ruff linter"
+ - id: ruff-format
+ name: "Run ruff formatter"
ci:
autoupdate_schedule: weekly
diff --git a/ecommerce_integrations/__init__.py b/ecommerce_integrations/__init__.py
index 3e49871f9..67d42d310 100644
--- a/ecommerce_integrations/__init__.py
+++ b/ecommerce_integrations/__init__.py
@@ -1 +1 @@
-__version__ = "1.20.1"
+__version__ = "1.20.2"
diff --git a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_repository.py b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_repository.py
index e5456d927..503808e2c 100644
--- a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_repository.py
+++ b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_repository.py
@@ -6,6 +6,7 @@
import urllib
import dateutil
+
import frappe
from frappe import _
@@ -47,7 +48,7 @@ def call_sp_api_method(self, sp_api_method, **kwargs) -> dict:
errors = {}
max_retries = self.amz_setting.max_retry_limit
- for x in range(max_retries):
+ for _x in range(max_retries):
try:
result = sp_api_method(**kwargs)
return result.get("payload")
@@ -62,7 +63,8 @@ def call_sp_api_method(self, sp_api_method, **kwargs) -> dict:
msg = f"Error: {error}
Error Description: {errors.get(error)}"
frappe.msgprint(msg, alert=True, indicator="red")
frappe.log_error(
- message=f"{error}: {errors.get(error)}", title=f'Method "{sp_api_method.__name__}" failed',
+ message=f"{error}: {errors.get(error)}",
+ title=f'Method "{sp_api_method.__name__}" failed',
)
self.amz_setting.enable_sync = 0
@@ -76,11 +78,11 @@ def get_finances_instance(self) -> Finances:
return Finances(**self.instance_params)
def get_account(self, name) -> str:
- account_name = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)})
+ account_name = frappe.db.get_value("Account", {"account_name": f"Amazon {name}"})
if not account_name:
new_account = frappe.new_doc("Account")
- new_account.account_name = "Amazon {0}".format(name)
+ new_account.account_name = f"Amazon {name}"
new_account.company = self.amz_setting.company
new_account.parent_account = self.amz_setting.market_place_account_group
new_account.insert(ignore_permissions=True)
@@ -271,9 +273,7 @@ def get_item_code(self, order_item) -> str:
def get_order_items(self, order_id) -> list:
orders = self.get_orders_instance()
- order_items_payload = self.call_sp_api_method(
- sp_api_method=orders.get_order_items, order_id=order_id
- )
+ order_items_payload = self.call_sp_api_method(sp_api_method=orders.get_order_items, order_id=order_id)
final_order_items = []
warehouse = self.amz_setting.warehouse
@@ -301,7 +301,9 @@ def get_order_items(self, order_id) -> list:
break
order_items_payload = self.call_sp_api_method(
- sp_api_method=orders.get_order_items, order_id=order_id, next_token=next_token,
+ sp_api_method=orders.get_order_items,
+ order_id=order_id,
+ next_token=next_token,
)
return final_order_items
@@ -333,7 +335,8 @@ def create_customer(order) -> str:
new_contact = frappe.new_doc("Contact")
new_contact.first_name = order_customer_name
new_contact.append(
- "links", {"link_doctype": "Customer", "link_name": existing_customer_name},
+ "links",
+ {"link_doctype": "Customer", "link_name": existing_customer_name},
)
new_contact.insert()
@@ -469,7 +472,9 @@ def get_orders(self, created_after) -> list:
break
orders_payload = self.call_sp_api_method(
- sp_api_method=orders.get_orders, created_after=created_after, next_token=next_token,
+ sp_api_method=orders.get_orders,
+ created_after=created_after,
+ next_token=next_token,
)
return sales_orders
diff --git a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api.py b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api.py
index c2abd0de2..decdcccbe 100644
--- a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api.py
+++ b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api.py
@@ -113,7 +113,7 @@ def __call__(self, request):
uri = parsed_url.path
if len(parsed_url.query) > 0:
- query_string = dict(map(lambda i: i.split("="), parsed_url.query.split("&")))
+ query_string = dict(i.split("=") for i in parsed_url.query.split("&"))
else:
query_string = dict()
@@ -132,11 +132,10 @@ def __call__(self, request):
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
# Query string values must be URL-encoded (space=%20) and be sorted by name.
- canonical_query_string = "&".join(map(lambda p: "=".join(p), sorted(query_string.items())))
-
+ canonical_query_string = "&".join("=".join(p) for p in sorted(query_string.items()))
# Create payload hash (hash of the request body content).
if request.method == "GET":
- payload_hash = hashlib.sha256(("").encode("utf-8")).hexdigest()
+ payload_hash = hashlib.sha256(b"").hexdigest()
else:
if request.body:
if isinstance(request.body, bytes):
@@ -150,15 +149,14 @@ def __call__(self, request):
# Create the canonical headers and signed headers. Header names
# must be trimmed and lowercase, and sorted in code point order from
# low to high. Note that there is a trailing \n.
- headers_to_sign = sorted(
- filter(
- lambda h: h.startswith("x-amz-") or h == "host",
- map(lambda H: H.lower(), request.headers.keys()),
- )
- )
- canonical_headers = "".join(
- map(lambda h: ":".join((h, request.headers[h])) + "\n", headers_to_sign)
- )
+ headers_to_sign = []
+ for header in request.headers.keys():
+ header = header.lower()
+ if header.startswith("x-amz-") or header == "host":
+ headers_to_sign.append(header)
+ headers_to_sign.sort()
+
+ canonical_headers = "".join(f"{h}:{request.headers[h]}\n" for h in headers_to_sign)
signed_headers = ";".join(headers_to_sign)
# Combine elements to create canonical request.
@@ -207,8 +205,8 @@ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args)
-class SPAPI(object):
- """ Base Amazon SP-API class """
+class SPAPI:
+ """Base Amazon SP-API class"""
# https://github.com/amzn/selling-partner-api-docs/blob/main/guides/en-US/developer-guide/SellingPartnerApiDeveloperGuide.md#connecting-to-the-selling-partner-api
AUTH_URL = "https://api.amazon.com/auth/o2/token"
@@ -246,9 +244,7 @@ def get_access_token(self) -> str:
result = response.json()
if response.status_code == 200:
return result.get("access_token")
- exception = SPAPIError(
- error=result.get("error"), error_description=result.get("error_description")
- )
+ exception = SPAPIError(error=result.get("error"), error_description=result.get("error_description"))
raise exception
def get_auth(self) -> AWSSigV4:
@@ -281,7 +277,11 @@ def get_headers(self) -> dict:
return {"x-amz-access-token": self.get_access_token()}
def make_request(
- self, method: str = "GET", append_to_base_uri: str = "", params: dict = None, data: dict = None,
+ self,
+ method: str = "GET",
+ append_to_base_uri: str = "",
+ params: dict | None = None,
+ data: dict | None = None,
) -> dict:
if isinstance(params, dict):
params = Util.remove_empty(params)
@@ -307,45 +307,45 @@ def list_to_dict(self, key: str, values: list, data: dict) -> None:
class Finances(SPAPI):
- """ Amazon Finances API """
+ """Amazon Finances API"""
BASE_URI = "/finances/v0/"
def list_financial_events_by_order_id(
- self, order_id: str, max_results: int = None, next_token: str = None
+ self, order_id: str, max_results: int | None = None, next_token: str | None = None
) -> dict:
- """ Returns all financial events for the specified order. """
+ """Returns all financial events for the specified order."""
append_to_base_uri = f"orders/{order_id}/financialEvents"
data = dict(MaxResultsPerPage=max_results, NextToken=next_token)
return self.make_request(append_to_base_uri=append_to_base_uri, params=data)
class Orders(SPAPI):
- """ Amazon Orders API """
+ """Amazon Orders API"""
BASE_URI = "/orders/v0/orders"
def get_orders(
self,
created_after: str,
- created_before: str = None,
- last_updated_after: str = None,
- last_updated_before: str = None,
- order_statuses: list = None,
- marketplace_ids: list = None,
- fulfillment_channels: list = None,
- payment_methods: list = None,
- buyer_email: str = None,
- seller_order_id: str = None,
+ created_before: str | None = None,
+ last_updated_after: str | None = None,
+ last_updated_before: str | None = None,
+ order_statuses: list | None = None,
+ marketplace_ids: list | None = None,
+ fulfillment_channels: list | None = None,
+ payment_methods: list | None = None,
+ buyer_email: str | None = None,
+ seller_order_id: str | None = None,
max_results: int = 100,
- easyship_shipment_statuses: list = None,
- next_token: str = None,
- amazon_order_ids: list = None,
- actual_fulfillment_supply_source_id: str = None,
+ easyship_shipment_statuses: list | None = None,
+ next_token: str | None = None,
+ amazon_order_ids: list | None = None,
+ actual_fulfillment_supply_source_id: str | None = None,
is_ispu: bool = False,
- store_chain_store_id: str = None,
+ store_chain_store_id: str | None = None,
) -> dict:
- """ Returns orders created or updated during the time frame indicated by the specified parameters. You can also apply a range of filtering criteria to narrow the list of orders returned. If NextToken is present, that will be used to retrieve the orders instead of other criteria. """
+ """Returns orders created or updated during the time frame indicated by the specified parameters. You can also apply a range of filtering criteria to narrow the list of orders returned. If NextToken is present, that will be used to retrieve the orders instead of other criteria."""
data = dict(
CreatedAfter=created_after,
CreatedBefore=created_before,
@@ -373,20 +373,24 @@ def get_orders(
return self.make_request(params=data)
- def get_order_items(self, order_id: str, next_token: str = None) -> dict:
- """ Returns detailed order item information for the order indicated by the specified order ID. If NextToken is provided, it's used to retrieve the next page of order items. """
+ def get_order_items(self, order_id: str, next_token: str | None = None) -> dict:
+ """Returns detailed order item information for the order indicated by the specified order ID. If NextToken is provided, it's used to retrieve the next page of order items."""
append_to_base_uri = f"/{order_id}/orderItems"
data = dict(NextToken=next_token)
return self.make_request(append_to_base_uri=append_to_base_uri, params=data)
class CatalogItems(SPAPI):
- """ Amazon Catalog Items API """
+ """Amazon Catalog Items API"""
BASE_URI = "/catalog/v0"
- def get_catalog_item(self, asin: str, marketplace_id: str = None,) -> dict:
- """ Returns a specified item and its attributes. """
+ def get_catalog_item(
+ self,
+ asin: str,
+ marketplace_id: str | None = None,
+ ) -> dict:
+ """Returns a specified item and its attributes."""
if not marketplace_id:
marketplace_id = self.marketplace_id
diff --git a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api_settings.py b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api_settings.py
index 42ed32252..bcafd5ac7 100644
--- a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api_settings.py
+++ b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/amazon_sp_api_settings.py
@@ -32,7 +32,7 @@ def validate(self):
frappe.throw(frappe._("Value for Max Retry Limit must be less than or equal to 5."))
def save(self):
- super(AmazonSPAPISettings, self).save()
+ super().save()
if not self.is_old_data_migrated:
migrate_old_data()
@@ -96,8 +96,16 @@ def validate_credentials(self):
def set_default_fields_map(self):
for field_map in [
{"amazon_field": "ASIN", "item_field": "item_code", "use_to_find_item_code": 1},
- {"amazon_field": "SellerSKU", "item_field": None, "use_to_find_item_code": 0,},
- {"amazon_field": "Title", "item_field": None, "use_to_find_item_code": 0,},
+ {
+ "amazon_field": "SellerSKU",
+ "item_field": None,
+ "use_to_find_item_code": 0,
+ },
+ {
+ "amazon_field": "Title",
+ "item_field": None,
+ "use_to_find_item_code": 0,
+ },
]:
self.append("amazon_fields_map", field_map)
@@ -124,9 +132,7 @@ def get_order_details(self):
frappe.msgprint(_("Order details will be fetched in the background."))
else:
- frappe.msgprint(
- _("Please enable the Amazon SP API Settings {0}.").format(frappe.bold(self.name))
- )
+ frappe.msgprint(_("Please enable the Amazon SP API Settings {0}.").format(frappe.bold(self.name)))
# Called via a hook in every hour.
@@ -167,9 +173,7 @@ def migrate_old_data():
if column_exists:
item = frappe.qb.DocType("Item")
- items = (frappe.qb.from_(item).select("*").where(item.amazon_item_code.notnull())).run(
- as_dict=True
- )
+ items = (frappe.qb.from_(item).select("*").where(item.amazon_item_code.notnull())).run(as_dict=True)
for item in items:
if not frappe.db.exists("Ecommerce Item", {"erpnext_item_code": item.name}):
diff --git a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/test_amazon_sp_api_settings.py b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/test_amazon_sp_api_settings.py
index 9f72487dc..e90c744bd 100644
--- a/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/test_amazon_sp_api_settings.py
+++ b/ecommerce_integrations/amazon/doctype/amazon_sp_api_settings/test_amazon_sp_api_settings.py
@@ -6,13 +6,15 @@
import os
import time
import unittest
+from typing import Any, ClassVar
-import frappe
import responses
-from frappe.exceptions import ValidationError
from requests import request
from requests.exceptions import HTTPError
+import frappe
+from frappe.exceptions import ValidationError
+
from ecommerce_integrations.amazon.doctype.amazon_sp_api_settings.amazon_repository import (
AmazonRepository,
validate_amazon_sp_api_credentials,
@@ -30,7 +32,7 @@
)
file_path = os.path.join(os.path.dirname(__file__), "test_data.json")
-with open(file_path, "r") as json_file:
+with open(file_path) as json_file:
try:
DATA = json.load(json_file)
except json.decoder.JSONDecodeError as e:
@@ -38,13 +40,16 @@
class TestSPAPI(SPAPI):
-
# Expected response after hitting the URL.
- expected_response = {}
+ expected_response: ClassVar[dict[str, Any]] = {}
@responses.activate
def make_request(
- self, method: str = "GET", append_to_base_uri: str = "", params: dict = None, data: dict = None,
+ self,
+ method: str = "GET",
+ append_to_base_uri: str = "",
+ params: dict | None = None,
+ data: dict | None = None,
) -> object:
if isinstance(params, dict):
params = Util.remove_empty(params)
@@ -78,7 +83,7 @@ def make_request(
class TestFinances(Finances, TestSPAPI):
def list_financial_events_by_order_id(
- self, order_id: str, max_results: int = None, next_token: str = None
+ self, order_id: str, max_results: int | None = None, next_token: str | None = None
) -> object:
self.expected_response = DATA.get("list_financial_events_by_order_id_200")
return super().list_financial_events_by_order_id(order_id, max_results, next_token)
@@ -88,22 +93,22 @@ class TestOrders(Orders, TestSPAPI):
def get_orders(
self,
created_after: str,
- created_before: str = None,
- last_updated_after: str = None,
- last_updated_before: str = None,
- order_statuses: list = None,
- marketplace_ids: list = None,
- fulfillment_channels: list = None,
- payment_methods: list = None,
- buyer_email: str = None,
- seller_order_id: str = None,
+ created_before: str | None = None,
+ last_updated_after: str | None = None,
+ last_updated_before: str | None = None,
+ order_statuses: list | None = None,
+ marketplace_ids: list | None = None,
+ fulfillment_channels: list | None = None,
+ payment_methods: list | None = None,
+ buyer_email: str | None = None,
+ seller_order_id: str | None = None,
max_results: int = 100,
- easyship_shipment_statuses: list = None,
- next_token: str = None,
- amazon_order_ids: list = None,
- actual_fulfillment_supply_source_id: str = None,
+ easyship_shipment_statuses: list | None = None,
+ next_token: str | None = None,
+ amazon_order_ids: list | None = None,
+ actual_fulfillment_supply_source_id: str | None = None,
is_ispu: bool = False,
- store_chain_store_id: str = None,
+ store_chain_store_id: str | None = None,
) -> object:
self.expected_response = DATA.get("get_orders_200")
return super().get_orders(
@@ -126,13 +131,17 @@ def get_orders(
store_chain_store_id,
)
- def get_order_items(self, order_id: str, next_token: str = None) -> object:
+ def get_order_items(self, order_id: str, next_token: str | None = None) -> object:
self.expected_response = DATA.get("get_order_items_200")
return super().get_order_items(order_id, next_token)
class TestCatalogItems(CatalogItems, TestSPAPI):
- def get_catalog_item(self, asin: str, marketplace_id: str = None,) -> object:
+ def get_catalog_item(
+ self,
+ asin: str,
+ marketplace_id: str | None = None,
+ ) -> object:
self.expected_response = DATA.get("get_catalog_item_200")
return super().get_catalog_item(asin, marketplace_id)
@@ -163,7 +172,11 @@ def get_company():
def get_warehouse():
warehouse_name = frappe.db.get_value(
- "Warehouse", {"warehouse_name": "Amazon Test Warehouse",}, "warehouse_name"
+ "Warehouse",
+ {
+ "warehouse_name": "Amazon Test Warehouse",
+ },
+ "warehouse_name",
)
if not warehouse_name:
@@ -181,12 +194,19 @@ def get_warehouse():
def get_item_group():
item_group_name = frappe.db.get_value(
- "Item Group", {"item_group_name": "Amazon Test Warehouse",}, "item_group_name"
+ "Item Group",
+ {
+ "item_group_name": "Amazon Test Warehouse",
+ },
+ "item_group_name",
)
if not item_group_name:
item_group = frappe.get_doc(
- {"doctype": "Item Group", "item_group_name": "Amazon Test Warehouse",}
+ {
+ "doctype": "Item Group",
+ "item_group_name": "Amazon Test Warehouse",
+ }
)
item_group.insert(ignore_permissions=True)
item_group_name = item_group.item_group_name
@@ -205,7 +225,7 @@ def get_item_group():
self.warehouse = get_warehouse()
self.parent_item_group = get_item_group()
self.price_list = "Standard Selling"
- self.customer_group = "All Customer Groups"
+ self.customer_group = "Individual"
self.territory = "All Territories"
self.customer_type = "Individual"
self.market_place_account_group = "Accounts Receivable - ATC"
@@ -235,7 +255,7 @@ def __init__(self) -> None:
def call_sp_api_method(self, sp_api_method, **kwargs):
max_retries = self.amz_setting.max_retry_limit
- for x in range(max_retries):
+ for _ in range(max_retries):
try:
result = sp_api_method(**kwargs)
return result.get("payload")
diff --git a/ecommerce_integrations/controllers/customer.py b/ecommerce_integrations/controllers/customer.py
index b6b231c4d..4c543b7ac 100644
--- a/ecommerce_integrations/controllers/customer.py
+++ b/ecommerce_integrations/controllers/customer.py
@@ -1,5 +1,3 @@
-from typing import Dict
-
import frappe
from frappe import _
from frappe.utils.nestedset import get_root_of
@@ -50,7 +48,7 @@ def get_customer_address_doc(self, address_type: str):
except frappe.DoesNotExistError:
return None
- def create_customer_address(self, address: Dict[str, str]) -> None:
+ def create_customer_address(self, address: dict[str, str]) -> None:
"""Create address from dictionary containing fields used in Address doctype of ERPNext."""
customer_doc = self.get_customer_doc()
@@ -63,7 +61,7 @@ def create_customer_address(self, address: Dict[str, str]) -> None:
}
).insert(ignore_mandatory=True)
- def create_customer_contact(self, contact: Dict[str, str]) -> None:
+ def create_customer_contact(self, contact: dict[str, str]) -> None:
"""Create contact from dictionary containing fields used in Address doctype of ERPNext."""
customer_doc = self.get_customer_doc()
diff --git a/ecommerce_integrations/controllers/inventory.py b/ecommerce_integrations/controllers/inventory.py
index 3438086f7..05918e8ee 100644
--- a/ecommerce_integrations/controllers/inventory.py
+++ b/ecommerce_integrations/controllers/inventory.py
@@ -1,12 +1,11 @@
-from typing import List, Tuple
-
import frappe
from frappe import _dict
+from frappe.query_builder.functions import Max, Sum
from frappe.utils import now
from frappe.utils.nestedset import get_descendants_of
-def get_inventory_levels(warehouses: Tuple[str], integration: str) -> List[_dict]:
+def get_inventory_levels(warehouses: tuple[str], integration: str) -> list[_dict]:
"""
Get list of dict containing items for which the inventory needs to be updated on Integeration.
@@ -16,21 +15,26 @@ def get_inventory_levels(warehouses: Tuple[str], integration: str) -> List[_dict
returns: list of _dict containing ecom_item, item_code, integration_item_code, variant_id, actual_qty, warehouse, reserved_qty
"""
- data = frappe.db.sql(
- f"""
- SELECT ei.name as ecom_item, bin.item_code as item_code, integration_item_code, variant_id, actual_qty, warehouse, reserved_qty
- FROM `tabEcommerce Item` ei
- JOIN tabBin bin
- ON ei.erpnext_item_code = bin.item_code
- WHERE bin.warehouse in ({', '.join('%s' for _ in warehouses)})
- AND bin.modified > ei.inventory_synced_on
- AND ei.integration = %s
- """,
- values=warehouses + (integration,),
- as_dict=1,
- )
-
- return data
+ bin = frappe.qb.DocType("Bin")
+ ecommerce_item = frappe.qb.DocType("Ecommerce Item")
+
+ return (
+ frappe.qb.from_(ecommerce_item)
+ .join(bin)
+ .on(ecommerce_item.erpnext_item_code == bin.item_code)
+ .select(
+ ecommerce_item.name.as_("ecom_item"),
+ bin.item_code,
+ ecommerce_item.integration_item_code,
+ ecommerce_item.variant_id,
+ bin.actual_qty,
+ bin.warehouse,
+ bin.reserved_qty,
+ )
+ .where(bin.warehouse.isin(warehouses))
+ .where(bin.modified > ecommerce_item.inventory_synced_on)
+ .where(ecommerce_item.integration == integration)
+ ).run(as_dict=True)
def get_inventory_levels_of_group_warehouse(warehouse: str, integration: str):
@@ -40,30 +44,30 @@ def get_inventory_levels_of_group_warehouse(warehouse: str, integration: str):
leaf warehouses is required"""
child_warehouse = get_descendants_of("Warehouse", warehouse)
- all_warehouses = tuple(child_warehouse) + (warehouse,)
-
- data = frappe.db.sql(
- f"""
- SELECT ei.name as ecom_item, bin.item_code as item_code,
- integration_item_code,
- variant_id,
- sum(actual_qty) as actual_qty,
- sum(reserved_qty) as reserved_qty,
- max(bin.modified) as last_updated,
- max(ei.inventory_synced_on) as last_synced
- FROM `tabEcommerce Item` ei
- JOIN tabBin bin
- ON ei.erpnext_item_code = bin.item_code
- WHERE bin.warehouse in ({', '.join(['%s'] * len(all_warehouses))})
- AND integration = %s
- GROUP BY
- ei.erpnext_item_code
- HAVING
- last_updated > last_synced
- """,
- values=all_warehouses + (integration,),
- as_dict=1,
- )
+ all_warehouses = (*tuple(child_warehouse), warehouse)
+
+ bin = frappe.qb.DocType("Bin")
+ ecommerce_item = frappe.qb.DocType("Ecommerce Item")
+
+ data = (
+ frappe.qb.from_(ecommerce_item)
+ .join(bin)
+ .on(ecommerce_item.erpnext_item_code == bin.item_code)
+ .select(
+ ecommerce_item.name.as_("ecom_item"),
+ bin.item_code,
+ ecommerce_item.integration_item_code,
+ ecommerce_item.variant_id,
+ Sum(bin.actual_qty).as_("actual_qty"),
+ Sum(bin.reserved_qty).as_("reserved_qty"),
+ Max(bin.modified).as_("last_updated"),
+ Max(ecommerce_item.inventory_synced_on).as_("last_synced"),
+ )
+ .where(bin.warehouse.isin(all_warehouses))
+ .where(ecommerce_item.integration == integration)
+ .groupby(ecommerce_item.erpnext_item_code)
+ .having(Max(bin.modified) > Max(ecommerce_item.inventory_synced_on))
+ ).run(as_dict=True)
# add warehouse as group warehouse for sending to integrations
for item in data:
diff --git a/ecommerce_integrations/controllers/scheduling.py b/ecommerce_integrations/controllers/scheduling.py
index b97b4f81e..9b59e30ae 100644
--- a/ecommerce_integrations/controllers/scheduling.py
+++ b/ecommerce_integrations/controllers/scheduling.py
@@ -16,9 +16,7 @@ def need_to_run(setting, interval_field, timestamp_field) -> bool:
interval = frappe.db.get_single_value(setting, interval_field, cache=True)
last_run = frappe.db.get_single_value(setting, timestamp_field)
- if last_run and get_datetime() < get_datetime(
- add_to_date(last_run, minutes=cint(interval, default=10))
- ):
+ if last_run and get_datetime() < get_datetime(add_to_date(last_run, minutes=cint(interval, default=10))):
return False
frappe.db.set_value(setting, None, timestamp_field, now(), update_modified=False)
diff --git a/ecommerce_integrations/controllers/setting.py b/ecommerce_integrations/controllers/setting.py
index 7b48c66b7..8f6e0da29 100644
--- a/ecommerce_integrations/controllers/setting.py
+++ b/ecommerce_integrations/controllers/setting.py
@@ -1,4 +1,4 @@
-from typing import Dict, List, NewType
+from typing import NewType
from frappe.model.document import Document
@@ -11,11 +11,11 @@ def is_enabled(self) -> bool:
"""Check if integration is enabled or not."""
raise NotImplementedError()
- def get_erpnext_warehouses(self) -> List[ERPNextWarehouse]:
+ def get_erpnext_warehouses(self) -> list[ERPNextWarehouse]:
raise NotImplementedError()
- def get_erpnext_to_integration_wh_mapping(self) -> Dict[ERPNextWarehouse, IntegrationWarehouse]:
+ def get_erpnext_to_integration_wh_mapping(self) -> dict[ERPNextWarehouse, IntegrationWarehouse]:
raise NotImplementedError()
- def get_integration_to_erpnext_wh_mapping(self) -> Dict[IntegrationWarehouse, ERPNextWarehouse]:
+ def get_integration_to_erpnext_wh_mapping(self) -> dict[IntegrationWarehouse, ERPNextWarehouse]:
raise NotImplementedError()
diff --git a/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_integration_log/ecommerce_integration_log.py b/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_integration_log/ecommerce_integration_log.py
index 4a9f67231..c5ed9f380 100644
--- a/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_integration_log/ecommerce_integration_log.py
+++ b/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_integration_log/ecommerce_integration_log.py
@@ -33,7 +33,7 @@ def _set_title(self):
def clear_old_logs(days=90):
table = frappe.qb.DocType("Ecommerce Integration Log")
frappe.db.delete(
- table, filters=((table.modified < (Now() - Interval(days=days)))) & (table.status == "Success")
+ table, filters=(table.modified < (Now() - Interval(days=days))) & (table.status == "Success")
)
diff --git a/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_item/ecommerce_item.py b/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_item/ecommerce_item.py
index 423a26829..b3b4ebfa9 100644
--- a/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_item/ecommerce_item.py
+++ b/ecommerce_integrations/ecommerce_integrations/doctype/ecommerce_item/ecommerce_item.py
@@ -1,14 +1,13 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see LICENSE
-from typing import Dict, Optional
-
import frappe
-from erpnext import get_default_company
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, now
+from erpnext import get_default_company
+
class EcommerceItem(Document):
erpnext_item_code: str # item_code in ERPNext
@@ -56,8 +55,8 @@ def set_defaults(self):
def is_synced(
integration: str,
integration_item_code: str,
- variant_id: Optional[str] = None,
- sku: Optional[str] = None,
+ variant_id: str | None = None,
+ sku: str | None = None,
) -> bool:
"""Check if item is synced from integration.
@@ -86,9 +85,9 @@ def _is_sku_synced(integration: str, sku: str) -> bool:
def get_erpnext_item_code(
integration: str,
integration_item_code: str,
- variant_id: Optional[str] = None,
- has_variants: Optional[int] = 0,
-) -> Optional[str]:
+ variant_id: str | None = None,
+ has_variants: int | None = 0,
+) -> str | None:
filters = {"integration": integration, "integration_item_code": integration_item_code}
if variant_id:
filters.update({"variant_id": variant_id})
@@ -101,9 +100,9 @@ def get_erpnext_item_code(
def get_erpnext_item(
integration: str,
integration_item_code: str,
- variant_id: Optional[str] = None,
- sku: Optional[str] = None,
- has_variants: Optional[int] = 0,
+ variant_id: str | None = None,
+ sku: str | None = None,
+ has_variants: int | None = 0,
):
"""Get ERPNext item for specified ecommerce_item.
@@ -127,10 +126,10 @@ def get_erpnext_item(
def create_ecommerce_item(
integration: str,
integration_item_code: str,
- item_dict: Dict,
- variant_id: Optional[str] = None,
- sku: Optional[str] = None,
- variant_of: Optional[str] = None,
+ item_dict: dict,
+ variant_id: str | None = None,
+ sku: str | None = None,
+ variant_of: str | None = None,
has_variants=0,
) -> None:
"""Create Item in erpnext and link it with Ecommerce item doctype.
diff --git a/ecommerce_integrations/hooks.py b/ecommerce_integrations/hooks.py
index face21c3f..c2ebc85c2 100644
--- a/ecommerce_integrations/hooks.py
+++ b/ecommerce_integrations/hooks.py
@@ -137,9 +137,7 @@
scheduler_events = {
"all": ["ecommerce_integrations.shopify.inventory.update_inventory_on_shopify"],
"daily": [],
- "daily_long": [
- "ecommerce_integrations.zenoti.doctype.zenoti_settings.zenoti_settings.sync_stocks"
- ],
+ "daily_long": ["ecommerce_integrations.zenoti.doctype.zenoti_settings.zenoti_settings.sync_stocks"],
"hourly": [
"ecommerce_integrations.shopify.order.sync_old_orders",
"ecommerce_integrations.amazon.doctype.amazon_sp_api_settings.amazon_sp_api_settings.schedule_get_order_details",
diff --git a/ecommerce_integrations/patches/set_default_amazon_item_fields_map.py b/ecommerce_integrations/patches/set_default_amazon_item_fields_map.py
index 9bf161bcf..5fcebd20d 100644
--- a/ecommerce_integrations/patches/set_default_amazon_item_fields_map.py
+++ b/ecommerce_integrations/patches/set_default_amazon_item_fields_map.py
@@ -6,8 +6,16 @@ def execute():
default_fields_map = [
{"amazon_field": "ASIN", "item_field": "item_code", "use_to_find_item_code": 1},
- {"amazon_field": "SellerSKU", "item_field": None, "use_to_find_item_code": 0,},
- {"amazon_field": "Title", "item_field": None, "use_to_find_item_code": 0,},
+ {
+ "amazon_field": "SellerSKU",
+ "item_field": None,
+ "use_to_find_item_code": 0,
+ },
+ {
+ "amazon_field": "Title",
+ "item_field": None,
+ "use_to_find_item_code": 0,
+ },
]
amz_settings = frappe.db.get_all("Amazon SP API Settings", pluck="name")
diff --git a/ecommerce_integrations/shopify/connection.py b/ecommerce_integrations/shopify/connection.py
index 003b51ee1..9805443d9 100644
--- a/ecommerce_integrations/shopify/connection.py
+++ b/ecommerce_integrations/shopify/connection.py
@@ -3,13 +3,13 @@
import hashlib
import hmac
import json
-from typing import List
-import frappe
-from frappe import _
from shopify.resources import Webhook
from shopify.session import Session
+import frappe
+from frappe import _
+
from ecommerce_integrations.shopify.constants import (
API_VERSION,
EVENT_MAPPER,
@@ -24,7 +24,6 @@ def temp_shopify_session(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
-
# no auth in testing
if frappe.flags.in_test:
return func(*args, **kwargs)
@@ -39,7 +38,7 @@ def wrapper(*args, **kwargs):
return wrapper
-def register_webhooks(shopify_url: str, password: str) -> List[Webhook]:
+def register_webhooks(shopify_url: str, password: str) -> list[Webhook]:
"""Register required webhooks with shopify and return registered webhooks."""
new_webhooks = []
@@ -54,7 +53,9 @@ def register_webhooks(shopify_url: str, password: str) -> List[Webhook]:
new_webhooks.append(webhook)
else:
create_shopify_log(
- status="Error", response_data=webhook.to_dict(), exception=webhook.errors.full_messages(),
+ status="Error",
+ response_data=webhook.to_dict(),
+ exception=webhook.errors.full_messages(),
)
return new_webhooks
@@ -65,7 +66,6 @@ def unregister_webhooks(shopify_url: str, password: str) -> None:
url = get_current_domain_name()
with Session.temp(shopify_url, API_VERSION, password):
-
for webhook in Webhook.find():
if url in webhook.address:
webhook.destroy()
@@ -106,7 +106,6 @@ def store_request_data() -> None:
def process_request(data, event):
-
# create log
log = create_shopify_log(method=EVENT_MAPPER[event], request_data=data)
diff --git a/ecommerce_integrations/shopify/customer.py b/ecommerce_integrations/shopify/customer.py
index e4b4b9bc2..3a0ee952f 100644
--- a/ecommerce_integrations/shopify/customer.py
+++ b/ecommerce_integrations/shopify/customer.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, Optional
+from typing import Any
import frappe
from frappe import _
@@ -18,7 +18,7 @@ def __init__(self, customer_id: str):
self.setting = frappe.get_doc(SETTING_DOCTYPE)
super().__init__(customer_id, CUSTOMER_ID_FIELD, MODULE_NAME)
- def sync_customer(self, customer: Dict[str, Any]) -> None:
+ def sync_customer(self, customer: dict[str, Any]) -> None:
"""Create Customer in ERPNext using shopify's Customer dict."""
customer_name = cstr(customer.get("first_name")) + " " + cstr(customer.get("last_name"))
@@ -45,9 +45,9 @@ def sync_customer(self, customer: Dict[str, Any]) -> None:
def create_customer_address(
self,
customer_name,
- shopify_address: Dict[str, Any],
+ shopify_address: dict[str, Any],
address_type: str = "Billing",
- email: Optional[str] = None,
+ email: str | None = None,
) -> None:
"""Create customer address(es) using Customer dict provided by shopify."""
address_fields = _map_address_fields(shopify_address, customer_name, address_type, email)
@@ -68,9 +68,9 @@ def update_existing_addresses(self, customer):
def _update_existing_address(
self,
customer_name,
- shopify_address: Dict[str, Any],
+ shopify_address: dict[str, Any],
address_type: str = "Billing",
- email: Optional[str] = None,
+ email: str | None = None,
) -> None:
old_address = self.get_customer_address_doc(address_type)
@@ -84,8 +84,7 @@ def _update_existing_address(
old_address.flags.ignore_mandatory = True
old_address.save()
- def create_customer_contact(self, shopify_customer: Dict[str, Any]) -> None:
-
+ def create_customer_contact(self, shopify_customer: dict[str, Any]) -> None:
if not (shopify_customer.get("first_name") and shopify_customer.get("email")):
return
@@ -99,9 +98,7 @@ def create_customer_contact(self, shopify_customer: Dict[str, Any]) -> None:
if shopify_customer.get("email"):
contact_fields["email_ids"] = [{"email_id": shopify_customer.get("email"), "is_primary": True}]
- phone_no = shopify_customer.get("phone") or shopify_customer.get("default_address", {}).get(
- "phone"
- )
+ phone_no = shopify_customer.get("phone") or shopify_customer.get("default_address", {}).get("phone")
if validate_phone_number(phone_no, throw=False):
contact_fields["phone_nos"] = [{"phone": phone_no, "is_primary_phone": True}]
@@ -110,7 +107,7 @@ def create_customer_contact(self, shopify_customer: Dict[str, Any]) -> None:
def _map_address_fields(shopify_address, customer_name, address_type, email):
- """ returns dict with shopify address fields mapped to equivalent ERPNext fields"""
+ """returns dict with shopify address fields mapped to equivalent ERPNext fields"""
address_fields = {
"address_title": customer_name,
"address_type": address_type,
diff --git a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.py b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.py
index 2fd82ba94..019c8ea61 100644
--- a/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.py
+++ b/ecommerce_integrations/shopify/doctype/shopify_setting/shopify_setting.py
@@ -1,14 +1,13 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see LICENSE
-from typing import Dict, List
+from shopify.collection import PaginatedIterator
+from shopify.resources import Location
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import get_datetime
-from shopify.collection import PaginatedIterator
-from shopify.resources import Location
from ecommerce_integrations.controllers.setting import (
ERPNextWarehouse,
@@ -94,19 +93,17 @@ def update_location_table(self):
{"shopify_location_id": location.id, "shopify_location_name": location.name},
)
- def get_erpnext_warehouses(self) -> List[ERPNextWarehouse]:
+ def get_erpnext_warehouses(self) -> list[ERPNextWarehouse]:
return [wh_map.erpnext_warehouse for wh_map in self.shopify_warehouse_mapping]
- def get_erpnext_to_integration_wh_mapping(self) -> Dict[ERPNextWarehouse, IntegrationWarehouse]:
+ def get_erpnext_to_integration_wh_mapping(self) -> dict[ERPNextWarehouse, IntegrationWarehouse]:
return {
- wh_map.erpnext_warehouse: wh_map.shopify_location_id
- for wh_map in self.shopify_warehouse_mapping
+ wh_map.erpnext_warehouse: wh_map.shopify_location_id for wh_map in self.shopify_warehouse_mapping
}
- def get_integration_to_erpnext_wh_mapping(self) -> Dict[IntegrationWarehouse, ERPNextWarehouse]:
+ def get_integration_to_erpnext_wh_mapping(self) -> dict[IntegrationWarehouse, ERPNextWarehouse]:
return {
- wh_map.shopify_location_id: wh_map.erpnext_warehouse
- for wh_map in self.shopify_warehouse_mapping
+ wh_map.shopify_location_id: wh_map.erpnext_warehouse for wh_map in self.shopify_warehouse_mapping
}
diff --git a/ecommerce_integrations/shopify/doctype/shopify_setting/test_shopify_setting.py b/ecommerce_integrations/shopify/doctype/shopify_setting/test_shopify_setting.py
index 5277352cf..ab05370b9 100644
--- a/ecommerce_integrations/shopify/doctype/shopify_setting/test_shopify_setting.py
+++ b/ecommerce_integrations/shopify/doctype/shopify_setting/test_shopify_setting.py
@@ -29,7 +29,6 @@ def setUpClass(cls):
)
def test_custom_field_creation(self):
-
setup_custom_fields()
created_fields = frappe.get_all(
diff --git a/ecommerce_integrations/shopify/fulfillment.py b/ecommerce_integrations/shopify/fulfillment.py
index 97a6706eb..b61d661fa 100644
--- a/ecommerce_integrations/shopify/fulfillment.py
+++ b/ecommerce_integrations/shopify/fulfillment.py
@@ -1,9 +1,10 @@
from copy import deepcopy
import frappe
-from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from frappe.utils import cint, cstr, getdate
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+
from ecommerce_integrations.shopify.constants import (
FULLFILLMENT_ID_FIELD,
ORDER_ID_FIELD,
@@ -41,7 +42,6 @@ def create_delivery_note(shopify_order, setting, so):
not frappe.db.get_value("Delivery Note", {FULLFILLMENT_ID_FIELD: fulfillment.get("id")}, "name")
and so.docstatus == 1
):
-
dn = make_delivery_note(so.name)
setattr(dn, ORDER_ID_FIELD, fulfillment.get("order_id"))
setattr(dn, ORDER_NUMBER_FIELD, shopify_order.get("name"))
@@ -82,8 +82,6 @@ def find_matching_fullfilement_item(dn_item):
for dn_item in dn_items:
if shopify_item := find_matching_fullfilement_item(dn_item):
- final_items.append(
- dn_item.update({"qty": shopify_item.get("quantity"), "warehouse": warehouse})
- )
+ final_items.append(dn_item.update({"qty": shopify_item.get("quantity"), "warehouse": warehouse}))
return final_items
diff --git a/ecommerce_integrations/shopify/inventory.py b/ecommerce_integrations/shopify/inventory.py
index 526107dd3..d27683fd7 100644
--- a/ecommerce_integrations/shopify/inventory.py
+++ b/ecommerce_integrations/shopify/inventory.py
@@ -1,10 +1,11 @@
from collections import Counter
-import frappe
-from frappe.utils import cint, create_batch, now
from pyactiveresource.connection import ResourceNotFound
from shopify.resources import InventoryLevel, Variant
+import frappe
+from frappe.utils import cint, create_batch, now
+
from ecommerce_integrations.controllers.inventory import (
get_inventory_levels,
update_inventory_sync_status,
diff --git a/ecommerce_integrations/shopify/invoice.py b/ecommerce_integrations/shopify/invoice.py
index 26afb8258..1fffd6a08 100644
--- a/ecommerce_integrations/shopify/invoice.py
+++ b/ecommerce_integrations/shopify/invoice.py
@@ -1,7 +1,8 @@
import frappe
-from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from frappe.utils import cint, cstr, getdate, nowdate
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
from ecommerce_integrations.shopify.constants import (
ORDER_ID_FIELD,
ORDER_NUMBER_FIELD,
@@ -37,7 +38,6 @@ def create_sales_invoice(shopify_order, setting, so):
and not so.per_billed
and cint(setting.sync_sales_invoice)
):
-
posting_date = getdate(shopify_order.get("created_at")) or nowdate()
sales_invoice = make_sales_invoice(so.name, ignore_permissions=True)
diff --git a/ecommerce_integrations/shopify/order.py b/ecommerce_integrations/shopify/order.py
index 431c431cf..88978522c 100644
--- a/ecommerce_integrations/shopify/order.py
+++ b/ecommerce_integrations/shopify/order.py
@@ -1,11 +1,12 @@
import json
-from typing import Literal, Optional
+from typing import Literal
+
+from shopify.collection import PaginatedIterator
+from shopify.resources import Order
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt, get_datetime, getdate, nowdate
-from shopify.collection import PaginatedIterator
-from shopify.resources import Order
from ecommerce_integrations.shopify.connection import temp_shopify_session
from ecommerce_integrations.shopify.constants import (
@@ -172,7 +173,6 @@ def get_order_items(order_items, setting, delivery_date, taxes_inclusive):
def _get_item_price(line_item, taxes_inclusive: bool) -> float:
-
price = flt(line_item.get("price"))
qty = cint(line_item.get("quantity"))
@@ -206,7 +206,8 @@ def get_order_taxes(shopify_order, setting, items):
"charge_type": "Actual",
"account_head": get_tax_account_head(tax, charge_type="sales_tax"),
"description": (
- get_tax_account_description(tax) or f"{tax.get('title')} - {tax.get('rate') * 100.0:.2f}%"
+ get_tax_account_description(tax)
+ or f"{tax.get('title')} - {tax.get('rate') * 100.0:.2f}%"
),
"tax_amount": tax.get("price"),
"included_in_print_rate": 0,
@@ -259,11 +260,13 @@ def consolidate_order_taxes(taxes):
return tax_account_wise_data.values()
-def get_tax_account_head(tax, charge_type: Optional[Literal["shipping", "sales_tax"]] = None):
+def get_tax_account_head(tax, charge_type: Literal["shipping", "sales_tax"] | None = None):
tax_title = str(tax.get("title"))
tax_account = frappe.db.get_value(
- "Shopify Tax Account", {"parent": SETTING_DOCTYPE, "shopify_tax": tax_title}, "tax_account",
+ "Shopify Tax Account",
+ {"parent": SETTING_DOCTYPE, "shopify_tax": tax_title},
+ "tax_account",
)
if not tax_account and charge_type:
@@ -279,7 +282,9 @@ def get_tax_account_description(tax):
tax_title = tax.get("title")
tax_description = frappe.db.get_value(
- "Shopify Tax Account", {"parent": SETTING_DOCTYPE, "shopify_tax": tax_title}, "tax_description",
+ "Shopify Tax Account",
+ {"parent": SETTING_DOCTYPE, "shopify_tax": tax_title},
+ "tax_description",
)
return tax_description
@@ -317,7 +322,8 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, setting, items, taxe
{
"charge_type": "Actual",
"account_head": get_tax_account_head(shipping_charge, charge_type="shipping"),
- "description": get_tax_account_description(shipping_charge) or shipping_charge["title"],
+ "description": get_tax_account_description(shipping_charge)
+ or shipping_charge["title"],
"tax_amount": shipping_charge_amount,
"cost_center": setting.cost_center,
}
@@ -329,7 +335,8 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, setting, items, taxe
"charge_type": "Actual",
"account_head": get_tax_account_head(tax, charge_type="sales_tax"),
"description": (
- get_tax_account_description(tax) or f"{tax.get('title')} - {tax.get('rate') * 100.0:.2f}%"
+ get_tax_account_description(tax)
+ or f"{tax.get('title')} - {tax.get('rate') * 100.0:.2f}%"
),
"tax_amount": tax["price"],
"cost_center": setting.cost_center,
diff --git a/ecommerce_integrations/shopify/page/shopify_import_products/shopify_import_products.py b/ecommerce_integrations/shopify/page/shopify_import_products/shopify_import_products.py
index ad2f94f3a..8216a539b 100644
--- a/ecommerce_integrations/shopify/page/shopify_import_products/shopify_import_products.py
+++ b/ecommerce_integrations/shopify/page/shopify_import_products/shopify_import_products.py
@@ -1,8 +1,9 @@
from time import process_time
+from shopify.resources import Product
+
import frappe
from frappe.exceptions import UniqueValidationError
-from shopify.resources import Product
from ecommerce_integrations.ecommerce_integrations.doctype.ecommerce_item import ecommerce_item
from ecommerce_integrations.shopify.connection import temp_shopify_session
@@ -119,7 +120,10 @@ def is_synced(product):
@frappe.whitelist()
def import_all_products():
frappe.enqueue(
- queue_sync_all_products, queue="long", job_name=SYNC_JOB_NAME, key=REALTIME_KEY,
+ queue_sync_all_products,
+ queue="long",
+ job_name=SYNC_JOB_NAME,
+ key=REALTIME_KEY,
)
@@ -150,12 +154,12 @@ def queue_sync_all_products(*args, **kwargs):
publish(f"✅ Synced Product {product.id}", synced=True)
except UniqueValidationError as e:
- publish(f"❌ Error Syncing Product {product.id} : {str(e)}", error=True)
+ publish(f"❌ Error Syncing Product {product.id} : {e!s}", error=True)
frappe.db.rollback(save_point=savepoint)
continue
except Exception as e:
- publish(f"❌ Error Syncing Product {product.id} : {str(e)}", error=True)
+ publish(f"❌ Error Syncing Product {product.id} : {e!s}", error=True)
frappe.db.rollback(save_point=savepoint)
continue
diff --git a/ecommerce_integrations/shopify/page/shopify_import_products/test_shopify_import_products.py b/ecommerce_integrations/shopify/page/shopify_import_products/test_shopify_import_products.py
index 0f03997e2..6619ac244 100644
--- a/ecommerce_integrations/shopify/page/shopify_import_products/test_shopify_import_products.py
+++ b/ecommerce_integrations/shopify/page/shopify_import_products/test_shopify_import_products.py
@@ -1,9 +1,10 @@
import json
import os
-import frappe
import shopify
+import frappe
+
from ecommerce_integrations.shopify.product import ShopifyProduct
from ...tests.utils import TestCase
@@ -12,16 +13,13 @@
class TestShopifyImportProducts(TestCase):
def __init__(self, obj):
- with open(
- os.path.join(os.path.dirname(__file__), "../../tests/data/bulk_products.json"), "rb"
- ) as f:
+ with open(os.path.join(os.path.dirname(__file__), "../../tests/data/bulk_products.json"), "rb") as f:
products_json = json.loads(f.read())
self._products = products_json["products"]
- super(TestShopifyImportProducts, self).__init__(obj)
+ super().__init__(obj)
def test_import_all_products(self):
-
required_products = {
"6808908169263": [
"40279118250031",
@@ -31,7 +29,12 @@ def test_import_all_products(self):
"40279118381103",
"40279118413871",
],
- "6808928124975": ["40279218028591", "40279218061359", "40279218094127", "40279218126895",],
+ "6808928124975": [
+ "40279218028591",
+ "40279218061359",
+ "40279218094127",
+ "40279218126895",
+ ],
"6808887689263": ["40279042883631", "40279042916399", "40279042949167"],
"6808908955695": ["40279122673711", "40279122706479", "40279122739247"],
"6808917737519": ["40279168221231", "40279168253999", "40279168286767"],
@@ -64,7 +67,6 @@ def test_import_all_products(self):
queue_sync_all_products()
for product, required_variants in required_products.items():
-
# has_variants is needed to avoid get_erpnext_item()
# fetching the variant instead of template because of
# matching integration_item_code
@@ -95,7 +97,7 @@ def test_import_all_products(self):
self.assertEqual(sorted(required_variants), sorted(created_ecom_variants))
def fake_single_product_from_bulk(self, product):
- item = [p for p in self._products if str(p["id"]) == product][0]
+ item = next(p for p in self._products if str(p["id"]) == product)
product_json = json.dumps({"product": item})
diff --git a/ecommerce_integrations/shopify/product.py b/ecommerce_integrations/shopify/product.py
index ffae32049..9f51c1f1d 100644
--- a/ecommerce_integrations/shopify/product.py
+++ b/ecommerce_integrations/shopify/product.py
@@ -1,10 +1,9 @@
-from typing import Optional
+from shopify.resources import Product, Variant
import frappe
from frappe import _, msgprint
from frappe.utils import cint, cstr
from frappe.utils.nestedset import get_root_of
-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
@@ -23,9 +22,9 @@ class ShopifyProduct:
def __init__(
self,
product_id: str,
- variant_id: Optional[str] = None,
- sku: Optional[str] = None,
- has_variants: Optional[int] = 0,
+ variant_id: str | None = None,
+ sku: str | None = None,
+ has_variants: int | None = 0,
):
self.product_id = str(product_id)
self.variant_id = str(variant_id) if variant_id else None
@@ -38,7 +37,10 @@ def __init__(
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,
+ MODULE_NAME,
+ integration_item_code=self.product_id,
+ variant_id=self.variant_id,
+ sku=self.sku,
)
def get_erpnext_item(self):
@@ -81,7 +83,8 @@ def _create_attribute(self, product_dict):
"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")
+ {"attribute_value": attr_value, "abbr": attr_value}
+ for attr_value in attr.get("values")
],
}
).insert()
@@ -175,7 +178,11 @@ def _create_item_variants(self, product_dict, warehouse, attributes):
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])}
+ {
+ "attribute_value": self._get_attribute_value(
+ variant.get(variant_attr), attributes[i]
+ )
+ }
)
self._create_item(shopify_item_variant, warehouse, 0, attributes, template_item.name)
@@ -263,9 +270,7 @@ def _get_item_image(product_dict):
return None
-def _match_sku_and_link_item(
- item_dict, product_id, variant_id, variant_of=None, has_variant=False
-) -> bool:
+def _match_sku_and_link_item(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.
@@ -298,7 +303,6 @@ def _match_sku_and_link_item(
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")
@@ -401,7 +405,9 @@ def upload_erpnext_item(doc, method=None):
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))
+ frappe.throw(
+ _("Shopify Error: Missing value for attribute {}").format(attr.attribute)
+ )
product.variants.append(Variant(variant_attributes))
product.save() # push variant
@@ -429,7 +435,9 @@ def upload_erpnext_item(doc, method=None):
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)
+ 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)}
@@ -448,7 +456,9 @@ def upload_erpnext_item(doc, method=None):
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))
+ frappe.throw(
+ _("Shopify Error: Missing value for attribute {}").format(attr.attribute)
+ )
product.variants.append(Variant(variant_attributes))
is_successful = product.save()
@@ -458,9 +468,7 @@ def upload_erpnext_item(doc, method=None):
write_upload_log(status=is_successful, product=product, item=item, action="Updated")
-def map_erpnext_variant_to_shopify_variant(
- shopify_product: Product, erpnext_item, variant_attributes
-):
+def map_erpnext_variant_to_shopify_variant(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},
@@ -520,8 +528,8 @@ def get_shopify_weight_uom(erpnext_weight_uom: str) -> str:
def update_default_variant_properties(
shopify_product: Product,
is_stock_item: bool,
- sku: Optional[str] = None,
- price: Optional[float] = None,
+ sku: str | None = None,
+ price: float | None = None,
):
"""Shopify creates default variant upon saving the product.
@@ -547,7 +555,10 @@ def write_upload_log(status: bool, product: Product, item, action="Created") ->
msgprint(msg, title="Note", indicator="orange")
create_shopify_log(
- status="Error", request_data=product.to_dict(), message=msg, method="upload_erpnext_item",
+ status="Error",
+ request_data=product.to_dict(),
+ message=msg,
+ method="upload_erpnext_item",
)
else:
create_shopify_log(
diff --git a/ecommerce_integrations/shopify/tests/test_connection.py b/ecommerce_integrations/shopify/tests/test_connection.py
index 697b88066..640256bf7 100644
--- a/ecommerce_integrations/shopify/tests/test_connection.py
+++ b/ecommerce_integrations/shopify/tests/test_connection.py
@@ -3,10 +3,11 @@
import unittest
-import frappe
from shopify.resources import Webhook
from shopify.session import Session
+import frappe
+
from ecommerce_integrations.shopify import connection
from ecommerce_integrations.shopify.constants import API_VERSION, SETTING_DOCTYPE
@@ -18,7 +19,6 @@ def setUpClass(cls):
@unittest.skip("Can't run these tests in CI")
def test_register_webhooks(self):
-
webhooks = connection.register_webhooks(
self.setting.shopify_url, self.setting.get_password("password")
)
@@ -30,7 +30,6 @@ def test_register_webhooks(self):
@unittest.skip("Can't run these tests in CI")
def test_unregister_webhooks(self):
-
connection.unregister_webhooks(self.setting.shopify_url, self.setting.get_password("password"))
callback_url = connection.get_callback_url()
diff --git a/ecommerce_integrations/shopify/tests/test_product.py b/ecommerce_integrations/shopify/tests/test_product.py
index 31758ad7a..78d04d240 100644
--- a/ecommerce_integrations/shopify/tests/test_product.py
+++ b/ecommerce_integrations/shopify/tests/test_product.py
@@ -165,7 +165,10 @@ def make_item(item_code=None, properties=None):
"item_name": item_code,
"description": item_code,
"item_group": "Products",
- "attributes": [{"attribute": "Test Sync Size"}, {"attribute": "Test Sync Colour"},],
+ "attributes": [
+ {"attribute": "Test Sync Size"},
+ {"attribute": "Test Sync Colour"},
+ ],
"has_variants": 1,
}
)
diff --git a/ecommerce_integrations/shopify/tests/utils.py b/ecommerce_integrations/shopify/tests/utils.py
index fd4f075ca..ee70d9cc6 100644
--- a/ecommerce_integrations/shopify/tests/utils.py
+++ b/ecommerce_integrations/shopify/tests/utils.py
@@ -3,12 +3,14 @@
import unittest
from unittest.mock import patch
-import frappe
import shopify
-from erpnext import get_default_cost_center
from pyactiveresource.activeresource import ActiveResource
from pyactiveresource.testing import http_fake
+import frappe
+
+from erpnext import get_default_cost_center
+
from ecommerce_integrations.shopify.constants import API_VERSION, SETTING_DOCTYPE
# Following code is adapted from Shopify python api under MIT license with minor changes.
@@ -95,7 +97,7 @@ def setUp(self):
self.http.site = "https://frappetest.myshopify.com"
def load_fixture(self, name, format="json"):
- with open(os.path.dirname(__file__) + "/data/%s.%s" % (name, format), "rb") as f:
+ with open(os.path.dirname(__file__) + f"/data/{name}.{format}", "rb") as f:
return f.read()
def fake(self, endpoint, **kwargs):
@@ -106,9 +108,9 @@ def fake(self, endpoint, **kwargs):
if "extension" in kwargs and not kwargs["extension"]:
extension = ""
else:
- extension = ".%s" % (kwargs.pop("extension", "json"))
+ extension = f".{kwargs.pop('extension', 'json')}"
- url = "https://frappetest.myshopify.com%s/%s%s" % (prefix, endpoint, extension)
+ url = f"https://frappetest.myshopify.com{prefix}/{endpoint}{extension}"
try:
url = kwargs["url"]
except KeyError:
@@ -116,7 +118,7 @@ def fake(self, endpoint, **kwargs):
headers = {}
if kwargs.pop("has_user_agent", True):
- userAgent = "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0])
+ userAgent = f"ShopifyPythonAPI/{shopify.VERSION} Python/{sys.version.split(' ', 1)[0]}"
headers["User-agent"] = userAgent
try:
diff --git a/ecommerce_integrations/shopify/utils.py b/ecommerce_integrations/shopify/utils.py
index d1d55c00f..06bf1f582 100644
--- a/ecommerce_integrations/shopify/utils.py
+++ b/ecommerce_integrations/shopify/utils.py
@@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see LICENSE
-from typing import List
import frappe
from frappe import _, _dict
@@ -26,11 +25,15 @@ def migrate_from_old_connector(payload=None, request_id=None):
log = frappe.get_doc("Ecommerce Integration Log", request_id)
else:
log = create_shopify_log(
- status="Queued", method="ecommerce_integrations.shopify.utils.migrate_from_old_connector",
+ status="Queued",
+ method="ecommerce_integrations.shopify.utils.migrate_from_old_connector",
)
frappe.enqueue(
- method=_migrate_items_to_ecommerce_item, queue="long", is_async=True, log=log,
+ method=_migrate_items_to_ecommerce_item,
+ queue="long",
+ is_async=True,
+ log=log,
)
@@ -48,7 +51,6 @@ def ensure_old_connector_is_disabled():
def _migrate_items_to_ecommerce_item(log):
-
shopify_fields = ["shopify_product_id", "shopify_variant_id"]
for field in shopify_fields:
@@ -70,7 +72,7 @@ def _migrate_items_to_ecommerce_item(log):
log.save()
-def _get_items_to_migrate() -> List[_dict]:
+def _get_items_to_migrate() -> list[_dict]:
"""get all list of items that have shopify fields but do not have associated ecommerce item."""
old_data = frappe.db.sql(
@@ -84,7 +86,7 @@ def _get_items_to_migrate() -> List[_dict]:
return old_data or []
-def _create_ecommerce_items(items: List[_dict]) -> None:
+def _create_ecommerce_items(items: list[_dict]) -> None:
for item in items:
if not all((item.erpnext_item_code, item.shopify_product_id, item.shopify_variant_id)):
continue
diff --git a/ecommerce_integrations/unicommerce/api_client.py b/ecommerce_integrations/unicommerce/api_client.py
index 1b3581eb1..82febe6bf 100644
--- a/ecommerce_integrations/unicommerce/api_client.py
+++ b/ecommerce_integrations/unicommerce/api_client.py
@@ -1,17 +1,16 @@
import base64
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any
-import frappe
import requests
-from frappe import _
-from frappe.utils import cint, cstr, get_datetime
from pytz import timezone
+import frappe
+from frappe import _, _dict
+from frappe.utils import cint, cstr, get_datetime
+
from ecommerce_integrations.unicommerce.constants import SETTINGS_DOCTYPE
from ecommerce_integrations.unicommerce.utils import create_unicommerce_log
-JsonDict = Dict[str, Any]
-
class UnicommerceAPIClient:
"""Wrapper around Unicommerce REST API
@@ -20,7 +19,9 @@ class UnicommerceAPIClient:
"""
def __init__(
- self, url: Optional[str] = None, access_token: Optional[str] = None,
+ self,
+ url: str | None = None,
+ access_token: str | None = None,
):
self.settings = frappe.get_doc(SETTINGS_DOCTYPE)
self.base_url = url or f"https://{self.settings.unicommerce_site}"
@@ -39,13 +40,12 @@ def request(
self,
endpoint: str,
method: str = "POST",
- headers: Optional[JsonDict] = None,
- body: Optional[JsonDict] = None,
- params: Optional[JsonDict] = None,
- files: Optional[JsonDict] = None,
+ headers: dict[str, Any] | None = None,
+ body: dict[str, Any] | None = None,
+ params: dict[str, Any] | None = None,
+ files: dict[str, Any] | None = None,
log_error=True,
- ) -> Tuple[JsonDict, bool]:
-
+ ) -> tuple[_dict | bytes | None, bool]:
if headers is None:
headers = {}
@@ -83,7 +83,7 @@ def request(
return data, status
- def get_unicommerce_item(self, sku: str, log_error=True) -> Optional[JsonDict]:
+ def get_unicommerce_item(self, sku: str, log_error=True) -> _dict | None:
"""Get Unicommerce item data for specified SKU code.
ref: https://documentation.unicommerce.com/docs/itemtype-get.html
@@ -94,7 +94,9 @@ def get_unicommerce_item(self, sku: str, log_error=True) -> Optional[JsonDict]:
if status:
return item
- def create_update_item(self, item_dict: JsonDict, update=False) -> Tuple[JsonDict, bool]:
+ def create_update_item(
+ self, item_dict: dict[str, Any], update=False
+ ) -> tuple[_dict | bytes | None, bool]:
"""Create/update item on unicommerce.
ref: https://documentation.unicommerce.com/docs/createoredit-itemtype.html
@@ -106,7 +108,7 @@ def create_update_item(self, item_dict: JsonDict, update=False) -> Tuple[JsonDic
endpoint = "/services/rest/v1/catalog/itemType/edit"
return self.request(endpoint=endpoint, body={"itemType": item_dict})
- def get_sales_order(self, order_code: str) -> Optional[JsonDict]:
+ def get_sales_order(self, order_code: str) -> _dict | None:
"""Get details for a sales order.
ref: https://documentation.unicommerce.com/docs/saleorder-get.html
@@ -120,13 +122,13 @@ def get_sales_order(self, order_code: str) -> Optional[JsonDict]:
def search_sales_order(
self,
- from_date: Optional[str] = None,
- to_date: Optional[str] = None,
- status: Optional[str] = None,
- channel: Optional[str] = None,
- facility_codes: Optional[List[str]] = None,
- updated_since: Optional[int] = None,
- ) -> Optional[List[JsonDict]]:
+ from_date: str | None = None,
+ to_date: str | None = None,
+ status: str | None = None,
+ channel: str | None = None,
+ facility_codes: list[str] | None = None,
+ updated_since: int | None = None,
+ ) -> list[dict[str, Any]] | None:
"""Search sales order using specified parameters and return search results.
ref: https://documentation.unicommerce.com/docs/saleorder-search.html
@@ -143,16 +145,14 @@ def search_sales_order(
# remove None values.
body = {k: v for k, v in body.items() if v is not None}
- search_results, status = self.request(
- endpoint="/services/rest/v1/oms/saleOrder/search", body=body
- )
+ search_results, status = self.request(endpoint="/services/rest/v1/oms/saleOrder/search", body=body)
if status and "elements" in search_results:
return search_results["elements"]
def get_inventory_snapshot(
- self, sku_codes: List[str], facility_code: str, updated_since: int = 1430
- ) -> Optional[JsonDict]:
+ self, sku_codes: list[str], facility_code: str, updated_since: int = 1430
+ ) -> _dict | None:
"""Get current inventory snapshot.
ref: https://documentation.unicommerce.com/docs/inventory-snapshot.html
@@ -163,13 +163,15 @@ def get_inventory_snapshot(
body = {"itemTypeSKUs": sku_codes, "updatedSinceInMinutes": updated_since}
response, status = self.request(
- endpoint="/services/rest/v1/inventory/inventorySnapshot/get", headers=extra_headers, body=body,
+ endpoint="/services/rest/v1/inventory/inventorySnapshot/get",
+ headers=extra_headers,
+ body=body,
)
if status:
return response
- def bulk_inventory_update(self, facility_code: str, inventory_map: Dict[str, int]):
+ def bulk_inventory_update(self, facility_code: str, inventory_map: dict[str, int]):
"""Bulk update inventory on unicommerce using SKU and qty.
The qty should be "total" quantity.
@@ -219,8 +221,8 @@ def bulk_inventory_update(self, facility_code: str, inventory_map: Dict[str, int
return response, False
def create_sales_invoice(
- self, so_code: str, so_item_codes: List[str], facility_code: str
- ) -> Optional[JsonDict]:
+ self, so_code: str, so_item_codes: list[str], facility_code: str
+ ) -> _dict | None:
body = {"saleOrderCode": so_code, "saleOrderItemCodes": so_item_codes}
extra_headers = {"Facility": facility_code}
@@ -280,7 +282,7 @@ def create_invoice_and_label_by_shipping_code(
def get_sales_invoice(
self, shipping_package_code: str, facility_code: str, is_return: bool = False
- ) -> Optional[JsonDict]:
+ ) -> _dict | None:
"""Get invoice details
ref: https://documentation.unicommerce.com/docs/invoice-getdetails.html
@@ -329,10 +331,12 @@ def _positive(numbers):
extra_headers = {"Facility": facility_code}
return self.request(
- endpoint="/services/rest/v1/oms/shippingPackage/edit", body=body, headers=extra_headers,
+ endpoint="/services/rest/v1/oms/shippingPackage/edit",
+ body=body,
+ headers=extra_headers,
)
- def get_invoice_label(self, shipping_package_code: str, facility_code: str) -> Optional[str]:
+ def get_invoice_label(self, shipping_package_code: str, facility_code: str) -> bytes | None:
"""Get the generated label for a given shipping package.
ref: undocumented.
@@ -352,7 +356,7 @@ def create_and_close_shipping_manifest(
channel: str,
shipping_provider_code: str,
shipping_method_code: str,
- shipping_packages: List[str],
+ shipping_packages: list[str],
facility_code: str,
third_party_shipping: bool = True,
):
@@ -371,7 +375,9 @@ def create_and_close_shipping_manifest(
}
response, status = self.request(
- endpoint="/services/rest/v1/oms/shippingManifest/createclose", body=body, headers=extra_headers,
+ endpoint="/services/rest/v1/oms/shippingManifest/createclose",
+ body=body,
+ headers=extra_headers,
)
if status:
@@ -390,9 +396,9 @@ def get_shipping_manifest(self, shipping_manifest_code, facility_code):
def search_shipping_packages(
self,
facility_code: str,
- channel: Optional[str] = None,
- statuses: Optional[List[str]] = None,
- updated_since: Optional[int] = 6 * 60,
+ channel: str | None = None,
+ statuses: list[str] | None = None,
+ updated_since: int | None = 6 * 60,
):
"""Search shipping packages on unicommerce matching specified criterias.
@@ -408,14 +414,20 @@ def search_shipping_packages(
body = {k: v for k, v in body.items() if v is not None}
search_results, statuses = self.request(
- endpoint="/services/rest/v1/oms/shippingPackage/search", body=body, headers=extra_headers,
+ endpoint="/services/rest/v1/oms/shippingPackage/search",
+ body=body,
+ headers=extra_headers,
)
if statuses and "elements" in search_results:
return search_results["elements"]
def create_import_job(
- self, job_name: str, csv_filename: str, facility_code: str, job_type: str = "CREATE_NEW",
+ self,
+ job_name: str,
+ csv_filename: str,
+ facility_code: str,
+ job_type: str = "CREATE_NEW",
):
"""Create import job by specifying job name and CSV file
@@ -448,7 +460,7 @@ def create_import_job(
def _utc_timeformat(datetime) -> str:
- """ Get datetime in UTC/GMT as required by Unicommerce"""
+ """Get datetime in UTC/GMT as required by Unicommerce"""
return get_datetime(datetime).astimezone(timezone("UTC")).strftime("%Y-%m-%dT%H:%M:%SZ")
diff --git a/ecommerce_integrations/unicommerce/cancellation_and_returns.py b/ecommerce_integrations/unicommerce/cancellation_and_returns.py
index 264ea6406..29bd59681 100644
--- a/ecommerce_integrations/unicommerce/cancellation_and_returns.py
+++ b/ecommerce_integrations/unicommerce/cancellation_and_returns.py
@@ -1,12 +1,12 @@
import json
from collections import defaultdict
from datetime import date, datetime
-from typing import List
import frappe
+from frappe.utils import now_datetime
+
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
from erpnext.controllers.accounts_controller import update_child_qty_rate
-from frappe.utils import now_datetime
from ecommerce_integrations.unicommerce.api_client import UnicommerceAPIClient
from ecommerce_integrations.unicommerce.constants import (
@@ -22,7 +22,7 @@
)
-def fully_cancel_orders(unicommerce_order_codes: List[str]) -> None:
+def fully_cancel_orders(unicommerce_order_codes: list[str]) -> None:
"""Perform "cancel" action on ERPNext sales orders which are fully cancelled in Unicommerce."""
current_orders_status = frappe.db.get_values(
@@ -90,9 +90,7 @@ def update_erpnext_order_items(so_data, so=None):
def _delete_cancelled_items(erpnext_items, cancelled_items):
- items = [
- d.as_dict() for d in erpnext_items if d.get(ORDER_ITEM_CODE_FIELD) not in cancelled_items
- ]
+ items = [d.as_dict() for d in erpnext_items if d.get(ORDER_ITEM_CODE_FIELD) not in cancelled_items]
# add `docname` same as name, required for Update Items functionality
for item in items:
@@ -104,7 +102,7 @@ def _serialize_items(trans_items) -> str:
# serialie date/datetime objects to string
for item in trans_items:
for k, v in item.items():
- if isinstance(v, (datetime, date)):
+ if isinstance(v, datetime | date):
item[k] = str(v)
return json.dumps(trans_items)
@@ -157,7 +155,7 @@ def create_credit_note(invoice_name):
for tax in credit_note.taxes:
tax.item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
- for item, tax_distribution in tax.item_wise_tax_detail.items():
+ for _, tax_distribution in tax.item_wise_tax_detail.items():
tax_distribution[1] *= -1
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail)
@@ -177,7 +175,6 @@ def check_and_update_customer_initiated_returns(orders, client: UnicommerceAPICl
def sync_customer_initiated_returns(so_data):
-
customer_returns = [r for r in so_data.get("returns", []) if r["type"] == "Customer Returned"]
if not customer_returns:
return
@@ -194,9 +191,7 @@ def create_cir_credit_note(so_data, return_data):
# Get items from SO which are returned, map SO item -> SI item with linked rows.
so_item_code_map = {item.get(ORDER_ITEM_CODE_FIELD): item.name for item in so.items}
- invoice_name = frappe.db.get_value(
- "Sales Invoice", {ORDER_CODE_FIELD: so_data["code"], "is_return": 0}
- )
+ invoice_name = frappe.db.get_value("Sales Invoice", {ORDER_CODE_FIELD: so_data["code"], "is_return": 0})
si = frappe.get_doc("Sales Invoice", invoice_name)
so_si_item_map = {item.so_detail: item.name for item in si.items}
@@ -215,7 +210,7 @@ def create_cir_credit_note(so_data, return_data):
credit_note.save()
-def _handle_partial_returns(credit_note, returned_items: List[str]) -> None:
+def _handle_partial_returns(credit_note, returned_items: list[str]) -> None:
"""Remove non-returned item from credit note and update taxes"""
item_code_to_qty_map = defaultdict(float)
@@ -223,9 +218,7 @@ def _handle_partial_returns(credit_note, returned_items: List[str]) -> None:
item_code_to_qty_map[item.item_code] += item.qty
# remove non-returned items
- credit_note.items = [
- item for item in credit_note.items if item.sales_invoice_item in returned_items
- ]
+ credit_note.items = [item for item in credit_note.items if item.sales_invoice_item in returned_items]
returned_qty_map = defaultdict(float)
for item in credit_note.items:
diff --git a/ecommerce_integrations/unicommerce/customer.py b/ecommerce_integrations/unicommerce/customer.py
index c1b5248ae..7cac4a309 100644
--- a/ecommerce_integrations/unicommerce/customer.py
+++ b/ecommerce_integrations/unicommerce/customer.py
@@ -1,5 +1,5 @@
import json
-from typing import Any, Dict, List
+from typing import Any
import frappe
from frappe import _
@@ -78,7 +78,7 @@ def _check_if_customer_exists(address, customer_code):
return frappe.get_doc("Customer", customer_name)
-def _create_customer_addresses(addresses: List[Dict[str, Any]], customer) -> None:
+def _create_customer_addresses(addresses: list[dict[str, Any]], customer) -> None:
"""Create address from dictionary containing fields used in Address doctype of ERPNext.
Unicommerce orders contain address list,
@@ -93,7 +93,6 @@ def _create_customer_addresses(addresses: List[Dict[str, Any]], customer) -> Non
def _create_customer_address(uni_address, address_type, customer, also_shipping=False):
-
country_code = uni_address.get("country")
country = UNICOMMERCE_COUNTRY_MAPPING.get(country_code)
diff --git a/ecommerce_integrations/unicommerce/delivery_note.py b/ecommerce_integrations/unicommerce/delivery_note.py
index c578f6ab7..4856bb35f 100644
--- a/ecommerce_integrations/unicommerce/delivery_note.py
+++ b/ecommerce_integrations/unicommerce/delivery_note.py
@@ -24,9 +24,7 @@ def prepare_delivery_note():
)
for facility in enabled_facilities:
- updated_packages = client.search_shipping_packages(
- updated_since=minutes, facility_code=facility
- )
+ updated_packages = client.search_shipping_packages(updated_since=minutes, facility_code=facility)
valid_packages = [p for p in updated_packages if p.get("channel") in enabled_channels]
if not valid_packages:
continue
@@ -50,41 +48,9 @@ def prepare_delivery_note():
def create_delivery_note(so, sales_invoice):
try:
# Create the delivery note
- from frappe.model.mapper import make_mapped_doc
+ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
- res = make_mapped_doc(
- method="erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", source_name=so.name
- )
- res.update({"items": []})
- for item in sales_invoice.items:
- res.append(
- "items",
- {
- "item_code": item.item_code,
- "item_name": item.item_name,
- "description": item.description,
- "qty": item.qty,
- "uom": item.uom,
- "rate": item.rate,
- "amount": item.amount,
- "warehouse": item.warehouse,
- "against_sales_order": item.sales_order,
- "batch_no": item.batch_no,
- "so_detail": item.so_detail,
- },
- )
- for item in sales_invoice.taxes:
- res.append(
- "taxes",
- {
- "charge_type": item.charge_type,
- "account_head": item.account_head,
- "tax_amount": item.tax_amount,
- "description": item.description,
- "item_wise_tax_detail": item.item_wise_tax_detail,
- "dont_recompute_tax": item.dont_recompute_tax,
- },
- )
+ res = make_delivery_note(source_name=so.name)
res.unicommerce_order_code = sales_invoice.unicommerce_order_code
res.unicommerce_shipment_id = sales_invoice.unicommerce_shipping_package_code
res.save()
diff --git a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/test_unicommerce_settings.py b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/test_unicommerce_settings.py
index d3fc0fc50..8a8fc9a97 100644
--- a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/test_unicommerce_settings.py
+++ b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/test_unicommerce_settings.py
@@ -1,8 +1,9 @@
# Copyright (c) 2021, Frappe and Contributors
# See LICENSE
-import frappe
import responses
+
+import frappe
from frappe.utils import now, now_datetime
from ecommerce_integrations.unicommerce.constants import SETTINGS_DOCTYPE
@@ -44,9 +45,7 @@ def test_failed_auth(self):
"""requirement: When improper credentials are provided, system throws error."""
# failure case
- responses.add(
- responses.GET, "https://demostaging.unicommerce.com/oauth/token", json={}, status=401
- )
+ responses.add(responses.GET, "https://demostaging.unicommerce.com/oauth/token", json={}, status=401)
self.assertRaises(frappe.ValidationError, self.settings.update_tokens)
@responses.activate
diff --git a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py
index cd5a46da5..a4510a25d 100644
--- a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py
+++ b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py
@@ -1,10 +1,9 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see LICENSE
-from typing import Dict, List, Optional, Tuple
+import requests
import frappe
-import requests
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import add_to_date, get_datetime, now_datetime
@@ -143,18 +142,16 @@ def validate_warehouse_mapping(self):
_("Warehouse Mapping should be unique and one-to-one without repeating same warehouses.")
)
- def get_erpnext_warehouses(self, all_wh=False) -> List[ERPNextWarehouse]:
+ def get_erpnext_warehouses(self, all_wh=False) -> list[ERPNextWarehouse]:
"""Get list of configured ERPNext warehouses.
all_wh flag ignores enabled status.
"""
- return [
- wh_map.erpnext_warehouse for wh_map in self.warehouse_mapping if wh_map.enabled or all_wh
- ]
+ return [wh_map.erpnext_warehouse for wh_map in self.warehouse_mapping if wh_map.enabled or all_wh]
def get_erpnext_to_integration_wh_mapping(
self, all_wh=False
- ) -> Dict[ERPNextWarehouse, IntegrationWarehouse]:
+ ) -> dict[ERPNextWarehouse, IntegrationWarehouse]:
"""Get enabled mapping from ERPNextWarehouse to Unicommerce facility.
all_wh flag ignores enabled status."""
@@ -166,7 +163,7 @@ def get_erpnext_to_integration_wh_mapping(
def get_integration_to_erpnext_wh_mapping(
self, all_wh=False
- ) -> Dict[IntegrationWarehouse, ERPNextWarehouse]:
+ ) -> dict[IntegrationWarehouse, ERPNextWarehouse]:
"""Get enabled mapping from Unicommerce facility to ERPNext warehouse.
all_wh flag ignores enabled status."""
@@ -174,8 +171,8 @@ def get_integration_to_erpnext_wh_mapping(
return {v: k for k, v in reverse_map.items()}
- def get_company_addresses(self, facility_code: str) -> Tuple[Optional[str], Optional[str]]:
- """ Get mapped company billing and shipping addresses."""
+ def get_company_addresses(self, facility_code: str) -> tuple[str | None, str | None]:
+ """Get mapped company billing and shipping addresses."""
for wh_map in self.warehouse_mapping:
if wh_map.unicommerce_facility_code == facility_code:
return wh_map.company_address, wh_map.dispatch_address
@@ -183,7 +180,6 @@ def get_company_addresses(self, facility_code: str) -> Tuple[Optional[str], Opti
def setup_custom_fields(update=True):
-
custom_sections = {
"Sales Order": [
dict(
diff --git a/ecommerce_integrations/unicommerce/doctype/unicommerce_shipment_manifest/unicommerce_shipment_manifest.py b/ecommerce_integrations/unicommerce/doctype/unicommerce_shipment_manifest/unicommerce_shipment_manifest.py
index 7dc3e9df6..61ddf87f2 100644
--- a/ecommerce_integrations/unicommerce/doctype/unicommerce_shipment_manifest/unicommerce_shipment_manifest.py
+++ b/ecommerce_integrations/unicommerce/doctype/unicommerce_shipment_manifest/unicommerce_shipment_manifest.py
@@ -2,7 +2,6 @@
# For license information, please see LICENSE
import json
-from typing import Optional
import frappe
from frappe import _
@@ -80,7 +79,7 @@ def get_facility_code(self) -> str:
",".join(facility_codes)
)
)
- return list(facility_codes)[0]
+ return next(iter(facility_codes))
def create_and_close_manifest_on_unicommerce(self):
shipping_packages = [d.shipping_package_code for d in self.manifest_items]
@@ -152,9 +151,7 @@ def get_sales_invoice_details(sales_invoice):
as_dict=True,
)
- items = frappe.db.get_values(
- "Sales Invoice Item", {"parent": sales_invoice}, "item_name", as_dict=True
- )
+ items = frappe.db.get_values("Sales Invoice Item", {"parent": sales_invoice}, "item_name", as_dict=True)
unique_items = {item.item_name for item in items}
si_data["item_list"] = ",".join(unique_items)
@@ -163,9 +160,7 @@ def get_sales_invoice_details(sales_invoice):
@frappe.whitelist()
-def search_packages(
- search_term: str, channel: Optional[str] = None, shipper: Optional[str] = None
-):
+def search_packages(search_term: str, channel: str | None = None, shipper: str | None = None):
filters = {
CHANNEL_ID_FIELD: channel,
SHIPPING_PROVIDER_CODE: shipper,
@@ -181,19 +176,18 @@ def search_packages(
INVOICE_CODE_FIELD: search_term,
}
- packages = frappe.get_list(
- "Sales Invoice", filters=filters, or_filters=or_filters, limit_page_length=1
- )
+ packages = frappe.get_list("Sales Invoice", filters=filters, or_filters=or_filters, limit_page_length=1)
if packages:
return packages[0].name
@frappe.whitelist()
-def get_shipping_package_list(source_name, target_doc=None):
-
+def get_shipping_package_list(source_name: str, target_doc: dict | str | None = None) -> dict:
if target_doc and isinstance(target_doc, str):
- target_doc = json.loads(target_doc)
+ target_doc = frappe._dict(json.loads(target_doc))
+ elif target_doc is None:
+ target_doc = frappe._dict()
target_doc.setdefault("manifest_items", []).append({"sales_invoice": source_name})
diff --git a/ecommerce_integrations/unicommerce/grn.py b/ecommerce_integrations/unicommerce/grn.py
index a28db7c46..45554133b 100644
--- a/ecommerce_integrations/unicommerce/grn.py
+++ b/ecommerce_integrations/unicommerce/grn.py
@@ -1,13 +1,13 @@
from dataclasses import dataclass
-from typing import List
import frappe
-from erpnext.stock.doctype.batch.batch import Batch
from frappe import _
from frappe.utils import cint, getdate
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils.file_manager import save_file
+from erpnext.stock.doctype.batch.batch import Batch
+
from ecommerce_integrations.unicommerce.api_client import UnicommerceAPIClient
from ecommerce_integrations.unicommerce.constants import (
GRN_STOCK_ENTRY_TYPE,
@@ -95,7 +95,7 @@ def get_facility_code(stock_entry, unicommerce_settings) -> str:
_("{} only supports one target warehouse (unicommerce facility)").format(GRN_STOCK_ENTRY_TYPE)
)
- warehouse = list(target_warehouses)[0]
+ warehouse = next(iter(target_warehouses))
warehouse_mapping = unicommerce_settings.get_erpnext_to_integration_wh_mapping(all_wh=True)
facility = warehouse_mapping.get(warehouse)
@@ -130,9 +130,7 @@ def upload_grn(doc, method=None):
msg += _("Confirm the status on Import Log in Uniware.")
frappe.msgprint(msg, title="Success")
elif response.successful and errors:
- frappe.msgprint(
- "Partial success, unicommerce reported errors:
{}".format("
".join(errors))
- )
+ frappe.msgprint("Partial success, unicommerce reported errors:
{}".format("
".join(errors)))
def _prepare_grn_import_csv(stock_entry) -> str:
@@ -190,8 +188,7 @@ def _prepare_grn_import_csv(stock_entry) -> str:
return file.file_name
-def _get_csv_content(rows: List[GRNItemRow]) -> bytes:
-
+def _get_csv_content(rows: list[GRNItemRow]) -> bytes:
writer = UnicodeWriter()
for row in rows:
@@ -208,7 +205,7 @@ def _get_unicommerce_format_date(date) -> str:
def create_auto_grn_import(csv_filename: str, facility_code: str, client=None):
- """ Create new import job for Auto GRN items"""
+ """Create new import job for Auto GRN items"""
if client is None:
client = UnicommerceAPIClient()
resp = client.create_import_job(
diff --git a/ecommerce_integrations/unicommerce/inventory.py b/ecommerce_integrations/unicommerce/inventory.py
index 704c2c014..d8f6961de 100644
--- a/ecommerce_integrations/unicommerce/inventory.py
+++ b/ecommerce_integrations/unicommerce/inventory.py
@@ -1,5 +1,4 @@
from collections import defaultdict
-from typing import Dict
import frappe
from frappe.utils import cint, now
@@ -32,9 +31,7 @@ def update_inventory_on_unicommerce(client=None, force=False):
return
# check if need to run based on configured sync frequency
- if not force and not need_to_run(
- SETTINGS_DOCTYPE, "inventory_sync_frequency", "last_inventory_sync"
- ):
+ if not force and not need_to_run(SETTINGS_DOCTYPE, "inventory_sync_frequency", "last_inventory_sync"):
return
# get configured warehouses
@@ -45,7 +42,7 @@ def update_inventory_on_unicommerce(client=None, force=False):
client = UnicommerceAPIClient()
# track which ecommerce item was updated successfully
- success_map: Dict[str, bool] = defaultdict(lambda: True)
+ success_map: dict[str, bool] = defaultdict(lambda: True)
inventory_synced_on = now()
for warehouse in warehouses:
@@ -82,7 +79,7 @@ def update_inventory_on_unicommerce(client=None, force=False):
_update_inventory_sync_status(success_map, inventory_synced_on)
-def _update_inventory_sync_status(ecom_item_success_map: Dict[str, bool], timestamp: str) -> None:
+def _update_inventory_sync_status(ecom_item_success_map: dict[str, bool], timestamp: str) -> None:
for ecom_item, status in ecom_item_success_map.items():
if status:
update_inventory_sync_status(ecom_item, timestamp)
diff --git a/ecommerce_integrations/unicommerce/invoice.py b/ecommerce_integrations/unicommerce/invoice.py
index 8f99a520f..c8f8095eb 100644
--- a/ecommerce_integrations/unicommerce/invoice.py
+++ b/ecommerce_integrations/unicommerce/invoice.py
@@ -1,15 +1,17 @@
import base64
import json
from collections import defaultdict
-from typing import Any, Dict, List, NewType, Optional
+from typing import Any, NewType
-import frappe
import requests
-from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
-from frappe import _
+
+import frappe
+from frappe import _, _dict
from frappe.utils import cint, flt, nowdate
from frappe.utils.file_manager import save_file
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
from ecommerce_integrations.ecommerce_integrations.doctype.ecommerce_item import ecommerce_item
from ecommerce_integrations.unicommerce.api_client import UnicommerceAPIClient
from ecommerce_integrations.unicommerce.constants import (
@@ -34,7 +36,6 @@
remove_non_alphanumeric_chars,
)
-JsonDict = Dict[str, Any]
SOCode = NewType("SOCode", str)
# TypedDict
@@ -42,17 +43,17 @@
# item_code: str
# warehouse: str
# batch_no: str
-ItemWHAlloc = Dict[str, str]
+ItemWHAlloc = dict[str, str]
-WHAllocation = Dict[SOCode, List[ItemWHAlloc]]
+WHAllocation = dict[SOCode, list[ItemWHAlloc]]
INVOICED_STATE = ["PACKED", "READY_TO_SHIP", "DISPATCHED", "MANIFESTED", "SHIPPED", "DELIVERED"]
@frappe.whitelist()
def generate_unicommerce_invoices(
- sales_orders: List[SOCode], warehouse_allocation: Optional[WHAllocation] = None
+ sales_orders: list[SOCode], warehouse_allocation: WHAllocation | None = None
):
"""Request generation of invoice to Unicommerce and sync that invoice.
@@ -126,8 +127,8 @@ def generate_unicommerce_invoices(
def bulk_generate_invoices(
- sales_orders: List[SOCode],
- warehouse_allocation: Optional[WHAllocation] = None,
+ sales_orders: list[SOCode],
+ warehouse_allocation: WHAllocation | None = None,
request_id=None,
client=None,
):
@@ -153,7 +154,6 @@ def bulk_generate_invoices(
def _log_invoice_generation(sales_orders, failed_orders):
-
failed_orders = set(failed_orders)
failed_orders.update(_get_orders_with_missing_invoice(sales_orders))
successful_orders = list(set(sales_orders) - set(failed_orders))
@@ -187,7 +187,7 @@ def _get_orders_with_missing_invoice(sales_orders):
return missing_invoices
-def update_invoicing_status(sales_orders: List[str], status: str) -> None:
+def update_invoicing_status(sales_orders: list[str], status: str) -> None:
if not sales_orders:
return
@@ -236,9 +236,7 @@ def _validate_wh_allocation(warehouse_allocation: WHAllocation):
frappe.throw(msg)
-def _generate_invoice(
- client: UnicommerceAPIClient, erpnext_order, channel_config, warehouse_allocation=None
-):
+def _generate_invoice(client: UnicommerceAPIClient, erpnext_order, channel_config, warehouse_allocation=None):
unicommerce_so_code = erpnext_order.get(ORDER_CODE_FIELD)
so_data = client.get_sales_order(unicommerce_so_code)
@@ -286,16 +284,12 @@ def _fetch_and_sync_invoice(
"""
so_data = client.get_sales_order(unicommerce_so_code)
- shipping_packages = [
- d["code"] for d in so_data["shippingPackages"] if d["status"] in INVOICED_STATE
- ]
+ shipping_packages = [d["code"] for d in so_data["shippingPackages"] if d["status"] in INVOICED_STATE]
for package in shipping_packages:
invoice_response = invoice_responses.get(package) or {}
invoice_data = client.get_sales_invoice(package, facility_code)["invoice"]
- label_pdf = fetch_label_pdf(
- package, invoice_response, client=client, facility_code=facility_code
- )
+ label_pdf = fetch_label_pdf(package, invoice_response, client=client, facility_code=facility_code)
create_sales_invoice(
invoice_data,
erpnext_so_code,
@@ -308,14 +302,14 @@ def _fetch_and_sync_invoice(
def create_sales_invoice(
- si_data: JsonDict,
+ si_data: _dict,
so_code: str,
update_stock=0,
submit=True,
shipping_label=None,
warehouse_allocations=None,
invoice_response=None,
- so_data: Optional[JsonDict] = None,
+ so_data: _dict | None = None,
):
"""Create ERPNext Sales Invcoice using Unicommerce sales invoice data and related Sales order.
@@ -351,9 +345,7 @@ def create_sales_invoice(
shipping_package_code = si_data.get("shippingPackageCode")
shipping_package_info = _get_shipping_package(so_data, shipping_package_code) or {}
- tracking_no = invoice_response.get("trackingNumber") or shipping_package_info.get(
- "trackingNumber"
- )
+ tracking_no = invoice_response.get("trackingNumber") or shipping_package_info.get("trackingNumber")
shipping_provider_code = (
invoice_response.get("shippingProviderCode")
or shipping_package_info.get("shippingProvider")
@@ -412,10 +404,10 @@ def create_sales_invoice(
def attach_unicommerce_docs(
sales_invoice: str,
- invoice: Optional[str],
- label: Optional[str],
- invoice_code: Optional[str],
- package_code: Optional[str],
+ invoice: str | None,
+ label: str | None,
+ invoice_code: str | None,
+ package_code: str | None,
) -> None:
"""Attach invoice and label to specified sales invoice.
@@ -452,9 +444,9 @@ def _get_line_items(
warehouse: str,
so_code: str,
cost_center: str,
- warehouse_allocations: Optional[WHAllocation] = None,
-) -> List[Dict[str, Any]]:
- """ Invoice items can be different and are consolidated, hence recomputing is required """
+ warehouse_allocations: WHAllocation | None = None,
+) -> list[dict[str, Any]]:
+ """Invoice items can be different and are consolidated, hence recomputing is required"""
si_items = []
for item in line_items:
@@ -481,15 +473,12 @@ def _get_line_items(
return si_items
-def _assign_wh_and_so_row(line_items, warehouse_allocation: List[ItemWHAlloc], so_code: str):
-
+def _assign_wh_and_so_row(line_items, warehouse_allocation: list[ItemWHAlloc], so_code: str):
so_items = frappe.get_doc("Sales Order", so_code).items
so_item_price_map = {d.name: d.rate for d in so_items}
# remove cancelled items
- warehouse_allocation = [
- d for d in warehouse_allocation if d["sales_order_row"] in so_item_price_map
- ]
+ warehouse_allocation = [d for d in warehouse_allocation if d["sales_order_row"] in so_item_price_map]
# update price
for item in warehouse_allocation:
@@ -501,7 +490,7 @@ def _assign_wh_and_so_row(line_items, warehouse_allocation: List[ItemWHAlloc], s
line_items.sort(key=sort_key)
# update references
- for item, wh_alloc in zip(line_items, warehouse_allocation):
+ for item, wh_alloc in zip(line_items, warehouse_allocation, strict=True):
item["so_detail"] = wh_alloc["sales_order_row"]
item["warehouse"] = wh_alloc["warehouse"]
item["batch_no"] = wh_alloc.get("batch_no")
@@ -510,7 +499,7 @@ def _assign_wh_and_so_row(line_items, warehouse_allocation: List[ItemWHAlloc], s
def _verify_total(si, si_data) -> None:
- """ Leave a comment if grand total does not match unicommerce total"""
+ """Leave a comment if grand total does not match unicommerce total"""
if abs(si.grand_total - flt(si_data["total"])) > 0.5:
si.add_comment(text=f"Invoice totals mismatch: Unicommerce reported total of {si_data['total']}")
@@ -541,7 +530,6 @@ def make_payment_entry(invoice, channel_config, invoice_posting_date=None):
def fetch_label_pdf(package, invoicing_response, client, facility_code):
-
if invoicing_response and invoicing_response.get("shippingLabelLink"):
link = invoicing_response.get("shippingLabelLink")
return fetch_pdf_as_base64(link)
diff --git a/ecommerce_integrations/unicommerce/order.py b/ecommerce_integrations/unicommerce/order.py
index 6b1ea741b..7c78608b1 100644
--- a/ecommerce_integrations/unicommerce/order.py
+++ b/ecommerce_integrations/unicommerce/order.py
@@ -1,6 +1,7 @@
import json
from collections import defaultdict, namedtuple
-from typing import Any, Dict, Iterator, List, NewType, Optional, Set, Tuple
+from collections.abc import Iterator
+from typing import Any, NewType
import frappe
from frappe.utils import add_to_date, flt
@@ -29,7 +30,7 @@
from ecommerce_integrations.unicommerce.utils import create_unicommerce_log, get_unicommerce_date
from ecommerce_integrations.utils.taxation import get_dummy_tax_category
-UnicommerceOrder = NewType("UnicommerceOrder", Dict[str, Any])
+UnicommerceOrder = NewType("UnicommerceOrder", dict[str, Any])
def sync_new_orders(client: UnicommerceAPIClient = None, force=False):
@@ -61,10 +62,7 @@ def sync_new_orders(client: UnicommerceAPIClient = None, force=False):
_create_sales_invoices(order, sales_order, client)
-def _get_new_orders(
- client: UnicommerceAPIClient, status: Optional[str]
-) -> Optional[Iterator[UnicommerceOrder]]:
-
+def _get_new_orders(client: UnicommerceAPIClient, status: str | None) -> Iterator[UnicommerceOrder] | None:
"""Search new sales order from unicommerce."""
updated_since = 24 * 60 # minutes
@@ -124,8 +122,7 @@ def _create_sales_invoices(unicommerce_order, sales_order, client: UnicommerceAP
frappe.flags.request_id = None
-def create_order(payload: UnicommerceOrder, request_id: Optional[str] = None, client=None) -> None:
-
+def create_order(payload: UnicommerceOrder, request_id: str | None = None, client=None) -> None:
order = payload
existing_so = frappe.db.get_value("Sales Order", {ORDER_CODE_FIELD: order["code"]})
@@ -158,7 +155,7 @@ def create_order(payload: UnicommerceOrder, request_id: Optional[str] = None, cl
return order
-def _sync_order_items(order: UnicommerceOrder, client: UnicommerceAPIClient) -> Set[str]:
+def _sync_order_items(order: UnicommerceOrder, client: UnicommerceAPIClient) -> set[str]:
"""Ensure all items are synced before processing order.
If not synced then product sync for specific item is initiated"""
@@ -174,7 +171,6 @@ def _sync_order_items(order: UnicommerceOrder, client: UnicommerceAPIClient) ->
def _create_order(order: UnicommerceOrder, customer) -> None:
-
channel_config = frappe.get_doc("Unicommerce Channel", order["channel"])
settings = frappe.get_cached_doc(SETTINGS_DOCTYPE)
@@ -219,9 +215,8 @@ def _create_order(order: UnicommerceOrder, customer) -> None:
def _get_line_items(
- line_items, default_warehouse: Optional[str] = None, is_cancelled: bool = False
-) -> List[Dict[str, Any]]:
-
+ line_items, default_warehouse: str | None = None, is_cancelled: bool = False
+) -> list[dict[str, Any]]:
settings = frappe.get_cached_doc(SETTINGS_DOCTYPE)
wh_map = settings.get_integration_to_erpnext_wh_mapping(all_wh=True)
so_items = []
@@ -249,7 +244,7 @@ def _get_line_items(
return so_items
-def get_taxes(line_items, channel_config) -> List:
+def get_taxes(line_items, channel_config) -> list:
taxes = []
# Note: Tax details are NOT available during SO stage.
@@ -301,7 +296,7 @@ def _get_facility_code(line_items) -> str:
if len(facility_codes) > 1:
frappe.throw("Multiple facility codes found in single order")
- return list(facility_codes)[0]
+ return next(iter(facility_codes))
def update_shipping_info(doc, method=None):
@@ -330,9 +325,7 @@ def _update_package_info_on_unicommerce(so_code):
shipping_packages = updated_so_data.get("shippingPackages")
if not shipping_packages:
- frappe.throw(
- frappe._("Shipping package not present on Unicommerce for order {}").format(so.name)
- )
+ frappe.throw(frappe._("Shipping package not present on Unicommerce for order {}").format(so.name))
shipping_package_code = shipping_packages[0].get("code")
@@ -356,7 +349,7 @@ def _update_package_info_on_unicommerce(so_code):
raise
-def _get_batch_no(so_line_item) -> Optional[str]:
+def _get_batch_no(so_line_item) -> str | None:
"""If specified vendor batch code is valid batch number in ERPNext then get batch no.
SO line items contain batch no detail like this:
@@ -374,9 +367,7 @@ def _get_batch_no(so_line_item) -> Optional[str]:
}
},
"""
- batch_no = ((so_line_item.get("batchDTO") or {}).get("batchFieldsDTO") or {}).get(
- "vendorBatchNumber"
- )
+ batch_no = ((so_line_item.get("batchDTO") or {}).get("batchFieldsDTO") or {}).get("vendorBatchNumber")
if batch_no and frappe.db.exists("Batch", batch_no):
return batch_no
diff --git a/ecommerce_integrations/unicommerce/pick_list.py b/ecommerce_integrations/unicommerce/pick_list.py
index 7a8833470..c5334fc75 100644
--- a/ecommerce_integrations/unicommerce/pick_list.py
+++ b/ecommerce_integrations/unicommerce/pick_list.py
@@ -21,7 +21,9 @@ def validate(self, method=None):
if pl.picked_qty > pl.qty:
pl.picked_qty = pl.qty
- frappe.throw(_("Row {0} Picked Qty cannot be more than Sales Order Qty").format(pl.idx))
+ frappe.throw(
+ _("Row {0} Picked Qty cannot be more than Sales Order Qty").format(pl.idx)
+ )
if pl.picked_qty == 0 and pl.docstatus == 1:
frappe.throw(
_("You have not picked {0} in row {1} . Pick the item to proceed!").format(
diff --git a/ecommerce_integrations/unicommerce/product.py b/ecommerce_integrations/unicommerce/product.py
index 553bc9e38..86d2cd6ee 100644
--- a/ecommerce_integrations/unicommerce/product.py
+++ b/ecommerce_integrations/unicommerce/product.py
@@ -1,13 +1,14 @@
-from typing import List, NewType
+from typing import Any, NewType
+
+from stdnum.ean import is_valid as validate_barcode
import frappe
from frappe import _
from frappe.utils import get_url, now, to_markdown
from frappe.utils.nestedset import get_root_of
-from stdnum.ean import is_valid as validate_barcode
from ecommerce_integrations.ecommerce_integrations.doctype.ecommerce_item import ecommerce_item
-from ecommerce_integrations.unicommerce.api_client import JsonDict, UnicommerceAPIClient
+from ecommerce_integrations.unicommerce.api_client import UnicommerceAPIClient
from ecommerce_integrations.unicommerce.constants import (
DEFAULT_WEIGHT_UOM,
ITEM_BATCH_GROUP_FIELD,
@@ -91,7 +92,6 @@ def _create_item_dict(uni_item):
_validate_create_brand(uni_item.get("brand"))
for uni_field, erpnext_field in UNI_TO_ERPNEXT_ITEM_MAPPING.items():
-
value = uni_item.get(uni_field)
if not _validate_field(erpnext_field, value):
continue
@@ -218,7 +218,7 @@ def upload_new_items(force=False) -> None:
log.save()
-def _get_new_items() -> List[ItemCode]:
+def _get_new_items() -> list[ItemCode]:
new_items = frappe.db.sql(
f"""
SELECT item.item_code
@@ -234,8 +234,8 @@ def _get_new_items() -> List[ItemCode]:
def upload_items_to_unicommerce(
- item_codes: List[ItemCode], client: UnicommerceAPIClient = None
-) -> List[ItemCode]:
+ item_codes: list[ItemCode], client: UnicommerceAPIClient = None
+) -> list[ItemCode]:
"""Upload multiple items to Unicommerce.
Return Successfully synced item codes.
@@ -259,7 +259,7 @@ def upload_items_to_unicommerce(
return synced_items
-def _build_unicommerce_item(item_code: ItemCode) -> JsonDict:
+def _build_unicommerce_item(item_code: ItemCode) -> dict[str, Any]:
"""Build Unicommerce item JSON using an ERPNext item"""
item = frappe.get_doc("Item", item_code)
@@ -284,9 +284,7 @@ def _build_unicommerce_item(item_code: ItemCode) -> JsonDict:
elif barcode.barcode_type == "UPC-A":
item_json["upc"] = barcode.barcode
- item_json["categoryCode"] = frappe.db.get_value(
- "Item Group", item.item_group, PRODUCT_CATEGORY_FIELD
- )
+ item_json["categoryCode"] = frappe.db.get_value("Item Group", item.item_group, PRODUCT_CATEGORY_FIELD)
# append site prefix to image url
item_json["imageUrl"] = get_url(item.image)
item_json["maxRetailPrice"] = item.standard_rate
@@ -338,6 +336,4 @@ def validate_item(doc, method=None):
item_group = frappe.get_cached_doc("Item Group", item.item_group)
if not item_group.get(PRODUCT_CATEGORY_FIELD):
- frappe.throw(
- _("Unicommerce Product category required in Item Group: {}").format(item_group.name)
- )
+ frappe.throw(_("Unicommerce Product category required in Item Group: {}").format(item_group.name))
diff --git a/ecommerce_integrations/unicommerce/status_updater.py b/ecommerce_integrations/unicommerce/status_updater.py
index fdd85dd61..c81d79666 100644
--- a/ecommerce_integrations/unicommerce/status_updater.py
+++ b/ecommerce_integrations/unicommerce/status_updater.py
@@ -47,7 +47,6 @@
def update_sales_order_status():
-
settings = frappe.get_cached_doc(SETTINGS_DOCTYPE)
if not settings.is_enabled():
return
@@ -58,9 +57,7 @@ def update_sales_order_status():
minutes = days_to_sync * 24 * 60
updated_orders = client.search_sales_order(updated_since=minutes)
- enabled_channels = frappe.db.get_list(
- "Unicommerce Channel", filters={"enabled": 1}, pluck="channel_id"
- )
+ enabled_channels = frappe.db.get_list("Unicommerce Channel", filters={"enabled": 1}, pluck="channel_id")
valid_orders = [order for order in updated_orders if order.get("channel") in enabled_channels]
if valid_orders:
_update_order_status_fields(valid_orders)
@@ -79,7 +76,6 @@ def update_sales_order_status():
def _update_order_status_fields(orders):
-
order_status_map = {d["code"]: d["status"] for d in orders}
order_codes = list(order_status_map.keys())
@@ -121,9 +117,7 @@ def update_shipping_package_status():
# find all Facilities
enabled_facilities = list(settings.get_integration_to_erpnext_wh_mapping().keys())
- enabled_channels = frappe.db.get_list(
- "Unicommerce Channel", filters={"enabled": 1}, pluck="channel_id"
- )
+ enabled_channels = frappe.db.get_list("Unicommerce Channel", filters={"enabled": 1}, pluck="channel_id")
for facility in enabled_facilities:
updated_packages = client.search_shipping_packages(updated_since=minutes, facility_code=facility)
@@ -140,7 +134,6 @@ def update_shipping_package_status():
def _update_package_status_fields(packages):
-
package_status_map = {d["code"]: d["status"] for d in packages}
package_codes = list(package_status_map.keys())
diff --git a/ecommerce_integrations/unicommerce/tests/test_client.py b/ecommerce_integrations/unicommerce/tests/test_client.py
index 50f6c5f34..40671c69f 100644
--- a/ecommerce_integrations/unicommerce/tests/test_client.py
+++ b/ecommerce_integrations/unicommerce/tests/test_client.py
@@ -2,10 +2,11 @@
import json
from unittest.mock import patch
-import frappe
import responses
from responses.matchers import query_param_matcher
+import frappe
+
from ecommerce_integrations.unicommerce.api_client import UnicommerceAPIClient
from ecommerce_integrations.unicommerce.tests.utils import TestCase
@@ -131,7 +132,6 @@ def test_create_update_item(self):
self.assertTrue(response["successful"])
def test_bulk_inventory_sync(self):
-
expected_body = {
"inventoryAdjustments": [
{
@@ -289,9 +289,7 @@ def test_update_shipping_package(self):
],
)
- self.client.update_shipping_package(
- "SP_CODE", "TEST", "DEFAULT", length=100, width=200, height=300
- )
+ self.client.update_shipping_package("SP_CODE", "TEST", "DEFAULT", length=100, width=200, height=300)
self.assert_last_request_headers("Facility", "TEST")
def test_get_invoice_label(self):
@@ -323,7 +321,9 @@ def test_bulk_import(self):
responses.POST,
"https://demostaging.unicommerce.com/services/rest/v1/data/import/job/create",
status=200,
- match=[query_param_matcher({"name": "Auto GRN Items", "importOption": "CREATE_NEW"}),],
+ match=[
+ query_param_matcher({"name": "Auto GRN Items", "importOption": "CREATE_NEW"}),
+ ],
json={"successful": True},
)
diff --git a/ecommerce_integrations/unicommerce/tests/test_delivery_note.py b/ecommerce_integrations/unicommerce/tests/test_delivery_note.py
index 205ad8e10..9cdbdc421 100644
--- a/ecommerce_integrations/unicommerce/tests/test_delivery_note.py
+++ b/ecommerce_integrations/unicommerce/tests/test_delivery_note.py
@@ -1,8 +1,10 @@
import base64
import unittest
-import frappe
import responses
+
+import frappe
+
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from ecommerce_integrations.unicommerce.constants import (
diff --git a/ecommerce_integrations/unicommerce/tests/test_inventory.py b/ecommerce_integrations/unicommerce/tests/test_inventory.py
index b931c9f35..c92dbe5eb 100644
--- a/ecommerce_integrations/unicommerce/tests/test_inventory.py
+++ b/ecommerce_integrations/unicommerce/tests/test_inventory.py
@@ -1,7 +1,9 @@
from unittest.mock import patch
-import frappe
import responses
+
+import frappe
+
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.utils import get_stock_balance
@@ -93,7 +95,6 @@ def test_inventory_sync(self):
def make_ecommerce_item(item_code):
-
if ecommerce_item.is_synced(MODULE_NAME, item_code):
return
diff --git a/ecommerce_integrations/unicommerce/tests/test_invoice.py b/ecommerce_integrations/unicommerce/tests/test_invoice.py
index ac80d9fa0..28da6232a 100644
--- a/ecommerce_integrations/unicommerce/tests/test_invoice.py
+++ b/ecommerce_integrations/unicommerce/tests/test_invoice.py
@@ -1,8 +1,10 @@
import base64
import unittest
-import frappe
import responses
+
+import frappe
+
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from ecommerce_integrations.unicommerce.constants import (
@@ -55,9 +57,7 @@ def test_create_invoice(self):
attachments = frappe.get_all(
"File", fields=["name", "file_name"], filters={"attached_to_name": si.name}
)
- self.assertGreaterEqual(
- len(attachments), 2, msg=f"Expected 2 attachments, found: {str(attachments)}"
- )
+ self.assertGreaterEqual(len(attachments), 2, msg=f"Expected 2 attachments, found: {attachments!s}")
def test_end_to_end_invoice_generation(self):
"""Full invoice generation test with mocked responses."""
@@ -113,6 +113,4 @@ def test_end_to_end_invoice_generation(self):
attachments = frappe.get_all(
"File", fields=["name", "file_name"], filters={"attached_to_name": si.name}
)
- self.assertGreaterEqual(
- len(attachments), 2, msg=f"Expected 2 attachments, found: {str(attachments)}"
- )
+ self.assertGreaterEqual(len(attachments), 2, msg=f"Expected 2 attachments, found: {attachments!s}")
diff --git a/ecommerce_integrations/unicommerce/tests/test_order.py b/ecommerce_integrations/unicommerce/tests/test_order.py
index 20b3071fd..dd9797bfc 100644
--- a/ecommerce_integrations/unicommerce/tests/test_order.py
+++ b/ecommerce_integrations/unicommerce/tests/test_order.py
@@ -26,9 +26,15 @@ def setUpClass(cls):
def test_validate_item_list(self):
order_files = ["order-SO5905", "order-SO5906", "order-SO5907"]
- items_list = [{"MC-100", "TITANIUM_WATCH"}, {"MC-100",}, {"MC-100", "TITANIUM_WATCH"}]
-
- for order_file, items in zip(order_files, items_list):
+ items_list = [
+ {"MC-100", "TITANIUM_WATCH"},
+ {
+ "MC-100",
+ },
+ {"MC-100", "TITANIUM_WATCH"},
+ ]
+
+ for order_file, items in zip(order_files, items_list, strict=True):
order = self.load_fixture(order_file)["saleOrderDTO"]
self.assertEqual(items, _sync_order_items(order, client=self.client))
diff --git a/ecommerce_integrations/unicommerce/tests/test_product.py b/ecommerce_integrations/unicommerce/tests/test_product.py
index 96c72e5b5..380488a13 100644
--- a/ecommerce_integrations/unicommerce/tests/test_product.py
+++ b/ecommerce_integrations/unicommerce/tests/test_product.py
@@ -1,6 +1,7 @@
-import frappe
import responses
+import frappe
+
from ecommerce_integrations.ecommerce_integrations.doctype.ecommerce_item import ecommerce_item
from ecommerce_integrations.unicommerce.constants import MODULE_NAME
from ecommerce_integrations.unicommerce.product import (
@@ -28,9 +29,7 @@ def test_import_missing_item_raises_error(self):
json=self.load_fixture("missing_item"),
match=[responses.json_params_matcher({"skuCode": "MISSING"})],
)
- self.assertRaises(
- frappe.ValidationError, import_product_from_unicommerce, "MISSING", self.client
- )
+ self.assertRaises(frappe.ValidationError, import_product_from_unicommerce, "MISSING", self.client)
log = frappe.get_last_doc("Ecommerce Integration Log", filters={"integration": "unicommerce"})
self.assertTrue("Failed to import" in log.message, "Logging for missing item not working")
diff --git a/ecommerce_integrations/unicommerce/tests/test_status.py b/ecommerce_integrations/unicommerce/tests/test_status.py
index 6d61564c5..8cdc6581c 100644
--- a/ecommerce_integrations/unicommerce/tests/test_status.py
+++ b/ecommerce_integrations/unicommerce/tests/test_status.py
@@ -15,7 +15,6 @@ def test_serialization(self):
_serialize_items([si_item.as_dict()])
def test_delete_cancelled_items(self):
-
item1 = frappe.new_doc("Sales Order Item").update({ORDER_ITEM_CODE_FIELD: "cancelled"})
item2 = frappe.new_doc("Sales Order Item").update({ORDER_ITEM_CODE_FIELD: "not cancelled"})
diff --git a/ecommerce_integrations/unicommerce/tests/utils.py b/ecommerce_integrations/unicommerce/tests/utils.py
index 3c0e06880..a3bcf6369 100644
--- a/ecommerce_integrations/unicommerce/tests/utils.py
+++ b/ecommerce_integrations/unicommerce/tests/utils.py
@@ -1,6 +1,7 @@
import copy
import json
import os
+import typing
import unittest
import frappe
@@ -12,7 +13,7 @@
class TestCase(unittest.TestCase):
- config = {
+ config: typing.ClassVar[dict] = {
"is_enabled": 1,
"enable_inventory_sync": 1,
"use_stock_entry_for_grn": 1,
@@ -70,9 +71,8 @@ def tearDownClass(cls):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
def load_fixture(self, name):
- with open(os.path.dirname(__file__) + f"/fixtures/{name}.json", "rb") as f:
- data = f.read()
- return json.loads(data)
+ fixture_path = os.path.join(os.path.dirname(__file__), "fixtures", f"{name}.json")
+ return frappe.get_file_json(fixture_path)
def _setup_test_item_categories():
diff --git a/ecommerce_integrations/unicommerce/utils.py b/ecommerce_integrations/unicommerce/utils.py
index 966c53eea..cd4d699a6 100644
--- a/ecommerce_integrations/unicommerce/utils.py
+++ b/ecommerce_integrations/unicommerce/utils.py
@@ -48,7 +48,7 @@ def force_sync(document) -> None:
def get_unicommerce_date(timestamp: int) -> datetime.date:
- """ Convert unicommerce ms timestamp to datetime."""
+ """Convert unicommerce ms timestamp to datetime."""
return datetime.date.fromtimestamp(timestamp // 1000)
diff --git a/ecommerce_integrations/utils/before_test.py b/ecommerce_integrations/utils/before_test.py
index 0622afa6b..e1d319d12 100644
--- a/ecommerce_integrations/utils/before_test.py
+++ b/ecommerce_integrations/utils/before_test.py
@@ -1,7 +1,8 @@
import frappe
-from erpnext.setup.utils import enable_all_roles_and_domains
from frappe.utils import now_datetime
+from erpnext.setup.utils import enable_all_roles_and_domains
+
def before_tests():
frappe.clear_cache()
diff --git a/ecommerce_integrations/zenoti/doctype/zenoti_settings/zenoti_settings.py b/ecommerce_integrations/zenoti/doctype/zenoti_settings/zenoti_settings.py
index afc23434d..da33db2d5 100644
--- a/ecommerce_integrations/zenoti/doctype/zenoti_settings/zenoti_settings.py
+++ b/ecommerce_integrations/zenoti/doctype/zenoti_settings/zenoti_settings.py
@@ -1,8 +1,9 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see LICENSE
-import frappe
import requests
+
+import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
diff --git a/ecommerce_integrations/zenoti/purchase_transactions.py b/ecommerce_integrations/zenoti/purchase_transactions.py
index e49840c8e..c4a66e0e0 100644
--- a/ecommerce_integrations/zenoti/purchase_transactions.py
+++ b/ecommerce_integrations/zenoti/purchase_transactions.py
@@ -28,9 +28,7 @@ def get_list_of_purchase_orders_for_center(center, date=None):
start_date = add_to_date(end_date, days=-1)
route = "inventory/purchase_orders?center_id="
url_end = "&show_delivery_details=true&date_criteria=1&status=-1"
- full_url = (
- api_url + route + center + "&start_date=" + start_date + "&end_date=" + end_date + url_end
- )
+ full_url = api_url + route + center + "&start_date=" + start_date + "&end_date=" + end_date + url_end
all_orders = make_api_call(full_url)
return all_orders
@@ -208,7 +206,9 @@ def add_items(doc, item_data):
invoice_item[key] = value
if key == "item_code":
item_code = frappe.db.get_value(
- "Item", {"zenoti_item_code": item["item_code"], "item_name": item["item_name"]}, "item_code"
+ "Item",
+ {"zenoti_item_code": item["item_code"], "item_name": item["item_name"]},
+ "item_code",
)
invoice_item["item_code"] = item_code
diff --git a/ecommerce_integrations/zenoti/sales_transactions.py b/ecommerce_integrations/zenoti/sales_transactions.py
index 5fe131b1e..a1ae3cf6d 100644
--- a/ecommerce_integrations/zenoti/sales_transactions.py
+++ b/ecommerce_integrations/zenoti/sales_transactions.py
@@ -250,9 +250,7 @@ def process_sales_line_items(invoice, cost_center, center):
if len(item_err_msg_list):
item_err_msg = "\n".join(err for err in item_err_msg_list)
err_msg_list.append(item_err_msg)
- emp_err_msg = check_for_employee(
- line_item["employee"]["name"], line_item["employee"]["code"], center
- )
+ emp_err_msg = check_for_employee(line_item["employee"]["name"], line_item["employee"]["code"], center)
if emp_err_msg:
err_msg_list.append(emp_err_msg)
sold_by = frappe.db.get_value(
@@ -449,9 +447,7 @@ def make_invoice(invoice_details):
doc.posting_time = invoice_details["posting_time"]
doc.due_date = invoice_details["posting_date"]
doc.cost_center = invoice_details["cost_center"]
- doc.selling_price_list = frappe.db.get_single_value(
- "Zenoti Settings", "default_selling_price_list"
- )
+ doc.selling_price_list = frappe.db.get_single_value("Zenoti Settings", "default_selling_price_list")
doc.set_warehouse = invoice_details["set_warehouse"]
doc.update_stock = 1
doc.rounding_adjustment = invoice_details["rounding_adjustment"]
diff --git a/ecommerce_integrations/zenoti/stock_reconciliation.py b/ecommerce_integrations/zenoti/stock_reconciliation.py
index 249374f71..6df6b1887 100644
--- a/ecommerce_integrations/zenoti/stock_reconciliation.py
+++ b/ecommerce_integrations/zenoti/stock_reconciliation.py
@@ -1,8 +1,9 @@
import frappe
-from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
from frappe import _
from frappe.utils import flt, now
+from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
+
from ecommerce_integrations.zenoti.utils import api_url, check_for_item, make_api_call
@@ -10,9 +11,7 @@ def process_stock_reconciliation(center, error_logs, date=None):
if not date:
date = now()
list_for_entry = []
- stock_quantities_of_products_in_a_center = retrieve_stock_quantities_of_products(
- center.name, date
- )
+ stock_quantities_of_products_in_a_center = retrieve_stock_quantities_of_products(center.name, date)
if stock_quantities_of_products_in_a_center:
cost_center = center.get("erpnext_cost_center")
if not cost_center:
@@ -31,7 +30,7 @@ def process_stock_reconciliation(center, error_logs, date=None):
def retrieve_stock_quantities_of_products(center, date):
- url = api_url + "inventory/stock?center_id={0}&inventory_date={1}".format(center, date)
+ url = api_url + f"inventory/stock?center_id={center}&inventory_date={date}"
stock_quantities_of_products = make_api_call(url)
return stock_quantities_of_products
@@ -81,7 +80,9 @@ def add_items_to_reconcile(doc, list_for_entry):
invoice_item[key] = value
if key == "item_code":
item_code = frappe.db.get_value(
- "Item", {"zenoti_item_code": item["item_code"], "item_name": item["item_name"]}, "item_code"
+ "Item",
+ {"zenoti_item_code": item["item_code"], "item_name": item["item_name"]},
+ "item_code",
)
invoice_item["item_code"] = item_code
doc.append("items", invoice_item)
diff --git a/ecommerce_integrations/zenoti/utils.py b/ecommerce_integrations/zenoti/utils.py
index 16b696927..97d72c6fc 100644
--- a/ecommerce_integrations/zenoti/utils.py
+++ b/ecommerce_integrations/zenoti/utils.py
@@ -1,12 +1,14 @@
import json
import math
-import frappe
import requests
-from erpnext.controllers.accounts_controller import add_taxes_from_tax_template
+
+import frappe
from frappe import _
from frappe.utils import cint, flt
+from erpnext.controllers.accounts_controller import add_taxes_from_tax_template
+
api_url = "https://api.zenoti.com/v1/"
item_type = {
@@ -80,9 +82,7 @@ def check_for_item(list_of_items, item_group, center=None):
def make_item(item, item_group, center=None):
item_details, center = get_item_details(item, item_group, center)
if not item_details:
- err_msg = _("Details for Item {0} does not exist in Zenoti").format(
- frappe.bold(item["item_name"])
- )
+ err_msg = _("Details for Item {0} does not exist in Zenoti").format(frappe.bold(item["item_name"]))
return err_msg
create_item(item, item_details, item_group, center)
@@ -276,7 +276,7 @@ def get_state(country_id, state_id):
if list_of_states_of_the_country:
for states in list_of_states_of_the_country["states"]:
if states["id"] == state_id:
- state == states
+ state = states
return state
diff --git a/pyproject.toml b/pyproject.toml
index c11448c3f..eefc96dd7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,15 +4,23 @@ description='Ecommerce integrations for ERPNext'
authors = [
{ name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"}
]
-requires-python = ">=3.10"
+requires-python = ">=3.10,<3.15"
readme = "./README.md"
dynamic = ["version"]
dependencies = [
"ShopifyAPI==12.4.0", # update after resolving pyjwt conflict in frappe
- "boto3~=1.28.10",
+ "boto3~=1.34.143",
]
+[project.urls]
+Repository = "https://github.com/frappe/ecommerce_integrations.git"
+"Bug Reports" = "https://github.com/frappe/ecommerce_integrations/issues"
+
+[tool.bench.frappe-dependencies]
+frappe = ">=15.0.0,<16.0.0"
+erpnext = ">=15.0.0,<16.0.0"
+
[project.license]
file = "./LICENSE"
@@ -20,14 +28,48 @@ file = "./LICENSE"
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
-[tool.black]
-line-length = 99
-
-[tool.isort]
-line_length = 99
-multi_line_output = 3
-include_trailing_comma = true
-force_grid_wrap = 0
-use_parentheses = true
-ensure_newline_before_comments = true
-indent = " "
+[tool.ruff]
+line-length = 110
+target-version = "py310"
+
+[tool.ruff.lint]
+select = ["F", "E", "W", "I", "UP", "B", "RUF"]
+ignore = [
+ "B017", # assertRaises(Exception) - should be more specific
+ "B018", # useless expression, not assigned to anything
+ "B023", # function doesn't bind loop variable - will have last iteration's value
+ "B904", # raise inside except without from
+ "E101", # indentation contains mixed spaces and tabs
+ "E402", # module level import not at top of file
+ "E501", # line too long
+ "E741", # ambiguous variable name
+ "F401", # "unused" imports
+ "F403", # can't detect undefined names from * import
+ "F405", # can't detect undefined names from * import
+ "F722", # syntax error in forward type annotation
+ "W191", # indentation contains tabs
+ "RUF001", # string contains ambiguous unicode character
+]
+typing-modules = ["frappe.types.DF"]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "tab"
+docstring-code-format = true
+
+[tool.ruff.lint.isort.sections]
+"frappe" = ["frappe"]
+"erpnext" = ["erpnext"]
+"ecommerce_integrations" = ["ecommerce_integrations"]
+
+[tool.ruff.lint.isort]
+section-order = [
+ "future",
+ "standard-library",
+ "third-party",
+ "frappe",
+ "erpnext",
+ "ecommerce_integrations",
+ "first-party",
+ "local-folder",
+]