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", +]