From b94c6bbafbef5b046f31d8c9cc3201d0115646de Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 10 Jun 2024 18:57:36 +1200 Subject: [PATCH 01/87] Create the OpenStack Odoo Client library for Python --- .github/workflows/release.yml | 75 ++ .github/workflows/test.yml | 114 ++ .pre-commit-config.yaml | 33 + README.md | 995 +++++++++++++++++- openstack_odooclient/__init__.py | 98 ++ openstack_odooclient/client.py | 315 ++++++ openstack_odooclient/exceptions.py | 35 + openstack_odooclient/managers/__init__.py | 0 openstack_odooclient/managers/account_move.py | 187 ++++ .../managers/account_move_line.py | 154 +++ openstack_odooclient/managers/company.py | 108 ++ openstack_odooclient/managers/credit.py | 122 +++ .../managers/credit_transaction.py | 61 ++ openstack_odooclient/managers/credit_type.py | 128 +++ openstack_odooclient/managers/crm_team.py | 28 + openstack_odooclient/managers/currency.py | 68 ++ .../managers/customer_group.py | 84 ++ openstack_odooclient/managers/grant.py | 101 ++ openstack_odooclient/managers/grant_type.py | 130 +++ openstack_odooclient/managers/partner.py | 323 ++++++ .../managers/partner_category.py | 114 ++ openstack_odooclient/managers/pricelist.py | 158 +++ openstack_odooclient/managers/product.py | 373 +++++++ .../managers/product_category.py | 90 ++ openstack_odooclient/managers/project.py | 379 +++++++ .../managers/project_contact.py | 94 ++ .../managers/record/__init__.py | 30 + openstack_odooclient/managers/record/base.py | 324 ++++++ .../managers/record/manager_base.py | 498 +++++++++ .../managers/record/manager_code_base.py | 186 ++++ .../managers/record/manager_name_base.py | 186 ++++ .../record/manager_unique_field_base.py | 223 ++++ openstack_odooclient/managers/record/util.py | 132 +++ .../managers/referral_code.py | 121 +++ openstack_odooclient/managers/reseller.py | 118 +++ .../managers/reseller_tier.py | 100 ++ openstack_odooclient/managers/sale_order.py | 186 ++++ .../managers/sale_order_line.py | 374 +++++++ .../managers/support_subscription.py | 150 +++ .../managers/support_subscription_type.py | 101 ++ openstack_odooclient/managers/tax.py | 122 +++ openstack_odooclient/managers/tax_group.py | 28 + .../managers/term_discount.py | 140 +++ openstack_odooclient/managers/trial.py | 73 ++ openstack_odooclient/managers/uom.py | 102 ++ openstack_odooclient/managers/uom_category.py | 50 + openstack_odooclient/managers/user.py | 84 ++ .../managers/volume_discount_range.py | 136 +++ openstack_odooclient/managers/voucher_code.py | 229 ++++ openstack_odooclient/py.typed | 0 pdm.lock | 130 +++ pyproject.toml | 120 +++ 52 files changed, 8308 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .pre-commit-config.yaml create mode 100644 openstack_odooclient/__init__.py create mode 100644 openstack_odooclient/client.py create mode 100644 openstack_odooclient/exceptions.py create mode 100644 openstack_odooclient/managers/__init__.py create mode 100644 openstack_odooclient/managers/account_move.py create mode 100644 openstack_odooclient/managers/account_move_line.py create mode 100644 openstack_odooclient/managers/company.py create mode 100644 openstack_odooclient/managers/credit.py create mode 100644 openstack_odooclient/managers/credit_transaction.py create mode 100644 openstack_odooclient/managers/credit_type.py create mode 100644 openstack_odooclient/managers/crm_team.py create mode 100644 openstack_odooclient/managers/currency.py create mode 100644 openstack_odooclient/managers/customer_group.py create mode 100644 openstack_odooclient/managers/grant.py create mode 100644 openstack_odooclient/managers/grant_type.py create mode 100644 openstack_odooclient/managers/partner.py create mode 100644 openstack_odooclient/managers/partner_category.py create mode 100644 openstack_odooclient/managers/pricelist.py create mode 100644 openstack_odooclient/managers/product.py create mode 100644 openstack_odooclient/managers/product_category.py create mode 100644 openstack_odooclient/managers/project.py create mode 100644 openstack_odooclient/managers/project_contact.py create mode 100644 openstack_odooclient/managers/record/__init__.py create mode 100644 openstack_odooclient/managers/record/base.py create mode 100644 openstack_odooclient/managers/record/manager_base.py create mode 100644 openstack_odooclient/managers/record/manager_code_base.py create mode 100644 openstack_odooclient/managers/record/manager_name_base.py create mode 100644 openstack_odooclient/managers/record/manager_unique_field_base.py create mode 100644 openstack_odooclient/managers/record/util.py create mode 100644 openstack_odooclient/managers/referral_code.py create mode 100644 openstack_odooclient/managers/reseller.py create mode 100644 openstack_odooclient/managers/reseller_tier.py create mode 100644 openstack_odooclient/managers/sale_order.py create mode 100644 openstack_odooclient/managers/sale_order_line.py create mode 100644 openstack_odooclient/managers/support_subscription.py create mode 100644 openstack_odooclient/managers/support_subscription_type.py create mode 100644 openstack_odooclient/managers/tax.py create mode 100644 openstack_odooclient/managers/tax_group.py create mode 100644 openstack_odooclient/managers/term_discount.py create mode 100644 openstack_odooclient/managers/trial.py create mode 100644 openstack_odooclient/managers/uom.py create mode 100644 openstack_odooclient/managers/uom_category.py create mode 100644 openstack_odooclient/managers/user.py create mode 100644 openstack_odooclient/managers/volume_discount_range.py create mode 100644 openstack_odooclient/managers/voucher_code.py create mode 100644 openstack_odooclient/py.typed create mode 100644 pdm.lock create mode 100644 pyproject.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..24cd544 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,75 @@ +name: release + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: Clone full tree, and checkout tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Python and PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: "3.12" + version: "2.15.4" + - name: Build source dist and wheels + run: pdm build --verbose + - name: Upload source dist and wheels to artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 5 + if-no-files-found: error + + publish-pypi: + needs: build + runs-on: ubuntu-22.04 + environment: pypi + permissions: + id-token: write + steps: + - name: Clone full tree, and checkout tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download source dist and wheels from artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Setup Python and PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: "3.12" + version: "2.15.4" + - name: Publish source dist and wheels to PyPI + run: pdm publish --no-build --verbose + + publish-github-release: + needs: build + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - name: Clone and checkout tag + uses: actions/checkout@v4 + - name: Download source dist and wheels from artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish source dist and wheels to GitHub Release + uses: xresloader/upload-to-github-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + file: dist/* + release_id: ${{ github.event.release.id }} + overwrite: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c84ae3e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,114 @@ +name: test + +on: + pull_request: + push: + branches: + - main + +jobs: + pre-commit: + runs-on: ubuntu-22.04 + steps: + - name: Clone and checkout branch + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 + + build: + needs: pre-commit + runs-on: ubuntu-22.04 + steps: + - name: Clone full tree, and checkout branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Python and PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: "3.12" + version: "2.15.4" + - name: Build source dist and wheels + run: pdm build --verbose + - name: Upload source dist and wheels to artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 5 + if-no-files-found: error + + # test: + # needs: pre-commit + # permissions: + # checks: write + # strategy: + # fail-fast: false + # matrix: + # operating_system: + # - ubuntu-22.04 + # - windows-2022 + # python_version: + # - "3.8" + # - "3.9" + # - "3.10" + # - "3.11" + # - "3.12" + # runs-on: ${{ matrix.operating_system }} + # steps: + # - name: Clone full tree, and checkout branch + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # - name: Setup Python and PDM + # uses: pdm-project/setup-pdm@v4 + # with: + # python-version: ${{ matrix.python_version }} + # version: "2.15.4" + # - name: Create virtual environment + # run: pdm install + # - name: Run tests + # run: pdm run test + # - name: Publish test results + # uses: mikepenz/action-junit-report@v4 + # # Always run, even if the tests fail. + # if: success() || failure() + # with: + # report_paths: rspec.xml + # - name: Upload coverage report to artifacts + # uses: actions/upload-artifact@v4 + # # Always run, even if the tests fail. + # if: success() || failure() + # with: + # name: coverage-${{ matrix.operating_system}}-${{ matrix.python_version }} + # path: coverage.xml + # retention-days: 5 + # if-no-files-found: warn + + # coverage: + # needs: test + # # Always run, even if the test job failed. + # if: success() || failure() + # runs-on: ubuntu-22.04 + # steps: + # - name: Download coverage reports from artifacts + # uses: actions/download-artifact@v4 + # with: + # pattern: coverage-* + # merge-multiple: false + # - name: Generate code coverage summary report + # uses: irongut/CodeCoverageSummary@v1.3.0 + # with: + # filename: coverage-*/coverage.xml + # format: markdown + # hide_branch_rate: false + # hide_complexity: false + # indicators: false + # output: both + # - name: Write to job summary + # run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f4b7c30 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +--- +# .pre-commit-config.yml +# Pre-commit hook tasks. +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.5.0" + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + - id: end-of-file-fixer + - id: detect-private-key + - id: check-added-large-files + - id: check-merge-conflict + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.4.8" + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.10.0" + hooks: + - id: mypy + additional_dependencies: + - OdooRPC>=0.9.0 + - packaging + - typing-extensions>=4.0.0 + - repo: https://github.com/pdm-project/pdm + rev: "2.15.4" + hooks: + - id: pdm-lock-check diff --git a/README.md b/README.md index 5cbe30c..7b572df 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,993 @@ -# python-openstack-odooclient -Python client library for Odoo and the OpenStack integration add-on. +# OpenStack Odoo Client library for Python + +This is an Odoo client library for Python with support for the +[OpenStack Integration add-on](https://github.com/catalyst-cloud/odoo-openstack-integration), +intended to be used by OpenStack projects such as +[Distil](https://github.com/catalyst-cloud/distil). + +This library provides a higher level interface than [OdooRPC](https://pythonhosted.org/OdooRPC) +(which is used internally), and is intended to make it possible to develop applications against +a common API, without having to take into account considerations such as backward-incompatible +changes between Odoo versions. + +## Installation + +To install the library package, simply install `openstack-odooclient` using `pip`. + +```python +python -m pip install openstack-odooclient +``` + +## Connecting to Odoo + +To connect to an Odoo server, create an `openstack_odooclient.Client` object and +pass the connection details to it. + +This is the recommended way of creating the Odoo client object, +as it provides some extra parameters for convenience. + +```python +from openstack_odooclient import Client as OdooClient + +odoo_client = OdooClient( + hostname="localhost", + port=8069, + protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. + database="odoodb", + user="test-user", + password="", + # version="14.0", # Optionally specify the server version. Default is to auto-detect. + # verify=True, # Enable/disable SSL verification, or pass the path to a CA certificate. +) +``` + +If you have a pre-existing `odoorpc.ODOO` connection object, that can instead +be passed directly into `openstack_odooclient.Client`. + +```python +from odoorpc import ODOO +from openstack_odooclient import Client as OdooClient + +odoo = ODOO( + host="localhost", + port=8069, + protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. + # version="14.0", # Optionally specify the server version. Default is to auto-detect. +) +odoo.login("odoodb", "test-user", "") + +odoo_client = OdooClient(odoo=odoo) +``` + +## Managers + +The Odoo Client object exposes a number of record managers, which contain methods +used to query specific record types, or create one or more new records of that type. + +For example, performing a simple search query would look something like this: + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search([("id", "=", odoo_client.user_id)], as_id=True) +[1234] +``` + +### Available Managers + +* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) +* `account_move_lines` - Account Move (Invoice) Lines (Odoo Model: `account.move.line`) +* `companies` - Companies (Odoo Model: `res.company`) +* `credits` - OpenStack Credits (Odoo Model: `openstack.credit`) +* `credit_transactions` - OpenStack Credit Transactions (Odoo Model: `openstack.credit.transaction`) +* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) +* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) +* `currencies` - Currencies (Odoo Model: `res.currency`) +* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) +* `grants` - OpenStack Grants (Odoo Model: `openstack.grant`) +* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) +* `partners` - Partners (Odoo Model: `res.partner`) +* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) +* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) +* `products` - Products (Odoo Model: `product.product`) +* `product_categories` - Product Categories (Odoo Model: `product.category`) +* `projects` - OpenStack Projects (Odoo Model: `openstack.project`) +* `project_contacts` - OpenStack Project Contacts (Odoo Model: `openstack.project_contact`) +* `referral_codes` - OpenStack Referral Codes (Odoo Model: `openstack.referral_code`) +* `resellers` - OpenStack Resellers (Odoo Model: `openstack.reseller`) +* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) +* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) +* `sale_order_lines` - Sale Order Lines (Odoo Model: `sale.order.line`) +* `support_subscriptions` - OpenStack Support Subscriptions (Odoo Model: `openstack.support_subscription`) +* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) +* `taxes` - Taxes (Odoo Model: `account.tax`) +* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) +* `term_discounts` - OpenStack Term Discounts (Odoo Model: `openstack.term_discount`) +* `trials` - OpenStack Trials (Odoo Model: `openstack.trial`) +* `uoms` - Units of Measure (UoM) (Odoo Model: `uom.uom`) +* `uom_category` - Unit of Measure (UoM) Categories (Odoo Model: `uom.category`) +* `users` - Users (Odoo Model: `res.user`) +* `volume_discount_ranges` - OpenStack Volume Discount Ranges (Odoo Model: `openstack.volume_discount_range`) +* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) + +### Common Methods + +#### `list` + +```python +list( + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = False, +) -> list[Record] +``` + +```python +list( + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = True, +) -> list[dict[str, Any]] +``` + +Get one or more specific records by ID. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234) +[User(record={'id': 1234, ...}, fields=None)] +>>> odoo_client.users.list([1234, 5678]) +[User(record={'id': 1234, ...}, fields=None), User(record={'id': 5678, ...}, fields=None)] +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234, fields={"ids"}) +[User(record={'id': 1234}, fields=['ids'])] +``` + +Use the `as_dict` parameter to return records as `dict` +objects, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234, as_dict=True) +[{'id': 1234, ...}] +``` + +If `ids` is given an empty iterator, this method +returns an empty list. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list([]) +[] +``` + +##### Parameters + +| Name | Type | Description | Default | +|-----------|-------------------------|---------------------------------------------------|------------| +| `ids` | `int \| Iterable[int]` | Record ID, or list of record IDs | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | + +##### Returns + +| Type | Description | +|------------------------|------------------------------------------------| +| `list[Record]` | Record objects (when `as_dict` is `False`) | +| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | + +#### `get` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = False, +) -> Record +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = True, +) -> Record | None +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Get a single record by ID. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234) +User(record={'id': 1234, ...}, fields=None) +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234, fields={"ids"}) +User(record={'id': 1234}, fields=['ids']) +``` + +Use the ``as_dict`` parameter to return the record as +a ``dict`` object, instead of a record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234, as_dict=True) +{'id': 1234, ...} +``` + +##### Parameters + +| Name | Type | Description | Default | +|------------|-------------------------|---------------------------------------------------|------------| +| `id` | `int` | Record ID | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +##### Raises + +| Type | Description | +|-----------------------|--------------------------------------------------------------------| +| `RecordNotFoundError` | If the given record ID does not exist (when `optional` is `False`) | + +##### Returns + +| Type | Description | +|------------------|-------------------------------------------------------------| +| `Record` | Record object (when `as_dict` is `False`) | +| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | +| `None` | If the record ID does not exist (when `optional` is `True`) | + +#### `search` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> list[Record] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> list[Record] | None +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> list[int] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> list[int] | None +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> list[dict[str, Any]] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> list[dict[str, Any]] | None +``` + +Query the ERP for records, optionally defining +filters to constrain the search and other parameters, +and return the results. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search([("id", "=", 1234)]) +[User(record={'id': 1234, ...}, fields=None)] +``` + +Query filters should be defined using the same format as OdooRPC, +but some additional features are supported: + +* Odoo client field aliases can be specified as the field name, + in additional to the original field name on the Odoo model + (e.g. `create_user` instead of `create_uid`). +* Record objects can be directly passed as the value + on a filter, where a record ID would normally be expected. +* Sets and tuples are supported when specifying a range of values, + in addition to lists. + +To search *all* records, leave ``filters`` unset +(or set it to ``None``). + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search() +[User(record={'id': 1234, ...}, fields=None), ...] +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(fields={"ids"}) +[User(record={'id': 1234}, fields=['ids']), ...] +``` + +Use the `as_id` parameter to return the record as +a list of IDs, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(as_id=True) +[1234, ...] +``` + +Use the `as_dict` parameter to return the record as +a list of `dict` objects, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(as_dict=True) +[{'id': 1234, ...}, ...] +``` + +##### Parameters + +| Name | Type | Description | Default | +|-----------|-------------------------|---------------------------------------------------|---------| +| `filters` | `Sequence[Any] \| None` | Filters to query by (or `None` for no filters) | `None` | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `order` | `str \| None` | Field to order results by, if ordering results | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | + +##### Returns + +| Type | Description | +|------------------------|------------------------------------------------| +| `list[Record]` | Record objects (default) | +| `list[int]` | Record IDs (when `as_id` is `True`) | +| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | + +#### `create` + +```python +create(**fields: Any) -> int +``` + +Create a new record, using the specified keyword arguments +as input fields. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.create(...) +1234 +``` + +To fetch the newly created record object, +pass the returned ID to the [``get``](#get) method. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_order_lines.get( +... odoo_client.sales_order_lines.create(...), +... ) +SaleOrderLine(record={'id': 1234, ...}, fields=None) +``` + +##### Parameters + +| Name | Type | Description | Default | +|------------|-------|-----------------------------------------|------------| +| `**fields` | `Any` | Record field values (keyword arguments) | (required) | + +##### Returns + +| Type | Description | +|-------|------------------------------------| +| `int` | The ID of the newly created record | + +#### `create_multi` + +```python +create_multi(*records: Mapping[str, Any]) -> list[int] +``` + +Create one or more new records in a single request, +passing in the mappings containing the record's input fields +as positional arguments. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.create_multi({...}, {...}) +[1234, 1235] +``` + +To fetch the newly created record objects, +pass the returned IDs to the [``list``](#list) method. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_order_lines.list( +... odoo_client.sales_order_lines.create_multi({...}, {...}), +... ) +[SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] +``` + +##### Parameters + +| Name | Type | Description | Default | +|------------|---------------------|----------------------------------------------------|------------| +| `*records` | `Mapping[str, Any]` | Record field-value mappings (positional arguments) | (required) | + +##### Returns + +| Type | Description | +|-------------|--------------------------------------| +| `list[int]` | The IDs of the newly created records | + +#### `unlink`/`delete` + +```python +unlink(*records: Record | int | Iterable[Record | int]) -> None +``` + +```python +delete(*records: Record | int | Iterable[Record | int]) -> None +``` + +Delete one or more records from Odoo. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.unlink(1234) +``` + +This method accepts either a record object or ID, or an iterable of +either of those types. Multiple positional arguments are allowed. + +All specified records will be deleted in a single request. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> line1 = odoo_client.sales_order_lines.get(1234) +>>> line2 = odoo_client.sales_order_lines.get(5678) +>>> odoo_client.sales_order_lines.unlink(line1, 9012, [line2, 3456]) +``` + +##### Parameters + +| Name | Type | Description | Default | +|------------|--------------------------------------------|------------------------------------------------------------------------------|------------| +| `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | + +### Managers for Named Records + +Some record types have a name field that is generally expected to be unique. +The managers for these record types have additional methods for querying records by name. + +* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) +* `companies` - Companies (Odoo Model: `res.company`) +* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) +* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) +* `currencies` - Currencies (Odoo Model: `res.currency`) +* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) +* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) +* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) +* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) +* `product_categories` - Product Categories (Odoo Model: `product.category`) +* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) +* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) +* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) +* `taxes` - Taxes (Odoo Model: `account.tax`) +* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) +* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) + +#### `get_by_name` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> Record +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> Record | None +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> int +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> int | None +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Query a unique record by name. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD") +[Currency(record={'id': 1234, 'name': 'NZD', ...}, fields=None)] +``` + +A number of parameters are available to configure the return type, +and what happens when a result is not found. + +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", fields={"rounding"}) +Currency(record={'id': 1234, 'rounding': 0.001}, fields=['rounding']) +``` + +Use the `as_id` parameter to return the ID of the record, +instead of the record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", as_id=True) +1234 +``` + +Use the `as_dict` parameter to return the record as +a `dict` object, instead of a record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", as_dict=True) +{'id': 1234, ...} +``` + +When `optional` is `True`, `None` is returned if a record +with the given name does not exist, instead of raising an error. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("non-existent", optional=True) +None +``` + +##### Parameters + +| Name | Type | Description | Default | +|------------|-------------------------|---------------------------------------------------|------------| +| `name` | `str` | The record name | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +##### Raises + +| Type | Description | +|-----------------------------|-------------------------------------------------------------------------| +| `RecordNotFoundError` | If no record with the given name was found (when `optional` is `False`) | +| `MultipleRecordsFoundError` | If multiple records were found with the same name | + +##### Returns + +| Type | Description | +|------------------|----------------------------------------------------------------------------| +| `Record` | Record object (default) | +| `int` | Record ID (when `as_id` is `True`) | +| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | +| `None` | If a record with the given name does not exist (when `optional` is `True`) | + +### Account Move Lines + +### Core Managers + +The following managers are used to interact with core Odoo data structures. + +* `odooclient.Client.sale_order` +* `odooclient.Client.sale_order_lines` +* [`odooclient.Client.account_moves`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/account_moves.py) +* `odooclient.Client.account_move_lines` +* [`odooclient.Client.partners`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/partners.py) +* `odooclient.Client.price_lists` +* [`odooclient.Client.products`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/products.py) +* `odooclient.Client.countries` +* `odooclient.Client.mail_messages` +* `odooclient.Client.sales_teams` + +### OpenStack Managers + +The following OpenStack-related managers are available. + +* [`odooclient.Client.projects`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/projects.py) +* [`odooclient.Client.project_contacts`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/project_contacts.py) +* `odooclient.Client.credits` +* `odooclient.Client.credit_transactions` +* `odooclient.Client.credit_types` +* `odooclient.Client.customer_groups` +* `odooclient.Client.grants` +* `odooclient.Client.grant_types` +* `odooclient.Client.referrals` +* `odooclient.Client.resellers` +* `odooclient.Client.reseller_tiers` +* `odooclient.Client.support_subscriptions` +* `odooclient.Client.term_discounts` +* `odooclient.Client.trials` +* [`odooclient.Client.volume_discount_ranges`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/volume_discount_ranges.py) +* `odooclient.Client.voucher_codes` + +### Common Methods + +The following methods are available on every manager object. + +#### `get(ids: int | list[int] | tuple[int], read: bool = False, fields: list[str] | None = None) -> RecordSet` + +Get one or more `Resource` objects by ID. + +Args: + +* `ids` (`int | list[int] | tuple[int]`): Resource ID. Can be a single ID, or a list of IDs. +* `read` (`bool`): Read objects back as a `dict`. Default is `False`. +* `fields` (`list[str] | None`): A list of field names to include in a read. Default is `None`. + +Returns: + +A collection of resources + +#### `list(filters: list[tuple[Any, ...]] | None = None, get: bool = True, fields: list[str] | None = None, **kwargs) -> RecordSet` + +Get a list of `Resource` objects, or resource IDs, by filter. + +Args: + +* `filters` (`list[tuple[Any, ...]] | None`): A list of search option tuples, e.g. `[('field', '=', value)]`. +* `get` (`bool`): Fetch whole objects instead of just IDs. Default is `True`. +* `fields` (`list[str] | None`): A list of field names to include in a read. Default is `None`. +* `kwargs` (`Mapping[str, str]`): Direct field comparisons to be used as filters. Ignored if `filters` is defined. + +Returns: + +A collection of resources + +#### `create(**fields) -> Resource` + +Create a `Resource`, with the parameters to the function call used as resource fields. + +Returns: + +The created resource object + +#### `create_many(resources: list[dict[str, Any]]) -> RecordSet` + +Create multiple new `Resource` objects. + +Args: + +* `resources` (`list[dict[str, Any]]`): List of resources (in dictionary form) to create. + +Returns: + +The created resource objects + +#### `load(fields: list[str], rows: list[list[str]]) -> Resource` + +Load in a `Resource`. + +Args: + +* `fields` (`list[str]`): Fields to import. +* `rows` (`list[list[str]]`): The item data to import. + +Returns: + +The loaded resource object + +#### `delete(ids: int | list[int] | tuple[int]) -> bool` + +Delete one or more `Resource` objects by ID. + +Args: + +* `ids` (`int | list[int] | tuple[int]`): Resource ID, or list of IDs to delete. + +Returns: + +`True` if the resources were deleted (or already deleted), otherwise `False` diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py new file mode 100644 index 0000000..1070e7e --- /dev/null +++ b/openstack_odooclient/__init__.py @@ -0,0 +1,98 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from .client import Client +from .exceptions import ( + ClientError, + MultipleRecordsFoundError, + RecordNotFoundError, +) +from .managers.account_move import AccountMove +from .managers.account_move_line import AccountMoveLine +from .managers.company import Company +from .managers.credit import Credit +from .managers.credit_transaction import CreditTransaction +from .managers.credit_type import CreditType +from .managers.crm_team import CrmTeam +from .managers.currency import Currency +from .managers.customer_group import CustomerGroup +from .managers.grant import Grant +from .managers.grant_type import GrantType +from .managers.partner import Partner +from .managers.partner_category import PartnerCategory +from .managers.pricelist import Pricelist +from .managers.product import Product +from .managers.product_category import ProductCategory +from .managers.project import Project +from .managers.project_contact import ProjectContact +from .managers.referral_code import ReferralCode +from .managers.reseller import Reseller +from .managers.reseller_tier import ResellerTier +from .managers.sale_order import SaleOrder +from .managers.sale_order_line import SaleOrderLine +from .managers.support_subscription import SupportSubscription +from .managers.support_subscription_type import SupportSubscriptionType +from .managers.tax import Tax +from .managers.tax_group import TaxGroup +from .managers.term_discount import TermDiscount +from .managers.trial import Trial +from .managers.uom import Uom +from .managers.uom_category import UomCategory +from .managers.user import User +from .managers.volume_discount_range import VolumeDiscountRange +from .managers.voucher_code import VoucherCode + +__all__ = [ + "Client", + "ClientError", + "MultipleRecordsFoundError", + "RecordNotFoundError", + "AccountMove", + "AccountMoveLine", + "Company", + "Credit", + "CreditTransaction", + "CreditType", + "CrmTeam", + "Currency", + "CustomerGroup", + "Grant", + "GrantType", + "Partner", + "PartnerCategory", + "Pricelist", + "Product", + "ProductCategory", + "Project", + "ProjectContact", + "ReferralCode", + "Reseller", + "ResellerTier", + "SaleOrder", + "SaleOrderLine", + "SupportSubscription", + "SupportSubscriptionType", + "Tax", + "TaxGroup", + "TermDiscount", + "Trial", + "Uom", + "UomCategory", + "User", + "VolumeDiscountRange", + "VoucherCode", +] diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py new file mode 100644 index 0000000..47c86c3 --- /dev/null +++ b/openstack_odooclient/client.py @@ -0,0 +1,315 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import ssl +import urllib.request + +from pathlib import Path +from typing import TYPE_CHECKING, overload + +from odoorpc import ODOO # type: ignore[import] +from packaging.version import Version + +from .managers import ( + account_move, + account_move_line, + company, + credit, + credit_transaction, + credit_type, + crm_team, + currency, + customer_group, + grant, + grant_type, + partner, + partner_category, + pricelist, + product, + product_category, + project, + project_contact, + referral_code, + reseller, + reseller_tier, + sale_order, + sale_order_line, + support_subscription, + support_subscription_type, + tax, + tax_group, + term_discount, + trial, + uom, + uom_category, + user, + volume_discount_range, + voucher_code, +) + +if TYPE_CHECKING: + from typing import Literal, Optional, Union + + from odoorpc.db import DB # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + from odoorpc.report import Report # type: ignore[import] + + +class Client: + """A client class for managing the OpenStack Odoo ERP. + + Connect to an Odoo server by either passing the required + connection and authentication information, + or passing in a pre-existing OdooRPC ``ODOO`` object. + + When connecting to an Odoo server using SSL, set ``protocol`` + to ``jsonrpc+ssl``. SSL certificate verification can be disabled + by setting ``verify`` to ``False``. If a custom CA certificate + is required to verify the Odoo server's host certificate, + this can be configured by passing the certificate path to ``verify``. + + All parameters must be specified as keyword arguments. + + :param hostname: Server hostname, required if ``odoo`` is not set + :type hostname: Optional[str], optional + :param database: Database name, required if ``odoo`` is not set + :type database: Optional[str], optional + :param username: Username, required if ``odoo`` is not set + :type username: Optional[str], optional + :param password: Password (or API key), required if ``odoo`` is not set + :type password: Optional[str], optional + :param protocol: Communication protocol, defaults to ``jsonrpc`` + :type protocol: str, optional + :param port: Access port, defaults to ``8069`` + :type port: int, optional + :param verify: Configure SSL cert verification, defaults to ``True`` + :type verify: Union[bool, Path, str] + :param version: Server version, defaults to ``None`` (auto-detect) + :type version: Optional[str], optional + """ + + @overload + def __init__( + self, + *, + hostname: Optional[str] = ..., + database: Optional[str] = ..., + username: Optional[str] = ..., + password: Optional[str] = ..., + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: ODOO, + ) -> None: ... + + @overload + def __init__( + self, + *, + hostname: str, + database: str, + username: str, + password: str, + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: Literal[None] = ..., + ) -> None: ... + + @overload + def __init__( + self, + *, + hostname: Optional[str] = ..., + database: Optional[str] = ..., + username: Optional[str] = ..., + password: Optional[str] = ..., + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: Optional[ODOO] = ..., + ) -> None: ... + + def __init__( + self, + *, + hostname: Optional[str] = None, + database: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = True, + version: Optional[str] = None, + odoo: Optional[ODOO] = None, + ) -> None: + # If an OdooRPC object is provided, use that directly. + # Otherwise, make a new one with the provided settings. + if odoo: + self._odoo = odoo + else: + opener = None + if protocol.endswith("+ssl"): + ssl_verify = verify is not False + ssl_cafile = ( + str(verify) if isinstance(verify, (Path, str)) else None + ) + if not ssl_verify or ssl_cafile: + ssl_context = ssl.create_default_context(cafile=ssl_cafile) + if not ssl_verify: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + opener = urllib.request.build_opener( + urllib.request.HTTPSHandler(context=ssl_context), + urllib.request.HTTPCookieProcessor(), + ) + self._odoo = ODOO( + protocol=protocol, + host=hostname, + port=port, + version=version, + opener=opener, + ) + self._odoo.login(database, username, password) + # Create record managers. + self.account_moves = account_move.AccountMoveManager(self) + """Account Move (Invoice) manager.""" + self.account_move_lines = account_move_line.AccountMoveLineManager( + self, + ) + """Company manager.""" + self.companies = company.CompanyManager(self) + """Account Move (Invoice) Line manager.""" + self.credits = credit.CreditManager(self) + """Credit manager.""" + self.credit_transactions = credit_transaction.CreditTransactionManager( + self + ) + """Credit Transaction manager.""" + self.credit_types = credit_type.CreditTypeManager(self) + """Credit Type manager.""" + self.crm_teams = crm_team.CrmTeamManager(self) + """Customer Relations Management (CRM) Team manager.""" + self.currencies = currency.CurrencyManager(self) + """Currency manager.""" + self.customer_groups = customer_group.CustomerGroupManager(self) + """Customer Group manager.""" + self.grants = grant.GrantManager(self) + """Grant manager.""" + self.grant_types = grant_type.GrantTypeManager(self) + """Grant Type manager.""" + self.partners = partner.PartnerManager(self) + """Partner manager.""" + self.partner_categories = partner_category.PartnerCategoryManager( + self, + ) + """Partner Category manager.""" + self.pricelists = pricelist.PricelistManager(self) + """Pricelist manager.""" + self.products = product.ProductManager(self) + """Product manager.""" + self.product_categories = product_category.ProductCategoryManager( + self, + ) + """Product Category manager.""" + self.projects = project.ProjectManager(self) + """OpenStack Project manager.""" + self.project_contacts = project_contact.ProjectContactManager(self) + """Project Contact manager.""" + self.referral_codes = referral_code.ReferralCodeManager(self) + """Referral Code manager.""" + self.resellers = reseller.ResellerManager(self) + """Reseller manager.""" + self.reseller_tiers = reseller_tier.ResellerTierManager(self) + """Reseller Tier manager.""" + self.sale_orders = sale_order.SaleOrderManager(self) + """Sale Order manager.""" + self.sale_order_lines = sale_order_line.SaleOrderLineManager(self) + """Sale Order Line manager.""" + self.support_subscriptions = ( + support_subscription.SupportSubscriptionManager(self) + ) + """Support Subscription manager.""" + self.support_subscription_types = ( + support_subscription_type.SupportSubscriptionTypeManager(self) + ) + self.taxes = tax.TaxManager(self) + """Tax manager.""" + self.tax_groups = tax_group.TaxGroupManager(self) + """Tax Group manager.""" + """Support Subscription Type manager.""" + self.term_discounts = term_discount.TermDiscountManager(self) + """Term Discount manager.""" + self.trials = trial.TrialManager(self) + """Trial manager.""" + self.uoms = uom.UomManager(self) + """Unit of Measure (UoM) manager.""" + self.uom_categories = uom_category.UomCategoryManager(self) + """Unit of Measure (UoM) Category manager.""" + self.users = user.UserManager(self) + """User manager.""" + self.volume_discount_ranges = ( + volume_discount_range.VolumeDiscountRangeManager(self) + ) + """Volume Discount Range manager.""" + self.voucher_codes = voucher_code.VoucherCodeManager(self) + """Voucher Code manager.""" + + @property + def db(self) -> DB: + """The database management service.""" + return self._odoo.db + + @property + def report(self) -> Report: + """The report management service.""" + return self._odoo.report + + @property + def env(self) -> Environment: + """The OdooRPC environment wrapper object. + + This allows interacting with models that do not have managers + within this Odoo client. + Usage is the same as on a native ``odoorpc.ODOO`` object. + """ + return self._odoo.env + + @property + def user_id(self) -> int: + """The ID for the currently logged in user.""" + return self._odoo.env.uid + + @property + def user(self) -> user.User: + """The currently logged in user.""" + return self.users.get(self.user_id) + + @property + def version(self) -> Version: + """The version of the server, + as a comparable ``packaging.version.Version`` object. + """ + return Version(self._odoo.version) + + @property + def version_str(self) -> str: + """The version of the server, as a string.""" + return self._odoo.version diff --git a/openstack_odooclient/exceptions.py b/openstack_odooclient/exceptions.py new file mode 100644 index 0000000..f1e3ea0 --- /dev/null +++ b/openstack_odooclient/exceptions.py @@ -0,0 +1,35 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + + +class ClientError(Exception): + """Base class for Odoo client exceptions.""" + + pass + + +class MultipleRecordsFoundError(ClientError): + """Error raised when multiple records were found in a query, + when only one was expected. + """ + + pass + + +class RecordNotFoundError(ClientError): + """Error raised when a required record was not found.""" + + pass diff --git a/openstack_odooclient/managers/__init__.py b/openstack_odooclient/managers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py new file mode 100644 index 0000000..73f1e4e --- /dev/null +++ b/openstack_odooclient/managers/account_move.py @@ -0,0 +1,187 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Any, List, Literal, Mapping, Optional + +from . import record + +if TYPE_CHECKING: + from . import ( + account_move_line, + currency as currency_module, + partner, + project, + ) + + +class AccountMove(record.RecordBase): + amount_total: float + """Total (taxed) amount charged on the account move (invoice).""" + + amount_untaxed: float + """Total (untaxed) amount charged on the account move (invoice).""" + + @property + def attention_id(self) -> Optional[int]: + """The ID of the partner to send invoice emails to.""" + return self._get_ref_id("attention", optional=True) + + @property + def attention_name(self) -> Optional[str]: + """The name of the partner to send invoice emails to.""" + return self._get_ref_name("attention", optional=True) + + @cached_property + def attention(self) -> Optional[partner.Partner]: + """The partner to send invoice emails to. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.attention_id + return ( + self._client.partners.get(record_id) + if record_id is not None + else None + ) + + @property + def currency_id(self) -> int: + """The ID for the currency used in this account move (invoice).""" + return self._get_ref_id("currency_id") + + @property + def currency_name(self) -> str: + """The name of the currency used in this account move (invoice).""" + return self._get_ref_name("currency_id") + + @cached_property + def currency(self) -> currency_module.Currency: + """The currency used in this account move (invoice). + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.currencies.get(self.currency_id) + + invoice_date: str + """Date associated with the account move (invoice), + in YYYY-MM-DD format. + """ + + invoice_line_ids: List[int] + """The list of the IDs for the account move (invoice) lines + that comprise this account move (invoice). + """ + + @cached_property + def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: + """A list of account move (invoice) lines + that comprise this account move (invoice). + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.account_move_lines.list(self.invoice_line_ids) + + is_move_sent: bool + """Whether or not the account move (invoice) has been sent.""" + + move_type: str + """The type of account move (invoice).""" + + name: Optional[str] + """Name assigned to the account move (invoice), if posted.""" + + @property + def os_project_id(self) -> int: + """The ID of the OpenStack Project this Account Move (Invoice) + was generated for. + """ + return self._get_ref_id("os_project") + + @property + def os_project_name(self) -> str: + """The name of the OpenStack Project this Account Move (Invoice) + was generated for. + """ + return self._get_ref_name("os_project") + + @cached_property + def os_project(self) -> project.Project: + """The OpenStack Project this Account Move (Invoice) + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.projects.get(self.os_project_id) + + payment_state: str + """The current payment state of the account move (invoice).""" + + state: Literal["draft", "posted", "cancel"] + """The current state of the account move (invoice). + + Values: + + * ``draft`` - Draft invoice + * ``posted`` - Posted (finalised) invoice + * ``cancel`` - Cancelled invoice + """ + + _field_mapping = { + # Odoo version. + "13.0": { + # Key is local value, value is remote value. + "move_type": "type", + "is_move_sent": "invoice_sent", + "payment_state": "invoice_payment_state", + }, + } + + _alias_mapping = { + # Key is local alias, value is remote field name. + "attention": "attention_id", + "currency": "currency_id", + "invoice_lines": "invoice_line_ids", + "os_project_id": "os_project", + } + + def action_post(self) -> None: + """Change a draft account move (invoice) into "posted" state.""" + self._env.action_post(self.id) + + def send_openstack_invoice_email( + self, + email_ctx: Optional[Mapping[str, Any]] = None, + ) -> None: + """Send an OpenStack invoice email for this account move (invoice). + + :param email_ctx: Optional email context, defaults to None + :type email_ctx: Optional[Mapping[str, Any]], optional + """ + self._env.send_openstack_invoice_email( + self.id, + email_ctx=dict(email_ctx) if email_ctx else None, + ) + + +class AccountMoveManager(record.NamedRecordManagerBase[AccountMove]): + env_name = "account.move" + record_class = AccountMove diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py new file mode 100644 index 0000000..3b32fb8 --- /dev/null +++ b/openstack_odooclient/managers/account_move_line.py @@ -0,0 +1,154 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from . import record + +if TYPE_CHECKING: + from . import ( + currency as currency_module, + product as product_module, + project, + ) + + +class AccountMoveLine(record.RecordBase): + @property + def currency_id(self) -> int: + """The ID for the currency used in this + account move (invoice) line. + """ + return self._get_ref_id("currency_id") + + @property + def currency_name(self) -> str: + """The name of the currency used in this + account move (invoice) line. + """ + return self._get_ref_name("currency_id") + + @cached_property + def currency(self) -> currency_module.Currency: + """The currency used in this + account move (invoice) line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.currencies.get(self.currency_id) + + line_tax_amount: float + """Amount charged in tax on the account move (invoice) line.""" + + name: str + """Name of the product charged on the account move (invoice) line.""" + + @property + def os_project_id(self) -> int: + """The ID for the OpenStack Project this Account Move (Invoice) line + was generated for. + """ + return self._get_ref_id("os_project") + + @property + def os_project_name(self) -> str: + """The name of the OpenStack Project this Account Move (Invoice) line + was generated for. + """ + return self._get_ref_name("os_project") + + @cached_property + def os_project(self) -> project.Project: + """The OpenStack Project this Account Move (Invoice) line + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.projects.get(self.os_project_id) + + os_region: str + """The OpenStack region the Account Move (Invoice) Line + was created from. + """ + + os_resource_id: str + """The OpenStack resource ID for the resource that generated + this Account Move (Invoice) Line. + """ + + os_resource_name: str + """The name of the OpenStack resource tier or flavour, + as used by services such as Distil for rating purposes. + + For example, if this is the Account Move (Invoice) Line + for a compute instance, this would be set to the instance's flavour name. + """ + + os_resource_type: str + """A human-readable description of the type of resource captured + by this Account Move (Invoice) Line. + """ + + price_subtotal: float + """Amount charged for the product (untaxed) on the + Account Move (Invoice) Line. + """ + + price_unit: float + """Unit price for the product used on the account move (invoice) line.""" + + @property + def product_id(self) -> int: + """The ID for the product charged on the + account move (invoice) line. + """ + return self._get_ref_id("product_id") + + @property + def product_name(self) -> str: + """The name of the product charged on the + account move (invoice) line. + """ + return self._get_ref_name("product_id") + + @cached_property + def product(self) -> product_module.Product: + """The product charged on the + account move (invoice) line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.product_id) + + quantity: int + """Quantity of product charged on the account move (invoice) line.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "currency": "currency_id", + "os_project_id": "os_project", + "product": "product_id", + } + + +class AccountMoveLineManager(record.RecordManagerBase[AccountMoveLine]): + env_name = "account.move.line" + record_class = AccountMoveLine diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py new file mode 100644 index 0000000..36a0aa8 --- /dev/null +++ b/openstack_odooclient/managers/company.py @@ -0,0 +1,108 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal, Optional, Union + +from . import record + +if TYPE_CHECKING: + from . import partner as partner_module + + +class Company(record.RecordBase): + active: bool + """Whether or not this company is active (enabled).""" + + child_ids: List[int] + """A list of IDs for the child companies.""" + + @cached_property + def children(self) -> List[Company]: + """The list of child companies. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.companies.list(self.child_ids) + + name: str + """Company name, set from the partner name.""" + + @property + def parent_id(self) -> Optional[int]: + """The ID for the parent company, if this company + is the child of another company. + """ + return self._get_ref_id("parent_id", optional=True) + + @property + def parent_name(self) -> Optional[str]: + """The name of the parent company, if this company + is the child of another company. + """ + return self._get_ref_name("parent_id", optional=True) + + @cached_property + def parent(self) -> Optional[Company]: + """The parent company, if this company + is the child of another company. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.parent_id + return ( + self._client.companies.get(record_id) + if record_id is not None + else None + ) + + parent_path: Union[str, Literal[False]] + """The path of the parent company, if there is a parent.""" + + @property + def partner_id(self) -> int: + """The ID for the partner for the company.""" + return self._get_ref_id("partner_id") + + @property + def partner_name(self) -> str: + """The name of the partner for the company.""" + return self._get_ref_name("partner_id") + + @cached_property + def partner(self) -> partner_module.Partner: + """The partner for the company. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "children": "child_ids", + "company": "company_id", + "parent": "parent_id", + "partner": "partner_id", + } + + +class CompanyManager(record.NamedRecordManagerBase[Company]): + env_name = "res.company" + record_class = Company diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py new file mode 100644 index 0000000..98279f7 --- /dev/null +++ b/openstack_odooclient/managers/credit.py @@ -0,0 +1,122 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, List, Optional + +from . import record + +if TYPE_CHECKING: + from . import ( + credit_transaction, + credit_type as credit_type_module, + voucher_code as voucher_code_module, + ) + + +class Credit(record.RecordBase): + @property + def credit_type_id(self) -> int: + """The ID of the type of this credit.""" + return self._get_ref_id("credit_type") + + @property + def credit_type_name(self) -> str: + """The name of this type of credit.""" + return self._get_ref_name("credit_type") + + @cached_property + def credit_type(self) -> credit_type_module.CreditType: + """The type of this credit. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.credit_types.get(self.credit_type_id) + + current_balance: float + """The current remaining balance on the credit.""" + + expiry_date: date + """The date the credit expires.""" + + initial_balance: float + """The initial balance this credit started off with.""" + + name: str + """The automatically generated name of the credit.""" + + start_date: date + """The start date of the credit.""" + + @property + def transaction_ids(self) -> List[int]: + """A list of IDs for the transactions that have been made + using this credit. + """ + return self._get_field("transactions") + + @cached_property + def transactions(self) -> List[credit_transaction.CreditTransaction]: + """The transactions that have been made using this credit. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.credit_transactions.list(self.transaction_ids) + + @property + def voucher_code_id(self) -> Optional[int]: + """The ID of the voucher code used when applying for the credit, + if one was supplied. + """ + return self._get_ref_id("voucher_code", optional=True) + + @property + def voucher_code_name(self) -> Optional[str]: + """The name of the voucher code used when applying for the credit, + if one was supplied. + """ + return self._get_ref_name("voucher_code", optional=True) + + @cached_property + def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: + """Voucher code used when applying for the credit, + if one was supplied. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.voucher_code_id + return ( + self._client.voucher_codes.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "credit_type_id": "credit_type", + "transaction_ids": "transactions", + "voucher_code_id": "voucher_code", + } + + +class CreditManager(record.RecordManagerBase[Credit]): + env_name = "openstack.credit" + record_class = Credit diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py new file mode 100644 index 0000000..3ef0928 --- /dev/null +++ b/openstack_odooclient/managers/credit_transaction.py @@ -0,0 +1,61 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from . import record + +if TYPE_CHECKING: + from . import credit as credit_module + + +class CreditTransaction(record.RecordBase): + @property + def credit_id(self) -> int: + """The ID of the credit this transaction was made against.""" + return self._get_ref_id("credit") + + @property + def credit_name(self) -> str: + """The name of the credit this transaction was made against.""" + return self._get_ref_name("credit") + + @cached_property + def credit(self) -> credit_module.Credit: + """The credit this transaction was made against. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.credits.get(self.credit_id) + + description: str + """A description of this credit transaction.""" + + value: float + """The value of the credit transaction.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "credit_id": "credit", + } + + +class CreditTransactionManager(record.RecordManagerBase[CreditTransaction]): + env_name = "openstack.credit.transaction" + record_class = CreditTransaction diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py new file mode 100644 index 0000000..5c94982 --- /dev/null +++ b/openstack_odooclient/managers/credit_type.py @@ -0,0 +1,128 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List + +from . import record + +if TYPE_CHECKING: + from . import credit, product as product_module, product_category + + +class CreditType(record.RecordBase): + @property + def credit_ids(self) -> List[int]: + """A list of IDs for the credits which are of this credit type.""" + return self._get_field("credits") + + @cached_property + def credits(self) -> List[credit.Credit]: + """A list of credits which are of this credit type. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.credits.list(self.credit_ids) + + name: str + """Name of the Credit Type.""" + + @property + def only_for_product_ids(self) -> List[int]: + """A list of IDs for the products this credit applies to. + + Mutually exclusive with ``only_for_product_category_ids``. + If neither are specified, the credit applies to all products. + """ + return self._get_field("only_for_products") + + @cached_property + def only_for_products(self) -> List[product_module.Product]: + """A list of products which this credit applies to. + + Mutually exclusive with ``only_for_product_categories``. + If neither are specified, the credit applies to all products. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.products.list(self.only_for_product_ids) + + @property + def only_for_product_category_ids(self) -> List[int]: + """A list of IDs for the product categories this credit applies to. + + Mutually exclusive with ``only_for_product_ids``. + If neither are specified, the credit applies to all product + categories. + """ + return self._get_field("only_for_product_categories") + + @cached_property + def only_for_product_categories( + self, + ) -> List[product_category.ProductCategory]: + """A list of product categories which this credit applies to. + + Mutually exclusive with ``only_for_products``. + If neither are specified, the credit applies to all product + categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.product_categories.list(self.only_for_product_ids) + + @property + def product_id(self) -> int: + """The ID of the product to use when applying + the credit to invoices. + """ + return self._get_ref_id("product") + + @property + def product_name(self) -> str: + """The ID of the product to use when applying + the credit to invoices. + """ + return self._get_ref_name("product") + + @cached_property + def product(self) -> product_module.Product: + """The product to use when applying the credit to invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.product_id) + + refundable: bool + """Whether or not the credit is refundable.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "credit_ids": "credits", + "only_for_product_ids": "only_for_products", + "only_for_product_category_ids": "only_for_product_categories", + "product_id": "product", + } + + +class CreditTypeManager(record.NamedRecordManagerBase[CreditType]): + env_name = "openstack.credit.type" + record_class = CreditType diff --git a/openstack_odooclient/managers/crm_team.py b/openstack_odooclient/managers/crm_team.py new file mode 100644 index 0000000..b90ad1f --- /dev/null +++ b/openstack_odooclient/managers/crm_team.py @@ -0,0 +1,28 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from . import record + + +class CrmTeam(record.RecordBase): + name: str + """CRM team name.""" + + +class CrmTeamManager(record.NamedRecordManagerBase[CrmTeam]): + env_name = "crm.team" + record_class = CrmTeam diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py new file mode 100644 index 0000000..77797ec --- /dev/null +++ b/openstack_odooclient/managers/currency.py @@ -0,0 +1,68 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date as datetime_date +from typing import Literal, Union + +from . import record + + +class Currency(record.RecordBase): + active: bool + """Whether or not this currency is active (enabled).""" + + currency_unit_label: Union[str, Literal[False]] + """The unit label for this currency, if set.""" + + currency_subunit_label: Union[str, Literal[False]] + """The sub-unit label for this currency, if set.""" + + date: datetime_date + """The current date to which the currency rate is up to date.""" + + decimal_places: int + """Decimal places taken into account for operations on amounts + in this currency. + + It is determined by the rounding factor (``rounding`` field). + """ + + name: str + """The ISO-4217 currency code for the currency.""" + + position: Literal["before", "after"] + """The position of the currency unit relative to the amount. + + Values: + + * ``before`` - Place the unit before the amount + * ``after`` - Place the unit after the amount + """ + + rate: float + """The rate of the currency to the currency of rate 1.""" + + rounding: float + """The rounding factor configured for this currency.""" + + symbol: str + """The currency sign to be used when printing amounts.""" + + +class CurrencyManager(record.NamedRecordManagerBase[Currency]): + env_name = "res.currency" + record_class = Currency diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py new file mode 100644 index 0000000..11e0ec0 --- /dev/null +++ b/openstack_odooclient/managers/customer_group.py @@ -0,0 +1,84 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Optional + +from . import record + +if TYPE_CHECKING: + from . import partner, pricelist as pricelist_module + + +class CustomerGroup(record.RecordBase): + name: str + """Customer group name.""" + + @property + def partner_ids(self) -> List[int]: + """A list of IDs for the partners that are part + of this customer group. + """ + return self._get_field("partners") + + @cached_property + def partners(self) -> List[partner.Partner]: + """The partners that are part of this customer group. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.partners.list(self.partner_ids) + + @property + def pricelist_id(self) -> Optional[int]: + """The ID for the pricelist this customer group uses, + if not the default one. + """ + return self._get_ref_id("pricelist", optional=True) + + @property + def pricelist_name(self) -> Optional[str]: + """The name of the pricelist this customer group uses, + if not the default one. + """ + return self._get_ref_name("pricelist", optional=True) + + @cached_property + def pricelist(self) -> Optional[pricelist_module.Pricelist]: + """The pricelist this customer group uses, if not the default one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.pricelist_id + return ( + self._client.pricelists.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "partner_ids": "partners", + "pricelist_id": "pricelist", + } + + +class CustomerGroupManager(record.NamedRecordManagerBase[CustomerGroup]): + env_name = "openstack.customer_group" + record_class = CustomerGroup diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py new file mode 100644 index 0000000..ba73b5f --- /dev/null +++ b/openstack_odooclient/managers/grant.py @@ -0,0 +1,101 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, Optional + +from . import record + +if TYPE_CHECKING: + from . import ( + grant_type as grant_type_module, + voucher_code as voucher_code_module, + ) + + +class Grant(record.RecordBase): + expiry_date: date + """The date the grant expires.""" + + @property + def grant_type_id(self) -> int: + """The ID of the type of this grant.""" + return self._get_ref_id("grant_type") + + @property + def grant_type_name(self) -> str: + """The name of this type of grant.""" + return self._get_ref_name("grant_type") + + @cached_property + def grant_type(self) -> grant_type_module.GrantType: + """The type of this grant. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.grant_types.get(self.grant_type_id) + + name: str + """The automatically generated name of the grant.""" + + start_date: date + """The start date of the grant.""" + + value: float + """The value of the grant.""" + + @property + def voucher_code_id(self) -> Optional[int]: + """The ID of the voucher code used when applying for the grant, + if one was supplied. + """ + return self._get_ref_id("voucher_code", optional=True) + + @property + def voucher_code_name(self) -> Optional[str]: + """The name of the voucher code used when applying for the grant, + if one was supplied. + """ + return self._get_ref_name("voucher_code", optional=True) + + @cached_property + def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: + """Voucher code used when applying for the grant, + if one was supplied. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.voucher_code_id + return ( + self._client.voucher_codes.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "grant_type_id": "grant_type", + "voucher_code_id": "voucher_code", + } + + +class GrantManager(record.RecordManagerBase[Grant]): + env_name = "openstack.grant" + record_class = Grant diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py new file mode 100644 index 0000000..1af6086 --- /dev/null +++ b/openstack_odooclient/managers/grant_type.py @@ -0,0 +1,130 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List + +from . import record + +if TYPE_CHECKING: + from . import grant, product as product_module, product_category + + +class GrantType(record.RecordBase): + @property + def grant_ids(self) -> List[int]: + """A list of IDs for the grants which are of this grant type.""" + return self._get_field("grants") + + @cached_property + def grants(self) -> List[grant.Grant]: + """A list of grants which are of this grant type. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.grants.list(self.grant_ids) + + name: str + """Name of the Grant Type.""" + + @property + def only_for_product_ids(self) -> List[int]: + """A list of IDs for the products this credit applies to. + + Mutually exclusive with ``only_for_product_category_ids``. + If neither are specified, the credit applies to all products. + """ + return self._get_field("only_for_products") + + @cached_property + def only_for_products(self) -> List[product_module.Product]: + """A list of products which this credit applies to. + + Mutually exclusive with ``only_for_product_categories``. + If neither are specified, the credit applies to all products. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.products.list(self.only_for_product_ids) + + @property + def only_for_product_category_ids(self) -> List[int]: + """A list of IDs for the product categories this credit applies to. + + Mutually exclusive with ``only_for_product_ids``. + If neither are specified, the credit applies to all product + categories. + """ + return self._get_field("only_for_product_categories") + + @cached_property + def only_for_product_categories( + self, + ) -> List[product_category.ProductCategory]: + """A list of product categories which this credit applies to. + + Mutually exclusive with ``only_for_products``. + If neither are specified, the credit applies to all product + categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.product_categories.list(self.only_for_product_ids) + + only_on_group_root: bool + """When set to ``True``, this grant type is only allowed to be + part of an invoice grouping if it is on the group root project. + """ + + @property + def product_id(self) -> int: + """The ID of the product to use when applying + the grant to invoices. + """ + return self._get_ref_id("product") + + @property + def product_name(self) -> str: + """The ID of the product to use when applying + the grant to invoices. + """ + return self._get_ref_name("product") + + @cached_property + def product(self) -> product_module.Product: + """The product to use when applying the grant to invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.product_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "grant_ids": "grants", + "only_for_product_ids": "only_for_products", + "only_for_product_category_ids": "only_for_product_categories", + "product_id": "product", + } + + +class GrantTypeManager(record.NamedRecordManagerBase[GrantType]): + env_name = "openstack.grant.type" + record_class = GrantType diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py new file mode 100644 index 0000000..9c08cc0 --- /dev/null +++ b/openstack_odooclient/managers/partner.py @@ -0,0 +1,323 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal, Optional, Union + +from . import record + +if TYPE_CHECKING: + from . import ( + customer_group, + pricelist, + project, + project_contact, + referral_code, + reseller, + trial, + user as user_module, + ) + + +class Partner(record.RecordBase): + active: bool + """Whether or not this Partner is active.""" + + email: str + """Main e-mail address for the partner.""" + + name: str + """Full name of the partner.""" + + @property + def os_customer_group_id(self) -> Optional[int]: + """The ID for the customer group this partner is part of, + if it is part of one. + """ + return self._get_ref_id("os_customer_group", optional=True) + + @property + def os_customer_group_name(self) -> Optional[str]: + """The name of the customer group this partner is part of, + if it is part of one. + """ + return self._get_ref_name("os_customer_group", optional=True) + + @cached_property + def os_customer_group(self) -> Optional[customer_group.CustomerGroup]: + """The customer group this partner is part of, + if it is part of one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.os_customer_group_id + return ( + self._client.customer_groups.get(record_id) + if record_id is not None + else None + ) + + @property + def os_project_ids(self) -> List[int]: + """A list of IDs for the OpenStack projects that + belong to this partner. + """ + return self._get_field("os_projects") + + @cached_property + def os_projects(self) -> List[project.Project]: + """The OpenStack projects that belong to this partner. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.projects.list(self.os_project_ids) + + @property + def os_project_contact_ids(self) -> List[int]: + """A list of IDs for the project contacts that are associated + with this partner. + """ + return self._get_field("os_project_contacts") + + @cached_property + def os_project_contacts(self) -> List[project_contact.ProjectContact]: + """The project contacts that are associated with this partner. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.project_contacts.list(self.os_project_contact_ids) + + @property + def os_referral_id(self) -> Optional[int]: + """The ID for the referral code the partner used on sign-up, + if one was used. + """ + return self._get_ref_id("os_referral", optional=True) + + @property + def os_referral_name(self) -> Optional[str]: + """The name of the referral code the partner used on sign-up, + if one was used. + """ + return self._get_ref_name("os_referral", optional=True) + + @cached_property + def os_referral(self) -> Optional[referral_code.ReferralCode]: + """The referral code the partner used on sign-up, if one was used. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.os_referral_id + return ( + self._client.referral_codes.get(record_id) + if record_id is not None + else None + ) + + @property + def os_referral_code_ids(self) -> List[int]: + """A list of IDs for the referral codes the partner has used.""" + return self._get_field("os_referral_codes") + + @cached_property + def os_referral_codes(self) -> List[referral_code.ReferralCode]: + """The referral codes the partner has used. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.referral_codes.list(self.os_referral_code_ids) + + @property + def os_reseller_id(self) -> Optional[int]: + """The ID for the reseller for this partner, if this partner + is billed through a reseller. + """ + return self._get_ref_id("os_reseller", optional=True) + + @property + def os_reseller_name(self) -> Optional[str]: + """The name of the reseller for this partner, if this partner + is billed through a reseller. + """ + return self._get_ref_name("os_reseller", optional=True) + + @cached_property + def os_reseller(self) -> Optional[reseller.Reseller]: + """The reseller for this partner, if this partner + is billed through a reseller. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.os_reseller_id + return ( + self._client.resellers.get(record_id) + if record_id is not None + else None + ) + + @property + def os_trial_id(self) -> Optional[int]: + """The ID for the sign-up trial for this partner, + if signed up under a trial. + """ + return self._get_ref_id("os_trial", optional=True) + + @property + def os_trial_name(self) -> Optional[str]: + """The name of the sign-up trial for this partner, + if signed up under a trial. + """ + return self._get_ref_name("os_trial", optional=True) + + @cached_property + def os_trial(self) -> Optional[trial.Trial]: + """The sign-up trial for this partner, + if signed up under a trial. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.os_trial_id + return ( + self._client.trials.get(record_id) + if record_id is not None + else None + ) + + @property + def parent_id(self) -> Optional[int]: + """The ID for the parent partner of this partner, + if it has a parent. + """ + return self._get_ref_id("parent_id", optional=True) + + @property + def parent_name(self) -> Optional[str]: + """The name of the parent partner of this partner, + if it has a parent. + """ + return self._get_ref_name("parent_id", optional=True) + + @cached_property + def parent(self) -> Optional[Partner]: + """The parent partner of this partner, + if it has a parent. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.parent_id + return ( + self._client.partners.get(record_id) + if record_id is not None + else None + ) + + @property + def property_product_pricelist_id(self) -> Optional[int]: + """The ID for the pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + """ + return self._get_ref_id("property_product_pricelist", optional=True) + + @property + def property_product_pricelist_name(self) -> Optional[str]: + """The name of the pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + """ + return self._get_ref_name("property_product_pricelist", optional=True) + + @cached_property + def property_product_pricelist(self) -> Optional[pricelist.Pricelist]: + """The pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.property_product_pricelist_id + return ( + self._client.pricelists.get(record_id) + if record_id is not None + else None + ) + + stripe_customer_id: Union[str, Literal[False]] + """The Stripe customer ID for this Partner, if one has been assigned.""" + + @property + def user_id(self) -> Optional[int]: + """The ID of the internal user in charge of this partner, + if one is assigned. + """ + return self._get_ref_id("user_id", optional=True) + + @property + def user_name(self) -> Optional[str]: + """The ID of the internal user in charge of this partner, + if one is assigned. + """ + return self._get_ref_name("user_id") + + @cached_property + def user(self) -> Optional[user_module.User]: + """The internal user in charge of this partner, + if one is assigned. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.user_id + return ( + self._client.users.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "os_customer_group_id": "os_customer_group", + "os_project_ids": "os_projects", + "os_project_contact_ids": "os_project_contacts", + "os_referral_id": "os_referral", + "os_referral_code_ids": "os_referral_codes", + "os_reseller_id": "os_reseller", + "os_trial_id": "os_trial", + "parent": "parent_id", + "property_product_pricelist_id": "property_product_pricelist", + "user": "user_id", + } + + +class PartnerManager(record.RecordManagerBase[Partner]): + env_name = "res.partner" + record_class = Partner diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py new file mode 100644 index 0000000..f66fc7f --- /dev/null +++ b/openstack_odooclient/managers/partner_category.py @@ -0,0 +1,114 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal, Optional, Union + +from . import record + +if TYPE_CHECKING: + from . import partner + + +class PartnerCategory(record.RecordBase): + active: bool + """Whether or not the partner category is active.""" + + @property + def child_ids(self) -> List[int]: + """A list of IDs for the child categories.""" + return self._get_field("child_id") + + @cached_property + def children(self) -> List[PartnerCategory]: + """The list of child categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.partner_categories.list(self.child_ids) + + color: int + """Colour index for the partner category.""" + + @property + def colour(self) -> int: + """Alias for ``color``.""" + return self.color + + name: str + """Partner category name.""" + + @property + def parent_id(self) -> Optional[int]: + """The ID for the parent partner category, if this category + is the child of another category. + """ + return self._get_ref_id("parent_id", optional=True) + + @property + def parent_name(self) -> Optional[str]: + """The name of the parent partner category, if this category + is the child of another category. + """ + return self._get_ref_name("parent_id", optional=True) + + @cached_property + def parent(self) -> Optional[PartnerCategory]: + """The parent partner category, if this category + is the child of another category. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.parent_id + return ( + self._client.partner_categories.get(record_id) + if record_id is not None + else None + ) + + parent_path: Union[str, Literal[False]] + """The path of the parent partner category, if there is a parent.""" + + @property + def partner_ids(self) -> List[int]: + """A list of IDs for the partners in this category.""" + return self._get_field("partner_id") + + @cached_property + def partners(self) -> List[partner.Partner]: + """The list of partners in this category. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.partners.list(self.partner_ids) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "child_ids": "child_id", + "children": "child_id", + "parent": "parent_id", + "partner_ids": "partner_id", + "partners": "partner_id", + } + + +class PartnerCategoryManager(record.NamedRecordManagerBase[PartnerCategory]): + env_name = "res.partner.category" + record_class = PartnerCategory diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py new file mode 100644 index 0000000..91ce5da --- /dev/null +++ b/openstack_odooclient/managers/pricelist.py @@ -0,0 +1,158 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Optional, Union + +from . import product as product_module, record + +if TYPE_CHECKING: + from . import company as company_module, currency as currency_module + + +class Pricelist(record.RecordBase): + active: bool + """Whether or not the pricelist is active.""" + + @property + def company_id(self) -> Optional[int]: + """The ID for the company for this pricelist, if set.""" + return self._get_ref_id("company_id", optional=True) + + @property + def company_name(self) -> Optional[str]: + """The name of the company for this pricelist, if set.""" + return self._get_ref_name("company_id", optional=True) + + @cached_property + def company(self) -> Optional[company_module.Company]: + """The company for this pricelist, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.company_id + return ( + self._client.companies.get(record_id) + if record_id is not None + else None + ) + + @property + def currency_id(self) -> int: + """The ID for the currency used in this pricelist.""" + return self._get_ref_id("currency_id") + + @property + def currency_name(self) -> str: + """The name of the currency used in this pricelist.""" + return self._get_ref_name("currency_id") + + @cached_property + def currency(self) -> currency_module.Currency: + """The currency used in this pricelist. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.currencies.get(self.currency_id) + + discount_policy: Literal["with_discount", "without_discount"] + """Discount policy for the pricelist. + + Values: + + * ``with_discount`` - Discount included in the price + * ``without_discount`` - Show public price & discount to the customer + """ + + display_name: str + """The display name of the pricelist.""" + + default_code: str + """The unit of this product. + + Referred to as the "Default Code" in Odoo. + """ + + description: str + """A short description of this product.""" + + name: str + """The name of this pricelist.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "company": "company_id", + "currency": "currency_id", + } + + def get_price( + self, + product: Union[int, product_module.Product], + qty: float, + ) -> float: + """Get the price to charge for a given product and quantity. + + :param product: Product to get the price for (ID or object) + :type product: int or Product + :param qty: Quantity to charge for + :type qty: float + :return: Price to charge + :rtype: float + """ + return self._client.pricelists.get_price( + pricelist=self, + product=product, + qty=qty, + ) + + +class PricelistManager(record.NamedRecordManagerBase[Pricelist]): + env_name = "product.pricelist" + record_class = Pricelist + + def get_price( + self, + pricelist: Union[int, Pricelist], + product: Union[int, product_module.Product], + qty: float, + ) -> float: + """Get the price to charge for a given product and quantity. + + :param pricelist: Pricelist to reference (ID or object) + :type pricelist: int or Pricelist + :param product: Product to get the price for (ID or object) + :type product: int or Product + :param qty: Quantity to charge for + :type qty: float + :return: Price to charge + :rtype: float + """ + pricelist_id = ( + pricelist.id if isinstance(pricelist, Pricelist) else pricelist + ) + price = self._env.price_get( + pricelist_id, + ( + product.id + if isinstance(product, product_module.Product) + else product + ), + max(qty, 0), + )[str(pricelist_id)] + return price if qty >= 0 else -price diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py new file mode 100644 index 0000000..4f1ac60 --- /dev/null +++ b/openstack_odooclient/managers/product.py @@ -0,0 +1,373 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Literal, + Optional, + Union, + overload, +) + +from . import record + +if TYPE_CHECKING: + from . import company, product_category, uom as uom_module + + +class Product(record.RecordBase): + @property + def categ_id(self) -> int: + """The ID for the category this product is under.""" + return self._get_ref_id("categ_id") + + @property + def categ_name(self) -> str: + """The name of the category this product is under.""" + return self._get_ref_name("categ_id") + + @cached_property + def categ(self) -> product_category.ProductCategory: + """The category this product is under. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.product_categories.get(self.categ_id) + + @property + def company_id(self) -> Optional[int]: + """The ID for the company that owns this product, if set.""" + return self._get_ref_id("company_id", optional=True) + + @property + def company_name(self) -> Optional[str]: + """The name of the company that owns this product, if set.""" + return self._get_ref_name("company_id", optional=True) + + @cached_property + def company(self) -> Optional[company.Company]: + """The company that owns this product, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.company_id + return ( + self._client.companies.get(record_id) + if record_id is not None + else None + ) + + default_code: str + """The Default Code for this product. + + In the OpenStack Integration add-on, this is used to store + the rated unit for the service product. + """ + + description: str + """A short description of this product.""" + + display_name: str + """The name of this product in OpenStack, and on invoices.""" + + list_price: float + """The list price of the product. + + This becomes the unit price of the product on invoices. + """ + + name: str + """The name of the product.""" + + @property + def uom_id(self) -> int: + """The ID for the Unit of Measure for this product.""" + return self._get_ref_id("uom_id") + + @property + def uom_name(self) -> str: + """The name of the Unit of Measure for this product.""" + return self._get_ref_name("uom_id") + + @cached_property + def uom(self) -> uom_module.Uom: + """The Unit of Measure for this product. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.uoms.get(self.uom_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "categ": "categ_id", + "company": "company_id", + "uom": "uom_id", + } + + +class ProductManager(record.RecordManagerWithUniqueFieldBase[Product, str]): + env_name = "product.product" + record_class = Product + + @overload + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + ) -> List[Product]: ... + + @overload + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + ) -> List[int]: ... + + @overload + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[True], + ) -> List[int]: ... + + @overload + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + ) -> List[Dict[str, Any]]: ... + + @overload + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: bool = ..., + as_dict: bool = ..., + ) -> Union[List[Product], List[int], Union[List[Dict[str, Any]]]]: ... + + def get_sellable_products_for_company( + self, + company: Union[company.Company, int], + fields: Optional[Iterable[str]] = None, + order: Optional[str] = None, + as_id: bool = False, + as_dict: bool = False, + ) -> Union[List[Product], List[int], Union[List[Dict[str, Any]]]]: + """Fetch a list of active and saleable products for the given company. + + :param company: ID of the company to search for products + :type company: int + :param order: Order results by a specific field, defaults to None + :type order: Optional[str], optional + :param as_id: Return the record IDs only, defaults to False + :type as_id: bool, optional + :param as_dict: Return records as dictionaries, defaults to False + :type as_dict: bool, optional + :return: List of products + :rtype: Union[List[Product], List[int], Union[Dict[str, Any]]] + """ + return self.search( + [ + ("company_id", "=", company), + ("active", "=", True), + ("sale_ok", "=", True), + ], + fields=fields, + order=order, + as_id=as_id, + as_dict=as_dict, + ) + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Product]: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Product: ... + + @overload + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Product, int, Dict[str, Any]]]: ... + + def get_sellable_company_product_by_name( + self, + company: Union[company.Company, int], + name: str, + fields: Optional[Iterable[str]] = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Product, int, Dict[str, Any]]]: + """Query a unique product for the given company by name. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + :param company: ID of the company to search for products + :type company: int + :param name: The product name + :type name: str + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same name + :raises RecordNotFoundError: Record with the given name not found + :return: Product (or ``None`` if record not found and optional) + :rtype: Optional[Union[Record, int, Dict[str, Any]]] + """ + return self._get_by_unique_field( + field="name", + value=name, + filters=[ + ("company_id", "=", company), + ("active", "=", True), + ("sale_ok", "=", True), + ], + fields=fields, + as_id=as_id, + as_dict=as_dict, + optional=optional, + ) diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py new file mode 100644 index 0000000..3757a09 --- /dev/null +++ b/openstack_odooclient/managers/product_category.py @@ -0,0 +1,90 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import List, Literal, Optional, Union + +from . import record + + +class ProductCategory(record.RecordBase): + @property + def child_ids(self) -> List[int]: + """A list of IDs for the child categories.""" + return self._get_field("child_id") + + @cached_property + def children(self) -> List[ProductCategory]: + """The list of child categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.product_categories.list(self.child_ids) + + complete_name: str + """The complete product category tree.""" + + name: str + """Name of the product category.""" + + @property + def parent_id(self) -> Optional[int]: + """The ID for the parent product category, if this category + is the child of another category. + """ + return self._get_ref_id("parent_id", optional=True) + + @property + def parent_name(self) -> Optional[str]: + """The name of the parent product category, if this category + is the child of another category. + """ + return self._get_ref_name("parent_id", optional=True) + + @cached_property + def parent(self) -> Optional[ProductCategory]: + """The parent product category, if this category + is the child of another category. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.parent_id + return ( + self._client.product_categories.get(record_id) + if record_id is not None + else None + ) + + parent_path: Union[str, Literal[False]] + """The path of the parent product category, if there is a parent.""" + + product_count: int + """The number of products under this category.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "child_ids": "child_id", + "children": "child_id", + "parent": "parent_id", + } + + +class ProductCategoryManager(record.NamedRecordManagerBase[ProductCategory]): + env_name = "product.category" + record_class = ProductCategory diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py new file mode 100644 index 0000000..23db7a3 --- /dev/null +++ b/openstack_odooclient/managers/project.py @@ -0,0 +1,379 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Literal, + Optional, + Union, + overload, +) + +from . import record + +if TYPE_CHECKING: + from . import ( + credit, + grant, + partner as partner_module, + project_contact, + support_subscription as support_subscription_module, + term_discount, + ) + + +class Project(record.RecordBase): + billing_type: Literal["customer", "internal"] + """Billing type for this project. + + Values: + + * ``customer`` - Customer project (should be charged) + * ``internal`` - Internal project (should not be charged) + """ + + display_name: str + """The automatically generated display name for the project.""" + + enabled: bool + """Whether or not the project is enabled in Odoo.""" + + group_invoices: bool + """Whether or not to group invoices together for this project.""" + + name: str + """OpenStack project name.""" + + os_id: str + """OpenStack project ID.""" + + override_po_number: bool + """Whether or not to override the PO number with the value + set on this Project. + """ + + @property + def owner_id(self) -> int: + """The ID for the partner that owns this project.""" + return self._get_ref_id("owner") + + @property + def owner_name(self) -> str: + """The name of the partner that owns this project.""" + return self._get_ref_name("owner") + + @cached_property + def owner(self) -> partner_module.Partner: + """The partner that owns this project. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.owner_id) + + @property + def parent_id(self) -> Optional[int]: + """The ID for the parent project, if this project + is the child of another project. + """ + return self._get_ref_id("parent", optional=True) + + @property + def parent_name(self) -> Optional[str]: + """The name of the parent project, if this project + is the child of another project. + """ + return self._get_ref_name("parent", optional=True) + + @cached_property + def parent(self) -> Optional[Project]: + """The parent project, if this project + is the child of another project. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.parent_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + payment_method: Literal["invoice", "credit_card"] + """Payment method configured on the project. + + Values: + + * ``invoice`` - Project is paid by invoice + * ``credit_card`` - Project is paid by credit card + """ + + po_number: Union[str, Literal[False]] + """The PO number set for this specific Project (if set).""" + + @property + def project_contact_ids(self) -> List[int]: + """A list of IDs for the contacts for this project.""" + return self._get_field("project_contacts") + + @cached_property + def project_contacts(self) -> List[project_contact.ProjectContact]: + """The contacts for this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.project_contacts.list(self.project_contact_ids) + + @property + def project_credit_ids(self) -> List[int]: + """A list of IDs for the contacts for this project.""" + return self._get_field("project_credits") + + @cached_property + def project_credits(self) -> List[credit.Credit]: + """The credits that apply to this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.credits.list(self.project_credit_ids) + + @property + def project_grant_ids(self) -> List[int]: + """A list of IDs for the contacts for this project.""" + return self._get_field("project_grants") + + @cached_property + def project_grants(self) -> List[grant.Grant]: + """The grants that apply to this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.grants.list(self.project_grant_ids) + + stripe_card_id: Union[str, Literal[False]] + """The card ID used for credit card payments on this project + using Stripe, if the payment method is set to ``credit_card``. + + If a credit card has not been assigned to this project, + this field will be set to ``False``. + """ + + @property + def support_subscription_id(self) -> Optional[int]: + """The ID for the support subscription for this project, + if the project has one. + """ + return self._get_ref_id("support_subscription", optional=True) + + @property + def support_subscription_name(self) -> Optional[str]: + """The name of the support subscription for this project, + if the project has one. + """ + return self._get_ref_name("support_subscription", optional=True) + + @cached_property + def support_subscription( + self, + ) -> Optional[support_subscription_module.SupportSubscription]: + """The support subscription for this project, + if the project has one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.support_subscription_id + return ( + self._client.support_subscriptions.get(record_id) + if record_id is not None + else None + ) + + @property + def term_discount_ids(self) -> List[int]: + """A list of IDs for the term discounts that apply to this project.""" + return self._get_field("term_discounts") + + @cached_property + def term_discounts(self) -> List[term_discount.TermDiscount]: + """The term discounts that apply to this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.term_discounts.list(self.term_discount_ids) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "owner_id": "owner", + "parent_id": "parent", + "project_contact_ids": "project_contacts", + "project_credit_ids": "project_credits", + "project_grant_ids": "project_grants", + "support_subcription_id": "support_subscription", + "term_discount_ids": "term_discounts", + } + + +class ProjectManager(record.RecordManagerWithUniqueFieldBase[Project, str]): + env_name = "openstack.project" + record_class = Project + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Project]: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Project: ... + + @overload + def get_by_os_id( + self, + os_id: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Project, int, Dict[str, Any]]]: ... + + def get_by_os_id( + self, + os_id: str, + fields: Optional[Iterable[str]] = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Project, int, Dict[str, Any]]]: + """Query a unique record by OpenStack project ID. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + :param name: The record name + :type name: str + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same name + :raises RecordNotFoundError: Record with the given name not found + :return: Query result (or ``None`` if record not found and optional) + :rtype: Optional[Union[Project, int, Dict[str, Any]]] + """ + return self._get_by_unique_field( + field="os_id", + value=os_id, + fields=fields, + as_id=as_id, + as_dict=as_dict, + optional=optional, + ) diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py new file mode 100644 index 0000000..45a5b03 --- /dev/null +++ b/openstack_odooclient/managers/project_contact.py @@ -0,0 +1,94 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Optional + +from . import record + +if TYPE_CHECKING: + from . import partner as partner_module, project as project_module + + +class ProjectContact(record.RecordBase): + contact_type: Literal[ + "primary", + "billing", + "technical", + "legal", + "reseller customer", + ] + """The contact type to assign the Partner as + on the OpenStack Project. + """ + + inherit: bool + """Whether or not this contact should be inherited by child projects.""" + + @property + def partner_id(self) -> int: + """The ID for the partner linked to this project contact.""" + return self._get_ref_id("partner") + + @property + def partner_name(self) -> str: + """The name of the partner linked to this project contact.""" + return self._get_ref_name("partner") + + @cached_property + def partner(self) -> partner_module.Partner: + """The partner linked to this project contact. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + @property + def project_id(self) -> Optional[int]: + """The ID for the project this contact is linked to, if set.""" + return self._get_ref_id("project", optional=True) + + @property + def project_name(self) -> Optional[str]: + """The name of the project this contact is linked to, if set.""" + return self._get_ref_name("project", optional=True) + + @cached_property + def project(self) -> Optional[project_module.Project]: + """The project this contact is linked to, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "partner_id": "partner", + "project_id": "project", + } + + +class ProjectContactManager(record.RecordManagerBase[ProjectContact]): + env_name = "openstack.project_contact" + record_class = ProjectContact diff --git a/openstack_odooclient/managers/record/__init__.py b/openstack_odooclient/managers/record/__init__.py new file mode 100644 index 0000000..8daa7df --- /dev/null +++ b/openstack_odooclient/managers/record/__init__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from .base import RecordBase +from .manager_base import RecordManagerBase +from .manager_code_base import CodedRecordManagerBase +from .manager_name_base import NamedRecordManagerBase +from .manager_unique_field_base import RecordManagerWithUniqueFieldBase + +__all__ = [ + "RecordBase", + "RecordManagerBase", + "CodedRecordManagerBase", + "NamedRecordManagerBase", + "RecordManagerWithUniqueFieldBase", +] diff --git a/openstack_odooclient/managers/record/base.py b/openstack_odooclient/managers/record/base.py new file mode 100644 index 0000000..4b38788 --- /dev/null +++ b/openstack_odooclient/managers/record/base.py @@ -0,0 +1,324 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import copy + +from datetime import datetime +from functools import cached_property +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Literal, + Optional, + Sequence, + get_type_hints, + overload, +) + +from odoorpc import ODOO # type: ignore[import] +from odoorpc.env import Environment # type: ignore[import] +from typing_extensions import Self + +from .util import decode_value + +if TYPE_CHECKING: + from ... import client + from .. import partner + from . import manager_base + + +class RecordBase: + id: int + """The record's ID in Odoo.""" + + create_date: datetime + """The time the record was created.""" + + @property + def create_uid(self) -> int: + """The ID of the partner that created this record.""" + return self._get_ref_id("create_uid") + + @property + def create_name(self) -> str: + """The name of the partner that created this record.""" + return self._get_ref_name("create_uid") + + @cached_property + def create_user(self) -> partner.Partner: + """The object of the partner that created this record. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.create_uid) + + write_date: datetime + """The time the record was last modified.""" + + @property + def write_uid(self) -> int: + """The ID of the partner that last modified this record.""" + return self._get_ref_id("write_uid") + + @property + def write_name(self) -> str: + """The name of the partner that modified this record.""" + return self._get_ref_name("write_uid") + + @cached_property + def write_user(self) -> partner.Partner: + """The object of the partner that last modified this record. + + This fetches a full Partner object from Odoo once, + and caches it for subsequence access. + """ + return self._client.partners.get(self.write_uid) + + _field_mapping: Dict[Optional[str], Dict[str, str]] = {} + """A dictionary structure mapping field names in the local class + with the equivalents on specific versions of Odoo. + + This allows for providing for backwards compatibility for older versions + of Odoo, while still providing a consistent API for applications using + this library. + + Specify ``None`` instead of a version string to provide a general mapping + for all Odoo versions, allowing for local fields to have a different name + to their Odoo equivalent. + """ + + _alias_mapping: Dict[str, str] = {} + """A dictionary structure mapping aliases + (normally defined in the record class) to the corresponding field name + in Odoo. + + This is primarily used to define aliases for search filtering purposes, + to allow either e.g. ``write_uid`` or ``write_user`` to be specified, + instead of just ``write_uid``, when using the ``search`` method. + """ + + _base_alias_mapping = { + "create_user": "create_uid", + "write_user": "write_uid", + } + + def __init__( + self, + client: client.Client, + manager: manager_base.RecordManagerBase, + record: Dict[str, Any], + fields: Optional[Sequence[str]], + ) -> None: + self._client = client + self._manager = manager + self._record = record + self._fields = fields + self._values: Dict[str, Any] = {} + + @property + def _env(self) -> Environment: + return self._manager._env + + @property + def _odoo(self) -> ODOO: + return self._client._odoo + + @classmethod + def from_record_obj(cls, record_obj: RecordBase) -> Self: + """Create a record object of this class's type + from another record object. + + This is intended to be used to "cast" a record object from + one type to another (e.g. an organisation-specific implementation + of a model class). + + :param record_obj: Record to use to create the new object + :type record_obj: RecordBase + :return: Record object of the implementing class's type + :rtype: Self + """ + return cls( + client=record_obj._client, + manager=record_obj._manager, + record=record_obj._record, + fields=record_obj._fields, + ) + + def as_dict(self, raw: bool = False) -> Dict[str, Any]: + """Convert this record object to a dictionary. + + The fields and values in the dictionary are the same + as if the record was queried using ``as_dict=True``. + This changes field names to the record object equivalents, + if they are different, to take into account fields being + named differently across Odoo versions. + + Set ``raw=True`` to instead get the raw record dictionary + fields and values as returned by OdooRPC. + + :param raw: Return raw dictionary, defaults to False + :type raw: bool, optional + :return: Record dictionary + :rtype: Dict[str, Any] + """ + return ( + copy.deepcopy(self._record) + if raw + else { + self._manager._get_local_field(field): copy.deepcopy(value) + for field, value in self._record.items() + } + ) + + def refresh(self) -> Self: + """Fetch the latest version of this record from Odoo. + + :return: Latest version of the record object + :rtype: Self + """ + return type(self)( + client=self._client, + manager=self._manager, + record=self._env.read( + self.id, + fields=self._fields, + )[0], + fields=self._fields, + ) + + def unlink(self) -> None: + """Delete this record from Odoo.""" + self._manager.unlink(self) + + def delete(self) -> None: + """Delete this record from Odoo.""" + self._manager.delete(self) + + def _get_remote_field(self, field: str) -> str: + return self._manager._get_remote_field(field) + + def _get_local_field(self, field: str) -> str: + return self._manager._get_local_field(field) + + def _get_field(self, name: str) -> Any: + try: + return self._record[self._get_remote_field(name)] + except KeyError as err: + raise AttributeError(str(err)) from None + + @classmethod + def _resolve_alias(cls, alias: str) -> str: + return cls._alias_mapping.get( + alias, + cls._base_alias_mapping.get(alias, alias), + ) + + @overload + def _get_ref_id( + self, + name: str, + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def _get_ref_id( + self, + name: str, + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def _get_ref_id( + self, name: str, optional: bool = ... + ) -> Optional[int]: ... + + def _get_ref_id(self, name: str, optional: bool = False) -> Optional[int]: + # NOTE(callumdickinson): This method intentionally does not test + # for field existence, so an error is raised if the field is not + # actually selected in the query. + # If an optional ref is selected in a query but a ref is not set, + # ``False`` is returned instead of the expected 2-element list. + ref = self._get_field(name) + if not optional: + return ref[0] + return ref[0] if ref else None + + @overload + def _get_ref_name( + self, + name: str, + optional: Literal[False] = ..., + ) -> str: ... + + @overload + def _get_ref_name( + self, + name: str, + optional: Literal[True], + ) -> Optional[str]: ... + + @overload + def _get_ref_name( + self, name: str, optional: bool = ... + ) -> Optional[str]: ... + + def _get_ref_name( + self, + name: str, + optional: bool = False, + ) -> Optional[str]: + # NOTE(callumdickinson): This method intentionally does not test + # for field existence, so an error is raised if the field is not + # actually selected in the query. + # If an optional ref is selected in a query but a ref is not set, + # ``False`` is returned instead of the expected 2-element list. + ref = self._get_field(name) + if not optional: + return ref[1] + return ref[1] if ref else None + + def __getattr__(self, name: str) -> Any: + # If the field value has already been decoded, + # return the cached value. + if name in self._values: + return self._values[name] + value = self._get_field(name) + # NOTE(callumdickinson): Use the type annotation to coerce + # the field value returned in the record dict into the expected type. + # If no annotation was found for the field, cache the value + # unmodified. + annotations = get_type_hints(type(self)) + self._values[name] = ( + decode_value(annotations[name], value) + if name in annotations + else value + ) + # Return the now-cached value. + return self._values[name] + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"record={self._record}" + f", fields={self._fields}" + ")" + ) + + def __repr__(self) -> str: + return str(self) diff --git a/openstack_odooclient/managers/record/manager_base.py b/openstack_odooclient/managers/record/manager_base.py new file mode 100644 index 0000000..a3855f3 --- /dev/null +++ b/openstack_odooclient/managers/record/manager_base.py @@ -0,0 +1,498 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date, datetime +from typing import TYPE_CHECKING, Generic, TypeVar, overload + +from ...exceptions import RecordNotFoundError +from .base import RecordBase +from .util import get_mapped_field + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + List, + Literal, + Mapping, + Optional, + Sequence, + Set, + Type, + Union, + ) + + from odoorpc import ODOO # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + + from ... import client + +Record = TypeVar("Record", bound=RecordBase) + + +class RecordManagerBase(Generic[Record]): + env_name: str + """The Odoo environment (model) name to manage.""" + + record_class: Type[Record] + """The record object type to instatiate using this manager.""" + + default_fields: Optional[Set[str]] = None + """List of fields to fetch by default if a field list is not supplied + in queries. + + By default, all fields on the model will be fetched. + """ + + def __init__(self, client: client.Client) -> None: + self._client = client + self._field_mapping_reverse = { + odoo_version: { + remote_field: local_field + for local_field, remote_field in field_mapping.items() + } + for odoo_version, field_mapping in ( + self.record_class._field_mapping.items() + ) + } + + @property + def _odoo(self) -> ODOO: + return self._client._odoo + + @property + def _env(self) -> Environment: + return self._odoo.env[self.env_name] + + @overload + def list( + self, + ids: Union[int, Iterable[int]], + fields: Optional[Iterable[str]] = ..., + as_dict: Literal[False] = ..., + ) -> List[Record]: ... + + @overload + def list( + self, + ids: Union[int, Iterable[int]], + fields: Optional[Iterable[str]] = ..., + *, + as_dict: Literal[True], + ) -> List[Dict[str, Any]]: ... + + @overload + def list( + self, + ids: Union[int, Iterable[int]], + fields: Optional[Iterable[str]] = ..., + as_dict: bool = ..., + ) -> Union[List[Record], List[Dict[str, Any]]]: ... + + def list( + self, + ids: Union[int, Iterable[int]], + fields: Optional[Iterable[str]] = None, + as_dict: bool = False, + ) -> Union[List[Record], List[Dict[str, Any]]]: + """Get one or more specific records by ID. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_dict`` parameter to return records as ``dict`` + objects, instead of record objects. + + If ``ids`` is given an empty iterator, this method + returns an empty list. + + :param ids: Record ID, or list of record IDs + :type ids: Union[int, Iterable[int]] + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Optional[Iterable[str]], optional + :param as_dict: Return records as dictionaries, defaults to ``False`` + :type as_dict: bool, optional + :return: List of records + :rtype: list[Record] or list[dict[str, Any]] + """ + if isinstance(ids, int): + _ids: Union[int, List[int]] = ids + else: + _ids = list(ids) + if not _ids: + return [] # type: ignore[return-value] + fields = fields or self.default_fields or None + _fields = ( + list( + dict.fromkeys( + (self._get_remote_field(f) for f in fields), + ).keys(), + ) + if fields is not None + else None + ) + records: Iterable[Dict[str, Any]] = self._env.read( + _ids, + fields=_fields, + ) + if as_dict: + return [ + { + self._get_local_field(field): value + for field, value in record_dict.items() + } + for record_dict in records + ] + return [ + self.record_class( + client=self._client, + manager=self, + record=record, + fields=_fields, + ) + for record in records + ] + + @overload + def get( + self, + id: int, # noqa: A002 + *, + fields: Optional[Iterable[str]] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Record: ... + + @overload + def get( + self, + id: int, # noqa: A002 + *, + fields: Optional[Iterable[str]] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def get( + self, + id: int, # noqa: A002 + *, + fields: Optional[Iterable[str]] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Record]: ... + + @overload + def get( + self, + id: int, # noqa: A002 + *, + fields: Optional[Iterable[str]] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def get( + self, + id: int, # noqa: A002 + *, + fields: Optional[Iterable[str]] = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Record, Dict[str, Any]]]: ... + + def get( + self, + id: int, # noqa: A002 + fields: Optional[Iterable[str]] = None, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Record, Dict[str, Any]]]: + """Get a single record by ID. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + :param ids: Record ID + :type ids: int + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[str] or None, optional + :param as_dict: Return record as a dictionary, defaults to ``False`` + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to ``False`` + :raises RecordNotFoundError: Record with the given ID not found + :return: List of records + :rtype: Union[Record, List[str, Any]] + """ + try: + return self.list(id, fields=fields, as_dict=as_dict)[0] + except IndexError: + if optional: + return None + else: + raise RecordNotFoundError( + ( + f"{self.record_class.__name__} record not found " + f"with ID: {id}" + ), + ) from None + + @overload + def search( + self, + filters: Optional[Sequence[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + ) -> List[Record]: ... + + @overload + def search( + self, + filters: Optional[Sequence[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[False] = ..., + ) -> List[int]: ... + + @overload + def search( + self, + filters: Optional[Sequence[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + *, + as_dict: Literal[True], + ) -> List[Dict[str, Any]]: ... + + @overload + def search( + self, + filters: Optional[Sequence[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[True], + ) -> List[int]: ... + + @overload + def search( + self, + filters: Optional[Sequence[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: bool = ..., + as_dict: bool = ..., + ) -> Union[List[Record], List[int], List[Dict[str, Any]]]: ... + + def search( + self, + filters: Optional[Sequence[Any]] = None, + fields: Optional[Iterable[str]] = None, + order: Optional[str] = None, + as_id: bool = False, + as_dict: bool = False, + ) -> Union[List[Record], List[int], List[Dict[str, Any]]]: + """Query the ERP for records, optionally defining + filters to constrain the search and other parameters, + and return the results. + + Query filters should be defined using the same format as OdooRPC, + but some additional features are supported: + + * Odoo client field aliases can be specified as the field name, + in additional to the original field name on the Odoo model + (e.g. ``create_user`` instead of ``create_uid``). + * Record objects can be directly passed as the value + on a filter, where a record ID would normally be expected. + * Sets and tuples are supported when specifying a range of values, + in addition to lists. + + To search *all* records, leave ``filters`` unset + (or set it to ``None``). + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the record as + a list of IDs, instead of record objects. + + Use the ``as_dict`` parameter to return the record as + a list of ``dict`` objects, instead of record objects. + + :param filters: Filters to query by, defaults to ``None`` (no filters) + :type filters: Sequence[Any] or None, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param order: Order results by field name, defaults to ``None`` + :type order: str or None, optional + :param as_id: Return the record IDs only, defaults to ``False`` + :type as_id: bool, optional + :param as_dict: Return records as dictionaries, defaults to ``False`` + :type as_dict: bool, optional + :return: List of records + :rtype: list[Record] or list[int] or list[dict[str, Any]] + """ + ids: List[int] = self._env.search( + (self._encode_filters(filters) if filters else []), + order=order, + ) + if as_id: + return ids + if ids: + return self.list(ids, fields=fields, as_dict=as_dict) + return [] # type: ignore[return-value] + + def create(self, **fields) -> int: + """Create a new record, using the specified keyword arguments + as input fields. + + To fetch the newly created record object, + pass the returned ID to the ``get`` method. + + :return: The ID of the newly created record + :rtype: int + """ + return self._env.create( + { + self._encode_field(field): self._encode_value(value) + for field, value in fields.items() + }, + ) + + def create_multi(self, *records: Mapping[str, Any]) -> List[int]: + """Create one or more new records in a single request, + passing in the mappings containing the record's input fields + as positional arguments. + + To fetch the newly created record objects, + pass the returned IDs to the ``list`` method. + + :return: The IDs of the newly created records + :rtype: List[int] + """ + res: Union[int, List[int]] = self._env.create( + [ + { + self._get_remote_field(field): value + for field, value in record.items() + } + for record in records + ], + ) + if isinstance(res, int): + return [res] + return res + + def unlink( + self, + *records: Union[Record, int, Iterable[Union[Record, int]]], + ) -> None: + """Delete one or more records from Odoo. + + This method accepts either a record object or ID, or an iterable of + either of those types. Multiple positional arguments are allowed. + + All specified records will be deleted in a single request. + + :param records: The records to delete (object, ID, or record/ID list) + :type records: Union[Record, int, Iterable[Union[Record, int]]] + """ + _ids: List[int] = [] + for ids in records: + if isinstance(ids, int): + _ids.append(ids) + elif isinstance(ids, RecordBase): + _ids.append(ids.id) + else: + _ids.extend( + ((i.id if isinstance(i, RecordBase) else i) for i in ids), + ) + self._env.unlink(_ids) + + def delete( + self, + *records: Union[Record, int, Iterable[Union[Record, int]]], + ) -> None: + """Delete one or more records from Odoo. + + This method accepts either a record object or ID, or an iterable of + either of those types. Multiple positional arguments are allowed. + + All specified records will be deleted in a single request. + + :param records: The records to delete (object, ID, or record/ID list) + :type records: Union[Record, int, Iterable[Union[Record, int]]] + """ + self.unlink(*records) + + def _get_remote_field(self, field: str) -> str: + return get_mapped_field( + field_mapping=self.record_class._field_mapping, + odoo_version=self._odoo.version, + field=field, + ) + + def _get_local_field(self, field: str) -> str: + return get_mapped_field( + field_mapping=self._field_mapping_reverse, + odoo_version=self._odoo.version, + field=field, + ) + + def _resolve_alias(self, alias: str) -> str: + return self.record_class._resolve_alias(alias) + + def _encode_field(self, field: str) -> str: + return self._get_remote_field(self._resolve_alias(field)) + + def _encode_value(self, value: Any) -> Any: + if isinstance(value, RecordBase): + return value.id + if isinstance(value, (date, datetime)): + return value.isoformat() + if isinstance(value, (list, set, tuple)): + return [self._encode_value(v) for v in value] + return value + + def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: + _filters: List[Any] = [] + for f in filters: + if isinstance(f, tuple): + _filter = ( + self._encode_field(f[0]), # Field name. + f[1], # Filter operator (=, >=, in, etc). + self._encode_value(f[2]), # Possible value(s). + ) + else: + _filter = f + _filters.append(_filter) + return _filters diff --git a/openstack_odooclient/managers/record/manager_code_base.py b/openstack_odooclient/managers/record/manager_code_base.py new file mode 100644 index 0000000..fab5e3f --- /dev/null +++ b/openstack_odooclient/managers/record/manager_code_base.py @@ -0,0 +1,186 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +from .manager_unique_field_base import Record, RecordManagerWithUniqueFieldBase + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + Literal, + Optional, + Union, + ) + + +class CodedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): + code_field: str = "code" + """The field code to use when querying by code in + the ``get_by_code`` method. + """ + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Record]: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Record: ... + + @overload + def get_by_code( + self, + code: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Record, int, Dict[str, Any]]]: ... + + def get_by_code( + self, + code: str, + fields: Optional[Iterable[str]] = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Record, int, Dict[str, Any]]]: + """Query a unique record by code. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given code does not exist, instead of raising an error. + + :param code: The record code + :type code: str + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same code + :raises RecordNotFoundError: Record with the given code not found + :return: Query result (or ``None`` if record not found and optional) + :rtype: Optional[Union[Record, int, Dict[str, Any]]] + """ + return self._get_by_unique_field( + field=self.code_field, + value=code, + fields=fields, + as_id=as_id, + as_dict=as_dict, + optional=optional, + ) diff --git a/openstack_odooclient/managers/record/manager_name_base.py b/openstack_odooclient/managers/record/manager_name_base.py new file mode 100644 index 0000000..5b67cd7 --- /dev/null +++ b/openstack_odooclient/managers/record/manager_name_base.py @@ -0,0 +1,186 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +from .manager_unique_field_base import Record, RecordManagerWithUniqueFieldBase + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + Literal, + Optional, + Union, + ) + + +class NamedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): + name_field: str = "name" + """The field name to use when querying by name in + the ``get_by_name`` method. + """ + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Record]: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Record: ... + + @overload + def get_by_name( + self, + name: str, + *, + fields: Optional[Iterable[str]] = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Record, int, Dict[str, Any]]]: ... + + def get_by_name( + self, + name: str, + fields: Optional[Iterable[str]] = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Record, int, Dict[str, Any]]]: + """Query a unique record by name. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given name does not exist, instead of raising an error. + + :param name: The record name + :type name: str + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same name + :raises RecordNotFoundError: Record with the given name not found + :return: Query result (or ``None`` if record not found and optional) + :rtype: Optional[Union[Record, int, Dict[str, Any]]] + """ + return self._get_by_unique_field( + field=self.name_field, + value=name, + fields=fields, + as_id=as_id, + as_dict=as_dict, + optional=optional, + ) diff --git a/openstack_odooclient/managers/record/manager_unique_field_base.py b/openstack_odooclient/managers/record/manager_unique_field_base.py new file mode 100644 index 0000000..3a56f4b --- /dev/null +++ b/openstack_odooclient/managers/record/manager_unique_field_base.py @@ -0,0 +1,223 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import itertools + +from typing import TYPE_CHECKING, Generic, TypeVar, overload + +from ...exceptions import MultipleRecordsFoundError, RecordNotFoundError +from .manager_base import Record, RecordManagerBase + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + Literal, + Optional, + Union, + ) + +T = TypeVar("T") + + +class RecordManagerWithUniqueFieldBase( + RecordManagerBase[Record], Generic[Record, T] +): + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[int]: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> int: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[True], + ) -> Optional[Dict[str, Any]]: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + optional: Literal[False] = ..., + ) -> Dict[str, Any]: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[True], + ) -> Optional[Record]: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + optional: Literal[False] = ..., + ) -> Record: ... + + @overload + def _get_by_unique_field( + self, + field: str, + value: T, + *, + filters: Optional[Iterable[Any]] = ..., + fields: Optional[Iterable[str]] = ..., + as_id: bool = ..., + as_dict: bool = ..., + optional: bool = ..., + ) -> Optional[Union[Record, int, Dict[str, Any]]]: ... + + def _get_by_unique_field( + self, + field: str, + value: T, + filters: Optional[Iterable[Any]] = None, + fields: Optional[Iterable[str]] = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, + ) -> Optional[Union[Record, int, Dict[str, Any]]]: + """Query a unique record by a specific field. + + A number of parameters are available to configure the return type, + and what happens when a result is not found. + + :param value: The unique field name to query by + :type name: str + :param value: The unique field value + :type name: T + :param filters: Optional additional filters to apply, defaults to None + :type filters: Optional[Iterable[Any]], optional + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional + :param as_dict: Return the record as a dictionary, defaults to False + :type as_dict: bool, optional + :param optional: Return ``None`` if not found, defaults to False + :type optional: bool, optional + :raises MultipleRecordsFoundError: Multiple records with the same name + :raises RecordNotFoundError: Record with the given name not found + :return: Query result (or ``None`` if record not found and optional) + :rtype: Optional[Union[Record, int, Dict[str, Any]]] + """ + field_filter = [(field, "=", value)] + try: + records = self.search( + filters=( + list(itertools.chain(field_filter, filters)) + if filters + else field_filter + ), + fields=fields, + as_id=as_id, + as_dict=as_dict, + ) + if len(records) > 1: + raise MultipleRecordsFoundError( + ( + f"Multiple {self.record_class.__name__} records " + f"found with {field!r} value {value!r} " + "when only one was expected: " + f"{', '.join(str(r) for r in records)}" + ), + ) + return records[0] + except IndexError: + if optional: + return None + else: + raise RecordNotFoundError( + ( + f"{self.record_class.__name__} record not found " + f"with {field!r} value: {value}" + ), + ) from None diff --git a/openstack_odooclient/managers/record/util.py b/openstack_odooclient/managers/record/util.py new file mode 100644 index 0000000..1505d20 --- /dev/null +++ b/openstack_odooclient/managers/record/util.py @@ -0,0 +1,132 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date, datetime +from typing import ( + TYPE_CHECKING, + Literal, + Type, + TypeVar, + Union, + get_args as get_type_args, + get_origin as get_type_origin, +) + +if TYPE_CHECKING: + from typing import Any, List, Mapping, Optional + +T = TypeVar("T") + + +def get_mapped_field( + field_mapping: Mapping[Optional[str], Mapping[str, str]], + odoo_version: str, + field: str, +) -> str: + """Map a field name to its representative in the given field mapping, + based on the given Odoo version. + + If a representative value is not found for the given Odoo version, + check the ``None`` mapping for all Odoo versions. + If none is found there either, return the field name as is. + + :param field_mapping: Field mapping structure + :type field_mapping: Mapping[Optional[str], Mapping[str, str]] + :param odoo_version: Odoo server version + :type odoo_version: str + :param field: Field name to map + :type field: str + :return: Mapped field name + :rtype: str + """ + + try: + return field_mapping[odoo_version][field] + except KeyError: + try: + return field_mapping[None][field] + except KeyError: + return field + + +def decode_value(annotation: Type[T], value: Any) -> T: + """Decode a raw Odoo JSON field value to its local client representation, + based on the annotation defined in the record model. + + :param annotation: The annotation to use to decode the value + :type annotation: Type[T] + :param value: The value to decode + :type value: Any + :return: The decoded value + :rtype: T + """ + + # Create a type tree, which peels back the annotation layers + # to find the basic data type that is expected. + type_tree: List[Type[Any]] = [annotation] + while get_type_origin(type_tree[-1]) is not None: + origin_type = get_type_origin(type_tree[-1]) + if origin_type is not None: + type_tree.append(origin_type) + + # The basic data types that need special handling. + if type_tree[-1] is date: + return date.fromisoformat(value) # type: ignore[return-value] + elif type_tree[-1] is datetime: + return datetime.fromisoformat(value) # type: ignore[return-value] + # When a list is expected, decode each value individually + # and return the result as a new list with the same order. + elif type_tree[-1] is list: + return [ # type: ignore[return-value] + decode_value(get_type_args(type_tree[-2])[0], v) for v in value + ] + # When a dict is expected, decode the key and the value of each + # item separately, and combine the result into a new dict. + elif type_tree[-1] is dict: + key_type, value_type = get_type_args(type_tree[-2]) + return { # type: ignore[return-value] + decode_value(key_type, k): decode_value(value_type, v) + for k, v in value.items() + } + # Basic case for handling specific union structures. + # Not suitable for handling complicated union structures. + # TODO(callumdickinson): Find a way to handle complicated + # union structures more smartly. + elif type_tree[-1] is Union: + attr_union_types = get_type_args(type_tree[-2]) + if len(attr_union_types) == 2: # noqa: PLR2004 + # Optional[T] + if type(None) in attr_union_types and value is not None: + return decode_value( + next(t for t in attr_union_types if t is not type(None)), + value, + ) + # Union[T, Literal[False]] + if Literal[False] in attr_union_types and value is not False: + return decode_value( + next( + ( + t + for t in attr_union_types + if t is not Literal[False] + ), + ), + value, + ) + + # Base case: Return the passed value unmodified. + return value diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py new file mode 100644 index 0000000..54053cd --- /dev/null +++ b/openstack_odooclient/managers/referral_code.py @@ -0,0 +1,121 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List + +from . import record + +if TYPE_CHECKING: + from . import credit_type, partner + + +class ReferralCode(record.RecordBase): + allowed_uses: int + """The number of allowed uses of this referral code. + + Set to ``-1`` for unlimited uses. + """ + + before_reward_usage_threshold: float + """The amount of usage that must be recorded by the new sign-up + before the reward credit is awarded to the referrer. + """ + + code: str + """The unique referral code.""" + + name: str + """Automatically generated name for the referral code.""" + + @property + def referral_ids(self) -> List[int]: + """A list of IDs for the partners that signed up + using this referral code. + """ + return self._get_field("referrals") + + @cached_property + def referrals(self) -> List[partner.Partner]: + """The partners that signed up using this referral code. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.partners.list(self.referral_ids) + + referral_credit_amount: float + """Initial balance for the referral credit.""" + + referral_credit_duration: int + """Duration of the referral credit, in days.""" + + @property + def referral_credit_type_id(self) -> int: + """The ID of the credit type to use for the referral credit.""" + return self._get_ref_id("referral_credit_type") + + @property + def referral_credit_type_name(self) -> str: + """The name of the credit type to use for the referral credit.""" + return self._get_ref_name("referral_credit_type") + + @cached_property + def referral_credit_type(self) -> credit_type.CreditType: + """The credit type to use for the referral credit. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.credit_types.get(self.referral_credit_type_id) + + reward_credit_amount: float + """Initial balance for the reward credit.""" + + reward_credit_duration: int + """Duration of the reward credit, in days.""" + + @property + def reward_credit_type_id(self) -> int: + """The ID of the credit type to use for the reward credit.""" + return self._get_ref_id("reward_credit_type") + + @property + def reward_credit_type_name(self) -> str: + """The name of the credit type to use for the reward credit.""" + return self._get_ref_name("reward_credit_type") + + @cached_property + def reward_credit_type(self) -> credit_type.CreditType: + """The credit type to use for the reward credit. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.credit_types.get(self.reward_credit_type_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "referral_ids": "referrals", + "referral_credit_type_id": "referral_credit_type", + "reward_credit_type_id": "reward_credit_type", + } + + +class ReferralCodeManager(record.CodedRecordManagerBase[ReferralCode]): + env_name = "openstack.referral_code" + record_class = ReferralCode diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py new file mode 100644 index 0000000..043d9b6 --- /dev/null +++ b/openstack_odooclient/managers/reseller.py @@ -0,0 +1,118 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Optional + +from . import record + +if TYPE_CHECKING: + from . import partner as partner_module, project, reseller_tier + + +class Reseller(record.RecordBase): + alternative_billing_url: Optional[str] + """The URL to the cloud billing page for the reseller, if available.""" + + alternative_support_url: Optional[str] + """The URL to the cloud support centre for the reseller, if available.""" + + @property + def demo_project_id(self) -> Optional[int]: + """The ID for the optional demo project belonging to the reseller.""" + return self._get_ref_id("project_demo", optional=True) + + @property + def demo_project_name(self) -> Optional[str]: + """The name of the optional demo project belonging to the reseller.""" + return self._get_ref_name("project_demo", optional=True) + + @cached_property + def demo_project(self) -> Optional[project.Project]: + """An optional demo project belonging to the reseller. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + hide_billing: bool + """Whether or not the billing URL should be hidden.""" + + hide_support: bool + """Whether or not the support URL should be hidden.""" + + name: str + """The automatically generated reseller name. + + This is set to the reseller partner's name. + """ + + @property + def partner_id(self) -> int: + """The ID for the reseller partner.""" + return self._get_ref_id("partner") + + @property + def partner_name(self) -> str: + """The name of the reseller partner.""" + return self._get_ref_name("partner") + + @cached_property + def partner(self) -> partner_module.Partner: + """The reseller partner. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + @property + def tier_id(self) -> int: + """The ID for the tier this reseller is under.""" + return self._get_ref_id("tier") + + @property + def tier_name(self) -> str: + """The name of the tier this reseller is under.""" + return self._get_ref_name("tier") + + @cached_property + def tier(self) -> reseller_tier.ResellerTier: + """The tier this reseller is under. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.reseller_tiers.get(self.tier_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "demo_project_id": "demo_project", + "partner_id": "partner", + "tier_id": "tier", + } + + +class ResellerManager(record.RecordManagerBase[Reseller]): + env_name = "openstack.reseller" + record_class = Reseller diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py new file mode 100644 index 0000000..589df99 --- /dev/null +++ b/openstack_odooclient/managers/reseller_tier.py @@ -0,0 +1,100 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from . import record + +if TYPE_CHECKING: + from . import product + + +class ResellerTier(record.RecordBase): + discount_percent: float + """The maximum discount percentage for this reseller tier (0-100).""" + + @property + def discount_product_id(self) -> int: + """The ID of the discount product for the reseller tier.""" + return self._get_ref_id("discount_product") + + @property + def discount_product_name(self) -> str: + """The name of the discount product for the reseller tier.""" + return self._get_ref_name("discount_product") + + @cached_property + def discount_product(self) -> product.Product: + """The discount product for the reseller tier. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.discount_product_id) + + free_monthly_credit: float + """The amount the reseller gets monthly in credit for demo projects.""" + + @property + def free_monthly_credit_product_id(self) -> int: + """The ID of the product to use when adding the free monthly credit + to demo project invoices. + """ + return self._get_ref_id("free_monthly_credit_product") + + @property + def free_monthly_credit_product_name(self) -> str: + """The name of the product to use when adding the free monthly credit + to demo project invoices. + """ + return self._get_ref_name("free_monthly_credit_product") + + @cached_property + def free_monthly_credit_product(self) -> product.Product: + """The product to use when adding the free monthly credit + to demo project invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.free_monthly_credit_product_id) + + free_support_hours: int + """The amount of free support hours the reseller is entitled to + under this tier. + """ + + hide_support: bool + """Whether or not the support URL should be hidden.""" + + name: str + """Reseller tier name.""" + + min_usage_threshold: float + """The minimum required usage amount for the reseller tier.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "discount_product_id": "discount_product", + "free_monthly_credit_product_id": "free_monthly_credit_product", + } + + +class ResellerTierManager(record.NamedRecordManagerBase[ResellerTier]): + env_name = "openstack.reseller.tier" + record_class = ResellerTier diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py new file mode 100644 index 0000000..3bf1915 --- /dev/null +++ b/openstack_odooclient/managers/sale_order.py @@ -0,0 +1,186 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal, Union + +from . import record + +if TYPE_CHECKING: + from . import ( + currency as currency_module, + partner as partner_module, + sale_order_line, + ) + + +class SaleOrder(record.RecordBase): + amount_untaxed: float + """The untaxed total cost of the sale order.""" + + amount_tax: float + """The amount in taxes on this sale order.""" + + amount_total: float + """The taxed total cost of the sale order.""" + + client_order_ref: Union[str, Literal[False]] + """The customer reference for this sale order, if defined.""" + + @property + def currency_id(self) -> int: + """The ID for the currency used in this sale order.""" + return self._get_ref_id("currency_id") + + @property + def currency_name(self) -> str: + """The name of the currency used in this sale order.""" + return self._get_ref_name("currency_id") + + @cached_property + def currency(self) -> currency_module.Currency: + """The currency used in this sale order. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.currencies.get(self.currency_id) + + date_order: datetime + """The time the sale order was created.""" + + display_name: str + """The display name of the sale order.""" + + invoice_status: Literal["no", "to invoice", "invoiced", "upselling"] + """The current invoicing status of this sale order. + + Values: + + * ``no`` - Nothing to invoice + * ``to invoice`` - Has line items that need to be invoiced + * ``invoiced`` - Fully invoiced + * ``upselling`` - Upselling opportunity + """ + + name: str + """The name assigned to the sale order.""" + + note: str + """A note attached to the sale order. + + Generally used for terms and conditions. + """ + + @property + def order_line_ids(self) -> List[int]: + """A list of IDs for the lines added to the sale order.""" + return self._get_field("order_line") + + @cached_property + def order_line(self) -> List[sale_order_line.SaleOrderLine]: + """The lines added to the sale order. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.sale_order_lines.list(self.order_line_ids) + + @property + def order_lines(self) -> List[sale_order_line.SaleOrderLine]: + """An alias for ``order_line``.""" + return self.order_line + + @property + def partner_id(self) -> int: + """The ID for the recipient partner for the sale order.""" + return self._get_ref_id("partner_id") + + @property + def partner_name(self) -> str: + """The name of the recipient partner for the sale order.""" + return self._get_ref_name("partner_id") + + @cached_property + def partner(self) -> partner_module.Partner: + """The recipient partner for the sale order. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + state: Literal["draft", "sale", "done", "cancel"] + """State of the sale order. + + Values: + + * ``draft`` - Draft sale order (quotation), can still be modified + * ``sale`` - Finalised sale order, cannot be modified + * ``done`` - Finalised and settled sale order, cannot be modified + * ``cancel`` - Cancelled sale order, can be deleted + """ + + _alias_mapping = { + # Key is local alias, value is remote field name. + "currency": "currency_id", + "order_line_ids": "order_line", + "order_lines": "order_line", + "partner": "partner_id", + } + + def action_confirm(self) -> None: + """Confirm the sale order.""" + self._client.sale_orders.action_confirm(self) + + def create_invoices(self) -> None: + """Create invoices from this sale order.""" + self._client.sale_orders.create_invoices(self) + + +class SaleOrderManager(record.NamedRecordManagerBase[SaleOrder]): + env_name = "sale.order" + record_class = SaleOrder + + def action_confirm(self, sale_order: Union[int, SaleOrder]) -> None: + """Confirm the given sale order. + + :param sale_order: Sale order to confirm + :type sale_order: Union[int, SaleOrder] + """ + self._env.action_confirm( + ( + sale_order.id + if isinstance(sale_order, SaleOrder) + else sale_order + ), + ) + + def create_invoices(self, sale_order: Union[int, SaleOrder]) -> None: + """Create invoices from this sale order. + + :param sale_order: Sale order to create invoices for + :type sale_order: Union[int, SaleOrder] + """ + self._env.create_invoices( + ( + sale_order.id + if isinstance(sale_order, SaleOrder) + else sale_order + ), + ) diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py new file mode 100644 index 0000000..fc7bf89 --- /dev/null +++ b/openstack_odooclient/managers/sale_order_line.py @@ -0,0 +1,374 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal + +from . import record + +if TYPE_CHECKING: + from . import ( + account_move_line, + company as company_module, + currency as currency_module, + partner, + product as product_module, + project, + sale_order, + tax as tax_module, + uom, + ) + + +class SaleOrderLine(record.RecordBase): + @property + def company_id(self) -> int: + """The ID for the company this sale order line + was generated for. + """ + return self._get_ref_id("company_id") + + @property + def company_name(self) -> str: + """The name of the company this sale order line + was generated for. + """ + return self._get_ref_name("company_id") + + @cached_property + def company(self) -> company_module.Company: + """The company this sale order line + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.companies.get(self.company_id) + + @property + def currency_id(self) -> int: + """The ID for the currency used in this sale order line.""" + return self._get_ref_id("currency_id") + + @property + def currency_name(self) -> str: + """The name of the currency used in this sale order line.""" + return self._get_ref_name("currency_id") + + @cached_property + def currency(self) -> currency_module.Currency: + """The currency used in this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.currencies.get(self.currency_id) + + discount: float + """Discount on the sale order line, in percent.""" + + display_name: str + """Display name for the sale order line in the sale order.""" + + @property + def invoice_line_ids(self) -> List[int]: + """A list of IDs for the invoice (account move) lines created + from this sale order line. + """ + return self._get_field("invoice_lines") + + @cached_property + def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: + """The invoice (account move) lines created + from this sale order line. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.account_move_lines.list(self.invoice_line_ids) + + invoice_status: Literal["no", "to invoice", "invoiced", "upselling"] + """The current invoicing status of this sale order line. + + Values: + + * ``no`` - Nothing to invoice + * ``to invoice`` - Has quantity that needs to be invoiced + * ``invoiced`` - Fully invoiced + * ``upselling`` - Upselling opportunity + """ + + is_downpayment: bool + """Whether or not this sale order line is a downpayment.""" + + is_expense: bool + """Whether or not this sale order line is an expense.""" + + name: str + """Name assigned to the the sale order line. + + This is not the same as the product name. + In the OpenStack Integration add-on, this is normally used to store + the resource's name. + """ + + @property + def order_id(self) -> int: + """The ID for the sale order this line is linked to.""" + return self._get_ref_id("order_id") + + @property + def order_name(self) -> str: + """The name of the sale order this line is linked to.""" + return self._get_ref_name("order_id") + + @cached_property + def order(self) -> sale_order.SaleOrder: + """The sale order this line is linked to. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.sale_orders.get(self.order_id) + + @property + def order_partner_id(self) -> int: + """The ID for the recipient partner for the sale order.""" + return self._get_ref_id("order_partner_id") + + @property + def order_partner_name(self) -> str: + """The name of the recipient partner for the sale order.""" + return self._get_ref_name("order_partner_id") + + @cached_property + def order_partner(self) -> partner.Partner: + """The recipient partner for the sale order. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.order_partner_id) + + @property + def os_project_id(self) -> int: + """The ID for the he OpenStack project this sale order line was + was generated for. + """ + return self._get_ref_id("os_project") + + @property + def os_project_name(self) -> str: + """The name of the he OpenStack project this sale order line was + was generated for. + """ + return self._get_ref_name("os_project") + + @cached_property + def os_project(self) -> project.Project: + """The OpenStack project this sale order line was + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.projects.get(self.os_project_id) + + os_region: str + """The OpenStack region the sale order line was created from.""" + + os_resource_id: str + """The OpenStack resource ID for the resource that generated + this sale order line. + """ + + os_resource_name: str + """The name of the OpenStack resource tier or flavour, + as used by services such as Distil for rating purposes. + + For example, if this is the sale order line for a compute instance, + this would be set to the instance's flavour name. + """ + + os_resource_type: str + """A human-readable description of the type of resource captured + by this sale order line. + """ + + price_reduce: float + """Base unit price, less discount (see the ``discount`` field).""" + + price_reduce_taxecl: float + """Actual unit price, excluding tax.""" + + price_reduce_taxinc: float + """Actual unit price, including tax.""" + + price_subtotal: float + """Subtotal price for the sale order line, excluding tax.""" + + price_tax: float + """Tax charged on the sale order line.""" + + price_total: float + """Total price for the sale order line, including tax.""" + + price_unit: float + """Base unit price, excluding tax, before any discounts.""" + + @property + def product_id(self) -> int: + """The ID of the dproduct charged on this sale order line.""" + return self._get_ref_id("product_id") + + @property + def product_name(self) -> str: + """The name of the product charged on this sale order line.""" + return self._get_ref_name("product_id") + + @cached_property + def product(self) -> product_module.Product: + """The product charged on this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.product_id) + + @property + def product_uom_id(self) -> int: + """The ID for the Unit of Measure for the product being charged in + this sale order line. + """ + return self._get_ref_id("product_uom") + + @property + def product_uom_name(self) -> str: + """The name of the Unit of Measure for the product being charged in + this sale order line. + """ + return self._get_ref_name("product_uom") + + @cached_property + def product_uom(self) -> uom.Uom: + """The Unit of Measure for the product being charged in + this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.uoms.get(self.product_uom_id) + + product_uom_qty: float + """The product quantity on the sale order line.""" + + product_uom_readonly: bool + """Whether or not the product quantity can still be updated + on this sale order line. + """ + + product_updatable: bool + """Whether or not the product can be edited on this sale order line.""" + + qty_invoiced: float + """The product quantity that has already been invoiced.""" + + qty_to_invoice: float + """The product quantity that still needs to be invoiced.""" + + @property + def salesman_id(self) -> int: + """The ID for the salesperson partner assigned + to this sale order line. + """ + return self._get_ref_id("salesman_id") + + @property + def salesman_name(self) -> str: + """The name of the salesperson partner assigned + to this sale order line. + """ + return self._get_ref_name("salesman_id") + + @cached_property + def salesman(self) -> partner.Partner: + """The salesperson partner assigned + to this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.salesman_id) + + state: Literal["draft", "sale", "done", "cancel"] + """State of the sale order. + + Values: + + * ``draft`` - Draft sale order (quotation), can still be modified + * ``sale`` - Finalised sale order, cannot be modified + * ``done`` - Finalised and settled sale order, cannot be modified + * ``cancel`` - Cancelled sale order, can be deleted + """ + + @property + def tax_id(self) -> int: + """The ID for the tax used on this sale order line.""" + return self._get_ref_id("tax_id") + + @property + def tax_name(self) -> str: + """The name of the tax used on this sale order line.""" + return self._get_ref_name("tax_id") + + @cached_property + def tax(self) -> tax_module.Tax: + """The tax used on this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.taxes.get(self.tax_id) + + untaxed_amount_invoiced: float + """The balance, excluding tax, on the sale order line that + has already been invoiced. + """ + + untaxed_amount_to_invoice: float + """The balance, excluding tax, on the sale order line that + still needs to be invoiced. + """ + + _alias_mapping = { + # Key is local alias, value is remote field name. + "company": "company_id", + "currency": "currency_id", + "os_project_id": "os_project", + "invoice_line_ids": "invoice_lines", + "order": "order_id", + "order_partner": "order_partner_id", + "product": "product_id", + "product_uom": "product_uom_id", + "salesman": "salesman_id", + "tax": "tax_id", + } + + +class SaleOrderLineManager(record.RecordManagerBase[SaleOrderLine]): + env_name = "sale.order.line" + record_class = SaleOrderLine diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py new file mode 100644 index 0000000..ba16008 --- /dev/null +++ b/openstack_odooclient/managers/support_subscription.py @@ -0,0 +1,150 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Optional + +from . import record + +if TYPE_CHECKING: + from . import ( + partner as partner_module, + project as project_module, + support_subscription_type as support_subscription_type_module, + ) + + +class SupportSubscription(record.RecordBase): + billing_type: Literal["paid", "complimentary"] + """The method of billing for the support subscription. + + Values: + + * ``paid`` - Charge the subscription independently + * ``complimentary`` - Bundled with a contract that includes the charge + """ + + end_date: date + """The end date of the credit.""" + + @property + def partner_id(self) -> Optional[int]: + """The ID for the partner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + """ + return self._get_ref_id("partner", optional=True) + + @property + def partner_name(self) -> Optional[str]: + """The name of thepartner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + """ + return self._get_ref_name("partner", optional=True) + + @cached_property + def partner(self) -> Optional[partner_module.Partner]: + """The partner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.partner_id + return ( + self._client.partners.get(record_id) + if record_id is not None + else None + ) + + @property + def project_id(self) -> Optional[int]: + """The ID of the project this support subscription is for, + if it is linked to a specific project. + """ + return self._get_ref_id("project", optional=True) + + @property + def project_name(self) -> Optional[str]: + """The name of the project this support subscription is for, + if it is linked to a specific project. + """ + return self._get_ref_name("project", optional=True) + + @cached_property + def project(self) -> Optional[project_module.Project]: + """The project this support subscription is for, + if it is linked to a specific project. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + start_date: date + """The start date of the credit.""" + + @property + def support_subscription_type_id(self) -> int: + """The ID of the type of the support subscription.""" + return self._get_ref_id("support_subscription_type") + + @property + def support_subscription_type_name(self) -> str: + """The name of the type of the support subscription.""" + return self._get_ref_name("support_subscription_type") + + @cached_property + def support_subscription_type( + self, + ) -> support_subscription_type_module.SupportSubscriptionType: + """The type of the support subscription. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.support_subscription_types.get( + self.support_subscription_type_id, + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "partner_id": "partner", + "project_id": "project", + "support_subscription_type_id": "support_subscription_type", + } + + +class SupportSubscriptionManager( + record.RecordManagerBase[SupportSubscription], +): + env_name = "openstack.support_subscription" + record_class = SupportSubscription diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py new file mode 100644 index 0000000..7829876 --- /dev/null +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -0,0 +1,101 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal + +from . import record + +if TYPE_CHECKING: + from . import ( + product as product_module, + support_subscription as support_subscription_type, + ) + + +class SupportSubscriptionType(record.RecordBase): + billing_type: Literal["paid", "complimentary"] + """The type of support subscription.""" + + name: str + """The name of the support subscription type.""" + + @property + def product_id(self) -> int: + """The ID for the product to use to invoice + the support subscription. + """ + return self._get_ref_id("product") + + @property + def product_name(self) -> str: + """The name of the product to use to invoice + the support subscription. + """ + return self._get_ref_name("product") + + @cached_property + def product(self) -> product_module.Product: + """The product to use to invoice + the support subscription. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.products.get(self.product_id) + + usage_percent: float + """Percentage of usage compared to price (0-100).""" + + @property + def support_subscription_ids(self) -> List[int]: + """A list of IDs for the support subscriptions of this type.""" + return self._get_field("support_subscription") + + @cached_property + def support_subscription( + self, + ) -> List[support_subscription_type.SupportSubscription]: + """The list of support subscriptions of this type. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.support_subscriptions.list( + self.support_subscription_ids, + ) + + @cached_property + def support_subscriptions( + self, + ) -> List[support_subscription_type.SupportSubscription]: + """An alias for ``support_subscription``.""" + return self.support_subscription + + _alias_mapping = { + # Key is local alias, value is remote field name. + "product": "product_id", + "support_subscription_ids": "support_subscription", + "support_subscriptions": "support_subscription", + } + + +class SupportSubscriptionTypeManager( + record.NamedRecordManagerBase[SupportSubscriptionType], +): + env_name = "openstack.support_subscription.type" + record_class = SupportSubscriptionType diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py new file mode 100644 index 0000000..89dd119 --- /dev/null +++ b/openstack_odooclient/managers/tax.py @@ -0,0 +1,122 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal + +from . import record + +if TYPE_CHECKING: + from . import company as company_module, tax_group as tax_group_module + + +class Tax(record.RecordBase): + active: bool + """Whether or not this tax is active (enabled).""" + + amount: float + """The amount of tax to apply.""" + + amount_type: Literal["group", "fixed", "percent", "division"] + """ + Values: + + * ``group`` - Group of Taxes + * ``fixed`` - Fixed + * ``percent`` - Percentage of Price + * ``division`` - Percentage of Price Tax Included + """ + + analytic: bool + """When set to ``True``, the amount computed by this tax will be assigned + to the same analytic account as the invoice line (if any). + """ + + @property + def company_id(self) -> int: + """The ID for the company this tax is owned by.""" + return self._get_ref_id("company_id") + + @property + def company_name(self) -> str: + """The name of the company this tax is owned by.""" + return self._get_ref_name("company_id") + + @cached_property + def company(self) -> company_module.Company: + """The company this tax is owned by. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.companies.get(self.company_id) + + country_code: str + """The country code for this tax.""" + + description: str + """The label for this tax on invoices.""" + + include_base_amount: bool + """When set to ``True``, taxes included after this one will be calculated + based on the price with this tax included. + """ + + name: str + """Tax name.""" + + price_include: bool + """Whether or not prices included in invoices should include this tax.""" + + tax_eligibility: Literal["on_invoice", "on_payment"] + """When the tax is due for the invoice. + + Values: + + * ``on_invoice`` - Due as soon as the invoice is validated + * ``on_payment`` - Due as soon as payment of the invoice is received + """ + + @property + def tax_group_id(self) -> int: + """The ID for the company partner this tax is owned by.""" + return self._get_ref_id("tax_group_id") + + @property + def tax_group_name(self) -> str: + """The name of the tax_group partner this tax is owned by.""" + return self._get_ref_name("tax_group_id") + + @cached_property + def tax_group(self) -> tax_group_module.TaxGroup: + """The tax_group partner this tax is owned by. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.tax_groups.get(self.tax_group_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "company": "company_id", + "tax_group": "tax_group_id", + } + + +class TaxManager(record.NamedRecordManagerBase[Tax]): + env_name = "account.tax" + record_class = Tax diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py new file mode 100644 index 0000000..77a8db1 --- /dev/null +++ b/openstack_odooclient/managers/tax_group.py @@ -0,0 +1,28 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from . import record + + +class TaxGroup(record.RecordBase): + name: str + """Tax group name.""" + + +class TaxGroupManager(record.NamedRecordManagerBase[TaxGroup]): + env_name = "account.tax.group" + record_class = TaxGroup diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py new file mode 100644 index 0000000..980bcf2 --- /dev/null +++ b/openstack_odooclient/managers/term_discount.py @@ -0,0 +1,140 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, Optional + +from . import record + +if TYPE_CHECKING: + from . import partner as partner_module, project as project_module + + +class TermDiscount(record.RecordBase): + discount_percent: float + """The maximum discount percentage for this term discount (0-100).""" + + early_termination_date: Optional[date] + """An optional early termination date for the term discount.""" + + end_date: date + """The date that the term discount expires on.""" + + min_commit: float + """The minimum commitment for this term discount to apply.""" + + @property + def partner_id(self) -> int: + """The ID for the partner that receives this term discount.""" + return self._get_ref_id("partner_id") + + @property + def partner_name(self) -> str: + """The name of the partner that receives this term discount.""" + return self._get_ref_name("partner_id") + + @cached_property + def partner(self) -> partner_module.Partner: + """The partner that receives this term discount. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + @property + def project_id(self) -> Optional[int]: + """The ID for the project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + """ + return self._get_ref_id("project", optional=True) + + @property + def project_name(self) -> Optional[str]: + """The name of the project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + """ + return self._get_ref_name("project", optional=True) + + @cached_property + def project(self) -> Optional[project_module.Project]: + """The project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + start_date: date + """The date from which this term discount starts.""" + + @property + def superseded_by_id(self) -> Optional[int]: + """The ID for the term discount that supersedes this one, + if superseded. + """ + return self._get_ref_id("superseded_by", optional=True) + + @property + def superseded_by_name(self) -> Optional[str]: + """The name of the term discount that supersedes this one, + if superseded. + """ + return self._get_ref_name("superseded_by", optional=True) + + @cached_property + def superseded_by(self) -> Optional[TermDiscount]: + """The term discount that supersedes this one, + if superseded. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.superseded_by_id + return ( + self._client.term_discounts.get(record_id) + if record_id is not None + else None + ) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "partner_id": "partner", + "project_id": "project", + "superseded_by_id": "superseded_by", + } + + +class TermDiscountManager(record.RecordManagerBase[TermDiscount]): + env_name = "openstack.term_discount" + record_class = TermDiscount diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py new file mode 100644 index 0000000..44c8de3 --- /dev/null +++ b/openstack_odooclient/managers/trial.py @@ -0,0 +1,73 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Union + +from . import record + +if TYPE_CHECKING: + from . import partner as partner_module + + +class Trial(record.RecordBase): + account_suspended_date: Union[date, Literal[False]] + """The date the account was suspended, following the end of the trial.""" + + account_terminated_date: Union[date, Literal[False]] + """The date the account was terminated, following the end of the trial.""" + + account_upgraded_date: Union[date, Literal[False]] + """The date the account was upgraded to a full account, + following the end of the trial. + """ + + end_date: date + """The end date of this trial.""" + + @property + def partner_id(self) -> int: + """The ID for the target partner for this trial.""" + return self._get_ref_id("partner") + + @property + def partner_name(self) -> str: + """The name of the target partner for this trial.""" + return self._get_ref_name("partner") + + @cached_property + def partner(self) -> partner_module.Partner: + """The target partner for this trial. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + start_date: date + """The start date of this trial.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "partner_id": "partner", + } + + +class TrialManager(record.RecordManagerBase[Trial]): + env_name = "openstack.trial" + record_class = Trial diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py new file mode 100644 index 0000000..05224b2 --- /dev/null +++ b/openstack_odooclient/managers/uom.py @@ -0,0 +1,102 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal + +from . import record + +if TYPE_CHECKING: + from . import uom_category + + +class Uom(record.RecordBase): + active: bool + """Whether or not this Unit of Measure is active (enabled).""" + + @property + def category_id(self) -> int: + """The ID for the category this Unit of Measure is classified as.""" + return self._get_ref_id("category_id") + + @property + def category_name(self) -> str: + """The name of the category this Unit of Measure is classified as.""" + return self._get_ref_name("category_id") + + @cached_property + def category(self) -> uom_category.UomCategory: + """The category this Unit of Measure is classified as. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.uom_categories.get(self.category_id) + + factor: float + """How much bigger or smaller this unit is compared to the reference + Unit of Measure (UoM) for the classified category. + """ + + factor_inv: float + """How many times this Unit of Measure is bigger than the reference + Unit of Measure (UoM) for the classified category. + """ + + measure_type: Literal[ + "unit", + "weight", + "working_time", + "length", + "volume", + ] + """The type of category this Unit of Measure (UoM) is classified as. + + This field no longer exists from Odoo 14 onwards. + + Values: + + * ``unit`` - Default Units + * ``weight`` - Default Weight + * ``working_time`` - Default Working Time + * ``length`` - Default Length + * ``volume`` - Default Volume + """ + + name: str + """Unit of Measure (UoM) name.""" + + uom_type: Literal["bigger", "reference", "smaller"] + """The type of the Unit of Measure (UoM). + This determines its relationship with other UoMs in the same category. + + Values: + + * ``bigger`` - Bigger than the reference Unit of Measure + * ``reference`` - Reference Unit of Measure for the selected category + * ``smaller`` - Smaller than the reference Unit of Measure + """ + + _alias_mapping = { + # Key is local alias, value is remote field name. + "category": "category_id", + } + + +class UomManager(record.RecordManagerBase[Uom]): + env_name = "uom.uom" + record_class = Uom diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py new file mode 100644 index 0000000..46b1b47 --- /dev/null +++ b/openstack_odooclient/managers/uom_category.py @@ -0,0 +1,50 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Literal + +from . import record + + +class UomCategory(record.RecordBase): + measure_type: Literal[ + "unit", + "weight", + "working_time", + "length", + "volume", + ] + """The type of Unit of Measure (UoM) category. + + This field no longer exists from Odoo 14 onwards. + + Values: + + * ``unit`` - Default Units + * ``weight`` - Default Weight + * ``working_time`` - Default Working Time + * ``length`` - Default Length + * ``volume`` - Default Volume + """ + + name: str + """Unit of Measure (UoM) category name.""" + + +class UomCategoryManager(record.RecordManagerBase[UomCategory]): + env_name = "uom.category" + record_class = UomCategory diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py new file mode 100644 index 0000000..89543a1 --- /dev/null +++ b/openstack_odooclient/managers/user.py @@ -0,0 +1,84 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from . import record + +if TYPE_CHECKING: + from . import company as company_module, partner as partner_module + + +class User(record.RecordBase): + active: bool + """Whether or not this user is active.""" + + active_partner: bool + """Whether or not the partner this user is associated with is active.""" + + @property + def company_id(self) -> int: + """The ID for the default company this user is logged in as.""" + return self._get_ref_id("company_id") + + @property + def company_name(self) -> str: + """The name of the default company this user is logged in as.""" + return self._get_ref_name("company_id") + + @cached_property + def company(self) -> company_module.Company: + """The default company this user is logged in as. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.companies.get(self.company_id) + + name: str + """User name.""" + + @property + def partner_id(self) -> int: + """The ID for the partner that this user is associated with.""" + return self._get_ref_id("partner_id") + + @property + def partner_name(self) -> str: + """The name of the partner that this user is associated with.""" + return self._get_ref_name("partner_id") + + @cached_property + def partner(self) -> partner_module.Partner: + """The partner that this user is associated with. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + return self._client.partners.get(self.partner_id) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "company": "company_id", + "partner": "partner_id", + } + + +class UserManager(record.RecordManagerBase[User]): + env_name = "res.users" + record_class = User diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py new file mode 100644 index 0000000..268287c --- /dev/null +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -0,0 +1,136 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from functools import cached_property +from typing import List, Optional, Union + +from . import customer_group as customer_group_module, record + + +class VolumeDiscountRange(record.RecordBase): + @property + def customer_group_id(self) -> Optional[int]: + """The ID for the customer group this volume discount range + applies to, if a specific customer group is set. + """ + return self._get_ref_id("customer_group", optional=True) + + @property + def customer_group_name(self) -> Optional[str]: + """The name of the customer group this volume discount range + applies to, if a specific customer group is set. + """ + return self._get_ref_name("customer_group", optional=True) + + @cached_property + def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: + """The customer group this volume discount range + applies to, if a specific customer group is set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.customer_group_id + return ( + self._client.customer_groups.get(record_id) + if record_id is not None + else None + ) + + discount_percent: float + """Discount percentage of this volume discount range (0-100).""" + + name: str + """The automatically generated name (description) of + this volume discount range. + """ + + max: Optional[float] + """Optional maximum charge for this volume discount range. + + Intended to be used when creating tiered volume discounts for customers. + """ + + min: float + """Minimum charge for this volume discount range.""" + + use_max: bool + """Use the ``max`` field, if defined.""" + + _alias_mapping = { + # Key is local alias, value is remote field name. + "customer_group_id": "customer_group", + } + + +class VolumeDiscountRangeManager( + record.RecordManagerBase[VolumeDiscountRange], +): + env_name = "openstack.volume_discount_range" + record_class = VolumeDiscountRange + + def get_for_charge( + self, + charge: float, + customer_group: Optional[ + Union[customer_group_module.CustomerGroup, int], + ] = None, + ) -> Optional[VolumeDiscountRange]: + """Return the volume discount range to apply to a given charge. + + If ``customer_group`` is supplied, volume discount ranges for + a specific customer group are returned. When set to ``False`` + (the default), volume discount ranges for all customers are returned. + + If multiple volume discount ranges can be applied, the range with + the highest discount percentage is selected. + If no applicable volume discount ranges were found, + ``None`` is returned. + + :param charge: The charge for to find the applicable discount range + :type charge: float + :param customer_group: Get discount for a specific customer group + :type customer_group: Union[Model, int, Literal[False]], optional + :return: Highest percentage applicable discount range (if found) + :rtype: Optional[VolumeDiscountRange] + """ + ranges = self.search( + [ + ( + "customer_group", + "=", + ( + customer_group.id + if isinstance( + customer_group, + customer_group_module.CustomerGroup, + ) + else (customer_group or False) + ), + ), + ], + ) + found_ranges: List[VolumeDiscountRange] = [] + for vol_range in ranges: + if charge < vol_range.min: + continue + if vol_range.use_max and vol_range.max and charge >= vol_range.max: + continue + found_ranges.append(vol_range) + if not found_ranges: + return None + return sorted(found_ranges, key=lambda r: r.discount_percent)[-1] diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py new file mode 100644 index 0000000..2d51dfa --- /dev/null +++ b/openstack_odooclient/managers/voucher_code.py @@ -0,0 +1,229 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import date +from functools import cached_property +from typing import TYPE_CHECKING, List, Literal, Optional, Union + +from . import record + +if TYPE_CHECKING: + from . import ( + credit_type as credit_type_module, + customer_group as customer_group_module, + grant_type as grant_type_module, + partner, + partner_category, + ) + + +class VoucherCode(record.RecordBase): + claimed: bool + """Whether or not this voucher code has been claimed.""" + + code: str + """The code string for this voucher code.""" + + credit_amount: float + """The initial credit balance for the voucher code, if a credit is to be + created by the voucher code. + """ + + @property + def credit_type_id(self) -> Optional[int]: + """The ID of the credit type to use, if a credit is to be + created by this voucher code. + """ + return self._get_ref_id("credit_type", optional=True) + + @property + def credit_type_name(self) -> Optional[str]: + """The name of the credit type to use, if a credit is to be + created by this voucher code. + """ + return self._get_ref_name("credit_type", optional=True) + + @cached_property + def credit_type(self) -> Optional[credit_type_module.CreditType]: + """The credit type to use, if a credit is to be + created by this voucher code. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.credit_type_id + return ( + self._client.credit_types.get(record_id) + if record_id is not None + else None + ) + + credit_duration: int + """The duration of the credit, in days, if a credit is to be + created by the voucher code. + """ + + @property + def customer_group_id(self) -> Optional[int]: + """The ID of the customer group this voucher code is available to. + + If not set, the voucher code is available to all customers. + """ + return self._get_ref_id("customer_group", optional=True) + + @property + def customer_group_name(self) -> Optional[str]: + """The name of the customer group this voucher code is available to. + + If not set, the voucher code is available to all customers. + """ + return self._get_ref_name("customer_group", optional=True) + + @cached_property + def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: + """The customer group this voucher code is available to. + + If not set, the voucher code is available to all customers. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.customer_group_id + return ( + self._client.customer_groups.get(record_id) + if record_id is not None + else None + ) + + expiry_date: date + """The date the voucher code expires.""" + + grant_amount: float + """The value of the grant, if a grant is to be + created by the voucher code. + """ + + @property + def grant_type_id(self) -> Optional[int]: + """The ID of the grant type to use, if a grant is to be + created by this voucher code. + """ + return self._get_ref_id("grant_type", optional=True) + + @property + def grant_type_name(self) -> Optional[str]: + """The name of the grant type to use, if a grant is to be + created by this voucher code. + """ + return self._get_ref_name("grant_type", optional=True) + + @cached_property + def grant_type(self) -> Optional[grant_type_module.GrantType]: + """The grant type to use, if a grant is to be + created by this voucher code. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.grant_type_id + return ( + self._client.grant_types.get(record_id) + if record_id is not None + else None + ) + + grant_duration: int + """The duration of the grant, in days, if a grant is to be + created by the voucher code. + """ + + multi_use: bool + """Whether or not this is a multi-use voucher code. + + A multi-use voucher code can be used an unlimited number of times + until it expires. + """ + + name: str + """The unique name of this voucher code. + + This uses the code specified in the record as-is. + """ + + quota: Union[str, Literal[False]] + """The quota size to set for new projects signed up + using this voucher code. + + If unset, use the default quota size. + """ + + @property + def sales_person_id(self) -> Optional[int]: + """The ID for the salesperson responsible for this + voucher code, if assigned. + """ + return self._get_ref_id("sales_person", optional=True) + + @property + def sales_person_name(self) -> Optional[str]: + """The name of the salesperson responsible for this + voucher code, if assigned. + """ + return self._get_ref_name("sales_person", optional=True) + + @cached_property + def sales_person(self) -> Optional[partner.Partner]: + """The salesperson responsible for this + voucher code, if assigned. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.sales_person_id + return ( + self._client.partners.get(record_id) + if record_id is not None + else None + ) + + @property + def tag_ids(self) -> List[int]: + """A list of IDs for the tags (partner categories) to assign + to partners for new accounts that signed up using this voucher code. + """ + return self._get_field("tags") + + @cached_property + def tags(self) -> List[partner_category.PartnerCategory]: + """The list of tags (partner categories) to assign + to partners for new accounts that signed up using this voucher code. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + return self._client.partner_categories.list(self.tag_ids) + + _alias_mapping = { + # Key is local alias, value is remote field name. + "sales_person_id": "sales_person", + "tag_ids": "tags", + } + + +class VoucherCodeManager(record.NamedRecordManagerBase[VoucherCode]): + env_name = "openstack.voucher_code" + record_class = VoucherCode diff --git a/openstack_odooclient/py.typed b/openstack_odooclient/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..4d49148 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,130 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "lint"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:d0e03148a1ea1fbab3399c5ab4351cda37039d37482272dc2a555621c735fc1b" + +[[package]] +name = "mypy" +version = "1.10.0" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +groups = ["lint"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.1.0", +] +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +requires_python = ">=3.5" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["lint"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "odoorpc" +version = "0.10.1" +summary = "OdooRPC is a Python package providing an easy way to pilot your Odoo servers through RPC." +groups = ["default"] +files = [ + {file = "OdooRPC-0.10.1-py2.py3-none-any.whl", hash = "sha256:a0900bdd5c989c414b1ef40dafccd9363f179312d9166d9486cf70c7c2f0dd44"}, + {file = "OdooRPC-0.10.1.tar.gz", hash = "sha256:d0bc524c5b960781165575bad9c13d032d6f968c3c09276271045ddbbb483aa5"}, +] + +[[package]] +name = "packaging" +version = "24.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default"] +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "ruff" +version = "0.4.8" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["lint"] +files = [ + {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"}, + {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"}, + {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"}, + {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"}, + {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"}, + {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"}, + {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"}, + {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +groups = ["lint"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default", "lint"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c86a533 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,120 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "openstack-odooclient" +authors = [ + {name = "Callum Dickinson", email = "callum.dickinson@catalystcloud.nz"}, +] +description = "Python client library for Odoo and the OpenStack integration add-on." +readme = "README.md" +keywords = [ + "openstack", + "odoo", + "erp", + "billing", +] +license = {text = "Apache-2.0"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: System :: Systems Administration", + "Typing :: Typed", +] +requires-python = ">=3.8" +dependencies = [ + "OdooRPC>=0.9.0", + "packaging", + "typing-extensions>=4.0.0", +] +dynamic = ["version"] + +[tool.setuptools_scm] + +[tool.pdm.dev-dependencies] +lint = [ + "mypy==1.10.0", + "ruff==0.4.8", +] + +[tool.pdm.scripts] +lint = {cmd = "ruff check"} +format = {cmd = "ruff format"} + +[tool.ruff] +fix = true +indent-width = 4 +line-length = 79 +output-format = "grouped" + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = "dynamic" +indent-style = "space" +line-ending = "auto" +quote-style = "double" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +select = [ + "A", + "B", + "BLE", + "E", + "F", + "G", + "I", + "INP", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "PTH", + "RUF", + "S", + "T10", + "T20", + "W", + "YTT", +] +extend-select = [ + # COM812 is currently disabled due to a conflict with the Ruff formatter. + # https://github.com/astral-sh/ruff/issues/9216 + # TODO(callumdickinson): Decide whether to enable or remove. + # "COM812", + "COM818", + "UP009", +] +extend-ignore = [ + "A003", + "B023", + "N805", + "N806", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR0915", + "RUF012", +] + +[tool.ruff.lint.isort] +lines-between-types = 1 +combine-as-imports = true +required-imports = [ + "from __future__ import annotations", +] + +[tool.mypy] +python_version = "3.8" +pretty = true From 8fb4920c3bd81f0909430a9713a599391e6967a9 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 10 Jun 2024 19:02:00 +1200 Subject: [PATCH 02/87] Remove old README parts --- README.md | 116 ------------------------------------------------------ 1 file changed, 116 deletions(-) diff --git a/README.md b/README.md index 7b572df..7548b61 100644 --- a/README.md +++ b/README.md @@ -875,119 +875,3 @@ None | `int` | Record ID (when `as_id` is `True`) | | `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | | `None` | If a record with the given name does not exist (when `optional` is `True`) | - -### Account Move Lines - -### Core Managers - -The following managers are used to interact with core Odoo data structures. - -* `odooclient.Client.sale_order` -* `odooclient.Client.sale_order_lines` -* [`odooclient.Client.account_moves`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/account_moves.py) -* `odooclient.Client.account_move_lines` -* [`odooclient.Client.partners`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/partners.py) -* `odooclient.Client.price_lists` -* [`odooclient.Client.products`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/products.py) -* `odooclient.Client.countries` -* `odooclient.Client.mail_messages` -* `odooclient.Client.sales_teams` - -### OpenStack Managers - -The following OpenStack-related managers are available. - -* [`odooclient.Client.projects`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/projects.py) -* [`odooclient.Client.project_contacts`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/project_contacts.py) -* `odooclient.Client.credits` -* `odooclient.Client.credit_transactions` -* `odooclient.Client.credit_types` -* `odooclient.Client.customer_groups` -* `odooclient.Client.grants` -* `odooclient.Client.grant_types` -* `odooclient.Client.referrals` -* `odooclient.Client.resellers` -* `odooclient.Client.reseller_tiers` -* `odooclient.Client.support_subscriptions` -* `odooclient.Client.term_discounts` -* `odooclient.Client.trials` -* [`odooclient.Client.volume_discount_ranges`](https://gitlab.com/catalyst-cloud/python-odooclient/-/blob/master/odooclient/volume_discount_ranges.py) -* `odooclient.Client.voucher_codes` - -### Common Methods - -The following methods are available on every manager object. - -#### `get(ids: int | list[int] | tuple[int], read: bool = False, fields: list[str] | None = None) -> RecordSet` - -Get one or more `Resource` objects by ID. - -Args: - -* `ids` (`int | list[int] | tuple[int]`): Resource ID. Can be a single ID, or a list of IDs. -* `read` (`bool`): Read objects back as a `dict`. Default is `False`. -* `fields` (`list[str] | None`): A list of field names to include in a read. Default is `None`. - -Returns: - -A collection of resources - -#### `list(filters: list[tuple[Any, ...]] | None = None, get: bool = True, fields: list[str] | None = None, **kwargs) -> RecordSet` - -Get a list of `Resource` objects, or resource IDs, by filter. - -Args: - -* `filters` (`list[tuple[Any, ...]] | None`): A list of search option tuples, e.g. `[('field', '=', value)]`. -* `get` (`bool`): Fetch whole objects instead of just IDs. Default is `True`. -* `fields` (`list[str] | None`): A list of field names to include in a read. Default is `None`. -* `kwargs` (`Mapping[str, str]`): Direct field comparisons to be used as filters. Ignored if `filters` is defined. - -Returns: - -A collection of resources - -#### `create(**fields) -> Resource` - -Create a `Resource`, with the parameters to the function call used as resource fields. - -Returns: - -The created resource object - -#### `create_many(resources: list[dict[str, Any]]) -> RecordSet` - -Create multiple new `Resource` objects. - -Args: - -* `resources` (`list[dict[str, Any]]`): List of resources (in dictionary form) to create. - -Returns: - -The created resource objects - -#### `load(fields: list[str], rows: list[list[str]]) -> Resource` - -Load in a `Resource`. - -Args: - -* `fields` (`list[str]`): Fields to import. -* `rows` (`list[list[str]]`): The item data to import. - -Returns: - -The loaded resource object - -#### `delete(ids: int | list[int] | tuple[int]) -> bool` - -Delete one or more `Resource` objects by ID. - -Args: - -* `ids` (`int | list[int] | tuple[int]`): Resource ID, or list of IDs to delete. - -Returns: - -`True` if the resources were deleted (or already deleted), otherwise `False` From 68da1fcb584d624765453370733eecd28cf2ddff Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 12:21:24 +1200 Subject: [PATCH 03/87] Add to docs, fix bugs in account move record type defs --- README.md | 472 +++++++++++++++++- openstack_odooclient/managers/account_move.py | 110 ++-- .../managers/account_move_line.py | 47 +- openstack_odooclient/managers/record/base.py | 9 +- 4 files changed, 540 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 7548b61..9e93289 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,19 @@ python -m pip install openstack-odooclient To connect to an Odoo server, create an `openstack_odooclient.Client` object and pass the connection details to it. +```python +openstack_odooclient.Client( + hostname: str, + database: str, + username: str, + password: str, + protocol: str = "jsonrpc", + port: int = 8069, + verify: bool | Path | str = True, + version: str | None = None, +) -> Client +``` + This is the recommended way of creating the Odoo client object, as it provides some extra parameters for convenience. @@ -30,20 +43,27 @@ as it provides some extra parameters for convenience. from openstack_odooclient import Client as OdooClient odoo_client = OdooClient( + *, hostname="localhost", - port=8069, - protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. database="odoodb", user="test-user", password="", - # version="14.0", # Optionally specify the server version. Default is to auto-detect. + protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. + port=8069, # verify=True, # Enable/disable SSL verification, or pass the path to a CA certificate. + # version="14.0", # Optionally specify the server version. Default is to auto-detect. ) ``` If you have a pre-existing `odoorpc.ODOO` connection object, that can instead be passed directly into `openstack_odooclient.Client`. +```python +openstack_odooclient.Client(*, odoo: odoorpc.ODOO) -> Client +``` + +This allows for sharing a single OdooRPC connection object with other code. + ```python from odoorpc import ODOO from openstack_odooclient import Client as OdooClient @@ -117,9 +137,9 @@ For example, performing a simple search query would look something like this: * `volume_discount_ranges` - OpenStack Volume Discount Ranges (Odoo Model: `openstack.volume_discount_range`) * `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) -### Common Methods +### Methods {#manager-methods} -#### `list` +#### `list` {#manager-list} ```python list( @@ -207,7 +227,7 @@ returns an empty list. [] ``` -##### Parameters +##### Parameters {#manager-list-parameters} | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|------------| @@ -215,14 +235,14 @@ returns an empty list. | `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | | `as_dict` | `bool` | Return records as dictionaries | `False` | -##### Returns +##### Returns {#manager-list-returns} | Type | Description | |------------------------|------------------------------------------------| | `list[Record]` | Record objects (when `as_dict` is `False`) | | `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | -#### `get` +#### `get` {#manager-get} ```python get( @@ -311,7 +331,7 @@ a ``dict`` object, instead of a record object. {'id': 1234, ...} ``` -##### Parameters +##### Parameters {#manager-get-paramters} | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| @@ -320,13 +340,13 @@ a ``dict`` object, instead of a record object. | `as_dict` | `bool` | Return record as a dictionary | `False` | | `optional` | `bool` | Return `None` if not found | `False` | -##### Raises +##### Raises {#manager-get-raises} | Type | Description | |-----------------------|--------------------------------------------------------------------| | `RecordNotFoundError` | If the given record ID does not exist (when `optional` is `False`) | -##### Returns +##### Returns {#manager-get-returns} | Type | Description | |------------------|-------------------------------------------------------------| @@ -334,7 +354,7 @@ a ``dict`` object, instead of a record object. | `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | | `None` | If the record ID does not exist (when `optional` is `True`) | -#### `search` +#### `search` {#manager-search} ```python search( @@ -500,7 +520,7 @@ a list of `dict` objects, instead of record objects. [{'id': 1234, ...}, ...] ``` -##### Parameters +##### Parameters {#manager-search-parameters} | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|---------| @@ -510,7 +530,7 @@ a list of `dict` objects, instead of record objects. | `as_id` | `bool` | Return the record IDs only | `False` | | `as_dict` | `bool` | Return records as dictionaries | `False` | -##### Returns +##### Returns {#manager-search-parameters} | Type | Description | |------------------------|------------------------------------------------| @@ -518,7 +538,7 @@ a list of `dict` objects, instead of record objects. | `list[int]` | Record IDs (when `as_id` is `True`) | | `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | -#### `create` +#### `create` {#manager-create} ```python create(**fields: Any) -> int @@ -560,19 +580,19 @@ pass the returned ID to the [``get``](#get) method. SaleOrderLine(record={'id': 1234, ...}, fields=None) ``` -##### Parameters +##### Parameters {#manager-create-parameters} | Name | Type | Description | Default | |------------|-------|-----------------------------------------|------------| | `**fields` | `Any` | Record field values (keyword arguments) | (required) | -##### Returns +##### Returns {#manager-create-returns} | Type | Description | |-------|------------------------------------| | `int` | The ID of the newly created record | -#### `create_multi` +#### `create_multi` {#manager-create_multi} ```python create_multi(*records: Mapping[str, Any]) -> list[int] @@ -615,19 +635,19 @@ pass the returned IDs to the [``list``](#list) method. [SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] ``` -##### Parameters +##### Parameters {#manager-create_multi-parameters} | Name | Type | Description | Default | |------------|---------------------|----------------------------------------------------|------------| | `*records` | `Mapping[str, Any]` | Record field-value mappings (positional arguments) | (required) | -##### Returns +##### Returns {#manager-create_multi-returns} | Type | Description | |-------------|--------------------------------------| | `list[int]` | The IDs of the newly created records | -#### `unlink`/`delete` +#### `unlink`/`delete` {#manager-unlink-delete} ```python unlink(*records: Record | int | Iterable[Record | int]) -> None @@ -672,13 +692,13 @@ All specified records will be deleted in a single request. >>> odoo_client.sales_order_lines.unlink(line1, 9012, [line2, 3456]) ``` -##### Parameters +##### Parameters {#manager-unlink-delete-parameters} | Name | Type | Description | Default | |------------|--------------------------------------------|------------------------------------------------------------------------------|------------| | `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | -### Managers for Named Records +### Named Record Types {#manager-named} Some record types have a name field that is generally expected to be unique. The managers for these record types have additional methods for querying records by name. @@ -700,7 +720,7 @@ The managers for these record types have additional methods for querying records * `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) * `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) -#### `get_by_name` +#### `get_by_name` {#manager-named-get_by_name} ```python get_by_name( @@ -850,7 +870,7 @@ with the given name does not exist, instead of raising an error. None ``` -##### Parameters +##### Parameters {#manager-named-get_by_name-parameters} | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| @@ -860,14 +880,14 @@ None | `as_dict` | `bool` | Return records as dictionaries | `False` | | `optional` | `bool` | Return `None` if not found | `False` | -##### Raises +##### Raises {#manager-named-get_by_name-raises} | Type | Description | |-----------------------------|-------------------------------------------------------------------------| | `RecordNotFoundError` | If no record with the given name was found (when `optional` is `False`) | | `MultipleRecordsFoundError` | If multiple records were found with the same name | -##### Returns +##### Returns {#manager-named-get_by_name-returns} | Type | Description | |------------------|----------------------------------------------------------------------------| @@ -875,3 +895,401 @@ None | `int` | Record ID (when `as_id` is `True`) | | `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | | `None` | If a record with the given name does not exist (when `optional` is `True`) | + +## Records + +Record manager methods return record objects for the corresponding model +in Odoo. + +Record fields can be accessed as attributes on these record objects. +The record classes are fully type hinted, allowing IDEs and validation +tools such as Mypy to verify that your application is using the fields +correctly. + +```python +>>> from openstack_odooclient import Client as OdooClient, User +>>> user: User | None = None +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.id +1234 +``` + +### Custom Attributes + +Most of the model fields commonly used by applications have been defined +in the record classes, but if your installation of Odoo has add-ons +installed that define custom fields that the Odoo client library +does not know about, these can still be used (just without type hinting). + +Access these fields as object attributes, the same way as you would any +other field. + +```python +>>> user.custom_field_name +'custom-field-value' +``` + +If the custom field is a reference to another model record, +it will be available on the record object as 2-member list. +The first value is the record ID, and the second value +is the display name of the record. + +```python +>>> user.custom_model_ref +[5678, 'custom-record-name'] +``` + +If the custom field is a list of model records, +the record IDs will be made available as type `list[int]`. + +```python +>>> user.custom_model_refs +[5678, 9012, ...] +``` + +### Attributes and Methods {#record-attributes-methods} + +The following attributes and methods are available on all record types. + +#### `id: int` {#record-id} + +The record's ID in Odoo. + +#### `create_date: datetime` {#record-create_date} + +The time the record was created. + +#### `create_uid: int` {#record-create_uid} + +The ID of the partner that created this record. + +#### `create_name: str` {#record-create_name} + +The name of the partner that created this record. + +#### `create_user: Partner` {#record-create_user} + +The partner that created this record. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `write_date: datetime` {#record-write_date} + +The time the record was last modified. + +#### `write_uid: int` {#record-write_uid} + +The ID of the partner that last modified this record. + +#### `write_name: str` {#record-write_name} + +The name of the partner that modified this record. + +#### `write_user: Partner` {#record-write_user} + +The partner that last modified this record. + +This fetches a full Partner object from Odoo once, +and caches it for subsequence access. + +#### `as_dict` {#record-as_dict} + +```python +as_dict(raw: bool = False) -> dict[str, Any] +``` + +Convert this record object to a dictionary. + +The fields and values in the dictionary are the same +as if the record was queried using `as_dict=True`. +This changes field names to the record object equivalents, +if they are different, to take into account fields being +named differently across Odoo versions. + +```python +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.as_dict() +{'id': 1234, ...} +``` + +Set `raw=True` to instead get the raw record dictionary +fields and values as returned by OdooRPC. + +```python +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.as_dict(raw=True) +{'id': 1234, ...} +``` + +##### Parameters + +| Name | Type | Description | Default | +|-------|--------|------------------------------------|---------| +| `raw` | `bool` | Return raw dictionary from OdooRPC | `False` | + +##### Returns + +| Type | Description | +|------------------|-------------------| +| `dict[str, Any]` | Record dictionary | + +#### `refresh` {#record-refresh} + +```python +refresh() -> Self +``` + +Fetch the latest version of this record from Odoo. + +This does not update the record object in place, +a new object is returned with the up-to-date field values. + +```python +>>> user +User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) +>>> user.refresh() +User(record={'id': 1234, 'name': 'New Name', ...}, fields=None) +``` + +##### Returns + +| Type | Description | +|--------|-------------------------------------| +| `Self` | Latest version of the record object | + +#### `unlink`/`delete` {#record-unlink-delete} + +```python +unlink() -> None +``` + +```python +delete() -> None +``` + +Delete this record from Odoo. + +```python +>>> user +User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) +>>> user.unlink() +>>> user.refresh() +... +openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ID: 1234 +``` + +### Account Move + +#### `amount_total: float` {#account_move-amount_total} + +Total (taxed) amount charged on the account move (invoice). + +#### `amount_untaxed: float` {#account_move-amount_untaxed} + +Total (untaxed) amount charged on the account move (invoice). + +#### `currency_id: int` {#account_move-currency_id} + +The ID for the currency used in this account move (invoice). + +#### `currency_name: str` {#account_move-currency_id} + +The name of the currency used in this account move (invoice). + +#### `currency: Currency` {#account_move-currency} + +The currency used in this account move (invoice). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `invoice_date: date` {#account_move-invoice_date} + +Date associated with the account move (invoice). + +#### `invoice_line_ids: list[int]` {#account_move-invoice_line_ids} + +The list of the IDs for the account move (invoice) lines +that comprise this account move (invoice). + +#### `invoice_lines: list[AccountMoveLine]` {#account_move-invoice_lines} + +A list of account move (invoice) lines +that comprise this account move (invoice). + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +#### `is_move_sent: bool` {#account_move-is_move_sent} + +Whether or not the account move (invoice) has been sent. + +#### `move_type: Literal["entry", "out_invoice", "out_refund", "in_invoice", "in_refund", "out_receipt", "in_receipt"]` {#account_move-move_type} + +The type of account move (invoice). + +Values: + +* ``entry`` - Journal Entry +* ``out_invoice`` - Customer Invoice +* ``out_refund`` - Customer Credit Note +* ``in_invoice`` - Vendor Bill +* ``in_refund`` - Vendor Credit Note +* ``out_receipt`` - Sales Receipt +* ``in_receipt`` - Purchase Receipt + +#### `name: str | Literal[False]` {#account_move-name} + +Name assigned to the account move (invoice), if posted. + +#### `os_project_id: int | None` {#account_move-os_project_id} + +The ID of the OpenStack project this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +#### `os_project_name: str | None` {#account_move-os_project_name} + +The name of the OpenStack project this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +#### `os_project: Project | None` {#account_move-os_project} + +The OpenStack project this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `payment_state: Literal["not_paid", "in_payment", "paid", "partial", "reversed", "invoicing_legacy"]` {#account_move-payment_state} + +The current payment state of the account move (invoice). + +Values: + +* ``not_paid`` - Not Paid +* ``in_payment`` - In Payment +* ``paid`` - Paid +* ``partial`` - Partially Paid +* ``reversed`` - Reversed +* ``invoicing_legacy`` - Invoicing App Legacy + +#### `state: Literal["draft", "posted", "cancel"]` {#account_move-state} +The current state of the account move (invoice). + +Values: + +* ``draft`` - Draft invoice +* ``posted`` - Posted (finalised) invoice +* ``cancel`` - Cancelled invoice + +### Account Move Line + +#### `currency_id: int` {#account_move_line-currency_id} + +The ID for the currency used in this account move (invoice) line. + +#### `currency_name: str` {#account_move_line-currency_id} + +The name of the currency used in this account move (invoice) line. + +#### `currency: Currency` {#account_move_line-currency} + +The currency used in this account move (invoice) line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `line_tax_amount: float` {#account_move_line-line_tax_amount} + +Amount charged in tax on the account move (invoice) line. + +#### `name: str` {#account_move_line-name} + +Name of the product charged on the account move (invoice) line. + +#### `os_project_id: int | None` {#account_move_line-os_project_id} + +The ID for the OpenStack project this account move (invoice) line +was generated for. + +#### `os_project_name: str | None` {#account_move_line-os_project_name} + +The name of the OpenStack project this account move (invoice) line +was generated for. + +#### `os_project: Project | None` {#account_move_line-os_project + +he OpenStack project this account move (invoice) line +was generated for. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `os_region: str | Literal[False]` {#account_move_line-os_region} + +The OpenStack region the account move (invoice) line +was created from. + +#### `os_resource_id: str | Literal[False]` {#account_move_line-os_resource_id} + +The OpenStack resource ID for the resource that generated +this account move (invoice) line. + +#### `os_resource_name: str | Literal[False]` {#account_move_line-os_resource_name} + +The name of the OpenStack resource tier or flavour, +as used by services such as Distil for rating purposes. + +For example, if this is the account move (invoice) line +for a compute instance, this would be set to the instance's flavour name. + +#### `os_resource_type: str | Literal[False]` {#account_move_line-os_resource_type} + +A human-readable description of the type of resource captured +by this account move (invoice) line. + + +#### `price_subtotal: float` {#account_move_line-price_subtotal} + +Amount charged for the product (untaxed) on the +account move (invoice) line. + +#### `price_unit: float` {#account_move_line-price_unit} + +Unit price for the product used on the account move (invoice) line. + +#### `product_id: int` {#account_move_line-product_id} + +The ID for the product charged on the +account move (invoice) line. + +#### `product_name: int` {#account_move_line-product_name} + +The name of the product charged on the +account move (invoice) line. + +#### `product: Product` {#account_move_line-product} + +The product charged on the +account move (invoice) line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + + +#### `quantity: float` {#account_move_line-quantity} + +Quantity of product charged on the account move (invoice) line. diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index 73f1e4e..7d525fb 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -15,8 +15,9 @@ from __future__ import annotations +from datetime import date from functools import cached_property -from typing import TYPE_CHECKING, Any, List, Literal, Mapping, Optional +from typing import TYPE_CHECKING, Any, List, Literal, Mapping, Optional, Union from . import record @@ -24,7 +25,6 @@ from . import ( account_move_line, currency as currency_module, - partner, project, ) @@ -36,30 +36,6 @@ class AccountMove(record.RecordBase): amount_untaxed: float """Total (untaxed) amount charged on the account move (invoice).""" - @property - def attention_id(self) -> Optional[int]: - """The ID of the partner to send invoice emails to.""" - return self._get_ref_id("attention", optional=True) - - @property - def attention_name(self) -> Optional[str]: - """The name of the partner to send invoice emails to.""" - return self._get_ref_name("attention", optional=True) - - @cached_property - def attention(self) -> Optional[partner.Partner]: - """The partner to send invoice emails to. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.attention_id - return ( - self._client.partners.get(record_id) - if record_id is not None - else None - ) - @property def currency_id(self) -> int: """The ID for the currency used in this account move (invoice).""" @@ -79,10 +55,8 @@ def currency(self) -> currency_module.Currency: """ return self._client.currencies.get(self.currency_id) - invoice_date: str - """Date associated with the account move (invoice), - in YYYY-MM-DD format. - """ + invoice_date: date + """Date associated with the account move (invoice).""" invoice_line_ids: List[int] """The list of the IDs for the account move (invoice) lines @@ -102,38 +76,80 @@ def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: is_move_sent: bool """Whether or not the account move (invoice) has been sent.""" - move_type: str - """The type of account move (invoice).""" + move_type: Literal[ + "entry", + "out_invoice", + "out_refund", + "in_invoice", + "in_refund", + "out_receipt", + "in_receipt", + ] + """The type of account move (invoice). + + Values: - name: Optional[str] + * ``entry`` - Journal Entry + * ``out_invoice`` - Customer Invoice + * ``out_refund`` - Customer Credit Note + * ``in_invoice`` - Vendor Bill + * ``in_refund`` - Vendor Credit Note + * ``out_receipt`` - Sales Receipt + * ``in_receipt`` - Purchase Receipt + """ + + name: Union[str, Literal[False]] """Name assigned to the account move (invoice), if posted.""" @property - def os_project_id(self) -> int: - """The ID of the OpenStack Project this Account Move (Invoice) - was generated for. + def os_project_id(self) -> Optional[int]: + """The ID of the OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. """ - return self._get_ref_id("os_project") + return self._get_ref_id("os_project", optional=True) @property - def os_project_name(self) -> str: - """The name of the OpenStack Project this Account Move (Invoice) - was generated for. + def os_project_name(self) -> Optional[str]: + """The name of the OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. """ - return self._get_ref_name("os_project") + return self._get_ref_name("os_project", optional=True) @cached_property - def os_project(self) -> project.Project: - """The OpenStack Project this Account Move (Invoice) - was generated for. + def os_project(self) -> Optional[project.Project]: + """The OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - return self._client.projects.get(self.os_project_id) + record_id = self.os_project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + payment_state: Literal[ + "not_paid", + "in_payment", + "paid", + "partial", + "reversed", + "invoicing_legacy", + ] + """ + The current payment state of the account move (invoice). - payment_state: str - """The current payment state of the account move (invoice).""" + Values: + + * ``not_paid`` - Not Paid + * ``in_payment`` - In Payment + * ``paid`` - Paid + * ``partial`` - Partially Paid + * ``reversed`` - Reversed + * ``invoicing_legacy`` - Invoicing App Legacy + """ state: Literal["draft", "posted", "cancel"] """The current state of the account move (invoice). diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index 3b32fb8..07ee28a 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -16,7 +16,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, Optional, Union from . import record @@ -60,55 +60,60 @@ def currency(self) -> currency_module.Currency: """Name of the product charged on the account move (invoice) line.""" @property - def os_project_id(self) -> int: - """The ID for the OpenStack Project this Account Move (Invoice) line + def os_project_id(self) -> Optional[int]: + """The ID for the OpenStack project this account move (invoice) line was generated for. """ - return self._get_ref_id("os_project") + return self._get_ref_id("os_project", optional=True) @property - def os_project_name(self) -> str: - """The name of the OpenStack Project this Account Move (Invoice) line + def os_project_name(self) -> Optional[str]: + """The name of the OpenStack project this account move (invoice) line was generated for. """ - return self._get_ref_name("os_project") + return self._get_ref_name("os_project", optional=True) @cached_property - def os_project(self) -> project.Project: - """The OpenStack Project this Account Move (Invoice) line + def os_project(self) -> Optional[project.Project]: + """The OpenStack project this account move (invoice) line was generated for. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - return self._client.projects.get(self.os_project_id) - - os_region: str - """The OpenStack region the Account Move (Invoice) Line + record_id = self.os_project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + os_region: Union[str, Literal[False]] + """The OpenStack region the account move (invoice) line was created from. """ - os_resource_id: str + os_resource_id: Union[str, Literal[False]] """The OpenStack resource ID for the resource that generated - this Account Move (Invoice) Line. + this account move (invoice) line. """ - os_resource_name: str + os_resource_name: Union[str, Literal[False]] """The name of the OpenStack resource tier or flavour, as used by services such as Distil for rating purposes. - For example, if this is the Account Move (Invoice) Line + For example, if this is the account move (invoice) line for a compute instance, this would be set to the instance's flavour name. """ - os_resource_type: str + os_resource_type: Union[str, Literal[False]] """A human-readable description of the type of resource captured - by this Account Move (Invoice) Line. + by this account move (invoice) line. """ price_subtotal: float """Amount charged for the product (untaxed) on the - Account Move (Invoice) Line. + account move (invoice) line. """ price_unit: float @@ -138,7 +143,7 @@ def product(self) -> product_module.Product: """ return self._client.products.get(self.product_id) - quantity: int + quantity: float """Quantity of product charged on the account move (invoice) line.""" _alias_mapping = { diff --git a/openstack_odooclient/managers/record/base.py b/openstack_odooclient/managers/record/base.py index 4b38788..f5c41d6 100644 --- a/openstack_odooclient/managers/record/base.py +++ b/openstack_odooclient/managers/record/base.py @@ -61,7 +61,7 @@ def create_name(self) -> str: @cached_property def create_user(self) -> partner.Partner: - """The object of the partner that created this record. + """The partner that created this record. This fetches the full record from Odoo once, and caches it for subsequent accesses. @@ -83,7 +83,7 @@ def write_name(self) -> str: @cached_property def write_user(self) -> partner.Partner: - """The object of the partner that last modified this record. + """The partner that last modified this record. This fetches a full Partner object from Odoo once, and caches it for subsequence access. @@ -172,7 +172,7 @@ def as_dict(self, raw: bool = False) -> Dict[str, Any]: Set ``raw=True`` to instead get the raw record dictionary fields and values as returned by OdooRPC. - :param raw: Return raw dictionary, defaults to False + :param raw: Return raw dictionary from OdooRPC, defaults to False :type raw: bool, optional :return: Record dictionary :rtype: Dict[str, Any] @@ -189,6 +189,9 @@ def as_dict(self, raw: bool = False) -> Dict[str, Any]: def refresh(self) -> Self: """Fetch the latest version of this record from Odoo. + This does not update the record object in place, + a new object is returned with the up-to-date field values. + :return: Latest version of the record object :rtype: Self """ From 1ff5a85b86251b197ba64c24b5b11f514eb4d6f1 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 12:34:02 +1200 Subject: [PATCH 04/87] Remove custom heading IDs (not supported by GitHub) --- README.md | 331 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 259 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 9e93289..1ca2f2c 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,9 @@ For example, performing a simple search query would look something like this: * `volume_discount_ranges` - OpenStack Volume Discount Ranges (Odoo Model: `openstack.volume_discount_range`) * `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) -### Methods {#manager-methods} +### Methods -#### `list` {#manager-list} +#### `list` ```python list( @@ -227,7 +227,7 @@ returns an empty list. [] ``` -##### Parameters {#manager-list-parameters} +##### Parameters | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|------------| @@ -235,14 +235,14 @@ returns an empty list. | `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | | `as_dict` | `bool` | Return records as dictionaries | `False` | -##### Returns {#manager-list-returns} +##### Returns | Type | Description | |------------------------|------------------------------------------------| | `list[Record]` | Record objects (when `as_dict` is `False`) | | `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | -#### `get` {#manager-get} +#### `get` ```python get( @@ -331,7 +331,7 @@ a ``dict`` object, instead of a record object. {'id': 1234, ...} ``` -##### Parameters {#manager-get-paramters} +##### Parameters | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| @@ -340,13 +340,13 @@ a ``dict`` object, instead of a record object. | `as_dict` | `bool` | Return record as a dictionary | `False` | | `optional` | `bool` | Return `None` if not found | `False` | -##### Raises {#manager-get-raises} +##### Raises | Type | Description | |-----------------------|--------------------------------------------------------------------| | `RecordNotFoundError` | If the given record ID does not exist (when `optional` is `False`) | -##### Returns {#manager-get-returns} +##### Returns | Type | Description | |------------------|-------------------------------------------------------------| @@ -354,7 +354,7 @@ a ``dict`` object, instead of a record object. | `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | | `None` | If the record ID does not exist (when `optional` is `True`) | -#### `search` {#manager-search} +#### `search` ```python search( @@ -520,7 +520,7 @@ a list of `dict` objects, instead of record objects. [{'id': 1234, ...}, ...] ``` -##### Parameters {#manager-search-parameters} +##### Parameters | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|---------| @@ -530,7 +530,7 @@ a list of `dict` objects, instead of record objects. | `as_id` | `bool` | Return the record IDs only | `False` | | `as_dict` | `bool` | Return records as dictionaries | `False` | -##### Returns {#manager-search-parameters} +##### Returns | Type | Description | |------------------------|------------------------------------------------| @@ -538,7 +538,7 @@ a list of `dict` objects, instead of record objects. | `list[int]` | Record IDs (when `as_id` is `True`) | | `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | -#### `create` {#manager-create} +#### `create` ```python create(**fields: Any) -> int @@ -580,19 +580,19 @@ pass the returned ID to the [``get``](#get) method. SaleOrderLine(record={'id': 1234, ...}, fields=None) ``` -##### Parameters {#manager-create-parameters} +##### Parameters | Name | Type | Description | Default | |------------|-------|-----------------------------------------|------------| | `**fields` | `Any` | Record field values (keyword arguments) | (required) | -##### Returns {#manager-create-returns} +##### Returns | Type | Description | |-------|------------------------------------| | `int` | The ID of the newly created record | -#### `create_multi` {#manager-create_multi} +#### `create_multi` ```python create_multi(*records: Mapping[str, Any]) -> list[int] @@ -635,19 +635,19 @@ pass the returned IDs to the [``list``](#list) method. [SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] ``` -##### Parameters {#manager-create_multi-parameters} +##### Parameters | Name | Type | Description | Default | |------------|---------------------|----------------------------------------------------|------------| | `*records` | `Mapping[str, Any]` | Record field-value mappings (positional arguments) | (required) | -##### Returns {#manager-create_multi-returns} +##### Returns | Type | Description | |-------------|--------------------------------------| | `list[int]` | The IDs of the newly created records | -#### `unlink`/`delete` {#manager-unlink-delete} +#### `unlink`/`delete` ```python unlink(*records: Record | int | Iterable[Record | int]) -> None @@ -692,13 +692,13 @@ All specified records will be deleted in a single request. >>> odoo_client.sales_order_lines.unlink(line1, 9012, [line2, 3456]) ``` -##### Parameters {#manager-unlink-delete-parameters} +##### Parameters | Name | Type | Description | Default | |------------|--------------------------------------------|------------------------------------------------------------------------------|------------| | `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | -### Named Record Types {#manager-named} +### Named Record Types Some record types have a name field that is generally expected to be unique. The managers for these record types have additional methods for querying records by name. @@ -720,7 +720,7 @@ The managers for these record types have additional methods for querying records * `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) * `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) -#### `get_by_name` {#manager-named-get_by_name} +#### `get_by_name` ```python get_by_name( @@ -870,7 +870,7 @@ with the given name does not exist, instead of raising an error. None ``` -##### Parameters {#manager-named-get_by_name-parameters} +##### Parameters | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| @@ -880,14 +880,14 @@ None | `as_dict` | `bool` | Return records as dictionaries | `False` | | `optional` | `bool` | Return `None` if not found | `False` | -##### Raises {#manager-named-get_by_name-raises} +##### Raises | Type | Description | |-----------------------------|-------------------------------------------------------------------------| | `RecordNotFoundError` | If no record with the given name was found (when `optional` is `False`) | | `MultipleRecordsFoundError` | If multiple records were found with the same name | -##### Returns {#manager-named-get_by_name-returns} +##### Returns | Type | Description | |------------------|----------------------------------------------------------------------------| @@ -957,53 +957,89 @@ the record IDs will be made available as type `list[int]`. [5678, 9012, ...] ``` -### Attributes and Methods {#record-attributes-methods} +### Attributes and Methods The following attributes and methods are available on all record types. -#### `id: int` {#record-id} +#### id + +```python +id: int +``` The record's ID in Odoo. -#### `create_date: datetime` {#record-create_date} +#### create_date + +```python +create_date: datetime +``` The time the record was created. -#### `create_uid: int` {#record-create_uid} +#### create_uid + +```python +create_uid: int +``` The ID of the partner that created this record. -#### `create_name: str` {#record-create_name} +#### create_name + +```python +create_name: str +``` The name of the partner that created this record. -#### `create_user: Partner` {#record-create_user} +#### create_user + +```python +create_user: Partner +``` The partner that created this record. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### `write_date: datetime` {#record-write_date} +#### write_date + +```python +write_date: datetime +``` The time the record was last modified. -#### `write_uid: int` {#record-write_uid} +#### write_uid + +```python +write_uid: int +``` The ID of the partner that last modified this record. -#### `write_name: str` {#record-write_name} +#### write_name + +```python +write_name: str +``` The name of the partner that modified this record. -#### `write_user: Partner` {#record-write_user} +#### write_user + +```python +write_user: Partner +``` The partner that last modified this record. This fetches a full Partner object from Odoo once, and caches it for subsequence access. -#### `as_dict` {#record-as_dict} +#### `as_dict` ```python as_dict(raw: bool = False) -> dict[str, Any] @@ -1046,7 +1082,7 @@ User(record={'id': 1234, ...}, fields=None) |------------------|-------------------| | `dict[str, Any]` | Record dictionary | -#### `refresh` {#record-refresh} +#### `refresh` ```python refresh() -> Self @@ -1070,7 +1106,7 @@ User(record={'id': 1234, 'name': 'New Name', ...}, fields=None) |--------|-------------------------------------| | `Self` | Latest version of the record object | -#### `unlink`/`delete` {#record-unlink-delete} +#### `unlink`/`delete` ```python unlink() -> None @@ -1093,39 +1129,71 @@ openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ### Account Move -#### `amount_total: float` {#account_move-amount_total} +#### amount_total + +```python +amount_total: float +``` Total (taxed) amount charged on the account move (invoice). -#### `amount_untaxed: float` {#account_move-amount_untaxed} +#### amount_untaxed + +```python +amount_untaxed: float +``` Total (untaxed) amount charged on the account move (invoice). -#### `currency_id: int` {#account_move-currency_id} +#### currency_id + +```python +currency_id: int +``` The ID for the currency used in this account move (invoice). -#### `currency_name: str` {#account_move-currency_id} +#### currency_name + +```python +currency_name: str +``` The name of the currency used in this account move (invoice). -#### `currency: Currency` {#account_move-currency} +#### currency + +```python +currency: Currency +``` The currency used in this account move (invoice). This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### `invoice_date: date` {#account_move-invoice_date} +#### invoice_date + +```python +invoice_date: date +``` Date associated with the account move (invoice). -#### `invoice_line_ids: list[int]` {#account_move-invoice_line_ids} +#### invoice_line_ids + +```python +invoice_line_ids: list[int] +``` The list of the IDs for the account move (invoice) lines that comprise this account move (invoice). -#### `invoice_lines: list[AccountMoveLine]` {#account_move-invoice_lines} +#### invoice_lines + +```python +invoice_lines: list[AccountMoveLine] +``` A list of account move (invoice) lines that comprise this account move (invoice). @@ -1133,11 +1201,27 @@ that comprise this account move (invoice). This fetches the full records from Odoo once, and caches them for subsequent accesses. -#### `is_move_sent: bool` {#account_move-is_move_sent} +#### is_move_sent + +```python +is_move_sent: bool +``` Whether or not the account move (invoice) has been sent. -#### `move_type: Literal["entry", "out_invoice", "out_refund", "in_invoice", "in_refund", "out_receipt", "in_receipt"]` {#account_move-move_type} +#### move_type + +```python +move_type: Literal[ + "entry", + "out_invoice", + "out_refund", + "in_invoice", + "in_refund", + "out_receipt", + "in_receipt", +] +``` The type of account move (invoice). @@ -1151,21 +1235,37 @@ Values: * ``out_receipt`` - Sales Receipt * ``in_receipt`` - Purchase Receipt -#### `name: str | Literal[False]` {#account_move-name} +#### name + +```python +name: str | Literal[False] +``` Name assigned to the account move (invoice), if posted. -#### `os_project_id: int | None` {#account_move-os_project_id} +#### os_project_id + +```python +os_project_id: int | None +``` The ID of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. -#### `os_project_name: str | None` {#account_move-os_project_name} +#### os_project_name + +```python +os_project_name: str | None +``` The name of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. -#### `os_project: Project | None` {#account_move-os_project} +#### os_project + +```python +os_project: Project | None +``` The OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. @@ -1173,7 +1273,18 @@ was generated for, if this is an invoice for OpenStack project usage. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### `payment_state: Literal["not_paid", "in_payment", "paid", "partial", "reversed", "invoicing_legacy"]` {#account_move-payment_state} +#### payment_state + +```python +payment_state: Literal[ + "not_paid", + "in_payment", + "paid", + "partial", + "reversed", + "invoicing_legacy", +] +``` The current payment state of the account move (invoice). @@ -1186,7 +1297,12 @@ Values: * ``reversed`` - Reversed * ``invoicing_legacy`` - Invoicing App Legacy -#### `state: Literal["draft", "posted", "cancel"]` {#account_move-state} +#### state + +```python +state: Literal["draft", "posted", "cancel"] +``` + The current state of the account move (invoice). Values: @@ -1197,58 +1313,102 @@ Values: ### Account Move Line -#### `currency_id: int` {#account_move_line-currency_id} +#### currency_id + +```python +currency_id: int +``` The ID for the currency used in this account move (invoice) line. -#### `currency_name: str` {#account_move_line-currency_id} +#### currency_name + +```python +currency_name: str +``` The name of the currency used in this account move (invoice) line. -#### `currency: Currency` {#account_move_line-currency} +#### currency + +```python +currency: Currency +``` The currency used in this account move (invoice) line. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### `line_tax_amount: float` {#account_move_line-line_tax_amount} +#### line_tax_amount + +```python +line_tax_amount: float +``` Amount charged in tax on the account move (invoice) line. -#### `name: str` {#account_move_line-name} +#### name + +```python +name: str +``` Name of the product charged on the account move (invoice) line. -#### `os_project_id: int | None` {#account_move_line-os_project_id} +#### os_project_id + +```python +os_project_id: int | None +``` The ID for the OpenStack project this account move (invoice) line was generated for. -#### `os_project_name: str | None` {#account_move_line-os_project_name} +#### os_project_name + +```python +os_project_name: str | None +``` The name of the OpenStack project this account move (invoice) line was generated for. -#### `os_project: Project | None` {#account_move_line-os_project +#### os_project -he OpenStack project this account move (invoice) line +```python +os_project: Project | None +``` + +The OpenStack project this account move (invoice) line was generated for. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### `os_region: str | Literal[False]` {#account_move_line-os_region} +#### os_region + +```python +os_region: str | Literal[False] +``` The OpenStack region the account move (invoice) line was created from. -#### `os_resource_id: str | Literal[False]` {#account_move_line-os_resource_id} +#### os_resource_id + +```python +os_resource_id: str | Literal[False] +``` The OpenStack resource ID for the resource that generated this account move (invoice) line. -#### `os_resource_name: str | Literal[False]` {#account_move_line-os_resource_name} +#### os_resource_name + +```python +os_resource_name: str | Literal[False] +``` The name of the OpenStack resource tier or flavour, as used by services such as Distil for rating purposes. @@ -1256,32 +1416,56 @@ as used by services such as Distil for rating purposes. For example, if this is the account move (invoice) line for a compute instance, this would be set to the instance's flavour name. -#### `os_resource_type: str | Literal[False]` {#account_move_line-os_resource_type} +#### os_resource_type + +```python +os_resource_type: str | Literal[False] +``` A human-readable description of the type of resource captured by this account move (invoice) line. -#### `price_subtotal: float` {#account_move_line-price_subtotal} +#### price_subtotal + +```python +price_subtotal: float +``` Amount charged for the product (untaxed) on the account move (invoice) line. -#### `price_unit: float` {#account_move_line-price_unit} +#### price_unit + +```python +price_unit: float +``` Unit price for the product used on the account move (invoice) line. -#### `product_id: int` {#account_move_line-product_id} +#### product_id + +```python +product_id: int +``` The ID for the product charged on the account move (invoice) line. -#### `product_name: int` {#account_move_line-product_name} +#### product_name + +```python +product_name: int +``` The name of the product charged on the account move (invoice) line. -#### `product: Product` {#account_move_line-product} +#### product + +```python +product: Product +``` The product charged on the account move (invoice) line. @@ -1289,7 +1473,10 @@ account move (invoice) line. This fetches the full record from Odoo once, and caches it for subsequent accesses. +#### quantity -#### `quantity: float` {#account_move_line-quantity} +```python +quantity: float +``` Quantity of product charged on the account move (invoice) line. From fd7b86c8fa32208df4c9164bb96a552f918e3f06 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 12:50:25 +1200 Subject: [PATCH 05/87] Add code-formatting to attribute headings, add Company attributes --- README.md | 186 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 1ca2f2c..dfadd3e 100644 --- a/README.md +++ b/README.md @@ -961,7 +961,7 @@ the record IDs will be made available as type `list[int]`. The following attributes and methods are available on all record types. -#### id +#### `id` ```python id: int @@ -969,7 +969,7 @@ id: int The record's ID in Odoo. -#### create_date +#### `create_date` ```python create_date: datetime @@ -977,7 +977,7 @@ create_date: datetime The time the record was created. -#### create_uid +#### `create_uid` ```python create_uid: int @@ -985,7 +985,7 @@ create_uid: int The ID of the partner that created this record. -#### create_name +#### `create_name` ```python create_name: str @@ -993,7 +993,7 @@ create_name: str The name of the partner that created this record. -#### create_user +#### `create_user` ```python create_user: Partner @@ -1004,7 +1004,7 @@ The partner that created this record. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### write_date +#### `write_date` ```python write_date: datetime @@ -1012,7 +1012,7 @@ write_date: datetime The time the record was last modified. -#### write_uid +#### `write_uid` ```python write_uid: int @@ -1020,7 +1020,7 @@ write_uid: int The ID of the partner that last modified this record. -#### write_name +#### `write_name` ```python write_name: str @@ -1028,7 +1028,7 @@ write_name: str The name of the partner that modified this record. -#### write_user +#### `write_user` ```python write_user: Partner @@ -1129,7 +1129,7 @@ openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ### Account Move -#### amount_total +#### `amount_total` ```python amount_total: float @@ -1137,7 +1137,7 @@ amount_total: float Total (taxed) amount charged on the account move (invoice). -#### amount_untaxed +#### `amount_untaxed` ```python amount_untaxed: float @@ -1145,7 +1145,7 @@ amount_untaxed: float Total (untaxed) amount charged on the account move (invoice). -#### currency_id +#### `currency_id` ```python currency_id: int @@ -1153,7 +1153,7 @@ currency_id: int The ID for the currency used in this account move (invoice). -#### currency_name +#### `currency_name` ```python currency_name: str @@ -1161,7 +1161,7 @@ currency_name: str The name of the currency used in this account move (invoice). -#### currency +#### `currency` ```python currency: Currency @@ -1172,7 +1172,7 @@ The currency used in this account move (invoice). This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### invoice_date +#### `invoice_date` ```python invoice_date: date @@ -1180,7 +1180,7 @@ invoice_date: date Date associated with the account move (invoice). -#### invoice_line_ids +#### `invoice_line_ids` ```python invoice_line_ids: list[int] @@ -1189,7 +1189,7 @@ invoice_line_ids: list[int] The list of the IDs for the account move (invoice) lines that comprise this account move (invoice). -#### invoice_lines +#### `invoice_lines` ```python invoice_lines: list[AccountMoveLine] @@ -1201,7 +1201,7 @@ that comprise this account move (invoice). This fetches the full records from Odoo once, and caches them for subsequent accesses. -#### is_move_sent +#### `is_move_sent` ```python is_move_sent: bool @@ -1209,7 +1209,7 @@ is_move_sent: bool Whether or not the account move (invoice) has been sent. -#### move_type +#### `move_type` ```python move_type: Literal[ @@ -1235,7 +1235,7 @@ Values: * ``out_receipt`` - Sales Receipt * ``in_receipt`` - Purchase Receipt -#### name +#### `name` ```python name: str | Literal[False] @@ -1243,7 +1243,7 @@ name: str | Literal[False] Name assigned to the account move (invoice), if posted. -#### os_project_id +#### `os_project_id` ```python os_project_id: int | None @@ -1252,7 +1252,7 @@ os_project_id: int | None The ID of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. -#### os_project_name +#### `os_project_name` ```python os_project_name: str | None @@ -1261,7 +1261,7 @@ os_project_name: str | None The name of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. -#### os_project +#### `os_project` ```python os_project: Project | None @@ -1273,7 +1273,7 @@ was generated for, if this is an invoice for OpenStack project usage. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### payment_state +#### `payment_state` ```python payment_state: Literal[ @@ -1313,7 +1313,7 @@ Values: ### Account Move Line -#### currency_id +#### `currency_id` ```python currency_id: int @@ -1321,7 +1321,7 @@ currency_id: int The ID for the currency used in this account move (invoice) line. -#### currency_name +#### `currency_name` ```python currency_name: str @@ -1329,7 +1329,7 @@ currency_name: str The name of the currency used in this account move (invoice) line. -#### currency +#### `currency` ```python currency: Currency @@ -1340,7 +1340,7 @@ The currency used in this account move (invoice) line. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### line_tax_amount +#### `line_tax_amount` ```python line_tax_amount: float @@ -1348,7 +1348,7 @@ line_tax_amount: float Amount charged in tax on the account move (invoice) line. -#### name +#### `name` ```python name: str @@ -1356,7 +1356,7 @@ name: str Name of the product charged on the account move (invoice) line. -#### os_project_id +#### `os_project_id` ```python os_project_id: int | None @@ -1365,7 +1365,7 @@ os_project_id: int | None The ID for the OpenStack project this account move (invoice) line was generated for. -#### os_project_name +#### `os_project_name` ```python os_project_name: str | None @@ -1374,7 +1374,7 @@ os_project_name: str | None The name of the OpenStack project this account move (invoice) line was generated for. -#### os_project +#### `os_project` ```python os_project: Project | None @@ -1386,7 +1386,7 @@ was generated for. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### os_region +#### `os_region` ```python os_region: str | Literal[False] @@ -1395,7 +1395,7 @@ os_region: str | Literal[False] The OpenStack region the account move (invoice) line was created from. -#### os_resource_id +#### `os_resource_id` ```python os_resource_id: str | Literal[False] @@ -1404,7 +1404,7 @@ os_resource_id: str | Literal[False] The OpenStack resource ID for the resource that generated this account move (invoice) line. -#### os_resource_name +#### `os_resource_name` ```python os_resource_name: str | Literal[False] @@ -1416,7 +1416,7 @@ as used by services such as Distil for rating purposes. For example, if this is the account move (invoice) line for a compute instance, this would be set to the instance's flavour name. -#### os_resource_type +#### `os_resource_type` ```python os_resource_type: str | Literal[False] @@ -1426,7 +1426,7 @@ A human-readable description of the type of resource captured by this account move (invoice) line. -#### price_subtotal +#### `price_subtotal` ```python price_subtotal: float @@ -1435,7 +1435,7 @@ price_subtotal: float Amount charged for the product (untaxed) on the account move (invoice) line. -#### price_unit +#### `price_unit` ```python price_unit: float @@ -1443,7 +1443,7 @@ price_unit: float Unit price for the product used on the account move (invoice) line. -#### product_id +#### `product_id` ```python product_id: int @@ -1452,7 +1452,7 @@ product_id: int The ID for the product charged on the account move (invoice) line. -#### product_name +#### `product_name` ```python product_name: int @@ -1461,7 +1461,7 @@ product_name: int The name of the product charged on the account move (invoice) line. -#### product +#### `product` ```python product: Product @@ -1473,10 +1473,112 @@ account move (invoice) line. This fetches the full record from Odoo once, and caches it for subsequent accesses. -#### quantity +#### `quantity` ```python quantity: float ``` Quantity of product charged on the account move (invoice) line. + +### Company + +#### `active` + +```python +active: bool +``` + +Whether or not this company is active (enabled). + +#### `child_ids` + +```python +child_ids: list[int] +``` + +A list of IDs for the child companies. + +#### `children` + +```python +children: list[Company] +``` + +The list of child companies. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +#### `name` + +```python +name: str +``` + +Company name, set from the partner name. + +#### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent company, if this company +is the child of another company. + +#### `parent_name` + +```python +parent_name: str | None +``` + +The name of the parent company, if this company +is the child of another company. + +#### `parent` + +```python +parent: Company | None +``` + +The parent company, if this company +is the child of another company. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `parent_path` + +```python +parent_path: str | Literal[False] +``` + +The path of the parent company, if there is a parent. + +#### `partner_id` + +```python +partner_id: int +``` + +The ID for the partner for the company. + +#### `partner_name` + +```python +partner_name: str +``` + +The name of the partner for the company. + +#### `partner` + +```python +partner: Partner +``` + +The partner for the company. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. From 7bd16412c478fa7f53b80983943a73a08d6983ea Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 17:50:02 +1200 Subject: [PATCH 06/87] Fix product_uom_id <-> product_uom mapping, add credit fields to README --- README.md | 192 ++++++++++++++++++ .../managers/sale_order_line.py | 2 +- 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dfadd3e..4e2c3b7 100644 --- a/README.md +++ b/README.md @@ -1129,6 +1129,12 @@ openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ### Account Move +To import the class for type hinting purposes: + +```python +from openstack_odooclient import AccountMove +``` + #### `amount_total` ```python @@ -1313,6 +1319,12 @@ Values: ### Account Move Line +To import the class for type hinting purposes: + +```python +from openstack_odooclient import AccountMoveLine +``` + #### `currency_id` ```python @@ -1483,6 +1495,12 @@ Quantity of product charged on the account move (invoice) line. ### Company +To import the class for type hinting purposes: + +```python +from openstack_odooclient import Company +``` + #### `active` ```python @@ -1582,3 +1600,177 @@ The partner for the company. This fetches the full record from Odoo once, and caches it for subsequent accesses. + +### Credit + +To import the class for type hinting purposes: + +```python +from openstack_odooclient import Credit +``` + +#### `credit_type_id` + +```python +credit_type_id: int +``` +The ID of the type of this credit. + +#### `credit_type_name` + +```python + credit_type_name: str +``` +The name of this type of credit. + +#### `credit_type` + +```python +credit_type: CreditType +``` + +The type of this credit. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `current_balance` + +```python +current_balance: float +``` + +The current remaining balance on the credit. + +#### `expiry_date` + +```python +expiry_date: date +``` + +The date the credit expires. + +#### `initial_balance` + +```python +initial_balance: float +``` + +The initial balance this credit started off with. + +#### `name` + +```python +name: str +``` + +The automatically generated name of the credit. + +#### `start_date` + +```python +start_date: date +``` + +The start date of the credit. + +#### `transaction_ids` + +```python +transaction_ids: list[int] +``` + +A list of IDs for the transactions that have been made +using this credit. + +#### `transactions` + +```python +transactions: list[CreditTransaction] +``` + +The transactions that have been made using this credit. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + + +#### `voucher_code_id` + +```python +voucher_code_id: int | None +``` + +The ID of the voucher code used when applying for the credit, +if one was supplied. + +#### `voucher_code_name` + +```python +voucher_code_name: str | None +``` + +The name of the voucher code used when applying for the credit, +if one was supplied. + +#### `voucher_code` + +```python +voucher_code: VoucherCode | None +``` + +Voucher code used when applying for the credit, +if one was supplied. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### Credit Transaction + +To import the class for type hinting purposes: + +```python +from openstack_odooclient import CreditTransaction +``` + +#### `credit_id` + +```python +credit_id: int +``` + +The ID of the credit this transaction was made against. + +#### `credit_name` + +```python +credit_name: str +``` + +The name of the credit this transaction was made against. + +#### `credit` + +```python +credit: Credit +``` +The credit this transaction was made against. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `description` + +```python +description: str +``` + +A description of this credit transaction. + +#### `value` + +```python +value: float +``` + +The value of the credit transaction. diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index fc7bf89..7681026 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -363,7 +363,7 @@ def tax(self) -> tax_module.Tax: "order": "order_id", "order_partner": "order_partner_id", "product": "product_id", - "product_uom": "product_uom_id", + "product_uom_id": "product_uom", "salesman": "salesman_id", "tax": "tax_id", } From d2be83ea89cb8b1c108ff70e1945fb4ac39364cf Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 18:07:50 +1200 Subject: [PATCH 07/87] Add Credit Type docs --- README.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/README.md b/README.md index 4e2c3b7..6355bb3 100644 --- a/README.md +++ b/README.md @@ -1774,3 +1774,122 @@ value: float ``` The value of the credit transaction. + +### Credit Type + +To import the class for type hinting purposes: + +```python +from openstack_odooclient import CreditType +``` + +#### `credit_ids` + +```python +credit_ids: list[int] +``` + +A list of IDs for the credits which are of this credit type. + +#### `credits` + +```python +credits: list[Credit] +``` + +#### `name` + +```python +name: str +``` + +Name of the Credit Type. + +#### `only_for_product_ids` + +```python +only_for_product_ids: list[int] +``` + +A list of IDs for the products this credit applies to. + +Mutually exclusive with [`only_for_product_category_ids`](#only_for_product_category_ids). +If neither are specified, the credit applies to all products. + +#### `only_for_products` + +```python +only_for_products: list[Product] +``` + +A list of products which this credit applies to. + +Mutually exclusive with [`only_for_product_categories`](#only_for_product_categories). +If neither are specified, the credit applies to all products. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +#### `only_for_product_category_ids` + +```python +only_for_product_category_ids: list[int] +``` + +A list of IDs for the product categories this credit applies to. + +Mutually exclusive with [`only_for_product_ids`](#only_for_product_ids). +If neither are specified, the credit applies to all product +categories. + +#### `only_for_product_categories` + +```python +only_for_product_categories: list[ProductCategory] +``` + +A list of product categories which this credit applies to. + +Mutually exclusive with [`only_for_products`](#only_for_products). +If neither are specified, the credit applies to all product +categories. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +#### `product_id` + +```python +product_id: int +``` + +The ID of the product to use when applying +the credit to invoices. + +#### `product_nane` + +```python +product_name: str +``` + +The ID of the product to use when applying +the credit to invoices. + +#### `product` + +```python +product: Product +``` + +The product to use when applying the credit to invoices. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `refundable` + +```python +refundable: bool +``` + +Whether or not the credit is refundable. From b5a72fe466c2c49804569445d1e592f58d15d22a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 18:16:03 +1200 Subject: [PATCH 08/87] Add missing docstrings to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6355bb3..d2c9a6a 100644 --- a/README.md +++ b/README.md @@ -1797,6 +1797,11 @@ A list of IDs for the credits which are of this credit type. credits: list[Credit] ``` +A list of credits which are of this credit type. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + #### `name` ```python From e9a21b65c91a9e8ad66038ddbb12d00aa29ab944 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 11 Jun 2024 18:32:06 +1200 Subject: [PATCH 09/87] Encode list fields, and fields/values in create_multi --- openstack_odooclient/managers/record/manager_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack_odooclient/managers/record/manager_base.py b/openstack_odooclient/managers/record/manager_base.py index a3855f3..ca27551 100644 --- a/openstack_odooclient/managers/record/manager_base.py +++ b/openstack_odooclient/managers/record/manager_base.py @@ -141,7 +141,7 @@ def list( _fields = ( list( dict.fromkeys( - (self._get_remote_field(f) for f in fields), + (self._encode_field(f) for f in fields), ).keys(), ) if fields is not None @@ -402,7 +402,7 @@ def create_multi(self, *records: Mapping[str, Any]) -> List[int]: res: Union[int, List[int]] = self._env.create( [ { - self._get_remote_field(field): value + self._encode_field(field): self._encode_value(value) for field, value in record.items() } for record in records From be61219f3d523ec4862ce00379a222f0eb989c87 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 12 Jun 2024 12:57:44 +1200 Subject: [PATCH 10/87] Move manager/record docs into separate files --- README.md | 1760 +---------------- docs/managers/account-move-line.md | 204 ++ docs/managers/account-move.md | 219 ++ docs/managers/company.md | 137 ++ docs/managers/credit-transaction.md | 79 + docs/managers/credit-type.md | 153 ++ docs/managers/credit.md | 155 ++ docs/managers/crm-team.md | 45 + docs/managers/currency.md | 125 ++ docs/managers/customer-group.md | 94 + docs/managers/grant-type.md | 154 ++ docs/managers/grant.md | 126 ++ docs/managers/index.md | 1050 ++++++++++ docs/managers/partner-category.md | 145 ++ docs/managers/partner.md | 348 ++++ docs/managers/pricelist.md | 209 ++ docs/managers/product-category.md | 119 ++ docs/managers/product.md | 370 ++++ docs/managers/project-contact.md | 0 docs/managers/project.md | 0 docs/managers/referral-code.md | 0 docs/managers/reseller-tier.md | 0 docs/managers/reseller.md | 0 docs/managers/sale-order-line.md | 0 docs/managers/sale-order.md | 0 docs/managers/support-subscription-type.md | 0 docs/managers/support-subscription.md | 0 docs/managers/tax-group.md | 0 docs/managers/tax.md | 0 docs/managers/term-discount.md | 0 docs/managers/trial.md | 0 docs/managers/uom-category.md | 0 docs/managers/uom.md | 0 docs/managers/user.md | 0 docs/managers/volume-discount-range.md | 0 docs/managers/voucher-code.md | 0 openstack_odooclient/managers/credit.py | 4 +- openstack_odooclient/managers/credit_type.py | 2 +- openstack_odooclient/managers/crm_team.py | 2 +- .../managers/customer_group.py | 2 +- openstack_odooclient/managers/grant.py | 4 +- openstack_odooclient/managers/grant_type.py | 18 +- openstack_odooclient/managers/partner.py | 10 +- .../managers/partner_category.py | 5 +- openstack_odooclient/managers/pricelist.py | 15 +- openstack_odooclient/managers/product.py | 71 +- 46 files changed, 3814 insertions(+), 1811 deletions(-) create mode 100644 docs/managers/account-move-line.md create mode 100644 docs/managers/account-move.md create mode 100644 docs/managers/company.md create mode 100644 docs/managers/credit-transaction.md create mode 100644 docs/managers/credit-type.md create mode 100644 docs/managers/credit.md create mode 100644 docs/managers/crm-team.md create mode 100644 docs/managers/currency.md create mode 100644 docs/managers/customer-group.md create mode 100644 docs/managers/grant-type.md create mode 100644 docs/managers/grant.md create mode 100644 docs/managers/index.md create mode 100644 docs/managers/partner-category.md create mode 100644 docs/managers/partner.md create mode 100644 docs/managers/pricelist.md create mode 100644 docs/managers/product-category.md create mode 100644 docs/managers/product.md create mode 100644 docs/managers/project-contact.md create mode 100644 docs/managers/project.md create mode 100644 docs/managers/referral-code.md create mode 100644 docs/managers/reseller-tier.md create mode 100644 docs/managers/reseller.md create mode 100644 docs/managers/sale-order-line.md create mode 100644 docs/managers/sale-order.md create mode 100644 docs/managers/support-subscription-type.md create mode 100644 docs/managers/support-subscription.md create mode 100644 docs/managers/tax-group.md create mode 100644 docs/managers/tax.md create mode 100644 docs/managers/term-discount.md create mode 100644 docs/managers/trial.md create mode 100644 docs/managers/uom-category.md create mode 100644 docs/managers/uom.md create mode 100644 docs/managers/user.md create mode 100644 docs/managers/volume-discount-range.md create mode 100644 docs/managers/voucher-code.md diff --git a/README.md b/README.md index d2c9a6a..51e6dc0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ pass the connection details to it. ```python openstack_odooclient.Client( + *, hostname: str, database: str, username: str, @@ -43,7 +44,6 @@ as it provides some extra parameters for convenience. from openstack_odooclient import Client as OdooClient odoo_client = OdooClient( - *, hostname="localhost", database="odoodb", user="test-user", @@ -137,153 +137,19 @@ For example, performing a simple search query would look something like this: * `volume_discount_ranges` - OpenStack Volume Discount Ranges (Odoo Model: `openstack.volume_discount_range`) * `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) -### Methods - -#### `list` - -```python -list( - ids: int | Iterable[int], - fields: Iterable[str] | None = None, - as_dict: bool = False, -) -> list[Record] -``` - -```python -list( - ids: int | Iterable[int], - fields: Iterable[str] | None = None, - as_dict: bool = True, -) -> list[dict[str, Any]] -``` - -Get one or more specific records by ID. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.list(1234) -[User(record={'id': 1234, ...}, fields=None)] ->>> odoo_client.users.list([1234, 5678]) -[User(record={'id': 1234, ...}, fields=None), User(record={'id': 5678, ...}, fields=None)] -``` - -By default all fields available on the record model -will be selected, but this can be filtered using the -`fields` parameter. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.list(1234, fields={"ids"}) -[User(record={'id': 1234}, fields=['ids'])] -``` - -Use the `as_dict` parameter to return records as `dict` -objects, instead of record objects. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.list(1234, as_dict=True) -[{'id': 1234, ...}] -``` - -If `ids` is given an empty iterator, this method -returns an empty list. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.list([]) -[] -``` - -##### Parameters - -| Name | Type | Description | Default | -|-----------|-------------------------|---------------------------------------------------|------------| -| `ids` | `int \| Iterable[int]` | Record ID, or list of record IDs | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | - -##### Returns - -| Type | Description | -|------------------------|------------------------------------------------| -| `list[Record]` | Record objects (when `as_dict` is `False`) | -| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | - -#### `get` - -```python -get( - id: int, - fields: Iterable[str] | None = None, - as_dict: bool = False, - optional: bool = False, -) -> Record -``` - -```python -get( - id: int, - fields: Iterable[str] | None = None, - as_dict: bool = False, - optional: bool = True, -) -> Record | None -``` - -```python -get( - id: int, - fields: Iterable[str] | None = None, - as_dict: bool = True, - optional: bool = False, -) -> dict[str, Any] -``` +## Records -```python -get( - id: int, - fields: Iterable[str] | None = None, - as_dict: bool = True, - optional: bool = True, -) -> dict[str, Any] | None -``` +Record manager methods return record objects for the corresponding model +in Odoo. -Get a single record by ID. +Record fields can be accessed as attributes on these record objects. +The record classes are fully type hinted, allowing IDEs and validation +tools such as Mypy to verify that your application is using the fields +correctly. ```python ->>> from openstack_odooclient import Client as OdooClient +>>> from openstack_odooclient import Client as OdooClient, User +>>> user: User | None = None >>> odoo_client = OdooClient( ... hostname="localhost", ... port=8069, @@ -292,1609 +158,9 @@ Get a single record by ID. ... user="test-user", ... password="", ... ) ->>> odoo_client.users.get(1234) +>>> user = odoo_client.users.get(1234) +>>> user User(record={'id': 1234, ...}, fields=None) -``` - -By default all fields available on the record model -will be selected, but this can be filtered using the -``fields`` parameter. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.get(1234, fields={"ids"}) -User(record={'id': 1234}, fields=['ids']) -``` - -Use the ``as_dict`` parameter to return the record as -a ``dict`` object, instead of a record object. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.get(1234, as_dict=True) -{'id': 1234, ...} -``` - -##### Parameters - -| Name | Type | Description | Default | -|------------|-------------------------|---------------------------------------------------|------------| -| `id` | `int` | Record ID | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `as_dict` | `bool` | Return record as a dictionary | `False` | -| `optional` | `bool` | Return `None` if not found | `False` | - -##### Raises - -| Type | Description | -|-----------------------|--------------------------------------------------------------------| -| `RecordNotFoundError` | If the given record ID does not exist (when `optional` is `False`) | - -##### Returns - -| Type | Description | -|------------------|-------------------------------------------------------------| -| `Record` | Record object (when `as_dict` is `False`) | -| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | -| `None` | If the record ID does not exist (when `optional` is `True`) | - -#### `search` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = False, - as_dict: bool = False, - optional: bool = False, -) -> list[Record] -``` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = False, - as_dict: bool = False, - optional: bool = True, -) -> list[Record] | None -``` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = True, - as_dict: bool = False, - optional: bool = False, -) -> list[int] -``` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = True, - as_dict: bool = False, - optional: bool = True, -) -> list[int] | None -``` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = False, - as_dict: bool = True, - optional: bool = False, -) -> list[dict[str, Any]] -``` - -```python -search( - filters: Sequence[Any] | None = None, - fields: Iterable[str] | None = None, - order: str | None = None, - as_id: bool = False, - as_dict: bool = True, - optional: bool = True, -) -> list[dict[str, Any]] | None -``` - -Query the ERP for records, optionally defining -filters to constrain the search and other parameters, -and return the results. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.search([("id", "=", 1234)]) -[User(record={'id': 1234, ...}, fields=None)] -``` - -Query filters should be defined using the same format as OdooRPC, -but some additional features are supported: - -* Odoo client field aliases can be specified as the field name, - in additional to the original field name on the Odoo model - (e.g. `create_user` instead of `create_uid`). -* Record objects can be directly passed as the value - on a filter, where a record ID would normally be expected. -* Sets and tuples are supported when specifying a range of values, - in addition to lists. - -To search *all* records, leave ``filters`` unset -(or set it to ``None``). - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.search() -[User(record={'id': 1234, ...}, fields=None), ...] -``` - -By default all fields available on the record model -will be selected, but this can be filtered using the -``fields`` parameter. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.search(fields={"ids"}) -[User(record={'id': 1234}, fields=['ids']), ...] -``` - -Use the `as_id` parameter to return the record as -a list of IDs, instead of record objects. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.search(as_id=True) -[1234, ...] -``` - -Use the `as_dict` parameter to return the record as -a list of `dict` objects, instead of record objects. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.users.search(as_dict=True) -[{'id': 1234, ...}, ...] -``` - -##### Parameters - -| Name | Type | Description | Default | -|-----------|-------------------------|---------------------------------------------------|---------| -| `filters` | `Sequence[Any] \| None` | Filters to query by (or `None` for no filters) | `None` | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `order` | `str \| None` | Field to order results by, if ordering results | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | - -##### Returns - -| Type | Description | -|------------------------|------------------------------------------------| -| `list[Record]` | Record objects (default) | -| `list[int]` | Record IDs (when `as_id` is `True`) | -| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | - -#### `create` - -```python -create(**fields: Any) -> int -``` - -Create a new record, using the specified keyword arguments -as input fields. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.sales_order_lines.create(...) +>>> user.id 1234 ``` - -To fetch the newly created record object, -pass the returned ID to the [``get``](#get) method. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.sale_order_lines.get( -... odoo_client.sales_order_lines.create(...), -... ) -SaleOrderLine(record={'id': 1234, ...}, fields=None) -``` - -##### Parameters - -| Name | Type | Description | Default | -|------------|-------|-----------------------------------------|------------| -| `**fields` | `Any` | Record field values (keyword arguments) | (required) | - -##### Returns - -| Type | Description | -|-------|------------------------------------| -| `int` | The ID of the newly created record | - -#### `create_multi` - -```python -create_multi(*records: Mapping[str, Any]) -> list[int] -``` - -Create one or more new records in a single request, -passing in the mappings containing the record's input fields -as positional arguments. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.sales_order_lines.create_multi({...}, {...}) -[1234, 1235] -``` - -To fetch the newly created record objects, -pass the returned IDs to the [``list``](#list) method. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.sale_order_lines.list( -... odoo_client.sales_order_lines.create_multi({...}, {...}), -... ) -[SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] -``` - -##### Parameters - -| Name | Type | Description | Default | -|------------|---------------------|----------------------------------------------------|------------| -| `*records` | `Mapping[str, Any]` | Record field-value mappings (positional arguments) | (required) | - -##### Returns - -| Type | Description | -|-------------|--------------------------------------| -| `list[int]` | The IDs of the newly created records | - -#### `unlink`/`delete` - -```python -unlink(*records: Record | int | Iterable[Record | int]) -> None -``` - -```python -delete(*records: Record | int | Iterable[Record | int]) -> None -``` - -Delete one or more records from Odoo. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.sales_order_lines.unlink(1234) -``` - -This method accepts either a record object or ID, or an iterable of -either of those types. Multiple positional arguments are allowed. - -All specified records will be deleted in a single request. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> line1 = odoo_client.sales_order_lines.get(1234) ->>> line2 = odoo_client.sales_order_lines.get(5678) ->>> odoo_client.sales_order_lines.unlink(line1, 9012, [line2, 3456]) -``` - -##### Parameters - -| Name | Type | Description | Default | -|------------|--------------------------------------------|------------------------------------------------------------------------------|------------| -| `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | - -### Named Record Types - -Some record types have a name field that is generally expected to be unique. -The managers for these record types have additional methods for querying records by name. - -* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) -* `companies` - Companies (Odoo Model: `res.company`) -* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) -* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) -* `currencies` - Currencies (Odoo Model: `res.currency`) -* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) -* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) -* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) -* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) -* `product_categories` - Product Categories (Odoo Model: `product.category`) -* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) -* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) -* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) -* `taxes` - Taxes (Odoo Model: `account.tax`) -* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) -* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) - -#### `get_by_name` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = False, - as_dict: bool = False, - optional: bool = False, -) -> Record -``` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = False, - as_dict: bool = False, - optional: bool = True, -) -> Record | None -``` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = True, - as_dict: bool = False, - optional: bool = False, -) -> int -``` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = True, - as_dict: bool = False, - optional: bool = True, -) -> int | None -``` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = False, - as_dict: bool = True, - optional: bool = False, -) -> dict[str, Any] -``` - -```python -get_by_name( - name: str, - fields: Iterable[str] | None = None, - as_id: bool = False, - as_dict: bool = True, - optional: bool = True, -) -> dict[str, Any] | None -``` - -Query a unique record by name. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.currencies.get_by_name("NZD") -[Currency(record={'id': 1234, 'name': 'NZD', ...}, fields=None)] -``` - -A number of parameters are available to configure the return type, -and what happens when a result is not found. - -By default all fields available on the record model -will be selected, but this can be filtered using the -`fields` parameter. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.currencies.get_by_name("NZD", fields={"rounding"}) -Currency(record={'id': 1234, 'rounding': 0.001}, fields=['rounding']) -``` - -Use the `as_id` parameter to return the ID of the record, -instead of the record object. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.currencies.get_by_name("NZD", as_id=True) -1234 -``` - -Use the `as_dict` parameter to return the record as -a `dict` object, instead of a record object. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.currencies.get_by_name("NZD", as_dict=True) -{'id': 1234, ...} -``` - -When `optional` is `True`, `None` is returned if a record -with the given name does not exist, instead of raising an error. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.currencies.get_by_name("non-existent", optional=True) -None -``` - -##### Parameters - -| Name | Type | Description | Default | -|------------|-------------------------|---------------------------------------------------|------------| -| `name` | `str` | The record name | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | -| `optional` | `bool` | Return `None` if not found | `False` | - -##### Raises - -| Type | Description | -|-----------------------------|-------------------------------------------------------------------------| -| `RecordNotFoundError` | If no record with the given name was found (when `optional` is `False`) | -| `MultipleRecordsFoundError` | If multiple records were found with the same name | - -##### Returns - -| Type | Description | -|------------------|----------------------------------------------------------------------------| -| `Record` | Record object (default) | -| `int` | Record ID (when `as_id` is `True`) | -| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | -| `None` | If a record with the given name does not exist (when `optional` is `True`) | - -## Records - -Record manager methods return record objects for the corresponding model -in Odoo. - -Record fields can be accessed as attributes on these record objects. -The record classes are fully type hinted, allowing IDEs and validation -tools such as Mypy to verify that your application is using the fields -correctly. - -```python ->>> from openstack_odooclient import Client as OdooClient, User ->>> user: User | None = None ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> user = odoo_client.users.get(1234) ->>> user -User(record={'id': 1234, ...}, fields=None) ->>> user.id -1234 -``` - -### Custom Attributes - -Most of the model fields commonly used by applications have been defined -in the record classes, but if your installation of Odoo has add-ons -installed that define custom fields that the Odoo client library -does not know about, these can still be used (just without type hinting). - -Access these fields as object attributes, the same way as you would any -other field. - -```python ->>> user.custom_field_name -'custom-field-value' -``` - -If the custom field is a reference to another model record, -it will be available on the record object as 2-member list. -The first value is the record ID, and the second value -is the display name of the record. - -```python ->>> user.custom_model_ref -[5678, 'custom-record-name'] -``` - -If the custom field is a list of model records, -the record IDs will be made available as type `list[int]`. - -```python ->>> user.custom_model_refs -[5678, 9012, ...] -``` - -### Attributes and Methods - -The following attributes and methods are available on all record types. - -#### `id` - -```python -id: int -``` - -The record's ID in Odoo. - -#### `create_date` - -```python -create_date: datetime -``` - -The time the record was created. - -#### `create_uid` - -```python -create_uid: int -``` - -The ID of the partner that created this record. - -#### `create_name` - -```python -create_name: str -``` - -The name of the partner that created this record. - -#### `create_user` - -```python -create_user: Partner -``` - -The partner that created this record. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `write_date` - -```python -write_date: datetime -``` - -The time the record was last modified. - -#### `write_uid` - -```python -write_uid: int -``` - -The ID of the partner that last modified this record. - -#### `write_name` - -```python -write_name: str -``` - -The name of the partner that modified this record. - -#### `write_user` - -```python -write_user: Partner -``` - -The partner that last modified this record. - -This fetches a full Partner object from Odoo once, -and caches it for subsequence access. - -#### `as_dict` - -```python -as_dict(raw: bool = False) -> dict[str, Any] -``` - -Convert this record object to a dictionary. - -The fields and values in the dictionary are the same -as if the record was queried using `as_dict=True`. -This changes field names to the record object equivalents, -if they are different, to take into account fields being -named differently across Odoo versions. - -```python ->>> user -User(record={'id': 1234, ...}, fields=None) ->>> user.as_dict() -{'id': 1234, ...} -``` - -Set `raw=True` to instead get the raw record dictionary -fields and values as returned by OdooRPC. - -```python ->>> user -User(record={'id': 1234, ...}, fields=None) ->>> user.as_dict(raw=True) -{'id': 1234, ...} -``` - -##### Parameters - -| Name | Type | Description | Default | -|-------|--------|------------------------------------|---------| -| `raw` | `bool` | Return raw dictionary from OdooRPC | `False` | - -##### Returns - -| Type | Description | -|------------------|-------------------| -| `dict[str, Any]` | Record dictionary | - -#### `refresh` - -```python -refresh() -> Self -``` - -Fetch the latest version of this record from Odoo. - -This does not update the record object in place, -a new object is returned with the up-to-date field values. - -```python ->>> user -User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) ->>> user.refresh() -User(record={'id': 1234, 'name': 'New Name', ...}, fields=None) -``` - -##### Returns - -| Type | Description | -|--------|-------------------------------------| -| `Self` | Latest version of the record object | - -#### `unlink`/`delete` - -```python -unlink() -> None -``` - -```python -delete() -> None -``` - -Delete this record from Odoo. - -```python ->>> user -User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) ->>> user.unlink() ->>> user.refresh() -... -openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ID: 1234 -``` - -### Account Move - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import AccountMove -``` - -#### `amount_total` - -```python -amount_total: float -``` - -Total (taxed) amount charged on the account move (invoice). - -#### `amount_untaxed` - -```python -amount_untaxed: float -``` - -Total (untaxed) amount charged on the account move (invoice). - -#### `currency_id` - -```python -currency_id: int -``` - -The ID for the currency used in this account move (invoice). - -#### `currency_name` - -```python -currency_name: str -``` - -The name of the currency used in this account move (invoice). - -#### `currency` - -```python -currency: Currency -``` - -The currency used in this account move (invoice). - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `invoice_date` - -```python -invoice_date: date -``` - -Date associated with the account move (invoice). - -#### `invoice_line_ids` - -```python -invoice_line_ids: list[int] -``` - -The list of the IDs for the account move (invoice) lines -that comprise this account move (invoice). - -#### `invoice_lines` - -```python -invoice_lines: list[AccountMoveLine] -``` - -A list of account move (invoice) lines -that comprise this account move (invoice). - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - -#### `is_move_sent` - -```python -is_move_sent: bool -``` - -Whether or not the account move (invoice) has been sent. - -#### `move_type` - -```python -move_type: Literal[ - "entry", - "out_invoice", - "out_refund", - "in_invoice", - "in_refund", - "out_receipt", - "in_receipt", -] -``` - -The type of account move (invoice). - -Values: - -* ``entry`` - Journal Entry -* ``out_invoice`` - Customer Invoice -* ``out_refund`` - Customer Credit Note -* ``in_invoice`` - Vendor Bill -* ``in_refund`` - Vendor Credit Note -* ``out_receipt`` - Sales Receipt -* ``in_receipt`` - Purchase Receipt - -#### `name` - -```python -name: str | Literal[False] -``` - -Name assigned to the account move (invoice), if posted. - -#### `os_project_id` - -```python -os_project_id: int | None -``` - -The ID of the OpenStack project this account move (invoice) -was generated for, if this is an invoice for OpenStack project usage. - -#### `os_project_name` - -```python -os_project_name: str | None -``` - -The name of the OpenStack project this account move (invoice) -was generated for, if this is an invoice for OpenStack project usage. - -#### `os_project` - -```python -os_project: Project | None -``` - -The OpenStack project this account move (invoice) -was generated for, if this is an invoice for OpenStack project usage. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `payment_state` - -```python -payment_state: Literal[ - "not_paid", - "in_payment", - "paid", - "partial", - "reversed", - "invoicing_legacy", -] -``` - -The current payment state of the account move (invoice). - -Values: - -* ``not_paid`` - Not Paid -* ``in_payment`` - In Payment -* ``paid`` - Paid -* ``partial`` - Partially Paid -* ``reversed`` - Reversed -* ``invoicing_legacy`` - Invoicing App Legacy - -#### state - -```python -state: Literal["draft", "posted", "cancel"] -``` - -The current state of the account move (invoice). - -Values: - -* ``draft`` - Draft invoice -* ``posted`` - Posted (finalised) invoice -* ``cancel`` - Cancelled invoice - -### Account Move Line - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import AccountMoveLine -``` - -#### `currency_id` - -```python -currency_id: int -``` - -The ID for the currency used in this account move (invoice) line. - -#### `currency_name` - -```python -currency_name: str -``` - -The name of the currency used in this account move (invoice) line. - -#### `currency` - -```python -currency: Currency -``` - -The currency used in this account move (invoice) line. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `line_tax_amount` - -```python -line_tax_amount: float -``` - -Amount charged in tax on the account move (invoice) line. - -#### `name` - -```python -name: str -``` - -Name of the product charged on the account move (invoice) line. - -#### `os_project_id` - -```python -os_project_id: int | None -``` - -The ID for the OpenStack project this account move (invoice) line -was generated for. - -#### `os_project_name` - -```python -os_project_name: str | None -``` - -The name of the OpenStack project this account move (invoice) line -was generated for. - -#### `os_project` - -```python -os_project: Project | None -``` - -The OpenStack project this account move (invoice) line -was generated for. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `os_region` - -```python -os_region: str | Literal[False] -``` - -The OpenStack region the account move (invoice) line -was created from. - -#### `os_resource_id` - -```python -os_resource_id: str | Literal[False] -``` - -The OpenStack resource ID for the resource that generated -this account move (invoice) line. - -#### `os_resource_name` - -```python -os_resource_name: str | Literal[False] -``` - -The name of the OpenStack resource tier or flavour, -as used by services such as Distil for rating purposes. - -For example, if this is the account move (invoice) line -for a compute instance, this would be set to the instance's flavour name. - -#### `os_resource_type` - -```python -os_resource_type: str | Literal[False] -``` - -A human-readable description of the type of resource captured -by this account move (invoice) line. - - -#### `price_subtotal` - -```python -price_subtotal: float -``` - -Amount charged for the product (untaxed) on the -account move (invoice) line. - -#### `price_unit` - -```python -price_unit: float -``` - -Unit price for the product used on the account move (invoice) line. - -#### `product_id` - -```python -product_id: int -``` - -The ID for the product charged on the -account move (invoice) line. - -#### `product_name` - -```python -product_name: int -``` - -The name of the product charged on the -account move (invoice) line. - -#### `product` - -```python -product: Product -``` - -The product charged on the -account move (invoice) line. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `quantity` - -```python -quantity: float -``` - -Quantity of product charged on the account move (invoice) line. - -### Company - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import Company -``` - -#### `active` - -```python -active: bool -``` - -Whether or not this company is active (enabled). - -#### `child_ids` - -```python -child_ids: list[int] -``` - -A list of IDs for the child companies. - -#### `children` - -```python -children: list[Company] -``` - -The list of child companies. - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - -#### `name` - -```python -name: str -``` - -Company name, set from the partner name. - -#### `parent_id` - -```python -parent_id: int | None -``` - -The ID for the parent company, if this company -is the child of another company. - -#### `parent_name` - -```python -parent_name: str | None -``` - -The name of the parent company, if this company -is the child of another company. - -#### `parent` - -```python -parent: Company | None -``` - -The parent company, if this company -is the child of another company. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `parent_path` - -```python -parent_path: str | Literal[False] -``` - -The path of the parent company, if there is a parent. - -#### `partner_id` - -```python -partner_id: int -``` - -The ID for the partner for the company. - -#### `partner_name` - -```python -partner_name: str -``` - -The name of the partner for the company. - -#### `partner` - -```python -partner: Partner -``` - -The partner for the company. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -### Credit - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import Credit -``` - -#### `credit_type_id` - -```python -credit_type_id: int -``` -The ID of the type of this credit. - -#### `credit_type_name` - -```python - credit_type_name: str -``` -The name of this type of credit. - -#### `credit_type` - -```python -credit_type: CreditType -``` - -The type of this credit. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `current_balance` - -```python -current_balance: float -``` - -The current remaining balance on the credit. - -#### `expiry_date` - -```python -expiry_date: date -``` - -The date the credit expires. - -#### `initial_balance` - -```python -initial_balance: float -``` - -The initial balance this credit started off with. - -#### `name` - -```python -name: str -``` - -The automatically generated name of the credit. - -#### `start_date` - -```python -start_date: date -``` - -The start date of the credit. - -#### `transaction_ids` - -```python -transaction_ids: list[int] -``` - -A list of IDs for the transactions that have been made -using this credit. - -#### `transactions` - -```python -transactions: list[CreditTransaction] -``` - -The transactions that have been made using this credit. - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - - -#### `voucher_code_id` - -```python -voucher_code_id: int | None -``` - -The ID of the voucher code used when applying for the credit, -if one was supplied. - -#### `voucher_code_name` - -```python -voucher_code_name: str | None -``` - -The name of the voucher code used when applying for the credit, -if one was supplied. - -#### `voucher_code` - -```python -voucher_code: VoucherCode | None -``` - -Voucher code used when applying for the credit, -if one was supplied. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -### Credit Transaction - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import CreditTransaction -``` - -#### `credit_id` - -```python -credit_id: int -``` - -The ID of the credit this transaction was made against. - -#### `credit_name` - -```python -credit_name: str -``` - -The name of the credit this transaction was made against. - -#### `credit` - -```python -credit: Credit -``` -The credit this transaction was made against. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `description` - -```python -description: str -``` - -A description of this credit transaction. - -#### `value` - -```python -value: float -``` - -The value of the credit transaction. - -### Credit Type - -To import the class for type hinting purposes: - -```python -from openstack_odooclient import CreditType -``` - -#### `credit_ids` - -```python -credit_ids: list[int] -``` - -A list of IDs for the credits which are of this credit type. - -#### `credits` - -```python -credits: list[Credit] -``` - -A list of credits which are of this credit type. - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - -#### `name` - -```python -name: str -``` - -Name of the Credit Type. - -#### `only_for_product_ids` - -```python -only_for_product_ids: list[int] -``` - -A list of IDs for the products this credit applies to. - -Mutually exclusive with [`only_for_product_category_ids`](#only_for_product_category_ids). -If neither are specified, the credit applies to all products. - -#### `only_for_products` - -```python -only_for_products: list[Product] -``` - -A list of products which this credit applies to. - -Mutually exclusive with [`only_for_product_categories`](#only_for_product_categories). -If neither are specified, the credit applies to all products. - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - -#### `only_for_product_category_ids` - -```python -only_for_product_category_ids: list[int] -``` - -A list of IDs for the product categories this credit applies to. - -Mutually exclusive with [`only_for_product_ids`](#only_for_product_ids). -If neither are specified, the credit applies to all product -categories. - -#### `only_for_product_categories` - -```python -only_for_product_categories: list[ProductCategory] -``` - -A list of product categories which this credit applies to. - -Mutually exclusive with [`only_for_products`](#only_for_products). -If neither are specified, the credit applies to all product -categories. - -This fetches the full records from Odoo once, -and caches them for subsequent accesses. - -#### `product_id` - -```python -product_id: int -``` - -The ID of the product to use when applying -the credit to invoices. - -#### `product_nane` - -```python -product_name: str -``` - -The ID of the product to use when applying -the credit to invoices. - -#### `product` - -```python -product: Product -``` - -The product to use when applying the credit to invoices. - -This fetches the full record from Odoo once, -and caches it for subsequent accesses. - -#### `refundable` - -```python -refundable: bool -``` - -Whether or not the credit is refundable. diff --git a/docs/managers/account-move-line.md b/docs/managers/account-move-line.md new file mode 100644 index 0000000..062984a --- /dev/null +++ b/docs/managers/account-move-line.md @@ -0,0 +1,204 @@ +# Account Move (Invoice) Lines + +This page documents how to use the manager and record objects +for account move (invoice) lines. + +## Manager + +The account move (invoice) line manager is available as the `account_move_lines` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.account_move_lines.get(1234) +AccountMoveLine(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The account move (invoice) line manager returns `AccountMoveLine` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import AccountMoveLine +``` + +The record class currently implements the following fields and methods. + +### `currency_id` + +```python +currency_id: int +``` + +The ID for the [currency](currency.md) used in this account move (invoice) line. + +### `currency_name` + +```python +currency_name: str +``` + +The name of the [currency](currency.md) used in this account move (invoice) line. + +### `currency` + +```python +currency: Currency +``` + +The [currency](currency.md) used in this account move (invoice) line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `line_tax_amount` + +```python +line_tax_amount: float +``` + +Amount charged in tax on the account move (invoice) line. + +### `name` + +```python +name: str +``` + +Name of the product charged on the account move (invoice) line. + +### `os_project_id` + +```python +os_project_id: int | None +``` + +The ID for the [OpenStack project](project.md) this account move (invoice) line +was generated for. + +### `os_project_name` + +```python +os_project_name: str | None +``` + +The name of the [OpenStack project](project.md) this account move (invoice) line +was generated for. + +### `os_project` + +```python +os_project: Project | None +``` + +The [OpenStack project](project.md) this account move (invoice) line +was generated for. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_region` + +```python +os_region: str | Literal[False] +``` + +The OpenStack region the account move (invoice) line +was created from. + +### `os_resource_id` + +```python +os_resource_id: str | Literal[False] +``` + +The OpenStack resource ID for the resource that generated +this account move (invoice) line. + +### `os_resource_name` + +```python +os_resource_name: str | Literal[False] +``` + +The name of the OpenStack resource tier or flavour, +as used by services such as Distil for rating purposes. + +For example, if this is the account move (invoice) line +for a compute instance, this would be set to the instance's flavour name. + +### `os_resource_type` + +```python +os_resource_type: str | Literal[False] +``` + +A human-readable description of the type of resource captured +by this account move (invoice) line. + +### `price_subtotal` + +```python +price_subtotal: float +``` + +Amount charged for the product (untaxed) on the +account move (invoice) line. + +### `price_unit` + +```python +price_unit: float +``` + +Unit price for the [product](product.md) used on the account move (invoice) line. + +### `product_id` + +```python +product_id: int +``` + +The ID for the [product](product.md) charged on the +account move (invoice) line. + +### `product_name` + +```python +product_name: int +``` + +The name of the [product](product.md) charged on the +account move (invoice) line. + +### `product` + +```python +product: Product +``` + +The [product](product.md) charged on the +account move (invoice) line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `quantity` + +```python +quantity: float +``` + +Quantity of product charged on the account move (invoice) line. diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md new file mode 100644 index 0000000..3c44609 --- /dev/null +++ b/docs/managers/account-move.md @@ -0,0 +1,219 @@ +# Account Moves (Invoices) + +This page documents how to use the manager and record objects +for account moves (invoices). + +## Manager + +The account move (invoice) manager is available as the `account_moves` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.account_moves.get(1234) +AccountMove(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The account move (invoice) manager returns `AccountMove` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import AccountMove +``` + +The record class currently implements the following fields and methods. + +### `amount_total` + +```python +amount_total: float +``` + +Total (taxed) amount charged on the account move (invoice). + +### `amount_untaxed` + +```python +amount_untaxed: float +``` + +Total (untaxed) amount charged on the account move (invoice). + +### `currency_id` + +```python +currency_id: int +``` + +The ID for the currency used in this account move (invoice). + +### `currency_name` + +```python +currency_name: str +``` + +The name of the currency used in this account move (invoice). + +### `currency` + +```python +currency: Currency +``` + +The currency used in this account move (invoice). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `invoice_date` + +```python +invoice_date: date +``` + +Date associated with the account move (invoice). + +### `invoice_line_ids` + +```python +invoice_line_ids: list[int] +``` + +The list of the IDs for the account move (invoice) lines +that comprise this account move (invoice). + +### `invoice_lines` + +```python +invoice_lines: list[AccountMoveLine] +``` + +A list of account move (invoice) lines +that comprise this account move (invoice). + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `is_move_sent` + +```python +is_move_sent: bool +``` + +Whether or not the account move (invoice) has been sent. + +### `move_type` + +```python +move_type: Literal[ + "entry", + "out_invoice", + "out_refund", + "in_invoice", + "in_refund", + "out_receipt", + "in_receipt", +] +``` + +The type of account move (invoice). + +Values: + +* ``entry`` - Journal Entry +* ``out_invoice`` - Customer Invoice +* ``out_refund`` - Customer Credit Note +* ``in_invoice`` - Vendor Bill +* ``in_refund`` - Vendor Credit Note +* ``out_receipt`` - Sales Receipt +* ``in_receipt`` - Purchase Receipt + +### `name` + +```python +name: str | Literal[False] +``` + +Name assigned to the account move (invoice), if posted. + +### `os_project_id` + +```python +os_project_id: int | None +``` + +The ID of the [OpenStack project](project.md) this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +### `os_project_name` + +```python +os_project_name: str | None +``` + +The name of the [OpenStack project](project.md) this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +### `os_project` + +```python +os_project: Project | None +``` + +The [OpenStack project](project.md) this account move (invoice) +was generated for, if this is an invoice for OpenStack project usage. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `payment_state` + +```python +payment_state: Literal[ + "not_paid", + "in_payment", + "paid", + "partial", + "reversed", + "invoicing_legacy", +] +``` + +The current payment state of the account move (invoice). + +Values: + +* ``not_paid`` - Not Paid +* ``in_payment`` - In Payment +* ``paid`` - Paid +* ``partial`` - Partially Paid +* ``reversed`` - Reversed +* ``invoicing_legacy`` - Invoicing App Legacy + +### state + +```python +state: Literal["draft", "posted", "cancel"] +``` + +The current state of the account move (invoice). + +Values: + +* ``draft`` - Draft invoice +* ``posted`` - Posted (finalised) invoice +* ``cancel`` - Cancelled invoice diff --git a/docs/managers/company.md b/docs/managers/company.md new file mode 100644 index 0000000..28a43e1 --- /dev/null +++ b/docs/managers/company.md @@ -0,0 +1,137 @@ +# Companies + +This page documents how to use the manager and record objects +for companies. + +## Manager + +The company manager is available as the `companies` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.companies.get(1234) +Company(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The company manager returns `Company` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Company +``` + +The record class currently implements the following fields and methods. + +#### `active` + +```python +active: bool +``` + +Whether or not this company is active (enabled). + +#### `child_ids` + +```python +child_ids: list[int] +``` + +A list of IDs for the child companies. + +#### `children` + +```python +children: list[Company] +``` + +The list of child companies. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +#### `name` + +```python +name: str +``` + +Company name, set from the partner name. + +#### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent company, if this company +is the child of another company. + +#### `parent_name` + +```python +parent_name: str | None +``` + +The name of the parent company, if this company +is the child of another company. + +#### `parent` + +```python +parent: Company | None +``` + +The parent company, if this company +is the child of another company. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `parent_path` + +```python +parent_path: str | Literal[False] +``` + +The path of the parent company, if there is a parent. + +#### `partner_id` + +```python +partner_id: int +``` + +The ID for the partner for the company. + +#### `partner_name` + +```python +partner_name: str +``` + +The name of the partner for the company. + +#### `partner` + +```python +partner: Partner +``` + +The partner for the company. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/credit-transaction.md b/docs/managers/credit-transaction.md new file mode 100644 index 0000000..36bf8f9 --- /dev/null +++ b/docs/managers/credit-transaction.md @@ -0,0 +1,79 @@ +# Credit Transactions + +This page documents how to use the manager and record objects +for credit transactions. + +## Manager + +The credit transaction manager is available as the `credit_transactions` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.credit_transactions.get(1234) +CreditTransaction(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The credit transaction manager returns `CreditTransaction` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import CreditTransaction +``` + +The record class currently implements the following fields and methods. + +### `credit_id` + +```python +credit_id: int +``` + +The ID of the [credit](credit.md) this transaction was made against. + +### `credit_name` + +```python +credit_name: str +``` + +The name of the [credit](credit.md) this transaction was made against. + +### `credit` + +```python +credit: Credit +``` +The [credit](credit.md) this transaction was made against. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `description` + +```python +description: str +``` + +A description of this credit transaction. + +### `value` + +```python +value: float +``` + +The value of the credit transaction. diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md new file mode 100644 index 0000000..bfbf7ba --- /dev/null +++ b/docs/managers/credit-type.md @@ -0,0 +1,153 @@ +# Credit Types + +This page documents how to use the manager and record objects +for credit types. + +## Manager + +The credit type manager is available as the `credit_types` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.credit_types.get(1234) +CreditType(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The credit type manager returns `CreditType` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import CreditType +``` + +The record class currently implements the following fields and methods. + +### `credit_ids` + +```python +credit_ids: list[int] +``` + +A list of IDs for the [credits](credit.md) which are of this credit type. + +### `credits` + +```python +credits: list[Credit] +``` + +A list of [credits](credit.md) which are of this credit type. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `name` + +```python +name: str +``` + +Name of the Credit Type. + +### `only_for_product_ids` + +```python +only_for_product_ids: list[int] +``` + +A list of IDs for the [products](product.md) this credit applies to. + +Mutually exclusive with [`only_for_product_category_ids`](#only_for_product_category_ids). +If neither are specified, the credit applies to all products. + +### `only_for_products` + +```python +only_for_products: list[Product] +``` + +A list of [products](product.md) which this credit applies to. + +Mutually exclusive with [`only_for_product_categories`](#only_for_product_categories). +If neither are specified, the credit applies to all products. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `only_for_product_category_ids` + +```python +only_for_product_category_ids: list[int] +``` + +A list of IDs for the [product categories](product-category.md) this credit applies to. + +Mutually exclusive with [`only_for_product_ids`](#only_for_product_ids). +If neither are specified, the credit applies to all product +categories. + +### `only_for_product_categories` + +```python +only_for_product_categories: list[ProductCategory] +``` + +A list of [product categories](product-category.md) which this credit applies to. + +Mutually exclusive with [`only_for_products`](#only_for_products). +If neither are specified, the credit applies to all product +categories. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `product_id` + +```python +product_id: int +``` + +The ID of the [product](product.md) to use when applying +the credit to invoices. + +### `product_nane` + +```python +product_name: str +``` + +The name of the [product](product.md) to use when applying +the credit to invoices. + +### `product` + +```python +product: Product +``` + +The [product](product.md) to use when applying the credit to invoices. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `refundable` + +```python +refundable: bool +``` + +Whether or not the credit is refundable. diff --git a/docs/managers/credit.md b/docs/managers/credit.md new file mode 100644 index 0000000..be0c7f1 --- /dev/null +++ b/docs/managers/credit.md @@ -0,0 +1,155 @@ +# Credits + +This page documents how to use the manager and record objects +for credits. + +## Manager + +The credit manager is available as the `credits` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.credits.get(1234) +Credit(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The credit manager returns `Credit` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Credit +``` + +The record class currently implements the following fields and methods. + +### `credit_type_id` + +```python +credit_type_id: int +``` + +The ID of the [type of this credit](#credit-type.md). + +### `credit_type_name` + +```python +credit_type_name: str +``` + +The name of the [type of this credit](#credit-type.md). + +### `credit_type` + +```python +credit_type: CreditType +``` + +The [type of this credit](#credit-type.md). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `current_balance` + +```python +current_balance: float +``` + +The current remaining balance on the credit. + +### `expiry_date` + +```python +expiry_date: date +``` + +The date the credit expires. + +### `initial_balance` + +```python +initial_balance: float +``` + +The initial balance this credit started off with. + +### `name` + +```python +name: str +``` + +The automatically generated name of the credit. + +### `start_date` + +```python +start_date: date +``` + +The start date of the credit. + +### `transaction_ids` + +```python +transaction_ids: list[int] +``` + +A list of IDs for the [transactions](credit-transaction.md) that have been made +using this credit. + +### `transactions` + +```python +transactions: list[CreditTransaction] +``` + +The [transactions](credit-transaction.md) that have been made using this credit. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + + +### `voucher_code_id` + +```python +voucher_code_id: int | None +``` + +The ID of the [voucher code](voucher-code.md) used when applying for the credit, +if one was supplied. + +### `voucher_code_name` + +```python +voucher_code_name: str | None +``` + +The name of the [voucher code](voucher-code.md) used when applying for the credit, +if one was supplied. + +### `voucher_code` + +```python +voucher_code: VoucherCode | None +``` + +The [voucher code](voucher-code.md) used when applying for the credit, +if one was supplied. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/crm-team.md b/docs/managers/crm-team.md new file mode 100644 index 0000000..2c6aca4 --- /dev/null +++ b/docs/managers/crm-team.md @@ -0,0 +1,45 @@ +# CRM Teams + +This page documents how to use the manager and record objects +for CRM teams. + +## Manager + +The CRM team manager is available as the `crm_teams` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.crm_teams.get(1234) +CrmTeam(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The CRM team manager returns `CrmTeam` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import CrmTeam +``` + +The record class currently implements the following fields and methods. + +### `name` + +```python +name: str +``` + +The name of the CRM team. diff --git a/docs/managers/currency.md b/docs/managers/currency.md new file mode 100644 index 0000000..a67e659 --- /dev/null +++ b/docs/managers/currency.md @@ -0,0 +1,125 @@ +# Currencies + +This page documents how to use the manager and record objects +for currencies. + +## Manager + +The currency manager is available as the `currencies` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get(1234) +Currency(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The currency manager returns `Currency` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Currency +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this currency is active (enabled). + +### `currency_unit_label` + +```python +currency_unit_label: str | Literal[False] +``` + +The unit label for this currency, if set. + +### `currency_subunit_label` + +```python +currency_subunit_label: str | Literal[False] +``` + +The sub-unit label for this currency, if set. + +### `date` + +```python +date: date +``` + +The current date to which the currency rate is up to date. + +### `decimal_places` + +```python +decimal_places: int +``` + +Decimal places taken into account for operations on amounts +in this currency. + +It is determined by the rounding factor (``rounding`` field). + +### `name` + +```python +name: str +``` + +The ISO-4217 currency code for the currency. + +### `position` + +```python +position: Literal["before", "after"] +``` + +The position of the currency unit relative to the amount. + +Values: + +* ``before`` - Place the unit before the amount +* ``after`` - Place the unit after the amount + +### `rate` + +```python +rate: float +``` + +The rate of the currency to the currency of rate 1. + +### `rounding` + +```python +rounding: float +``` + +The rounding factor configured for this currency. + +### `symbol` + +```python +symbol: str +``` + +The currency sign to be used when printing amounts. diff --git a/docs/managers/customer-group.md b/docs/managers/customer-group.md new file mode 100644 index 0000000..8821b00 --- /dev/null +++ b/docs/managers/customer-group.md @@ -0,0 +1,94 @@ +# Customer Groups + +This page documents how to use the manager and record objects +for customer groups. + +## Manager + +The customer group manager is available as the `customer_groups` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.customer_groups.get(1234) +CustomerGroup(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The customer group manager returns `CustomerGroup` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import CustomerGroup +``` + +The record class currently implements the following fields and methods. + +### `name` + +```python +name: str +``` + +The name of the customer group. + +### `partner_ids` + +```python +partner_ids: list[int] +``` + +A list of IDs for the [partners](partner.md) that are part +of this customer group. + +### `partners` + +```python +partners: list[partner.Partner] +``` + +The [partners](partner.md) that are part of this customer group. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `pricelist_id` + +```python +pricelist_id: int | None +``` + +The ID for the [pricelist](pricelist.md) this customer group uses, +if not the default one. + +### `pricelist_name` + +```python +pricelist_name: str | None +``` + +The name of the [pricelist](pricelist.md) this customer group uses, +if not the default one. + +### `pricelist` + +```python +pricelist: Pricelist | None +``` + +The [pricelist](pricelist.md) this customer group uses, if not the default one. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/grant-type.md b/docs/managers/grant-type.md new file mode 100644 index 0000000..5faf9c1 --- /dev/null +++ b/docs/managers/grant-type.md @@ -0,0 +1,154 @@ +# Grant Types + +This page documents how to use the manager and record objects +for grant types. + +## Manager + +The grant type manager is available as the `grant_types` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.grant_types.get(1234) +GrantType(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The grant type manager returns `GrantType` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import GrantType +``` + +The record class currently implements the following fields and methods. + +### `grant_ids` + +```python +grant_ids: list[int] +``` + +A list of IDs for the [grants](grant.md) which are of this grant type. + +### `grants` + +```python +grants: list[Grant] +``` + +A list of [grants](grant.md) which are of this grant type. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `name` + +```python +name: str +``` + +Name of the Grant Type. + +### `only_for_product_ids` + +```python +only_for_product_ids: list[int] +``` + +A list of IDs for the [products](product.md) this grant applies to. + +Mutually exclusive with [`only_for_product_category_ids`](#only_for_product_category_ids). +If neither are specified, the grant applies to all products. + +### `only_for_products` + +```python +only_for_products: list[Product] +``` + +A list of [products](product.md) which this grant applies to. + +Mutually exclusive with [`only_for_product_categories`](#only_for_product_categories). +If neither are specified, the grant applies to all products. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `only_for_product_category_ids` + +```python +only_for_product_category_ids: list[int] +``` + +A list of IDs for the [product categories](product-category.md) this grant applies to. + +Mutually exclusive with [`only_for_product_ids`](#only_for_product_ids). +If neither are specified, the grant applies to all product +categories. + +### `only_for_product_categories` + +```python +only_for_product_categories: list[ProductCategory] +``` + +A list of [product categories](product-category.md) which this grant applies to. + +Mutually exclusive with [`only_for_products`](#only_for_products). +If neither are specified, the grant applies to all product +categories. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `only_on_group_root` + +```python +only_on_group_root: bool +``` + +When set to ``True``, this grant type is only allowed to be +part of an invoice grouping if it is on the group root project. + +### `product_id` + +```python +product_id: int +``` + +The ID of the [product](product.md) to use when applying +the grant to invoices. + +### `product_nane` + +```python +product_name: str +``` + +The name of the [product](product.md) to use when applying +the grant to invoices. + +### `product` + +```python +product: Product +``` + +The [product](product.md) to use when applying the grant to invoices. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/grant.md b/docs/managers/grant.md new file mode 100644 index 0000000..7dd4a3e --- /dev/null +++ b/docs/managers/grant.md @@ -0,0 +1,126 @@ +# Grants + +This page documents how to use the manager and record objects +for grants. + +## Manager + +The grant manager is available as the `grants` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.grants.get(1234) +Grant(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The grant manager returns `Grant` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Grant +``` + +The record class currently implements the following fields and methods. + +### `expiry_date` + +```python +expiry_date: date +``` + +The date the grant expires. + +### `grant_type_id` + +```python +grant_type_id: int +``` + +The ID of the [type of this grant](#grant-type.md). + +### `grant_type_name` + +```python +grant_type_name: str +``` + +The name of the [type of this grant](#grant-type.md). + +### `grant_type` + +```python +grant_type: GrantType +``` + +The [type of this grant](#grant-type.md). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `name` + +```python +name: str +``` + +The automatically generated name of the grant. + +### `start_date` + +```python +start_date: date +``` + +The start date of the grant. + +### `value` + +```python +value: float +``` + +The value of this grant. + +### `voucher_code_id` + +```python +voucher_code_id: int | None +``` + +The ID of the [voucher code](voucher-code.md) used when applying for the grant, +if one was supplied. + +### `voucher_code_name` + +```python +voucher_code_name: str | None +``` + +The name of the [voucher code](voucher-code.md) used when applying for the grant, +if one was supplied. + +### `voucher_code` + +```python +voucher_code: VoucherCode | None +``` + +The [voucher code](voucher-code.md) used when applying for the grant, +if one was supplied. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/index.md b/docs/managers/index.md new file mode 100644 index 0000000..00fdecf --- /dev/null +++ b/docs/managers/index.md @@ -0,0 +1,1050 @@ +# Managers + +The Odoo Client object exposes a number of record managers, which contain methods +used to query specific record types, or create one or more new records of that type. + +For example, performing a simple search query would look something like this: + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search([("id", "=", odoo_client.user_id)], as_id=True) +[1234] +``` + +## Available Managers + +* [Account Moves (Invoices)](account-move.md) +* [Account Move (Invoice) Lines](account-move-line.md) +* [Companies](company.md) +* [OpenStack Credits](credit.md) +* [OpenStack Credit Transactions](credit-transaction.md) +* [OpenStack Credit Types](credit-type.md) +* [CRM Teams](crm-team.md) +* [Currencies](currency.md) +* [OpenStack Customer Groups](customer-group.md) +* [OpenStack Grants](grant.md) +* [OpenStack Grant Types](grant-type.md) +* [Partners](partner.md) +* [Partner Categories](partner-category.md) +* [Pricelists](pricelist.md) +* [Products](product.md) +* [Product Categories](product-category.md) +* [OpenStack Projects](project.md) +* [OpenStack Project Contacts](project-contact.md) +* [OpenStack Referral Codes](referral-code.md) +* [OpenStack Resellers](reseller.md) +* [OpenStack Reseller Tiers](reseller-tier.md) +* [Sale Orders](sale-order.md) +* [Sale Order Lines](sale-order-line.md) +* [OpenStack Support Subscriptions](support-subscription.md) +* [OpenStack Support Subscription Types](support-subscription-type.md) +* [Taxes](tax.md) +* [Tax Groups](tax-group.md) +* [OpenStack Term Discounts](term-discount.md) +* [OpenStack Trials](trial.md) +* [Units of Measure (UoM)](uom.md) +* [Unit of Measure (UoM) Categories](uom-category.md) +* [Users](users.md) +* [OpenStack Volume Discount Ranges](volume-discount-range.md) +* [OpenStack Voucher Codes](voucher-code.md) + +## Methods + +All record managers implement the following methods for querying and +managing records. + +### `list` + +```python +list( + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = False, +) -> list[Record] +``` + +```python +list( + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = True, +) -> list[dict[str, Any]] +``` + +Get one or more specific records by ID. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234) +[User(record={'id': 1234, ...}, fields=None)] +>>> odoo_client.users.list([1234, 5678]) +[User(record={'id': 1234, ...}, fields=None), User(record={'id': 5678, ...}, fields=None)] +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234, fields={"ids"}) +[User(record={'id': 1234}, fields=['ids'])] +``` + +Use the `as_dict` parameter to return records as `dict` +objects, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(1234, as_dict=True) +[{'id': 1234, ...}] +``` + +If `ids` is given an empty iterator, this method +returns an empty list. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list([]) +[] +``` + +#### Parameters + +| Name | Type | Description | Default | +|-----------|-------------------------|---------------------------------------------------|------------| +| `ids` | `int \| Iterable[int]` | Record ID, or list of record IDs | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | + +#### Returns + +| Type | Description | +|------------------------|------------------------------------------------| +| `list[Record]` | Record objects (when `as_dict` is `False`) | +| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | + +### `get` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = False, +) -> Record +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = False, + optional: bool = True, +) -> Record | None +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get( + id: int, + fields: Iterable[str] | None = None, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Get a single record by ID. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234) +User(record={'id': 1234, ...}, fields=None) +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234, fields={"ids"}) +User(record={'id': 1234}, fields=['ids']) +``` + +Use the ``as_dict`` parameter to return the record as +a ``dict`` object, instead of a record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234, as_dict=True) +{'id': 1234, ...} +``` + +#### Parameters + +| Name | Type | Description | Default | +|------------|-------------------------|---------------------------------------------------|------------| +| `id` | `int` | Record ID | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +#### Raises + +| Type | Description | +|-----------------------|--------------------------------------------------------------------| +| `RecordNotFoundError` | If the given record ID does not exist (when `optional` is `False`) | + +#### Returns + +| Type | Description | +|------------------|-------------------------------------------------------------| +| `Record` | Record object (when `as_dict` is `False`) | +| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | +| `None` | If the record ID does not exist (when `optional` is `True`) | + +### `search` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> list[Record] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> list[Record] | None +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> list[int] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> list[int] | None +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> list[dict[str, Any]] +``` + +```python +search( + filters: Sequence[Any] | None = None, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> list[dict[str, Any]] | None +``` + +Query the ERP for records, optionally defining +filters to constrain the search and other parameters, +and return the results. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search([("id", "=", 1234)]) +[User(record={'id': 1234, ...}, fields=None)] +``` + +Query filters should be defined using the same format as OdooRPC, +but some additional features are supported: + +* Odoo client field aliases can be specified as the field name, + in additional to the original field name on the Odoo model + (e.g. `create_user` instead of `create_uid`). +* Record objects can be directly passed as the value + on a filter, where a record ID would normally be expected. +* Sets and tuples are supported when specifying a range of values, + in addition to lists. + +To search *all* records, leave ``filters`` unset +(or set it to ``None``). + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search() +[User(record={'id': 1234, ...}, fields=None), ...] +``` + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(fields={"ids"}) +[User(record={'id': 1234}, fields=['ids']), ...] +``` + +Use the `as_id` parameter to return the record as +a list of IDs, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(as_id=True) +[1234, ...] +``` + +Use the `as_dict` parameter to return the record as +a list of `dict` objects, instead of record objects. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search(as_dict=True) +[{'id': 1234, ...}, ...] +``` + +#### Parameters + +| Name | Type | Description | Default | +|-----------|-------------------------|---------------------------------------------------|---------| +| `filters` | `Sequence[Any] \| None` | Filters to query by (or `None` for no filters) | `None` | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `order` | `str \| None` | Field to order results by, if ordering results | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | + +#### Returns + +| Type | Description | +|------------------------|------------------------------------------------| +| `list[Record]` | Record objects (default) | +| `list[int]` | Record IDs (when `as_id` is `True`) | +| `list[dict[str, Any]]` | Record dictionaries (when `as_dict` is `True`) | + +### `create` + +```python +create(**fields: Any) -> int +``` + +Create a new record, using the specified keyword arguments +as input fields. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.create(...) +1234 +``` + +To fetch the newly created record object, +pass the returned ID to the [``get``](#get) method. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_order_lines.get( +... odoo_client.sales_order_lines.create(...), +... ) +SaleOrderLine(record={'id': 1234, ...}, fields=None) +``` + +#### Parameters + +| Name | Type | Description | Default | +|------------|-------|-----------------------------------------|------------| +| `**fields` | `Any` | Record field values (keyword arguments) | (required) | + +#### Returns + +| Type | Description | +|-------|------------------------------------| +| `int` | The ID of the newly created record | + +### `create_multi` + +```python +create_multi(*records: Mapping[str, Any]) -> list[int] +``` + +Create one or more new records in a single request, +passing in the mappings containing the record's input fields +as positional arguments. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.create_multi({...}, {...}) +[1234, 1235] +``` + +To fetch the newly created record objects, +pass the returned IDs to the [``list``](#list) method. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_order_lines.list( +... odoo_client.sales_order_lines.create_multi({...}, {...}), +... ) +[SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] +``` + +#### Parameters + +| Name | Type | Description | Default | +|------------|---------------------|----------------------------------------------------|------------| +| `*records` | `Mapping[str, Any]` | Record field-value mappings (positional arguments) | (required) | + +#### Returns + +| Type | Description | +|-------------|--------------------------------------| +| `list[int]` | The IDs of the newly created records | + +### `unlink`/`delete` + +```python +unlink(*records: Record | int | Iterable[Record | int]) -> None +``` + +```python +delete(*records: Record | int | Iterable[Record | int]) -> None +``` + +Delete one or more records from Odoo. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_order_lines.unlink(1234) +``` + +This method accepts either a record object or ID, or an iterable of +either of those types. Multiple positional arguments are allowed. + +All specified records will be deleted in a single request. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> line1 = odoo_client.sales_order_lines.get(1234) +>>> line2 = odoo_client.sales_order_lines.get(5678) +>>> odoo_client.sales_order_lines.unlink(line1, 9012, [line2, 3456]) +``` + +#### Parameters + +| Name | Type | Description | Default | +|------------|--------------------------------------------|------------------------------------------------------------------------------|------------| +| `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | + +## Named Record Managers + +Some record types have a name field that is generally expected to be unique. +The managers for these record types have additional methods for querying records by name. + +* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) +* `companies` - Companies (Odoo Model: `res.company`) +* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) +* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) +* `currencies` - Currencies (Odoo Model: `res.currency`) +* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) +* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) +* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) +* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) +* `product_categories` - Product Categories (Odoo Model: `product.category`) +* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) +* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) +* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) +* `taxes` - Taxes (Odoo Model: `account.tax`) +* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) +* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) + +### `get_by_name` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> Record +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> Record | None +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> int +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> int | None +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get_by_name( + name: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Query a unique record by name. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD") +[Currency(record={'id': 1234, 'name': 'NZD', ...}, fields=None)] +``` + +A number of parameters are available to configure the return type, +and what happens when a result is not found. + +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", fields={"rounding"}) +Currency(record={'id': 1234, 'rounding': 0.001}, fields=['rounding']) +``` + +Use the `as_id` parameter to return the ID of the record, +instead of the record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", as_id=True) +1234 +``` + +Use the `as_dict` parameter to return the record as +a `dict` object, instead of a record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("NZD", as_dict=True) +{'id': 1234, ...} +``` + +When `optional` is `True`, `None` is returned if a record +with the given name does not exist, instead of raising an error. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.currencies.get_by_name("non-existent", optional=True) +None +``` + +#### Parameters + +| Name | Type | Description | Default | +|------------|-------------------------|---------------------------------------------------|------------| +| `name` | `str` | The record name | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +#### Raises + +| Type | Description | +|-----------------------------|-------------------------------------------------------------------------| +| `RecordNotFoundError` | If no record with the given name was found (when `optional` is `False`) | +| `MultipleRecordsFoundError` | If multiple records were found with the same name | + +#### Returns + +| Type | Description | +|------------------|----------------------------------------------------------------------------| +| `Record` | Record object (default) | +| `int` | Record ID (when `as_id` is `True`) | +| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | +| `None` | If a record with the given name does not exist (when `optional` is `True`) | + +## Records + +Record manager methods return record objects for the corresponding model +in Odoo. + +Record fields can be accessed as attributes on these record objects. +The record classes are fully type hinted, allowing IDEs and validation +tools such as Mypy to verify that your application is using the fields +correctly. + +```python +>>> from openstack_odooclient import Client as OdooClient, User +>>> user: User | None = None +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.id +1234 +``` + +### Custom Attributes + +Most of the model fields commonly used by applications have been defined +in the record classes, but if your installation of Odoo has add-ons +installed that define custom fields that the Odoo client library +does not know about, these can still be used (just without type hinting). + +Access these fields as object attributes, the same way as you would any +other field. + +```python +>>> user.custom_field_name +'custom-field-value' +``` + +If the custom field is a reference to another model record, +it will be available on the record object as 2-member list. +The first value is the record ID, and the second value +is the display name of the record. + +```python +>>> user.custom_model_ref +[5678, 'custom-record-name'] +``` + +If the custom field is a list of model records, +the record IDs will be made available as type `list[int]`. + +```python +>>> user.custom_model_refs +[5678, 9012, ...] +``` + +### Attributes and Methods + +The following attributes and methods are available on all record types. + +#### `id` + +```python +id: int +``` + +The record's ID in Odoo. + +#### `create_date` + +```python +create_date: datetime +``` + +The time the record was created. + +#### `create_uid` + +```python +create_uid: int +``` + +The ID of the partner that created this record. + +#### `create_name` + +```python +create_name: str +``` + +The name of the partner that created this record. + +#### `create_user` + +```python +create_user: Partner +``` + +The partner that created this record. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +#### `write_date` + +```python +write_date: datetime +``` + +The time the record was last modified. + +#### `write_uid` + +```python +write_uid: int +``` + +The ID of the partner that last modified this record. + +#### `write_name` + +```python +write_name: str +``` + +The name of the partner that modified this record. + +#### `write_user` + +```python +write_user: Partner +``` + +The partner that last modified this record. + +This fetches a full Partner object from Odoo once, +and caches it for subsequence access. + +#### `as_dict` + +```python +as_dict(raw: bool = False) -> dict[str, Any] +``` + +Convert this record object to a dictionary. + +The fields and values in the dictionary are the same +as if the record was queried using `as_dict=True`. +This changes field names to the record object equivalents, +if they are different, to take into account fields being +named differently across Odoo versions. + +```python +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.as_dict() +{'id': 1234, ...} +``` + +Set `raw=True` to instead get the raw record dictionary +fields and values as returned by OdooRPC. + +```python +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.as_dict(raw=True) +{'id': 1234, ...} +``` + +##### Parameters + +| Name | Type | Description | Default | +|-------|--------|------------------------------------|---------| +| `raw` | `bool` | Return raw dictionary from OdooRPC | `False` | + +##### Returns + +| Type | Description | +|------------------|-------------------| +| `dict[str, Any]` | Record dictionary | + +#### `refresh` + +```python +refresh() -> Self +``` + +Fetch the latest version of this record from Odoo. + +This does not update the record object in place, +a new object is returned with the up-to-date field values. + +```python +>>> user +User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) +>>> user.refresh() +User(record={'id': 1234, 'name': 'New Name', ...}, fields=None) +``` + +##### Returns + +| Type | Description | +|--------|-------------------------------------| +| `Self` | Latest version of the record object | + +#### `unlink`/`delete` + +```python +unlink() -> None +``` + +```python +delete() -> None +``` + +Delete this record from Odoo. + +```python +>>> user +User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) +>>> user.unlink() +>>> user.refresh() +... +openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ID: 1234 +``` diff --git a/docs/managers/partner-category.md b/docs/managers/partner-category.md new file mode 100644 index 0000000..e2d1b69 --- /dev/null +++ b/docs/managers/partner-category.md @@ -0,0 +1,145 @@ +# Partner Categories + +This page documents how to use the manager and record objects +for partner categories. + +## Manager + +The partner category manager is available as the `partner_categories` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.partner_categories.get(1234) +PartnerCategory(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The partner category manager returns `PartnerCategory` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import PartnerCategory +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this partner category is active (enabled). + +### `child_ids` + +```python +child_ids: list[int] +``` + +A list of IDs for the child categories. + +### `children` + +```python +children: list[PartnerCategory] +``` + +The list of child categories. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `color` + +```python +color: int +``` + +Colour index for the partner category. + +### `colour` + +```python +colour: int +``` + +Alias for [``color``](#color). + +### `name` + +```python +name: str +``` + +The name of the partner category. + +### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent partner category, if this category +is the child of another category. + +### `parent_name` + +```python +parent_name: str | None +``` + +The name of the parent partner category, if this category +is the child of another category. + +### `parent` + +```python +parent: ParentCategory | None +``` + +The parent partner category, if this category +is the child of another category. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `parent_path` + +```python +parent_path: str | Literal[False] +``` + +The path of the parent partner category, if there is a parent. + +### `partner_ids` + +```python +partner_ids: list[int] +``` + +A list of IDs for the [partners](partner.md) in this category. + +### `partners` + +```python +partners: list[Partner] +``` + +The list of [partners](partner.md) in this category. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. diff --git a/docs/managers/partner.md b/docs/managers/partner.md new file mode 100644 index 0000000..96ba57e --- /dev/null +++ b/docs/managers/partner.md @@ -0,0 +1,348 @@ +# Partners + +This page documents how to use the manager and record objects +for partners. + +## Manager + +The partner manager is available as the `partners` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.partners.get(1234) +Partner(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The partner manager returns `Partner` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Partner +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this partner is active (enabled). + +### `email` + +```python +email: str +``` + +Main e-mail address for the partner. + +### `name` + +```python +name: str +``` + +Full name of the partner. + +### `os_customer_group_id` + +```python +os_customer_group_id: int | None +``` + +The ID for the [customer group](customer-group.md) this partner is part of, +if it is part of one. + +### `os_customer_group_name` + +```python +os_customer_group_name: str | None +``` + +The name of the [customer group](customer-group.md) this partner is part of, +if it is part of one. + +### `os_customer_group` + +```python +os_customer_group: CustomerGroup +``` + +The [customer group](customer-group.md) this partner is part of, +if it is part of one. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_project_ids` + +```python +os_project_ids: list[int] +``` + +A list of IDs for the [OpenStack projects](project.md) that +belong to this partner. + +### `os_projects` + +```python +os_projects: list[project.Project] +``` + +The [OpenStack projects](project.md) that belong to this partner. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `os_project_contact_ids` + +```python +os_project_contact_ids: list[int] +``` + +A list of IDs for the [project contacts](project-contact.md) that are associated +with this partner. + +### `os_project_contacts` + +```python +os_project_contacts: list[ProjectContact] +``` + +The [project contacts](project-contact.md) that are associated with this partner. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `os_referral_id` + +```python +os_referral_id: int | None +``` + +The ID for the [referral code](referral-code.md) the partner used on sign-up, +if one was used. + +return self._get_ref_id("os_referral", optional=True) + +### `os_referral_name` + +```python +os_referral_name: str | None +``` + +The name of the [referral code](referral-code.md) the partner used on sign-up, +if one was used. + +### `os_referral` + +```python +os_referral: ReferralCode +``` + +The [referral code](referral-code.md) the partner used on sign-up, if one was used. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_referral_code_ids` + +```python +os_referral_code_ids: list[int] +``` + +A list of IDs for the [referral codes](referral-code.md) the partner has used. + +### `os_referral_codes` + +```python +os_referral_codes: list[ReferralCode] +``` + +The [referral codes](referral-code.md) the partner has used. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `os_reseller_id` + +```python +os_reseller_id: int | None +``` + +The ID for the [reseller](reseller.md) for this partner, if this partner +is billed through a reseller. + +### `os_reseller_name` + +```python +os_reseller_name: str | None +``` + +The name of the [reseller](reseller.md) for this partner, if this partner +is billed through a reseller. + +### `os_reseller` + +```python +os_reseller: Reseller | None +``` + +The [reseller](reseller.md) for this partner, if this partner +is billed through a reseller. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_trial_id` + +```python +os_trial_id: int | None +``` + +The ID for the sign-up [trial](trial.md) for this partner, +if signed up under a trial. + +### `os_trial_name` + +```python +os_trial_name: str | None +``` + +The name of the sign-up [trial](trial.md) for this partner, +if signed up under a trial. + +### `os_trial` + +```python +os_trial: Trial | None +``` + +The sign-up [trial](trial.md) for this partner, +if signed up under a trial. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent partner of this partner, +if it has a parent. + +### `parent_name` + +```python +parent_name: str | None +``` + +The name of the parent partner of this partner, +if it has a parent. + +### `parent` + +```python +parent: Partner | None +``` + +The parent partner of this partner, +if it has a parent. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `property_product_pricelist_id` + +```python +property_product_pricelist_id: int | None +``` + +The ID for the [pricelist](pricelist.md) this partner uses, if explicitly set. + +If not set, the pricelist set for the customer group +is used (and if that is not set, the global default +pricelist is used). + +### `property_product_pricelist_name` + +```python +property_product_pricelist_name: str | None +``` + +The name of the [pricelist](pricelist.md) this partner uses, if explicitly set. + +If not set, the pricelist set for the customer group +is used (and if that is not set, the global default +pricelist is used). + +### `property_product_pricelist` + +```python +property_product_pricelist: Pricelist | None +``` + +The [pricelist](pricelist.md) this partner uses, if explicitly set. + +If not set, the pricelist set for the customer group +is used (and if that is not set, the global default +pricelist is used). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `stripe_customer_id` + +```python +stripe_customer_id: str | Literal[False] +``` + +Stripe customer ID for this partner, if one has been assigned. + +### `user_id` + +```python +user_id: int | None +``` + +The ID of the internal [user](user.md) associated with this partner, +if one is assigned. + +### `user_name` + +```python +user_name: str | None +``` + +The name of the internal [user](user.md) associated with this partner, +if one is assigned. + +### `user` + +```python +user: User | None +``` + +The internal [user](user.md) associated with this partner, +if one is assigned. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/pricelist.md b/docs/managers/pricelist.md new file mode 100644 index 0000000..1bae7d3 --- /dev/null +++ b/docs/managers/pricelist.md @@ -0,0 +1,209 @@ +# Pricelists + +This page documents how to use the manager and record objects +for pricelists. + +## Manager + +The partner manager is available as the `pricelists` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.pricelists.get(1234) +Pricelist(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +The following manager methods are also available, in addition to the standard methods. + +### `get_price` + +```python +def get_price( + pricelist: int | Pricelist, + product: int | Product, + qty: float, +) -> float +``` + +Get the price to charge for a given pricelist, product +and quantity. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.pricelists.get_price( +... pricelist=1234, # ID or object +... product=5678, # ID or object +... qty=100, +... ) +2.5 +``` + +#### Parameters + +| Name | Type | Description | Default | +|-------------|--------------------|---------------------------------------------|------------| +| `pricelist` | `int \| Pricelist` | Pricelist to reference (ID or object) | (required) | +| `product` | `int \| Product` | Product to get the price for (ID or object) | (required) | +| `qty` | `float` | Quantity to charge for | (required) | + +#### Returns + +| Type | Description | +|---------|-----------------| +| `float` | Price to charge | + +## Record + +The partner manager returns `Partner` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Partner +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this partner is active (enabled). + +### `company_id` + +```python +company_id: int | None +``` + +The ID for the [company](company.md) for this pricelist, if set. + +### `company_name` + +```python +company_name: str | None +``` + +The name of the [company](company.md) for this pricelist, if set. + +### `company` + +```python +company: Company | None +``` + +The [company](company.md) for this pricelist, if set. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `currency_id` + +```python +currency_id: int +``` + +The ID for the [currency](currency.md) used in this pricelist. + +### `currency_name` + +```python +currency_name: str +``` + +The name of the [currency](currency.md) used in this pricelist. + +### `currency` + +```python +currency: Currency +``` + +The [currency](currency.md) used in this pricelist. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `discount_policy` + +```python +discount_policy: Literal["with_discount", "without_discount"] +``` + +Discount policy for the pricelist. + +Values: + +* ``with_discount`` - Discount included in the price +* ``without_discount`` - Show public price & discount to the customer + +### `name` + +```python +name: str +``` + +The name of this pricelist. + +### `get_price` + +```python +def get_price( + product: int | Product, + qty: float, +) -> float +``` + +Get the price to charge for a given product and quantity. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> pricelist = odoo_client.pricelists.get(1234) +>>> pricelist.get_price( +... product=5678, # ID or object +... qty=100, +... ) +2.5 +``` + +#### Parameters + +| Name | Type | Description | Default | +|-------------|--------------------|---------------------------------------------|------------| +| `product` | `int \| Product` | Product to get the price for (ID or object) | (required) | +| `qty` | `float` | Quantity to charge for | (required) | + +#### Returns + +| Type | Description | +|---------|-----------------| +| `float` | Price to charge | diff --git a/docs/managers/product-category.md b/docs/managers/product-category.md new file mode 100644 index 0000000..85f1c33 --- /dev/null +++ b/docs/managers/product-category.md @@ -0,0 +1,119 @@ +# Product Categories + +This page documents how to use the manager and record objects +for product categories. + +## Manager + +The product category manager is available as the `product_categories` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.product_categories.get(1234) +ProductCategory(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The product category manager returns `ProductCategory` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import ProductCategory +``` + +The record class currently implements the following fields and methods. + +### `child_ids` + +```python +child_ids: list[int] +``` + +A list of IDs for the child categories. +return self._get_field("child_id") + +### `children` + +```python +children: list[ProductCategory] +``` + +The list of child categories. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `complete_name` + +```python +complete_name: str +``` + +The complete product category tree. + +### `name` + +```python +name: str +``` + +Name of the product category. + +### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent product category, if this category +is the child of another category. + +### `parent_id` + +```python +parent_name: str | None +``` + +The name of the parent product category, if this category +is the child of another category. + +### `parent` + +```python +parent: ProductCategory | None +``` + +The parent product category, if this category +is the child of another category. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `parent_path` + +```python +parent_path: str | Literal[False] +``` + +The path of the parent product category, if there is a parent. + +### `product_count` + +```python +product_count: int +``` + +The number of products under this category. diff --git a/docs/managers/product.md b/docs/managers/product.md new file mode 100644 index 0000000..0f8dae1 --- /dev/null +++ b/docs/managers/product.md @@ -0,0 +1,370 @@ +# Products + +This page documents how to use the manager and record objects +for products. + +## Manager + +The product manager is available as the `products` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.products.get(1234) +Product(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +The following manager methods are also available, in addition to the standard methods. + +### `get_sellable_company_products` + +```python +get_sellable_company_products( + company: int | Company, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, +) -> list[Product] +``` + +```python +get_sellable_company_products( + company: int | Company, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, +) -> list[int] +``` + +```python +get_sellable_company_products( + company: int | Company, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, +) -> list[dict[str, Any]] +``` + +Fetch a list of active and saleable products for the given company. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.products.get_sellable_company_products( +... company=1234, # ID or object +... ) +[Product(record={'id': 5678, ...}, fields=None), ...] +``` + +#### Parameters + +| Name | Type | Description | Default | +|-----------|-------------------------|---------------------------------------------------|------------| +| `company` | `int \| Company` | The company to search for products (ID or object) | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select, defaults to `None` (select all) | `None` | +| `order` | `str \| None` | Order results by a specific field | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | + +#### Returns + +| Type | Description | +|------------------------|---------------------------------------------------------| +| `list[Product]` | List of product objects (default) | +| `list[int]` | List of product IDs (when `as_id` is `True`) | +| `list[dict[str, Any]]` | List of product dictionaries (when `as_dict` is `True`) | + +### `get_sellable_company_product_by_name` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> Product +``` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> Product | None +``` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> int +``` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> int | None +``` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get_sellable_company_product_by_name( + company: int | Company, + name: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` +Query a unique product for the given company by name. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.products.get_sellable_company_product_by_name( +... company=1234, +... name="RegionOne.m1.small", +... ) +Product(record={'id': 5678, 'name': 'RegionOne.m1.small', ...}, fields=None) +``` + +A number of parameters are available to configure the return type, +and what happens when a result is not found. + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +Use the ``as_id`` parameter to return the ID of the record, +instead of the record object. + +Use the ``as_dict`` parameter to return the record as +a ``dict`` object, instead of a record object. + +When ``optional`` is ``True``, ``None`` is returned if a record +with the given name does not exist, instead of raising an error. + +#### Parameters + +| Name | Type | Description | Default | +|------------|-------------------------|---------------------------------------------------|------------| +| `company` | `int \| Company` | The company to search for products (ID or object) | (required) | +| `name` | `str` | The product name | (required) | +| `fields` | `Iterable[str] \| None` | Fields to select, defaults to `None` (select all) | `None` | +| `as_id` | `bool` | Return a record ID | `False` | +| `as_dict` | `bool` | Return the record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +#### Raises + +| Type | Description | +|-----------------------------|-------------------------------------------------------------------| +| `MultipleRecordsFoundError` | Multiple records with the same name were found | +| `RecordNotFoundError` | Record with the given name not found (when `optional` is `False`) | + +#### Returns + +| Type | Description | +|------------------|----------------------------------------------------------------------------| +| `Product` | Product object (default) | +| `int` | Product ID (when `as_id` is `True`) | +| `dict[str, Any]` | Product dictionary (when `as_dict` is `True`) | +| `None` | If a product with the given name was not found (when `optional` is `True`) | + +## Record + +The product manager returns `Product` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Product +``` + +The record class currently implements the following fields and methods. + +### `categ_id` + +```python +categ_id: int +``` + +The ID for the [category](product-category.md) this product is under. + +### `categ_name` + +```python +categ_name: str +``` + +The name of the [category](product-category.md) this product is under. + +### `categ` + +```python +categ: ProductCategory +``` + +The [category](product-category.md) this product is under. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `company_id` + +```python +company_id: int | None +``` + +The ID for the [company](company.md) that owns this product, if set. + +### `company_name` + +```python +company_name: str | None +``` + +The name of the [company](company.md) that owns this product, if set. + +### `company` + +```python +company: Company | None +``` + +The [company](company.md) that owns this product, if set. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `default_code` + +```python +default_code: str +``` + +The Default Code for this product. + +In the OpenStack Integration add-on, this is used to store +the rated unit for the service product. + + +### `description` + +```python +description: str +``` + +A short description of this product. + +### `display_name` + +```python +display_name: str +``` + +The name of this product in OpenStack, and on invoices. + +### `list_price` + +```python +list_price: float +``` + +The list price of the product. + +This becomes the unit price of the product on invoices. + + +### `name` + +```python +name: str +``` + +The name of the product. + +### `uom_id` + +```python +uom_id: int +``` + +The ID for the [Unit of Measure](uom.md) for this product. + +### `uom_name` + +```python +uom_name: str +``` + +The name of the [Unit of Measure](uom.md) for this product. + +### `uom` + +```python +uom: Uom +``` + +The [Unit of Measure](uom.md) for this product. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/project-contact.md b/docs/managers/project-contact.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/project.md b/docs/managers/project.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/referral-code.md b/docs/managers/referral-code.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/reseller-tier.md b/docs/managers/reseller-tier.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/reseller.md b/docs/managers/reseller.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/sale-order-line.md b/docs/managers/sale-order-line.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/sale-order.md b/docs/managers/sale-order.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/support-subscription-type.md b/docs/managers/support-subscription-type.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/support-subscription.md b/docs/managers/support-subscription.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/tax-group.md b/docs/managers/tax-group.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/tax.md b/docs/managers/tax.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/term-discount.md b/docs/managers/term-discount.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/trial.md b/docs/managers/trial.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/uom-category.md b/docs/managers/uom-category.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/uom.md b/docs/managers/uom.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/user.md b/docs/managers/user.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/volume-discount-range.md b/docs/managers/volume-discount-range.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/managers/voucher-code.md b/docs/managers/voucher-code.md new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index 98279f7..e3c662d 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -37,7 +37,7 @@ def credit_type_id(self) -> int: @property def credit_type_name(self) -> str: - """The name of this type of credit.""" + """The name of thie type of this credit.""" return self._get_ref_name("credit_type") @cached_property @@ -96,7 +96,7 @@ def voucher_code_name(self) -> Optional[str]: @cached_property def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: - """Voucher code used when applying for the credit, + """The voucher code used when applying for the credit, if one was supplied. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index 5c94982..ebf1471 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -97,7 +97,7 @@ def product_id(self) -> int: @property def product_name(self) -> str: - """The ID of the product to use when applying + """The name of the product to use when applying the credit to invoices. """ return self._get_ref_name("product") diff --git a/openstack_odooclient/managers/crm_team.py b/openstack_odooclient/managers/crm_team.py index b90ad1f..920adaa 100644 --- a/openstack_odooclient/managers/crm_team.py +++ b/openstack_odooclient/managers/crm_team.py @@ -20,7 +20,7 @@ class CrmTeam(record.RecordBase): name: str - """CRM team name.""" + """Name of the CRM team.""" class CrmTeamManager(record.NamedRecordManagerBase[CrmTeam]): diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index 11e0ec0..eed12f2 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -26,7 +26,7 @@ class CustomerGroup(record.RecordBase): name: str - """Customer group name.""" + """The name of the customer group.""" @property def partner_ids(self) -> List[int]: diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index ba73b5f..ba355b4 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -39,7 +39,7 @@ def grant_type_id(self) -> int: @property def grant_type_name(self) -> str: - """The name of this type of grant.""" + """The name of thie type of this grant.""" return self._get_ref_name("grant_type") @cached_property @@ -76,7 +76,7 @@ def voucher_code_name(self) -> Optional[str]: @cached_property def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: - """Voucher code used when applying for the grant, + """The voucher code used when applying for the grant, if one was supplied. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 1af6086..9a27e19 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -44,19 +44,19 @@ def grants(self) -> List[grant.Grant]: @property def only_for_product_ids(self) -> List[int]: - """A list of IDs for the products this credit applies to. + """A list of IDs for the products this grant applies to. Mutually exclusive with ``only_for_product_category_ids``. - If neither are specified, the credit applies to all products. + If neither are specified, the grant applies to all products. """ return self._get_field("only_for_products") @cached_property def only_for_products(self) -> List[product_module.Product]: - """A list of products which this credit applies to. + """A list of products which this grant applies to. Mutually exclusive with ``only_for_product_categories``. - If neither are specified, the credit applies to all products. + If neither are specified, the grant applies to all products. This fetches the full records from Odoo once, and caches them for subsequent accesses. @@ -65,10 +65,10 @@ def only_for_products(self) -> List[product_module.Product]: @property def only_for_product_category_ids(self) -> List[int]: - """A list of IDs for the product categories this credit applies to. + """A list of IDs for the product categories this grant applies to. Mutually exclusive with ``only_for_product_ids``. - If neither are specified, the credit applies to all product + If neither are specified, the grant applies to all product categories. """ return self._get_field("only_for_product_categories") @@ -77,10 +77,10 @@ def only_for_product_category_ids(self) -> List[int]: def only_for_product_categories( self, ) -> List[product_category.ProductCategory]: - """A list of product categories which this credit applies to. + """A list of product categories which this grant applies to. Mutually exclusive with ``only_for_products``. - If neither are specified, the credit applies to all product + If neither are specified, the grant applies to all product categories. This fetches the full records from Odoo once, @@ -102,7 +102,7 @@ def product_id(self) -> int: @property def product_name(self) -> str: - """The ID of the product to use when applying + """The name of the product to use when applying the grant to invoices. """ return self._get_ref_name("product") diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index 9c08cc0..d8c3499 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -35,7 +35,7 @@ class Partner(record.RecordBase): active: bool - """Whether or not this Partner is active.""" + """Whether or not this partner is active (enabled).""" email: str """Main e-mail address for the partner.""" @@ -272,25 +272,25 @@ def property_product_pricelist(self) -> Optional[pricelist.Pricelist]: ) stripe_customer_id: Union[str, Literal[False]] - """The Stripe customer ID for this Partner, if one has been assigned.""" + """The Stripe customer ID for this partner, if one has been assigned.""" @property def user_id(self) -> Optional[int]: - """The ID of the internal user in charge of this partner, + """The ID of the internal user associated with this partner, if one is assigned. """ return self._get_ref_id("user_id", optional=True) @property def user_name(self) -> Optional[str]: - """The ID of the internal user in charge of this partner, + """The name of the internal user associated with this partner, if one is assigned. """ return self._get_ref_name("user_id") @cached_property def user(self) -> Optional[user_module.User]: - """The internal user in charge of this partner, + """The internal user associated with this partner, if one is assigned. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index f66fc7f..37ca05c 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -26,7 +26,7 @@ class PartnerCategory(record.RecordBase): active: bool - """Whether or not the partner category is active.""" + """Whether or not the partner category is active (enabled).""" @property def child_ids(self) -> List[int]: @@ -51,7 +51,7 @@ def colour(self) -> int: return self.color name: str - """Partner category name.""" + """The name of the partner category.""" @property def parent_id(self) -> Optional[int]: @@ -103,6 +103,7 @@ def partners(self) -> List[partner.Partner]: # Key is local alias, value is remote field name. "child_ids": "child_id", "children": "child_id", + "colour": "color", "parent": "parent_id", "partner_ids": "partner_id", "partners": "partner_id", diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 91ce5da..2fc50d5 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -80,18 +80,6 @@ def currency(self) -> currency_module.Currency: * ``without_discount`` - Show public price & discount to the customer """ - display_name: str - """The display name of the pricelist.""" - - default_code: str - """The unit of this product. - - Referred to as the "Default Code" in Odoo. - """ - - description: str - """A short description of this product.""" - name: str """The name of this pricelist.""" @@ -132,7 +120,8 @@ def get_price( product: Union[int, product_module.Product], qty: float, ) -> float: - """Get the price to charge for a given product and quantity. + """Get the price to charge for a given pricelist, product + and quantity. :param pricelist: Pricelist to reference (ID or object) :type pricelist: int or Pricelist diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index 4f1ac60..66217f4 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -132,9 +132,9 @@ class ProductManager(record.RecordManagerWithUniqueFieldBase[Product, str]): record_class = Product @overload - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -143,9 +143,9 @@ def get_sellable_products_for_company( ) -> List[Product]: ... @overload - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -154,9 +154,9 @@ def get_sellable_products_for_company( ) -> List[int]: ... @overload - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -165,9 +165,9 @@ def get_sellable_products_for_company( ) -> List[int]: ... @overload - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -176,9 +176,9 @@ def get_sellable_products_for_company( ) -> List[Dict[str, Any]]: ... @overload - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -186,9 +186,9 @@ def get_sellable_products_for_company( as_dict: bool = ..., ) -> Union[List[Product], List[int], Union[List[Dict[str, Any]]]]: ... - def get_sellable_products_for_company( + def get_sellable_company_products( self, - company: Union[company.Company, int], + company: Union[int, company.Company], fields: Optional[Iterable[str]] = None, order: Optional[str] = None, as_id: bool = False, @@ -196,8 +196,10 @@ def get_sellable_products_for_company( ) -> Union[List[Product], List[int], Union[List[Dict[str, Any]]]]: """Fetch a list of active and saleable products for the given company. - :param company: ID of the company to search for products - :type company: int + :param company: The company to search for products (ID or object) + :type company: int | Company + :param fields: Fields to select, defaults to ``None`` (select all) + :type fields: Iterable[int] or None, optional :param order: Order results by a specific field, defaults to None :type order: Optional[str], optional :param as_id: Return the record IDs only, defaults to False @@ -222,7 +224,7 @@ def get_sellable_products_for_company( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -234,7 +236,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -246,7 +248,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -258,7 +260,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -270,7 +272,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -282,7 +284,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -294,7 +296,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -306,7 +308,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -318,7 +320,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -329,7 +331,7 @@ def get_sellable_company_product_by_name( def get_sellable_company_product_by_name( self, - company: Union[company.Company, int], + company: Union[int, company.Company], name: str, fields: Optional[Iterable[str]] = None, as_id: bool = False, @@ -341,14 +343,27 @@ def get_sellable_company_product_by_name( A number of parameters are available to configure the return type, and what happens when a result is not found. - :param company: ID of the company to search for products - :type company: int + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given name does not exist, instead of raising an error. + + :param company: The company to search for products (ID or object) + :type company: int | Company :param name: The product name :type name: str - :param as_id: Return a record ID, defaults to False - :type as_id: bool, optional :param fields: Fields to select, defaults to ``None`` (select all) :type fields: Iterable[int] or None, optional + :param as_id: Return a record ID, defaults to False + :type as_id: bool, optional :param as_dict: Return the record as a dictionary, defaults to False :type as_dict: bool, optional :param optional: Return ``None`` if not found, defaults to False From cd18f7232095b1a3545ae9b97459a9ecb0310244 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 12 Jun 2024 14:55:26 +1200 Subject: [PATCH 11/87] Write more manager/record docs --- docs/managers/project-contact.md | 37 +++ docs/managers/project.md | 283 +++++++++++++++++++++++ docs/managers/referral-code.md | 178 ++++++++++++++ docs/managers/reseller.md | 160 +++++++++++++ openstack_odooclient/managers/project.py | 4 +- 5 files changed, 660 insertions(+), 2 deletions(-) diff --git a/docs/managers/project-contact.md b/docs/managers/project-contact.md index e69de29..9dd1cf8 100644 --- a/docs/managers/project-contact.md +++ b/docs/managers/project-contact.md @@ -0,0 +1,37 @@ +# Project Contacts + +This page documents how to use the manager and record objects +for project contacts. + +## Manager + +The project contact manager is available as the `project_contacts` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.project_contacts.get(1234) +ProjectContact(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The project contact manager returns `ProjectContact` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import ProjectContact +``` + +The record class currently implements the following fields and methods. diff --git a/docs/managers/project.md b/docs/managers/project.md index e69de29..6ea7392 100644 --- a/docs/managers/project.md +++ b/docs/managers/project.md @@ -0,0 +1,283 @@ +# Projects + +This page documents how to use the manager and record objects +for projects. + +## Manager + +The project manager is available as the `projects` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.projects.get(1234) +Project(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The project manager returns `Project` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Project +``` + +The record class currently implements the following fields and methods. + +### `display_name` + +```python +display_name: str +``` + +The automatically generated display name for the project. + +### `enabled` + +```python +enabled: bool +``` + +Whether or not the project is enabled in Odoo. + +### `group_invoices` + +```python +group_invoices: bool +``` + +Whether or not to group invoices together for this project. + +### `name` + +```python +name: str +``` + +OpenStack project name. + +### `os_id` + +```python +os_id: str +``` + +OpenStack project ID. + +### `override_po_number` + +```python +override_po_number: bool +``` + +Whether or not to override the PO number with the value +set on this Project. + + +### `owner_id` + +```python +owner_id: int +``` + +The ID for the [partner](partner.md) that owns this project. + +### `owner_name` + +```python +owner_name: str +``` + +The name of the [partner](partner.md) that owns this project. + +### `owner` + +```python +owner: Partner +``` + +The [partner](partner.md) that owns this project. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `parent_id` + +```python +parent_id: int | None +``` + +The ID for the parent project, if this project +is the child of another project. + +### `parent_name` + +```python +parent_name: str | None +``` + +The name of the parent project, if this project +is the child of another project. + +### `parent` + +```python +parent: Project | None +``` + +The parent project, if this project +is the child of another project. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `payment_method` + +```python +payment_method: Literal["invoice", "credit_card"] +``` + +Payment method configured on the project. + +Values: + +* ``invoice`` - Project is paid by invoice +* ``credit_card`` - Project is paid by credit card + +### `po_number` + +```python +po_number: str | Literal[False] +``` + +The PO number set for this specific Project (if set). + +### `project_contact_ids` + +```python +project_contact_ids: list[int] +``` + +A list of IDs for the [contacts](project-contact.md) for this project. + +### `project_contacts` + +```python +project_contacts: list[ProjectContact] +``` + +The [contacts](project-contact.md) for this project. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `project_credit_ids` + +```python +project_credit_ids: list[int] +``` + +A list of IDs for the [credits](credit.md) that apply to this project. + +### `project_credits` + +```python +project_credits: list[Credit] +``` + +The [credits](credit.md) that apply to this project. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `project_grant_ids` + +```python +project_grant_ids: list[int] +``` + +A list of IDs for the [grants](grant.md) that apply to this project. + +### `project_grants` + +```python +project_grants: list[Grant] +``` + +The [grants](grant.md) that apply to this project. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `stripe_card_id` + +```python +stripe_card_id: str | Literal[False] +``` + +The card ID used for credit card payments on this project +using Stripe, if the payment method is set to `credit_card`. + +If a credit card has not been assigned to this project, +this field will be set to `False`. + +### `support_subscription_id` + +```python +support_subscription_id: int | None +``` + +The ID for the [support subscription](support-subscription.md) for this project, +if the project has one. + +### `support_subscription_name` + +```python +support_subscription_name: str | None +``` + +The name of the [support subscription](support-subscription.md) for this project, +if the project has one. + +### `support_subscription` + +```python +support_subscription: SupportSubscription | None +``` + +The [support subscription](support-subscription.md) for this project, +if the project has one. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `term_discount_ids` + +```python +term_discount_ids: list[int] +``` + +A list of IDs for the [term discounts](term-discount.md) that apply to this project. + +### `term_discounts` + +```python +term_discounts: list[TermDiscount] +``` + +The [term discounts](term-discount.md) that apply to this project. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. diff --git a/docs/managers/referral-code.md b/docs/managers/referral-code.md index e69de29..ce36b66 100644 --- a/docs/managers/referral-code.md +++ b/docs/managers/referral-code.md @@ -0,0 +1,178 @@ +# Referral Codes + +This page documents how to use the manager and record objects +for referral codes. + +## Manager + +The project manager is available as the `referral_codes` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.referral_codes.get(1234) +ReferralCode(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The referral code manager returns `ReferralCode` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import ReferralCode +``` + +The record class currently implements the following fields and methods. + +### `allowed_uses` + +```python +allowed_uses: int +``` + +The number of allowed uses of this referral code. + +Set to `-1` for unlimited uses. + +### `before_reward_usage_threshold` + +```python +before_reward_usage_threshold: float +``` + +The amount of usage that must be recorded by the new sign-up +before the reward credit is awarded to the referrer. + +### `code` + +```python +code: str +``` + +The unique referral code. + +### `name` + +```python +name: str +``` + +Automatically generated name for the referral code. + +### `referral_ids` + +```python +referral_ids: list[int] +``` + +A list of IDs for the [partners](partner.md) that signed up +using this referral code. + +### `referrals` + +```python +referrals: list[Partner] +``` + +The [partners](partner.md) that signed up using this referral code. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `referral_credit_amount` + +```python +referral_credit_amount: float +``` + +Initial balance for the referral credit. + +### `referral_credit_duration` + +```python +referral_credit_duration: int +``` + +Duration of the referral credit, in days. + +### `referral_credit_type_id` + +```python +referral_credit_type_id: int +``` + +The ID of the [credit type](credit-type.md) to use for the referral credit. + +### `referral_credit_type_name` + +```python +referral_credit_type_name: str +``` + +The name of the [credit type](credit-type.md) to use for the referral credit. + +### `referral_credit_type` + +```python +referral_credit_type: CreditType +``` + +The [credit type](credit-type.md) to use for the referral credit. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `reward_credit_amount` + +```python +reward_credit_amount: float +``` + +Initial balance for the reward credit. + +### `reward_credit_duration` + +```python +reward_credit_duration: int +``` + +Duration of the reward credit, in days. + +### `reward_credit_type_id` + +```python +reward_credit_type_id: int +``` + +The ID of the [credit type](credit-type.md) to use for the reward credit. + +### `reward_credit_type_name` + +```python +reward_credit_type_name: str +``` + +The name of the [credit type](credit-type.md) to use for the reward credit. + +### `reward_credit_type` + +```python +reward_credit_type: CreditType +``` + +The [credit type](credit-type.md) to use for the reward credit. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/reseller.md b/docs/managers/reseller.md index e69de29..9b77005 100644 --- a/docs/managers/reseller.md +++ b/docs/managers/reseller.md @@ -0,0 +1,160 @@ +# Resellers + +This page documents how to use the manager and record objects +for resellers. + +## Manager + +The reseller manager is available as the `resellers` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.resellers.get(1234) +Reseller(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The reseller manager returns `Reseller` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Reseller +``` + +The record class currently implements the following fields and methods. + +### `alternative_billing_url` + +```python +alternative_billing_url: str | None +``` + +The URL to the cloud billing page for the reseller, if available. + +### `alternative_support_url` + +```python +alternative_support_url: str | None +``` + +The URL to the cloud support centre for the reseller, if available. + +### `demo_project_id` + +```python +demo_project_id: int | None +``` + +The ID for the optional demo [project](project.md) belonging to the reseller. + +### `demo_project_name` + +```python +demo_project_name: str | None +``` + +The name of the optional demo project belonging to the reseller. + +### `demo_project` + +```python +demo_project: Project | None +``` + +An optional demo [project](project.md) belonging to the reseller. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `hide_billing` + +```python +hide_billing: bool +``` + +Whether or not the billing URL should be hidden. + +### `hide_support` + +```python +hide_support: bool +``` + +Whether or not the support URL should be hidden. + +### `name` + +```python +name: str +``` + +The automatically generated reseller name. + +This is set to the [reseller partner's](#partner) name. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the reseller [partner](partner.md). + +### `partner_name` + +```python +partner_name: str +``` + +The name of the reseller [partner](partner.md). + +### `partner` + +```python +partner: Partner +``` + +The reseller [partner](partner.md). + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `tier_id` + +```python +tier_id: int +``` + +The ID for the [tier](reseller-tier.md) this reseller is under. + +### `tier_name` + +```python +tier_name: str +``` + +The name of the [tier](reseller-tier.md) this reseller is under. + +### `tier` + +```python +tier: ResellerTier +``` + +The [tier](reseller-tier.md) this reseller is under. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index 23db7a3..f768dfd 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -147,7 +147,7 @@ def project_contacts(self) -> List[project_contact.ProjectContact]: @property def project_credit_ids(self) -> List[int]: - """A list of IDs for the contacts for this project.""" + """A list of IDs for the credits that apply to this project.""" return self._get_field("project_credits") @cached_property @@ -161,7 +161,7 @@ def project_credits(self) -> List[credit.Credit]: @property def project_grant_ids(self) -> List[int]: - """A list of IDs for the contacts for this project.""" + """A list of IDs for the grants that apply to this project.""" return self._get_field("project_grants") @cached_property From 0195abcc797f2c963b149f348dabb7e5341d485f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 12 Jun 2024 19:07:13 +1200 Subject: [PATCH 12/87] Add TODOs, remove CRMTeam from library, add more docs --- README.md | 1 - docs/managers/account-move-line.md | 9 + docs/managers/account-move.md | 9 + docs/managers/company.md | 9 + docs/managers/credit-transaction.md | 9 + docs/managers/credit-type.md | 9 + docs/managers/credit.md | 9 + docs/managers/crm-team.md | 45 -- docs/managers/currency.md | 9 + docs/managers/custom.md | 3 + docs/managers/customer-group.md | 9 + docs/managers/grant-type.md | 9 + docs/managers/grant.md | 9 + docs/managers/index.md | 32 +- docs/managers/partner-category.md | 9 + docs/managers/partner.md | 9 + docs/managers/pricelist.md | 9 + docs/managers/product-category.md | 9 + docs/managers/product.md | 9 + docs/managers/project-contact.md | 85 +++ docs/managers/project.md | 9 + docs/managers/referral-code.md | 9 + docs/managers/reseller-tier.md | 144 +++++ docs/managers/reseller.md | 9 + docs/managers/sale-order-line.md | 538 ++++++++++++++++++ docs/managers/sale-order.md | 379 ++++++++++++ openstack_odooclient/__init__.py | 2 - openstack_odooclient/client.py | 6 +- openstack_odooclient/managers/crm_team.py | 28 - .../managers/project_contact.py | 4 +- .../managers/record/manager_base.py | 3 + .../managers/reseller_tier.py | 7 +- openstack_odooclient/managers/sale_order.py | 53 +- .../managers/sale_order_line.py | 39 +- 34 files changed, 1404 insertions(+), 127 deletions(-) delete mode 100644 docs/managers/crm-team.md create mode 100644 docs/managers/custom.md delete mode 100644 openstack_odooclient/managers/crm_team.py diff --git a/README.md b/README.md index 51e6dc0..899926b 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ For example, performing a simple search query would look something like this: * `credits` - OpenStack Credits (Odoo Model: `openstack.credit`) * `credit_transactions` - OpenStack Credit Transactions (Odoo Model: `openstack.credit.transaction`) * `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) -* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) * `currencies` - Currencies (Odoo Model: `res.currency`) * `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) * `grants` - OpenStack Grants (Odoo Model: `openstack.grant`) diff --git a/docs/managers/account-move-line.md b/docs/managers/account-move-line.md index 062984a..0019cc1 100644 --- a/docs/managers/account-move-line.md +++ b/docs/managers/account-move-line.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for account move (invoice) lines. +## Details + +| Name | Value | +|-----------------|-----------------------------------| +| Odoo Modules | Accounting, OpenStack Integration | +| Odoo Model Name | `account.move.line` | +| Manager | `account_move_lines` | +| Record Type | `AccountMoveLine` | + ## Manager The account move (invoice) line manager is available as the `account_move_lines` diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md index 3c44609..aa31499 100644 --- a/docs/managers/account-move.md +++ b/docs/managers/account-move.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for account moves (invoices). +## Details + +| Name | Value | +|-----------------|-----------------------------------| +| Odoo Modules | Accounting, OpenStack Integration | +| Odoo Model Name | `account.move` | +| Manager | `account_moves` | +| Record Type | `AccountMove` | + ## Manager The account move (invoice) manager is available as the `account_moves` diff --git a/docs/managers/company.md b/docs/managers/company.md index 28a43e1..8508b6d 100644 --- a/docs/managers/company.md +++ b/docs/managers/company.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for companies. +## Details + +| Name | Value | +|-----------------|----------------------| +| Odoo Modules | Base, Product, Sales | +| Odoo Model Name | `res.company` | +| Manager | `companies` | +| Record Type | `Company` | + ## Manager The company manager is available as the `companies` diff --git a/docs/managers/credit-transaction.md b/docs/managers/credit-transaction.md index 36bf8f9..17e6f98 100644 --- a/docs/managers/credit-transaction.md +++ b/docs/managers/credit-transaction.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for credit transactions. +## Details + +| Name | Value | +|-----------------|--------------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.credit.transaction` | +| Manager | `credit_transactions` | +| Record Type | `CreditTransaction` | + ## Manager The credit transaction manager is available as the `credit_transactions` diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md index bfbf7ba..acc1230 100644 --- a/docs/managers/credit-type.md +++ b/docs/managers/credit-type.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for credit types. +## Details + +| Name | Value | +|-----------------|-------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.credit.type` | +| Manager | `credit_types` | +| Record Type | `CreditType` | + ## Manager The credit type manager is available as the `credit_types` diff --git a/docs/managers/credit.md b/docs/managers/credit.md index be0c7f1..200a52d 100644 --- a/docs/managers/credit.md +++ b/docs/managers/credit.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for credits. +## Details + +| Name | Value | +|-----------------|-----------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.credit` | +| Manager | `credits` | +| Record Type | `Credit` | + ## Manager The credit manager is available as the `credits` diff --git a/docs/managers/crm-team.md b/docs/managers/crm-team.md deleted file mode 100644 index 2c6aca4..0000000 --- a/docs/managers/crm-team.md +++ /dev/null @@ -1,45 +0,0 @@ -# CRM Teams - -This page documents how to use the manager and record objects -for CRM teams. - -## Manager - -The CRM team manager is available as the `crm_teams` -attribute on the Odoo client object. - -```python ->>> from openstack_odooclient import Client as OdooClient ->>> odoo_client = OdooClient( -... hostname="localhost", -... port=8069, -... protocol="jsonrpc", -... database="odoodb", -... user="test-user", -... password="", -... ) ->>> odoo_client.crm_teams.get(1234) -CrmTeam(record={'id': 1234, ...}, fields=None) -``` - -For more information on how to use managers, refer to [Managers](index.md). - -## Record - -The CRM team manager returns `CrmTeam` record objects. - -To import the record class for type hinting purposes: - -```python -from openstack_odooclient import CrmTeam -``` - -The record class currently implements the following fields and methods. - -### `name` - -```python -name: str -``` - -The name of the CRM team. diff --git a/docs/managers/currency.md b/docs/managers/currency.md index a67e659..1e6749c 100644 --- a/docs/managers/currency.md +++ b/docs/managers/currency.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for currencies. +## Details + +| Name | Value | +|-----------------|------------------| +| Odoo Modules | Base, Accounting | +| Odoo Model Name | `res.currency` | +| Manager | `currencies` | +| Record Type | `Currency` | + ## Manager The currency manager is available as the `currencies` diff --git a/docs/managers/custom.md b/docs/managers/custom.md new file mode 100644 index 0000000..89b6c7f --- /dev/null +++ b/docs/managers/custom.md @@ -0,0 +1,3 @@ +# Custom Managers and Record Types + +TODO(callumdickinson): Write this page. diff --git a/docs/managers/customer-group.md b/docs/managers/customer-group.md index 8821b00..8004880 100644 --- a/docs/managers/customer-group.md +++ b/docs/managers/customer-group.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for customer groups. +## Details + +| Name | Value | +|-----------------|----------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.customer_group` | +| Manager | `customer_groups` | +| Record Type | `CustomerGroup` | + ## Manager The customer group manager is available as the `customer_groups` diff --git a/docs/managers/grant-type.md b/docs/managers/grant-type.md index 5faf9c1..a640cf5 100644 --- a/docs/managers/grant-type.md +++ b/docs/managers/grant-type.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for grant types. +## Details + +| Name | Value | +|-----------------|------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.grant.type` | +| Manager | `grant_types` | +| Record Type | `GrantType` | + ## Manager The grant type manager is available as the `grant_types` diff --git a/docs/managers/grant.md b/docs/managers/grant.md index 7dd4a3e..b23c7cf 100644 --- a/docs/managers/grant.md +++ b/docs/managers/grant.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for grants. +## Details + +| Name | Value | +|-----------------|-----------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.grant` | +| Manager | `grants` | +| Record Type | `Grant` | + ## Manager The grant manager is available as the `grants` diff --git a/docs/managers/index.md b/docs/managers/index.md index 00fdecf..63d8b42 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -27,7 +27,6 @@ For example, performing a simple search query would look something like this: * [OpenStack Credits](credit.md) * [OpenStack Credit Transactions](credit-transaction.md) * [OpenStack Credit Types](credit-type.md) -* [CRM Teams](crm-team.md) * [Currencies](currency.md) * [OpenStack Customer Groups](customer-group.md) * [OpenStack Grants](grant.md) @@ -625,22 +624,21 @@ All specified records will be deleted in a single request. Some record types have a name field that is generally expected to be unique. The managers for these record types have additional methods for querying records by name. -* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) -* `companies` - Companies (Odoo Model: `res.company`) -* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) -* `crm_teams` - CRM Teams (Odoo Model: `crm.team`) -* `currencies` - Currencies (Odoo Model: `res.currency`) -* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) -* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) -* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) -* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) -* `product_categories` - Product Categories (Odoo Model: `product.category`) -* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) -* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) -* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) -* `taxes` - Taxes (Odoo Model: `account.tax`) -* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) -* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) +* [Account Moves (Invoices)](account-move.md) +* [Companies](company.md) +* [OpenStack Credit Types](credit-type.md) +* [Currencies](currency.md) +* [OpenStack Customer Groups](customer-group.md) +* [OpenStack Grant Types](grant-type.md) +* [Partner Categories](partner-category.md) +* [Pricelists](pricelist.md) +* [Product Categories](product-category.md) +* [OpenStack Projects](project.md) +* [OpenStack Reseller Tiers](reseller-tier.md) +* [Sale Orders](sale-order.md) +* [OpenStack Support Subscription Types](support-subscription-type.md) +* [Taxes](tax.md) +* [Tax Groups](tax-group.md) ### `get_by_name` diff --git a/docs/managers/partner-category.md b/docs/managers/partner-category.md index e2d1b69..40820cc 100644 --- a/docs/managers/partner-category.md +++ b/docs/managers/partner-category.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for partner categories. +## Details + +| Name | Value | +|-----------------|------------------------| +| Odoo Modules | Base | +| Odoo Model Name | `res.partner.category` | +| Manager | `partner_categories` | +| Record Type | `PartnerCategory` | + ## Manager The partner category manager is available as the `partner_categories` diff --git a/docs/managers/partner.md b/docs/managers/partner.md index 96ba57e..f75b596 100644 --- a/docs/managers/partner.md +++ b/docs/managers/partner.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for partners. +## Details + +| Name | Value | +|-----------------|---------------------------------------------| +| Odoo Modules | Base, Product, Sales, OpenStack Integration | +| Odoo Model Name | `res.partner` | +| Manager | `partners` | +| Record Type | `Partner` | + ## Manager The partner manager is available as the `partners` diff --git a/docs/managers/pricelist.md b/docs/managers/pricelist.md index 1bae7d3..f7496b1 100644 --- a/docs/managers/pricelist.md +++ b/docs/managers/pricelist.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for pricelists. +## Details + +| Name | Value | +|-----------------|---------------------| +| Odoo Modules | Product | +| Odoo Model Name | `product.pricelist` | +| Manager | `pricelists` | +| Record Type | `Pricelist` | + ## Manager The partner manager is available as the `pricelists` diff --git a/docs/managers/product-category.md b/docs/managers/product-category.md index 85f1c33..87410f2 100644 --- a/docs/managers/product-category.md +++ b/docs/managers/product-category.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for product categories. +## Details + +| Name | Value | +|-----------------|----------------------| +| Odoo Modules | Product, Accounting | +| Odoo Model Name | `product.category` | +| Manager | `product_categories` | +| Record Type | `ProductCategory` | + ## Manager The product category manager is available as the `product_categories` diff --git a/docs/managers/product.md b/docs/managers/product.md index 0f8dae1..17e2443 100644 --- a/docs/managers/product.md +++ b/docs/managers/product.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for products. +## Details + +| Name | Value | +|-----------------|----------------------------| +| Odoo Modules | Product, Accounting, Sales | +| Odoo Model Name | `product.product` | +| Manager | `products` | +| Record Type | `Product` | + ## Manager The product manager is available as the `products` diff --git a/docs/managers/project-contact.md b/docs/managers/project-contact.md index 9dd1cf8..78c624d 100644 --- a/docs/managers/project-contact.md +++ b/docs/managers/project-contact.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for project contacts. +## Details + +| Name | Value | +|-----------------|-----------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.project_contact` | +| Manager | `project_contacts` | +| Record Type | `ProjectContact` | + ## Manager The project contact manager is available as the `project_contacts` @@ -35,3 +44,79 @@ from openstack_odooclient import ProjectContact ``` The record class currently implements the following fields and methods. + +### `contact_type` + +```python +contact_type: Literal[ + "primary", + "billing", + "technical", + "legal", + "reseller customer", +] +``` + +The contact type to assign the partner as on the project. + +### `inherit` + +```python +inherit: bool +``` + +Whether or not this contact should be inherited by child projects. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the [partner](partner.md) linked to this project contact. + +### `partner_name` + +```python +partner_name: str +``` + +The name of the [partner](partner.md) linked to this project contact. + +### `partner` + +```python +partner: Partner +``` + +The [partner](partner.md) linked to this project contact. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `project_id` + +```python +project_id: int | None +``` + +The ID for the [project](project.md) this contact is linked to, if set. + +### `project_name` + +```python +project_name: str | None +``` + +The name of the [project](project.md) this contact is linked to, if set. + +### `project` + +```python +project: Project | None +``` + +The [project](project.md) this contact is linked to, if set. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/project.md b/docs/managers/project.md index 6ea7392..f438bc3 100644 --- a/docs/managers/project.md +++ b/docs/managers/project.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for projects. +## Details + +| Name | Value | +|-----------------|-----------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.project` | +| Manager | `projects` | +| Record Type | `Project` | + ## Manager The project manager is available as the `projects` diff --git a/docs/managers/referral-code.md b/docs/managers/referral-code.md index ce36b66..c8990b3 100644 --- a/docs/managers/referral-code.md +++ b/docs/managers/referral-code.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for referral codes. +## Details + +| Name | Value | +|-----------------|---------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.referral_code` | +| Manager | `referral_codes` | +| Record Type | `ReferralCode` | + ## Manager The project manager is available as the `referral_codes` diff --git a/docs/managers/reseller-tier.md b/docs/managers/reseller-tier.md index e69de29..de20572 100644 --- a/docs/managers/reseller-tier.md +++ b/docs/managers/reseller-tier.md @@ -0,0 +1,144 @@ +# Reseller Tiers + +This page documents how to use the manager and record objects +for reseller tiers. + +## Details + +| Name | Value | +|-----------------|---------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.reseller.tier` | +| Manager | `reseller_tiers` | +| Record Type | `ResellerTier` | + +## Manager + +The reseller tier manager is available as the `reseller_tiers` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.reseller_tiers.get(1234) +ResellerTier(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The reseller tier manager returns `ResellerTier` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import ResellerTier +``` + +The record class currently implements the following fields and methods. + +### `discount_percent` + +```python +discount_percent: float +``` + +The maximum discount percentage for this reseller tier (0-100). + +### `discount_product_id` + +```python +discount_product_id: int +``` + +The ID of the discount [product](product.md) for the reseller tier. + +### `discount_product_name` + +```python +discount_product_name: str +``` + +The name of the discount [product](product.md) for the reseller tier. + +### `discount_product` + +```python +discount_product: Product +``` + +The discount [product](product.md) for the reseller tier. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `free_monthly_credit` + +```python +free_monthly_credit: float +``` + +The amount the reseller gets monthly in credit for demo projects. + +### `free_monthly_credit_product_id` + +```python +free_monthly_credit_product_id: int +``` + +The ID of the [product](product.md) to use when adding the free monthly credit +to demo project invoices. + +### `free_monthly_credit_product_name` + +```python +free_monthly_credit_product_name: str +``` + +The name of the [product](product.md) to use when adding the free monthly credit +to demo project invoices. + +### `free_monthly_credit_product` + +```python +free_monthly_credit_product: Product +``` + +The [product](product.md) to use when adding the free monthly credit +to demo project invoices. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `free_support_hours` + +```python +free_support_hours: int +``` + +The amount of free support hours the reseller is entitled to +under this tier. + +### `name` + +```python +name: str +``` + +Reseller tier name. + +### `name` + +```python +min_usage_threshold: float +``` + +The minimum required usage amount for the reseller tier. diff --git a/docs/managers/reseller.md b/docs/managers/reseller.md index 9b77005..06c687f 100644 --- a/docs/managers/reseller.md +++ b/docs/managers/reseller.md @@ -3,6 +3,15 @@ This page documents how to use the manager and record objects for resellers. +## Details + +| Name | Value | +|-----------------|-----------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.reseller` | +| Manager | `resellers` | +| Record Type | `Reseller` | + ## Manager The reseller manager is available as the `resellers` diff --git a/docs/managers/sale-order-line.md b/docs/managers/sale-order-line.md index e69de29..e0328c3 100644 --- a/docs/managers/sale-order-line.md +++ b/docs/managers/sale-order-line.md @@ -0,0 +1,538 @@ +# Sale Order Lines + +This page documents how to use the manager and record objects +for sale order lines. + +## Details + +| Name | Value | +|-----------------|------------------------------| +| Odoo Modules | Sales, OpenStack Integration | +| Odoo Model Name | `sale.order.line` | +| Manager | `sale_order_lines` | +| Record Type | `SaleOrderLine` | + +## Manager + +The sale order line manager is available as the `sale_order_lines` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_order_lines.get(1234) +SaleOrderLine(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +### `company_id` + +```python +company_id: int +``` + +The ID for the [company](company.md) this sale order line +was generated for. + +### `company_name` + +```python +company_name: str +``` + +The name of the [company](company.md) this sale order line +was generated for. + +### `company` + +```python +company: Company +``` + +The [company](company.md) this sale order line +was generated for. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `currency_id` + +```python +currency_id: int +``` + +The ID for the [currency](currency.md) used in this sale order line. + +### `currency_name` + +```python +currency_name: str +``` + +The name of the [currency](currency.md) used in this sale order line. + +### `currency` + +```python +currency: Currency +``` + +The [currency](currency.md) used in this sale order line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `discount` + +```python +discount: float +``` + +Discount percentage on the sale order line (0-100). + +### `display_name` + +```python +display_name: str +``` + +Display name for the sale order line in the sale order. + +### `invoice_line_ids` + +```python +invoice_line_ids: list[int] +``` + +A list of IDs for the invoice (account move) lines created +from this sale order line. + +### `invoice_lines` + +```python +invoice_lines: list[AccountMoveLine] +``` + +The invoice (account move) lines created +from this sale order line. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `invoice_status` + +```python +invoice_status: Literal["no", "to invoice", "invoiced", "upselling"] +``` + +The current invoicing status of this sale order line. + +Values: + +* ``no`` - Nothing to invoice +* ``to invoice`` - Has quantity that needs to be invoiced +* ``invoiced`` - Fully invoiced +* ``upselling`` - Upselling opportunity + +### `is_downpayment` + +```python +is_downpayment: bool +``` + +Whether or not this sale order line is a downpayment. + +### `is_expense` + +```python +is_expense: bool +``` + +Whether or not this sale order line is an expense. + +### `name` + +```python +name: str +``` + +Name assigned to the the sale order line. + +This is not the same as the product name. +In the OpenStack Integration add-on, this is normally used to store +the resource's name. + +### `order_id` + +```python +order_id: int +``` + +The ID for the [sale order](sale-order.md) this line is linked to. + +### `order_name` + +```python +order_name: str +``` + +The name of the [sale order](sale-order.md) this line is linked to. + +### `order` + +```python +order: SaleOrder +``` + +The [sale order](sale-order.md) this line is linked to. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `order_partner_id` + +```python +order_partner_id: int +``` + +The ID for the recipient [partner](partner.md) for the sale order. + +### `order_partner_name` + +```python +order_partner_name: str +``` + +The name of the recipient [partner](partner.md) for the sale order. + +### `order_partner` + +```python +order_partner: Partner +``` + +The recipient [partner](partner.md) for the sale order. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_project_id` + +```python +os_project_id: int | None +``` + +The ID for the the [OpenStack project](project.md) this sale order line was +was generated for. + +### `os_project_name` + +```python +os_project_name: str | None +``` + +The name of the the [OpenStack project](project.md) this sale order line was +was generated for. + +### `os_project` + +```python +os_project: Project | None +``` + +The [OpenStack project](project.md) this sale order line was +was generated for. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `os_region` + +```python +os_region: str | Literal[False] +``` + +The OpenStack region the sale order line was created from. + +### `os_resource_id` + +```python +os_resource_id: str | Literal[False] +``` + +The OpenStack resource ID for the resource that generated +this sale order line. + +### `os_resource_name` + +```python +os_resource_name: str | Literal[False] +``` + +The name of the OpenStack resource tier or flavour, +as used by services such as Distil for rating purposes. + +For example, if this is the sale order line for a compute instance, +this would be set to the instance's flavour name. + +### `os_resource_type` + +```python +os_resource_type: str | Literal[False] +``` + +A human-readable description of the type of resource captured +by this sale order line. + + +### `price_reduce` + +```python +price_reduce: float +``` + +Base unit price, less discount (see the ``discount`` field). + +### `price_reduce_taxexcl` + +```python +price_reduce_taxexcl: float +``` + +Actual unit price, excluding tax. + +### `price_reduce_taxinc` + +```python +price_reduce_taxinc: float +``` + +Actual unit price, including tax. + +### `price_subtotal` + +```python +price_subtotal: float +``` + +Subtotal price for the sale order line, excluding tax. + +### `price_tax` + +```python +price_tax: float +``` + +Tax charged on the sale order line. + +### `price_total` + +```python +price_total: float +``` + +Total price for the sale order line, including tax. + +### `price_unit` + +```python +price_unit: float +``` + +Base unit price, excluding tax, before any discounts. + +### `product_id` + +```python +product_id: int +``` + +The ID of the [product](product.md) charged on this sale order line. + +### `product_name` + +```python +product_name: str +``` + +The name of the [product](product.md) charged on this sale order line. + +### `product` + +```python +product: Product +``` + +The [product](product.md) charged on this sale order line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `product` + +```python +product_uom_id: int +``` + +The ID for the [Unit of Measure](uom.md) for the product being charged in +this sale order line. + +### `product_uom_name` + +```python +product_uom_name: str +``` + +The name of the [Unit of Measure](uom.md) for the product being charged in +this sale order line. + +### `product_uom` + +```python +product_uom: Uom +``` + +The [Unit of Measure](uom.md) for the product being charged in +this sale order line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `product_uom_qty` + +```python +product_uom_qty: float +``` + +The product quantity on the sale order line. + +### `product_uom_readonly` + +```python +product_uom_readonly: bool +``` + +Whether or not the product quantity can still be updated +on this sale order line. + +### `product_updatable` + +```python +product_updatable: bool +``` + +Whether or not the product can be edited on this sale order line. + +### `qty_invoiced` + +```python +qty_invoiced: float +``` + +The product quantity that has already been invoiced. + +### `qty_to_invoice` + +```python +qty_to_invoice: float +``` + +The product quantity that still needs to be invoiced. + +### `salesman_id` + +```python +salesman_id: int +``` + +The ID for the salesperson [partner](partner.md) assigned +to this sale order line. + +### `salesman_name` + +```python +salesman_name: str +``` + +The name of the salesperson [partner](partner.md) assigned +to this sale order line. + +### `salesman` + +```python +salesman: Partner +``` + +The salesperson [partner](partner.md) assigned +to this sale order line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `state` + +```python +state: Literal["draft", "sale", "done", "cancel"] +``` + +State of the sale order. + +Values: + +* ``draft`` - Draft sale order (quotation), can still be modified +* ``sale`` - Finalised sale order, cannot be modified +* ``done`` - Finalised and settled sale order, cannot be modified +* ``cancel`` - Cancelled sale order, can be deleted + +### `tax_id` + +```python +tax_id: int +``` + +The ID for the [tax](tax.md) used on this sale order line. + +### `tax_name` + +```python +tax_name: str +``` + +The name of the [tax](tax.md) used on this sale order line. + +### `tax` + +```python +tax: Tax +``` + +The [tax](tax.md) used on this sale order line. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `untaxed_amount_invoiced` + +```python +untaxed_amount_invoiced: float +``` + +The balance, excluding tax, on the sale order line that +has already been invoiced. + +### `untaxed_amount_to_invoice` + +```python +untaxed_amount_to_invoice: float +``` + +The balance, excluding tax, on the sale order line that +still needs to be invoiced. diff --git a/docs/managers/sale-order.md b/docs/managers/sale-order.md index e69de29..78e40c1 100644 --- a/docs/managers/sale-order.md +++ b/docs/managers/sale-order.md @@ -0,0 +1,379 @@ +# Sale Orders + +This page documents how to use the manager and record objects +for sale orders. + +## Details + +| Name | Value | +|-----------------|------------------------------| +| Odoo Modules | Sales, OpenStack Integration | +| Odoo Model Name | `sale.order` | +| Manager | `sale_orders` | +| Record Type | `SaleOrder` | + +## Manager + +The sale order manager is available as the `sale_orders` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_orders.get(1234) +SaleOrder(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +The following manager methods are also available, in addition to the standard methods. + +### `action_confirm` + +```python +action_confirm( + sale_order: int | SaleOrder, +) -> None +``` + +Confirm the given sale order. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_orders.action_confirm( +... sale_order=1234, # ID or object +... ) +``` + +#### Parameters + +| Name | Type | Description | Default | +|--------------|--------------------|---------------------------|------------| +| `sale_order` | `int \| SaleOrder` | The sale order to confirm | (required) | + +### `create_invoices` + +```python +create_invoices( + sale_order: int | SaleOrder, +) -> None +``` + +Create invoices from the given sale order. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sale_orders.create_invoices( +... sale_order=1234, # ID or object +... ) +``` + +#### Parameters + +| Name | Type | Description | Default | +|--------------|--------------------|----------------------------------------|------------| +| `sale_order` | `int \| SaleOrder` | The sale order to create invoices from | (required) | + +## Record + +The sale order manager returns `SaleOrder` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import SaleOrder +``` + +The record class currently implements the following fields and methods. + +### `amount_untaxed` + +```python +amount_untaxed: float +``` + +The untaxed total cost of the sale order. + +### `amount_tax` + +```python +amount_tax: float +``` + +The amount in taxes on this sale order. + +### `amount_total` + +```python +amount_total: float +``` + +The taxed total cost of the sale order. + +### `client_order_ref` + +```python +client_order_ref: str | Literal[False] +``` + +The customer reference for this sale order, if defined. + +### `currency_id` + +```python +currency_id: int +``` + +The ID for the [currency](currency.md) used in this sale order. + +### `currency_name` + +```python +currency_name: str +``` + +The name of the [currency](currency.md) used in this sale order. + +### `currency` + +```python +currency: Currency +``` + +The [currency](currency.md) used in this sale order. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `date_order` + +```python +date_order: datetime +``` + +The time the sale order was created. + +### `display_name` + +```python +display_name: str +``` + +The display name of the sale order. + +### `invoice_status` + +```python +invoice_status: Literal["no", "to invoice", "invoiced", "upselling"] +``` + +The current invoicing status of this sale order. + +Values: + +* ``no`` - Nothing to invoice +* ``to invoice`` - Has line items that need to be invoiced +* ``invoiced`` - Fully invoiced +* ``upselling`` - Upselling opportunity + +### `name` + +```python +name: str +``` + +The name assigned to the sale order. + +### `note` + +```python +note: str +``` + +A note attached to the sale order. + +Generally used for terms and conditions. + +### `order_line_ids` + +```python +order_line_ids: list[int] +``` + +A list of IDs for the [lines](sale-order-line.md) added to the sale order. + +### `order_line` + +```python +order_line: list[SaleOrderLine] +``` + +The [lines](sale-order-line.md) added to the sale order. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `order_lines` + +```python +order_lines: list[SaleOrderLine] +``` + +An alias for [``order_line``](#order_line). + +### `os_invoice_date` + +```python +os_invoice_date: date +``` + +The invoicing date for the invoice that is created +from the sale order. + +### `os_invoice_due_date` + +```python +os_invoice_due_date: date +``` + +The due date for the invoice that is created +from the sale order. + +### `os_project_id` + +```python +os_project_id: int | None +``` + +The ID for the the [OpenStack project](project.md) this sale order was +was generated for. + +### `os_project_name` + +```python +os_project_name: str | None +``` + +The name of the the [OpenStack project](project.md) this sale order was +was generated for. + +### `os_project` + +```python +os_project: Project | None +``` + +The [OpenStack project](project.md) this sale order was +was generated for. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the recipient [partner](partner.md) for the sale order. + +### `partner_name` + +```python +partner_name: str +``` + +The name of the recipient partner for the sale order. + +### `partner` + +```python +partner: Partner +``` + +The recipient [partner](partner.md) for the sale order. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `state` + +```python +state: Literal["draft", "sale", "done", "cancel"] +``` + +State of the sale order. + +Values: + +* ``draft`` - Draft sale order (quotation), can still be modified +* ``sale`` - Finalised sale order, cannot be modified +* ``done`` - Finalised and settled sale order, cannot be modified +* ``cancel`` - Cancelled sale order, can be deleted in most cases + +### `action_confirm` + +```python +action_confirm() -> None +``` + +Confirm this sale order. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> sale_order = odoo_client.sale_orders.get(1234) +>>> sale_order.action_confirm() +``` + +### `create_invoices` + +```python +create_invoices() -> None +``` + +Create invoices from this sale order. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> sale_order = odoo_client.sale_orders.get(1234) +>>> sale_order.create_invoices() +``` diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index 1070e7e..cf1b93e 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -27,7 +27,6 @@ from .managers.credit import Credit from .managers.credit_transaction import CreditTransaction from .managers.credit_type import CreditType -from .managers.crm_team import CrmTeam from .managers.currency import Currency from .managers.customer_group import CustomerGroup from .managers.grant import Grant @@ -67,7 +66,6 @@ "Credit", "CreditTransaction", "CreditType", - "CrmTeam", "Currency", "CustomerGroup", "Grant", diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 47c86c3..c61f07c 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -31,7 +31,6 @@ credit, credit_transaction, credit_type, - crm_team, currency, customer_group, grant, @@ -102,6 +101,9 @@ class Client: :type version: Optional[str], optional """ + # TODO(callumdickinson): Use type hints to define managers, + # to allow for easy expansion of the Odoo client class. + @overload def __init__( self, @@ -205,8 +207,6 @@ def __init__( """Credit Transaction manager.""" self.credit_types = credit_type.CreditTypeManager(self) """Credit Type manager.""" - self.crm_teams = crm_team.CrmTeamManager(self) - """Customer Relations Management (CRM) Team manager.""" self.currencies = currency.CurrencyManager(self) """Currency manager.""" self.customer_groups = customer_group.CustomerGroupManager(self) diff --git a/openstack_odooclient/managers/crm_team.py b/openstack_odooclient/managers/crm_team.py deleted file mode 100644 index 920adaa..0000000 --- a/openstack_odooclient/managers/crm_team.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (C) 2024 Catalyst Cloud Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from . import record - - -class CrmTeam(record.RecordBase): - name: str - """Name of the CRM team.""" - - -class CrmTeamManager(record.NamedRecordManagerBase[CrmTeam]): - env_name = "crm.team" - record_class = CrmTeam diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index 45a5b03..c41d09a 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -32,9 +32,7 @@ class ProjectContact(record.RecordBase): "legal", "reseller customer", ] - """The contact type to assign the Partner as - on the OpenStack Project. - """ + """The contact type to assign the partner as on the project.""" inherit: bool """Whether or not this contact should be inherited by child projects.""" diff --git a/openstack_odooclient/managers/record/manager_base.py b/openstack_odooclient/managers/record/manager_base.py index ca27551..5ea1860 100644 --- a/openstack_odooclient/managers/record/manager_base.py +++ b/openstack_odooclient/managers/record/manager_base.py @@ -383,6 +383,9 @@ def create(self, **fields) -> int: """ return self._env.create( { + # TODO(callumdickinson): Handle nested model object + # encoding properly using type hints, + # e.g. sale order lines defined in sale orders. self._encode_field(field): self._encode_value(value) for field, value in fields.items() }, diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 589df99..41b1b98 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -52,14 +52,14 @@ def discount_product(self) -> product.Product: @property def free_monthly_credit_product_id(self) -> int: - """The ID of the product to use when adding the free monthly credit + """The ID of the product to use when adding the free monthly credit to demo project invoices. """ return self._get_ref_id("free_monthly_credit_product") @property def free_monthly_credit_product_name(self) -> str: - """The name of the product to use when adding the free monthly credit + """The name of the product to use when adding the free monthly credit to demo project invoices. """ return self._get_ref_name("free_monthly_credit_product") @@ -79,9 +79,6 @@ def free_monthly_credit_product(self) -> product.Product: under this tier. """ - hide_support: bool - """Whether or not the support URL should be hidden.""" - name: str """Reseller tier name.""" diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 3bf1915..7c52d10 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -15,9 +15,9 @@ from __future__ import annotations -from datetime import datetime +from datetime import date, datetime from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Union +from typing import TYPE_CHECKING, List, Literal, Optional, Union from . import record @@ -25,6 +25,7 @@ from . import ( currency as currency_module, partner as partner_module, + project, sale_order_line, ) @@ -106,6 +107,45 @@ def order_lines(self) -> List[sale_order_line.SaleOrderLine]: """An alias for ``order_line``.""" return self.order_line + os_invoice_date: date + """The invoicing date for the invoice that is created + from the sale order. + """ + + os_invoice_due_date: date + """The due date for the invoice that is created + from the sale order. + """ + + @property + def os_project_id(self) -> Optional[int]: + """The ID for the the OpenStack project this sale order was + was generated for. + """ + return self._get_ref_id("os_project", optional=True) + + @property + def os_project_name(self) -> Optional[str]: + """The name of the the OpenStack project this sale order was + was generated for. + """ + return self._get_ref_name("os_project", optional=True) + + @cached_property + def os_project(self) -> Optional[project.Project]: + """The OpenStack project this sale order was + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + record_id = self.os_project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + @property def partner_id(self) -> int: """The ID for the recipient partner for the sale order.""" @@ -133,7 +173,7 @@ def partner(self) -> partner_module.Partner: * ``draft`` - Draft sale order (quotation), can still be modified * ``sale`` - Finalised sale order, cannot be modified * ``done`` - Finalised and settled sale order, cannot be modified - * ``cancel`` - Cancelled sale order, can be deleted + * ``cancel`` - Cancelled sale order, can be deleted in most cases """ _alias_mapping = { @@ -141,6 +181,7 @@ def partner(self) -> partner_module.Partner: "currency": "currency_id", "order_line_ids": "order_line", "order_lines": "order_line", + "os_project_id": "os_project", "partner": "partner_id", } @@ -160,7 +201,7 @@ class SaleOrderManager(record.NamedRecordManagerBase[SaleOrder]): def action_confirm(self, sale_order: Union[int, SaleOrder]) -> None: """Confirm the given sale order. - :param sale_order: Sale order to confirm + :param sale_order: The sale order to confirm :type sale_order: Union[int, SaleOrder] """ self._env.action_confirm( @@ -172,9 +213,9 @@ def action_confirm(self, sale_order: Union[int, SaleOrder]) -> None: ) def create_invoices(self, sale_order: Union[int, SaleOrder]) -> None: - """Create invoices from this sale order. + """Create invoices from the given sale order. - :param sale_order: Sale order to create invoices for + :param sale_order: The sale order to create invoices from :type sale_order: Union[int, SaleOrder] """ self._env.create_invoices( diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index 7681026..66db698 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -16,7 +16,7 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, List, Literal +from typing import TYPE_CHECKING, List, Literal, Optional, Union from . import record @@ -79,7 +79,7 @@ def currency(self) -> currency_module.Currency: return self._client.currencies.get(self.currency_id) discount: float - """Discount on the sale order line, in percent.""" + """Discount percentage on the sale order line (0-100).""" display_name: str """Display name for the sale order line in the sale order.""" @@ -165,38 +165,43 @@ def order_partner(self) -> partner.Partner: return self._client.partners.get(self.order_partner_id) @property - def os_project_id(self) -> int: - """The ID for the he OpenStack project this sale order line was + def os_project_id(self) -> Optional[int]: + """The ID for the the OpenStack project this sale order line was was generated for. """ - return self._get_ref_id("os_project") + return self._get_ref_id("os_project", optional=True) @property - def os_project_name(self) -> str: - """The name of the he OpenStack project this sale order line was + def os_project_name(self) -> Optional[str]: + """The name of the the OpenStack project this sale order line was was generated for. """ - return self._get_ref_name("os_project") + return self._get_ref_name("os_project", optional=True) @cached_property - def os_project(self) -> project.Project: + def os_project(self) -> Optional[project.Project]: """The OpenStack project this sale order line was was generated for. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - return self._client.projects.get(self.os_project_id) - - os_region: str + record_id = self.os_project_id + return ( + self._client.projects.get(record_id) + if record_id is not None + else None + ) + + os_region: Union[str, Literal[False]] """The OpenStack region the sale order line was created from.""" - os_resource_id: str + os_resource_id: Union[str, Literal[False]] """The OpenStack resource ID for the resource that generated this sale order line. """ - os_resource_name: str + os_resource_name: Union[str, Literal[False]] """The name of the OpenStack resource tier or flavour, as used by services such as Distil for rating purposes. @@ -204,7 +209,7 @@ def os_project(self) -> project.Project: this would be set to the instance's flavour name. """ - os_resource_type: str + os_resource_type: Union[str, Literal[False]] """A human-readable description of the type of resource captured by this sale order line. """ @@ -212,7 +217,7 @@ def os_project(self) -> project.Project: price_reduce: float """Base unit price, less discount (see the ``discount`` field).""" - price_reduce_taxecl: float + price_reduce_taxexcl: float """Actual unit price, excluding tax.""" price_reduce_taxinc: float @@ -232,7 +237,7 @@ def os_project(self) -> project.Project: @property def product_id(self) -> int: - """The ID of the dproduct charged on this sale order line.""" + """The ID of the product charged on this sale order line.""" return self._get_ref_id("product_id") @property From 7e5731fddb49e94fca8292a5d9b74e928dc19907 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 13 Jun 2024 10:55:20 +1200 Subject: [PATCH 13/87] Add "OpenStack" to page titles --- docs/managers/credit-transaction.md | 2 +- docs/managers/credit-type.md | 2 +- docs/managers/credit.md | 2 +- docs/managers/customer-group.md | 2 +- docs/managers/grant-type.md | 2 +- docs/managers/grant.md | 2 +- docs/managers/project-contact.md | 2 +- docs/managers/project.md | 2 +- docs/managers/referral-code.md | 2 +- docs/managers/reseller-tier.md | 2 +- docs/managers/reseller.md | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/managers/credit-transaction.md b/docs/managers/credit-transaction.md index 17e6f98..a590050 100644 --- a/docs/managers/credit-transaction.md +++ b/docs/managers/credit-transaction.md @@ -1,4 +1,4 @@ -# Credit Transactions +# OpenStack Credit Transactions This page documents how to use the manager and record objects for credit transactions. diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md index acc1230..be68e6a 100644 --- a/docs/managers/credit-type.md +++ b/docs/managers/credit-type.md @@ -1,4 +1,4 @@ -# Credit Types +# OpenStack Credit Types This page documents how to use the manager and record objects for credit types. diff --git a/docs/managers/credit.md b/docs/managers/credit.md index 200a52d..6bcd512 100644 --- a/docs/managers/credit.md +++ b/docs/managers/credit.md @@ -1,4 +1,4 @@ -# Credits +# OpenStack Credits This page documents how to use the manager and record objects for credits. diff --git a/docs/managers/customer-group.md b/docs/managers/customer-group.md index 8004880..24bfe83 100644 --- a/docs/managers/customer-group.md +++ b/docs/managers/customer-group.md @@ -1,4 +1,4 @@ -# Customer Groups +# OpenStack Customer Groups This page documents how to use the manager and record objects for customer groups. diff --git a/docs/managers/grant-type.md b/docs/managers/grant-type.md index a640cf5..8c66625 100644 --- a/docs/managers/grant-type.md +++ b/docs/managers/grant-type.md @@ -1,4 +1,4 @@ -# Grant Types +# OpenStack Grant Types This page documents how to use the manager and record objects for grant types. diff --git a/docs/managers/grant.md b/docs/managers/grant.md index b23c7cf..f68a0fd 100644 --- a/docs/managers/grant.md +++ b/docs/managers/grant.md @@ -1,4 +1,4 @@ -# Grants +# OpenStack Grants This page documents how to use the manager and record objects for grants. diff --git a/docs/managers/project-contact.md b/docs/managers/project-contact.md index 78c624d..6df4dc2 100644 --- a/docs/managers/project-contact.md +++ b/docs/managers/project-contact.md @@ -1,4 +1,4 @@ -# Project Contacts +# OpenStack Project Contacts This page documents how to use the manager and record objects for project contacts. diff --git a/docs/managers/project.md b/docs/managers/project.md index f438bc3..b6308ce 100644 --- a/docs/managers/project.md +++ b/docs/managers/project.md @@ -1,4 +1,4 @@ -# Projects +# OpenStack Projects This page documents how to use the manager and record objects for projects. diff --git a/docs/managers/referral-code.md b/docs/managers/referral-code.md index c8990b3..435c56a 100644 --- a/docs/managers/referral-code.md +++ b/docs/managers/referral-code.md @@ -1,4 +1,4 @@ -# Referral Codes +# OpenStack Referral Codes This page documents how to use the manager and record objects for referral codes. diff --git a/docs/managers/reseller-tier.md b/docs/managers/reseller-tier.md index de20572..a984e67 100644 --- a/docs/managers/reseller-tier.md +++ b/docs/managers/reseller-tier.md @@ -1,4 +1,4 @@ -# Reseller Tiers +# OpenStack Reseller Tiers This page documents how to use the manager and record objects for reseller tiers. diff --git a/docs/managers/reseller.md b/docs/managers/reseller.md index 06c687f..e1734fb 100644 --- a/docs/managers/reseller.md +++ b/docs/managers/reseller.md @@ -1,4 +1,4 @@ -# Resellers +# OpenStack Resellers This page documents how to use the manager and record objects for resellers. From 25aa02b5aa8c1747466dc81795564d7535a71bde Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 13 Jun 2024 17:31:22 +1200 Subject: [PATCH 14/87] Start converting to the new style of defining aliases and model refs --- docs/managers/account-move-line.md | 27 + docs/managers/partner.md | 27 + openstack_odooclient/__init__.py | 15 + openstack_odooclient/client.py | 12 +- openstack_odooclient/managers/account_move.py | 127 ++--- .../managers/account_move_line.py | 188 +++---- openstack_odooclient/managers/company.py | 109 ++-- openstack_odooclient/managers/credit.py | 6 +- .../managers/credit_transaction.py | 8 +- openstack_odooclient/managers/credit_type.py | 8 +- openstack_odooclient/managers/currency.py | 8 +- .../managers/customer_group.py | 8 +- openstack_odooclient/managers/grant.py | 6 +- openstack_odooclient/managers/grant_type.py | 8 +- openstack_odooclient/managers/partner.py | 530 ++++++++---------- .../managers/partner_category.py | 8 +- openstack_odooclient/managers/pricelist.py | 8 +- openstack_odooclient/managers/product.py | 11 +- .../managers/product_category.py | 8 +- openstack_odooclient/managers/project.py | 11 +- .../managers/project_contact.py | 8 +- .../managers/record/__init__.py | 30 - openstack_odooclient/managers/record/util.py | 132 ----- .../{record/base.py => record_base.py} | 216 ++++--- ...manager_base.py => record_manager_base.py} | 7 +- ...de_base.py => record_manager_code_base.py} | 5 +- ...me_base.py => record_manager_name_base.py} | 5 +- ...py => record_manager_unique_field_base.py} | 7 +- .../managers/referral_code.py | 8 +- openstack_odooclient/managers/reseller.py | 6 +- .../managers/reseller_tier.py | 8 +- openstack_odooclient/managers/sale_order.py | 8 +- .../managers/sale_order_line.py | 8 +- .../managers/support_subscription.py | 6 +- .../managers/support_subscription_type.py | 6 +- openstack_odooclient/managers/tax.py | 6 +- openstack_odooclient/managers/tax_group.py | 8 +- .../managers/term_discount.py | 8 +- openstack_odooclient/managers/trial.py | 6 +- openstack_odooclient/managers/uom.py | 6 +- openstack_odooclient/managers/uom_category.py | 6 +- openstack_odooclient/managers/user.py | 75 +-- openstack_odooclient/managers/util.py | 271 +++++++++ .../managers/volume_discount_range.py | 10 +- openstack_odooclient/managers/voucher_code.py | 8 +- 45 files changed, 1111 insertions(+), 886 deletions(-) delete mode 100644 openstack_odooclient/managers/record/__init__.py delete mode 100644 openstack_odooclient/managers/record/util.py rename openstack_odooclient/managers/{record/base.py => record_base.py} (54%) rename openstack_odooclient/managers/{record/manager_base.py => record_manager_base.py} (98%) rename openstack_odooclient/managers/{record/manager_code_base.py => record_manager_code_base.py} (98%) rename openstack_odooclient/managers/{record/manager_name_base.py => record_manager_name_base.py} (98%) rename openstack_odooclient/managers/{record/manager_unique_field_base.py => record_manager_unique_field_base.py} (97%) create mode 100644 openstack_odooclient/managers/util.py diff --git a/docs/managers/account-move-line.md b/docs/managers/account-move-line.md index 0019cc1..c44069f 100644 --- a/docs/managers/account-move-line.md +++ b/docs/managers/account-move-line.md @@ -80,6 +80,33 @@ line_tax_amount: float Amount charged in tax on the account move (invoice) line. +### `move_id` + +```python +move_id: int +``` + +The ID for the [account move (invoice)](account-move.md) this line is part of. + +### `move_name` + +```python +move_name: str +``` + +The name of the [account move (invoice)](account-move.md) this line is part of. + +### `move` + +```python +move: AccountMove +``` + +The [account move (invoice)](account-move.md) this line is part of. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + ### `name` ```python diff --git a/docs/managers/partner.md b/docs/managers/partner.md index f75b596..46ee961 100644 --- a/docs/managers/partner.md +++ b/docs/managers/partner.md @@ -53,6 +53,33 @@ active: bool Whether or not this partner is active (enabled). +### `company_id` + +```python +company_id: int +``` + +The ID for the [company](company.md) this partner is owned by. + +### `company_name` + +```python +company_name: str +``` + +The name of the [company](company.md) this partner is owned by. + +### `company` + +```python +company: Company +``` + +The [company](company.md) this partner is owned by. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + ### `email` ```python diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index cf1b93e..fadf35e 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -38,6 +38,13 @@ from .managers.product_category import ProductCategory from .managers.project import Project from .managers.project_contact import ProjectContact +from .managers.record_base import RecordBase +from .managers.record_manager_base import RecordManagerBase +from .managers.record_manager_code_base import CodedRecordManagerBase +from .managers.record_manager_name_base import NamedRecordManagerBase +from .managers.record_manager_unique_field_base import ( + RecordManagerWithUniqueFieldBase, +) from .managers.referral_code import ReferralCode from .managers.reseller import Reseller from .managers.reseller_tier import ResellerTier @@ -52,6 +59,7 @@ from .managers.uom import Uom from .managers.uom_category import UomCategory from .managers.user import User +from .managers.util import FieldAlias, ModelRef from .managers.volume_discount_range import VolumeDiscountRange from .managers.voucher_code import VoucherCode @@ -77,6 +85,11 @@ "ProductCategory", "Project", "ProjectContact", + "RecordBase", + "RecordManagerBase", + "CodedRecordManagerBase", + "NamedRecordManagerBase", + "RecordManagerWithUniqueFieldBase", "ReferralCode", "Reseller", "ResellerTier", @@ -91,6 +104,8 @@ "Uom", "UomCategory", "User", + "FieldAlias", + "ModelRef", "VolumeDiscountRange", "VoucherCode", ] diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index c61f07c..4311c3e 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -61,12 +61,14 @@ ) if TYPE_CHECKING: - from typing import Literal, Optional, Union + from typing import Dict, Literal, Optional, Type, Union from odoorpc.db import DB # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] from odoorpc.report import Report # type: ignore[import] + from .managers import record_base, record_manager_base + class Client: """A client class for managing the OpenStack Odoo ERP. @@ -190,6 +192,14 @@ def __init__( opener=opener, ) self._odoo.login(database, username, password) + # Create aninternal mapping between record classes and their managers. + # This is populated by the manager classes themselves when created, + # and used when converting model references on record objects into + # # new record objects. + self._record_manager_mapping: Dict[ + Type[record_base.RecordBase], + record_manager_base.RecordManagerBase, + ] = {} # Create record managers. self.account_moves = account_move.AccountMoveManager(self) """Account Move (Invoice) manager.""" diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index 7d525fb..a367d7f 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -16,62 +16,63 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, Any, List, Literal, Mapping, Optional, Union +from typing import Any, List, Literal, Mapping, Optional, Union -from . import record +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - account_move_line, - currency as currency_module, - project, - ) +from . import ( + currency as currency_module, + project, + record_base, + record_manager_name_base, + util, +) -class AccountMove(record.RecordBase): +class AccountMove(record_base.RecordBase): amount_total: float """Total (taxed) amount charged on the account move (invoice).""" amount_untaxed: float """Total (untaxed) amount charged on the account move (invoice).""" - @property - def currency_id(self) -> int: - """The ID for the currency used in this account move (invoice).""" - return self._get_ref_id("currency_id") + currency_id: Annotated[int, util.ModelRef("currency_id")] + """The ID for the currency used in this account move (invoice).""" - @property - def currency_name(self) -> str: - """The name of the currency used in this account move (invoice).""" - return self._get_ref_name("currency_id") + currency_name: Annotated[str, util.ModelRef("currency_id")] + """The name of the currency used in this account move (invoice).""" - @cached_property - def currency(self) -> currency_module.Currency: - """The currency used in this account move (invoice). + currency: Annotated[ + currency_module.Currency, + util.ModelRef("currency_id"), + ] + """The currency used in this account move (invoice). - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.currencies.get(self.currency_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ invoice_date: date """Date associated with the account move (invoice).""" - invoice_line_ids: List[int] + invoice_line_ids: Annotated[ + List[int], + record_base.ModelRef("invoice_line_ids"), + ] """The list of the IDs for the account move (invoice) lines that comprise this account move (invoice). """ - @cached_property - def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: - """A list of account move (invoice) lines - that comprise this account move (invoice). + invoice_lines: Annotated[ + List[account_move_line.AccountMoveLine], + record_base.ModelRef("invoice_line_ids"), + ] + """A list of account move (invoice) lines + that comprise this account move (invoice). - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.account_move_lines.list(self.invoice_line_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ is_move_sent: bool """Whether or not the account move (invoice) has been sent.""" @@ -101,34 +102,26 @@ def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: name: Union[str, Literal[False]] """Name assigned to the account move (invoice), if posted.""" - @property - def os_project_id(self) -> Optional[int]: - """The ID of the OpenStack project this account move (invoice) - was generated for, if this is an invoice for OpenStack project usage. - """ - return self._get_ref_id("os_project", optional=True) + os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + """The ID of the OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. + """ - @property - def os_project_name(self) -> Optional[str]: - """The name of the OpenStack project this account move (invoice) - was generated for, if this is an invoice for OpenStack project usage. - """ - return self._get_ref_name("os_project", optional=True) + os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + """The name of the OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. + """ - @cached_property - def os_project(self) -> Optional[project.Project]: - """The OpenStack project this account move (invoice) - was generated for, if this is an invoice for OpenStack project usage. + os_project: Annotated[ + Optional[project.Project], + util.ModelRef("os_project"), + ] + """The OpenStack project this account move (invoice) + was generated for, if this is an invoice for OpenStack project usage. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ payment_state: Literal[ "not_paid", @@ -171,14 +164,6 @@ def os_project(self) -> Optional[project.Project]: }, } - _alias_mapping = { - # Key is local alias, value is remote field name. - "attention": "attention_id", - "currency": "currency_id", - "invoice_lines": "invoice_line_ids", - "os_project_id": "os_project", - } - def action_post(self) -> None: """Change a draft account move (invoice) into "posted" state.""" self._env.action_post(self.id) @@ -198,6 +183,12 @@ def send_openstack_invoice_email( ) -class AccountMoveManager(record.NamedRecordManagerBase[AccountMove]): +class AccountMoveManager( + record_manager_name_base.NamedRecordManagerBase[AccountMove], +): env_name = "account.move" record_class = AccountMove + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import account_move_line # noqa: E402 diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index 07ee28a..e467096 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -15,78 +15,81 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Literal, Optional, Union - -from . import record - -if TYPE_CHECKING: - from . import ( - currency as currency_module, - product as product_module, - project, - ) - - -class AccountMoveLine(record.RecordBase): - @property - def currency_id(self) -> int: - """The ID for the currency used in this - account move (invoice) line. - """ - return self._get_ref_id("currency_id") - - @property - def currency_name(self) -> str: - """The name of the currency used in this - account move (invoice) line. - """ - return self._get_ref_name("currency_id") - - @cached_property - def currency(self) -> currency_module.Currency: - """The currency used in this - account move (invoice) line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.currencies.get(self.currency_id) +from typing import Literal, Optional, Union + +from typing_extensions import Annotated + +from . import ( + currency as currency_module, + product as product_module, + project, + record_base, + record_manager_base, + util, +) + + +class AccountMoveLine(record_base.RecordBase): + currency_id: Annotated[int, util.ModelRef("currency_id")] + """The ID for the currency used in this + account move (invoice) line. + """ + + currency_name: Annotated[str, util.ModelRef("currency_id")] + """The name of the currency used in this + account move (invoice) line. + """ + + currency: Annotated[ + currency_module.Currency, + util.ModelRef("currency_id"), + ] + """The currency used in this + account move (invoice) line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ line_tax_amount: float """Amount charged in tax on the account move (invoice) line.""" + move_id: Annotated[int, util.ModelRef("move_id")] + """The ID for the account move (invoice) this line is part of.""" + + move_name: Annotated[str, util.ModelRef("move_id")] + """The name of the account move (invoice) this line is part of.""" + + move: Annotated[account_move.AccountMove, util.ModelRef("move_id")] + """The account move (invoice) this line is part of. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + name: str """Name of the product charged on the account move (invoice) line.""" - @property - def os_project_id(self) -> Optional[int]: - """The ID for the OpenStack project this account move (invoice) line - was generated for. - """ - return self._get_ref_id("os_project", optional=True) - - @property - def os_project_name(self) -> Optional[str]: - """The name of the OpenStack project this account move (invoice) line - was generated for. - """ - return self._get_ref_name("os_project", optional=True) - - @cached_property - def os_project(self) -> Optional[project.Project]: - """The OpenStack project this account move (invoice) line - was generated for. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + """The ID for the OpenStack project this account move (invoice) line + was generated for. + """ + + os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + """The name of the OpenStack project this account move (invoice) line + was generated for. + """ + + os_project: Annotated[ + Optional[project.Project], + util.ModelRef("os_project"), + ] + """The OpenStack project this account move (invoice) line + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ os_region: Union[str, Literal[False]] """The OpenStack region the account move (invoice) line @@ -119,41 +122,34 @@ def os_project(self) -> Optional[project.Project]: price_unit: float """Unit price for the product used on the account move (invoice) line.""" - @property - def product_id(self) -> int: - """The ID for the product charged on the - account move (invoice) line. - """ - return self._get_ref_id("product_id") - - @property - def product_name(self) -> str: - """The name of the product charged on the - account move (invoice) line. - """ - return self._get_ref_name("product_id") - - @cached_property - def product(self) -> product_module.Product: - """The product charged on the - account move (invoice) line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.product_id) + product_id: Annotated[int, util.ModelRef("product_id")] + """The ID for the product charged on the + account move (invoice) line. + """ + + product_name: Annotated[str, util.ModelRef("product_id")] + """The name of the product charged on the + account move (invoice) line. + """ + + product: Annotated[product_module.Product, util.ModelRef("product_id")] + """The product charged on the + account move (invoice) line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ quantity: float """Quantity of product charged on the account move (invoice) line.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "currency": "currency_id", - "os_project_id": "os_project", - "product": "product_id", - } - -class AccountMoveLineManager(record.RecordManagerBase[AccountMoveLine]): +class AccountMoveLineManager( + record_manager_base.RecordManagerBase[AccountMoveLine], +): env_name = "account.move.line" record_class = AccountMoveLine + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import account_move # noqa: E402 diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py index 36a0aa8..8057a98 100644 --- a/openstack_odooclient/managers/company.py +++ b/openstack_odooclient/managers/company.py @@ -15,94 +15,71 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner as partner_module +from . import record_base, record_manager_name_base, util -class Company(record.RecordBase): +class Company(record_base.RecordBase): active: bool """Whether or not this company is active (enabled).""" - child_ids: List[int] + child_ids: Annotated[List[int], util.ModelRef("child_ids")] """A list of IDs for the child companies.""" - @cached_property - def children(self) -> List[Company]: - """The list of child companies. + children: Annotated[List[Company], util.ModelRef("child_ids")] + """The list of child companies. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.companies.list(self.child_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ name: str """Company name, set from the partner name.""" - @property - def parent_id(self) -> Optional[int]: - """The ID for the parent company, if this company - is the child of another company. - """ - return self._get_ref_id("parent_id", optional=True) - - @property - def parent_name(self) -> Optional[str]: - """The name of the parent company, if this company - is the child of another company. - """ - return self._get_ref_name("parent_id", optional=True) - - @cached_property - def parent(self) -> Optional[Company]: - """The parent company, if this company - is the child of another company. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.parent_id - return ( - self._client.companies.get(record_id) - if record_id is not None - else None - ) + parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + """The ID for the parent company, if this company + is the child of another company. + """ + + parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + """The name of the parent company, if this company + is the child of another company. + """ + + parent: Annotated[Optional[Company], util.ModelRef("parent_id")] + """The parent company, if this company + is the child of another company. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ parent_path: Union[str, Literal[False]] """The path of the parent company, if there is a parent.""" - @property - def partner_id(self) -> int: - """The ID for the partner for the company.""" - return self._get_ref_id("partner_id") + partner_id: Annotated[int, util.ModelRef("partner_id")] + """The ID for the partner for the company.""" - @property - def partner_name(self) -> str: - """The name of the partner for the company.""" - return self._get_ref_name("partner_id") + partner_name: Annotated[str, util.ModelRef("partner_id")] + """The name of the partner for the company.""" - @cached_property - def partner(self) -> partner_module.Partner: - """The partner for the company. + partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + """The partner for the company. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - _alias_mapping = { - # Key is local alias, value is remote field name. - "children": "child_ids", - "company": "company_id", - "parent": "parent_id", - "partner": "partner_id", - } - -class CompanyManager(record.NamedRecordManagerBase[Company]): +class CompanyManager( + record_manager_name_base.NamedRecordManagerBase[Company], +): env_name = "res.company" record_class = Company + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import partner as partner_module # noqa: E402 diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index e3c662d..2c86a41 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -19,7 +19,7 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import ( @@ -29,7 +29,7 @@ ) -class Credit(record.RecordBase): +class Credit(record_base.RecordBase): @property def credit_type_id(self) -> int: """The ID of the type of this credit.""" @@ -117,6 +117,6 @@ def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: } -class CreditManager(record.RecordManagerBase[Credit]): +class CreditManager(record_manager_base.RecordManagerBase[Credit]): env_name = "openstack.credit" record_class = Credit diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index 3ef0928..ed995d6 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import credit as credit_module -class CreditTransaction(record.RecordBase): +class CreditTransaction(record_base.RecordBase): @property def credit_id(self) -> int: """The ID of the credit this transaction was made against.""" @@ -56,6 +56,8 @@ def credit(self) -> credit_module.Credit: } -class CreditTransactionManager(record.RecordManagerBase[CreditTransaction]): +class CreditTransactionManager( + record_manager_base.RecordManagerBase[CreditTransaction], +): env_name = "openstack.credit.transaction" record_class = CreditTransaction diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index ebf1471..e23e074 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, List -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import credit, product as product_module, product_category -class CreditType(record.RecordBase): +class CreditType(record_base.RecordBase): @property def credit_ids(self) -> List[int]: """A list of IDs for the credits which are of this credit type.""" @@ -123,6 +123,8 @@ def product(self) -> product_module.Product: } -class CreditTypeManager(record.NamedRecordManagerBase[CreditType]): +class CreditTypeManager( + record_manager_name_base.NamedRecordManagerBase[CreditType], +): env_name = "openstack.credit.type" record_class = CreditType diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py index 77797ec..24c09f2 100644 --- a/openstack_odooclient/managers/currency.py +++ b/openstack_odooclient/managers/currency.py @@ -18,10 +18,10 @@ from datetime import date as datetime_date from typing import Literal, Union -from . import record +from . import record_base, record_manager_name_base -class Currency(record.RecordBase): +class Currency(record_base.RecordBase): active: bool """Whether or not this currency is active (enabled).""" @@ -63,6 +63,8 @@ class Currency(record.RecordBase): """The currency sign to be used when printing amounts.""" -class CurrencyManager(record.NamedRecordManagerBase[Currency]): +class CurrencyManager( + record_manager_name_base.NamedRecordManagerBase[Currency], +): env_name = "res.currency" record_class = Currency diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index eed12f2..e29a7e2 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Optional -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import partner, pricelist as pricelist_module -class CustomerGroup(record.RecordBase): +class CustomerGroup(record_base.RecordBase): name: str """The name of the customer group.""" @@ -79,6 +79,8 @@ def pricelist(self) -> Optional[pricelist_module.Pricelist]: } -class CustomerGroupManager(record.NamedRecordManagerBase[CustomerGroup]): +class CustomerGroupManager( + record_manager_name_base.NamedRecordManagerBase[CustomerGroup], +): env_name = "openstack.customer_group" record_class = CustomerGroup diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index ba355b4..b0c53f9 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -19,7 +19,7 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import ( @@ -28,7 +28,7 @@ ) -class Grant(record.RecordBase): +class Grant(record_base.RecordBase): expiry_date: date """The date the grant expires.""" @@ -96,6 +96,6 @@ def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: } -class GrantManager(record.RecordManagerBase[Grant]): +class GrantManager(record_manager_base.RecordManagerBase[Grant]): env_name = "openstack.grant" record_class = Grant diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 9a27e19..7f4a3f2 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, List -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import grant, product as product_module, product_category -class GrantType(record.RecordBase): +class GrantType(record_base.RecordBase): @property def grant_ids(self) -> List[int]: """A list of IDs for the grants which are of this grant type.""" @@ -125,6 +125,8 @@ def product(self) -> product_module.Product: } -class GrantTypeManager(record.NamedRecordManagerBase[GrantType]): +class GrantTypeManager( + record_manager_name_base.NamedRecordManagerBase[GrantType], +): env_name = "openstack.grant.type" record_class = GrantType diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index d8c3499..9d06f5a 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -15,309 +15,265 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - customer_group, - pricelist, - project, - project_contact, - referral_code, - reseller, - trial, - user as user_module, - ) +from . import ( + pricelist, + project, + record_base, + record_manager_base, + util, +) -class Partner(record.RecordBase): +class Partner(record_base.RecordBase): active: bool """Whether or not this partner is active (enabled).""" + company_id: Annotated[int, util.ModelRef("company_id")] + """The ID for the company this partner is owned by.""" + + company_name: Annotated[str, util.ModelRef("company_id")] + """The name of the company this partner is owned by.""" + + company: Annotated[company_module.Company, util.ModelRef("company_id")] + """The company this partner is owned by. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + email: str """Main e-mail address for the partner.""" name: str """Full name of the partner.""" - @property - def os_customer_group_id(self) -> Optional[int]: - """The ID for the customer group this partner is part of, - if it is part of one. - """ - return self._get_ref_id("os_customer_group", optional=True) - - @property - def os_customer_group_name(self) -> Optional[str]: - """The name of the customer group this partner is part of, - if it is part of one. - """ - return self._get_ref_name("os_customer_group", optional=True) - - @cached_property - def os_customer_group(self) -> Optional[customer_group.CustomerGroup]: - """The customer group this partner is part of, - if it is part of one. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_customer_group_id - return ( - self._client.customer_groups.get(record_id) - if record_id is not None - else None - ) - - @property - def os_project_ids(self) -> List[int]: - """A list of IDs for the OpenStack projects that - belong to this partner. - """ - return self._get_field("os_projects") - - @cached_property - def os_projects(self) -> List[project.Project]: - """The OpenStack projects that belong to this partner. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.projects.list(self.os_project_ids) - - @property - def os_project_contact_ids(self) -> List[int]: - """A list of IDs for the project contacts that are associated - with this partner. - """ - return self._get_field("os_project_contacts") - - @cached_property - def os_project_contacts(self) -> List[project_contact.ProjectContact]: - """The project contacts that are associated with this partner. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.project_contacts.list(self.os_project_contact_ids) - - @property - def os_referral_id(self) -> Optional[int]: - """The ID for the referral code the partner used on sign-up, - if one was used. - """ - return self._get_ref_id("os_referral", optional=True) - - @property - def os_referral_name(self) -> Optional[str]: - """The name of the referral code the partner used on sign-up, - if one was used. - """ - return self._get_ref_name("os_referral", optional=True) - - @cached_property - def os_referral(self) -> Optional[referral_code.ReferralCode]: - """The referral code the partner used on sign-up, if one was used. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_referral_id - return ( - self._client.referral_codes.get(record_id) - if record_id is not None - else None - ) - - @property - def os_referral_code_ids(self) -> List[int]: - """A list of IDs for the referral codes the partner has used.""" - return self._get_field("os_referral_codes") - - @cached_property - def os_referral_codes(self) -> List[referral_code.ReferralCode]: - """The referral codes the partner has used. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.referral_codes.list(self.os_referral_code_ids) - - @property - def os_reseller_id(self) -> Optional[int]: - """The ID for the reseller for this partner, if this partner - is billed through a reseller. - """ - return self._get_ref_id("os_reseller", optional=True) - - @property - def os_reseller_name(self) -> Optional[str]: - """The name of the reseller for this partner, if this partner - is billed through a reseller. - """ - return self._get_ref_name("os_reseller", optional=True) - - @cached_property - def os_reseller(self) -> Optional[reseller.Reseller]: - """The reseller for this partner, if this partner - is billed through a reseller. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_reseller_id - return ( - self._client.resellers.get(record_id) - if record_id is not None - else None - ) - - @property - def os_trial_id(self) -> Optional[int]: - """The ID for the sign-up trial for this partner, - if signed up under a trial. - """ - return self._get_ref_id("os_trial", optional=True) - - @property - def os_trial_name(self) -> Optional[str]: - """The name of the sign-up trial for this partner, - if signed up under a trial. - """ - return self._get_ref_name("os_trial", optional=True) - - @cached_property - def os_trial(self) -> Optional[trial.Trial]: - """The sign-up trial for this partner, - if signed up under a trial. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_trial_id - return ( - self._client.trials.get(record_id) - if record_id is not None - else None - ) - - @property - def parent_id(self) -> Optional[int]: - """The ID for the parent partner of this partner, - if it has a parent. - """ - return self._get_ref_id("parent_id", optional=True) - - @property - def parent_name(self) -> Optional[str]: - """The name of the parent partner of this partner, - if it has a parent. - """ - return self._get_ref_name("parent_id", optional=True) - - @cached_property - def parent(self) -> Optional[Partner]: - """The parent partner of this partner, - if it has a parent. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.parent_id - return ( - self._client.partners.get(record_id) - if record_id is not None - else None - ) - - @property - def property_product_pricelist_id(self) -> Optional[int]: - """The ID for the pricelist this partner uses, if explicitly set. - - If not set, the pricelist set for the customer group - is used (and if that is not set, the global default - pricelist is used). - """ - return self._get_ref_id("property_product_pricelist", optional=True) - - @property - def property_product_pricelist_name(self) -> Optional[str]: - """The name of the pricelist this partner uses, if explicitly set. - - If not set, the pricelist set for the customer group - is used (and if that is not set, the global default - pricelist is used). - """ - return self._get_ref_name("property_product_pricelist", optional=True) - - @cached_property - def property_product_pricelist(self) -> Optional[pricelist.Pricelist]: - """The pricelist this partner uses, if explicitly set. - - If not set, the pricelist set for the customer group - is used (and if that is not set, the global default - pricelist is used). - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.property_product_pricelist_id - return ( - self._client.pricelists.get(record_id) - if record_id is not None - else None - ) + os_customer_group_id: Annotated[ + Optional[int], + util.ModelRef("os_customer_group"), + ] + """The ID for the customer group this partner is part of, + if it is part of one. + """ + + os_customer_group_name: Annotated[ + Optional[str], + util.ModelRef("os_customer_group"), + ] + """The name of the customer group this partner is part of, + if it is part of one. + """ + + os_customer_group: Annotated[ + Optional[customer_group.CustomerGroup], + util.ModelRef("os_customer_group"), + ] + """The customer group this partner is part of, + if it is part of one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + os_project_ids: Annotated[List[int], util.ModelRef("os_projects")] + """A list of IDs for the OpenStack projects that + belong to this partner. + """ + + os_projects: Annotated[ + List[project.Project], + util.ModelRef("os_projects"), + ] + """The OpenStack projects that belong to this partner. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + os_project_contact_ids: Annotated[ + List[int], + util.ModelRef("os_project_contacts"), + ] + """A list of IDs for the project contacts that are associated + with this partner. + """ + + os_project_contacts: Annotated[ + List[project_contact.ProjectContact], + util.ModelRef("os_project_contacts"), + ] + """The project contacts that are associated with this partner. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + os_referral_id: Annotated[Optional[int], util.ModelRef("os_referral")] + """The ID for the referral code the partner used on sign-up, + if one was used. + """ + + os_referral_name: Annotated[Optional[str], util.ModelRef("os_referral")] + """The name of the referral code the partner used on sign-up, + if one was used. + """ + + os_referral: Annotated[ + Optional[referral_code.ReferralCode], + util.ModelRef("os_referral"), + ] + """The referral code the partner used on sign-up, if one was used. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + os_referral_code_ids: Annotated[ + List[int], + util.ModelRef("os_referral_codes"), + ] + """A list of IDs for the referral codes the partner has used.""" + + os_referral_codes: Annotated[ + List[referral_code.ReferralCode], + util.ModelRef("os_referral_codes"), + ] + """The referral codes the partner has used. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + os_reseller_id: Annotated[Optional[int], util.ModelRef("os_reseller")] + """The ID for the reseller for this partner, if this partner + is billed through a reseller. + """ + + os_reseller_name: Annotated[Optional[str], util.ModelRef("os_reseller")] + """The name of the reseller for this partner, if this partner + is billed through a reseller. + """ + + os_reseller: Annotated[ + Optional[reseller.Reseller], + util.ModelRef("os_reseller"), + ] + """The reseller for this partner, if this partner + is billed through a reseller. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + os_trial_id: Annotated[Optional[int], util.ModelRef("os_trial")] + """The ID for the sign-up trial for this partner, + if signed up under a trial. + """ + + os_trial_name: Annotated[Optional[str], util.ModelRef("os_trial")] + """The name of the sign-up trial for this partner, + if signed up under a trial. + """ + + os_trial: Annotated[Optional[trial.Trial], util.ModelRef("os_trial")] + """The sign-up trial for this partner, + if signed up under a trial. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + """The ID for the parent partner of this partner, + if it has a parent. + """ + + parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + """The name of the parent partner of this partner, + if it has a parent. + """ + + parent: Annotated[Optional[Partner], util.ModelRef("parent_id")] + """The parent partner of this partner, + if it has a parent. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + property_product_pricelist_id: Annotated[ + Optional[int], + util.ModelRef("property_product_pricelist"), + ] + """The ID for the pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + """ + + property_product_pricelist_name: Annotated[ + Optional[str], + util.ModelRef("property_product_pricelist"), + ] + """The name of the pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + """ + + property_product_pricelist: Annotated[ + Optional[pricelist.Pricelist], + util.ModelRef("property_product_pricelist"), + ] + """The pricelist this partner uses, if explicitly set. + + If not set, the pricelist set for the customer group + is used (and if that is not set, the global default + pricelist is used). + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ stripe_customer_id: Union[str, Literal[False]] """The Stripe customer ID for this partner, if one has been assigned.""" - @property - def user_id(self) -> Optional[int]: - """The ID of the internal user associated with this partner, - if one is assigned. - """ - return self._get_ref_id("user_id", optional=True) - - @property - def user_name(self) -> Optional[str]: - """The name of the internal user associated with this partner, - if one is assigned. - """ - return self._get_ref_name("user_id") - - @cached_property - def user(self) -> Optional[user_module.User]: - """The internal user associated with this partner, - if one is assigned. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.user_id - return ( - self._client.users.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "os_customer_group_id": "os_customer_group", - "os_project_ids": "os_projects", - "os_project_contact_ids": "os_project_contacts", - "os_referral_id": "os_referral", - "os_referral_code_ids": "os_referral_codes", - "os_reseller_id": "os_reseller", - "os_trial_id": "os_trial", - "parent": "parent_id", - "property_product_pricelist_id": "property_product_pricelist", - "user": "user_id", - } - - -class PartnerManager(record.RecordManagerBase[Partner]): + user_id: Annotated[Optional[int], util.ModelRef("user_id")] + """The ID of the internal user associated with this partner, + if one is assigned. + """ + + user_name: Annotated[Optional[str], util.ModelRef("user_id")] + """The name of the internal user associated with this partner, + if one is assigned. + """ + + user: Annotated[Optional[user_module.User], util.ModelRef("user_id")] + """The internal user associated with this partner, + if one is assigned. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class PartnerManager(record_manager_base.RecordManagerBase[Partner]): env_name = "res.partner" record_class = Partner + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + company as company_module, + customer_group, + project_contact, + referral_code, + reseller, + trial, + user as user_module, +) diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index 37ca05c..6f6d549 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Literal, Optional, Union -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import partner -class PartnerCategory(record.RecordBase): +class PartnerCategory(record_base.RecordBase): active: bool """Whether or not the partner category is active (enabled).""" @@ -110,6 +110,8 @@ def partners(self) -> List[partner.Partner]: } -class PartnerCategoryManager(record.NamedRecordManagerBase[PartnerCategory]): +class PartnerCategoryManager( + record_manager_name_base.NamedRecordManagerBase[PartnerCategory], +): env_name = "res.partner.category" record_class = PartnerCategory diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 2fc50d5..0174dc1 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal, Optional, Union -from . import product as product_module, record +from . import product as product_module, record_base, record_manager_name_base if TYPE_CHECKING: from . import company as company_module, currency as currency_module -class Pricelist(record.RecordBase): +class Pricelist(record_base.RecordBase): active: bool """Whether or not the pricelist is active.""" @@ -110,7 +110,9 @@ def get_price( ) -class PricelistManager(record.NamedRecordManagerBase[Pricelist]): +class PricelistManager( + record_manager_name_base.NamedRecordManagerBase[Pricelist], +): env_name = "product.pricelist" record_class = Pricelist diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index 66217f4..67a8cd6 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -28,13 +28,13 @@ overload, ) -from . import record +from . import record_base, record_manager_unique_field_base if TYPE_CHECKING: from . import company, product_category, uom as uom_module -class Product(record.RecordBase): +class Product(record_base.RecordBase): @property def categ_id(self) -> int: """The ID for the category this product is under.""" @@ -127,7 +127,12 @@ def uom(self) -> uom_module.Uom: } -class ProductManager(record.RecordManagerWithUniqueFieldBase[Product, str]): +class ProductManager( + record_manager_unique_field_base.RecordManagerWithUniqueFieldBase[ + Product, + str, + ], +): env_name = "product.product" record_class = Product diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index 3757a09..f2d1b53 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -18,10 +18,10 @@ from functools import cached_property from typing import List, Literal, Optional, Union -from . import record +from . import record_base, record_manager_name_base -class ProductCategory(record.RecordBase): +class ProductCategory(record_base.RecordBase): @property def child_ids(self) -> List[int]: """A list of IDs for the child categories.""" @@ -85,6 +85,8 @@ def parent(self) -> Optional[ProductCategory]: } -class ProductCategoryManager(record.NamedRecordManagerBase[ProductCategory]): +class ProductCategoryManager( + record_manager_name_base.NamedRecordManagerBase[ProductCategory], +): env_name = "product.category" record_class = ProductCategory diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index f768dfd..da4d9d6 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -28,7 +28,7 @@ overload, ) -from . import record +from . import record_base, record_manager_unique_field_base if TYPE_CHECKING: from . import ( @@ -41,7 +41,7 @@ ) -class Project(record.RecordBase): +class Project(record_base.RecordBase): billing_type: Literal["customer", "internal"] """Billing type for this project. @@ -238,7 +238,12 @@ def term_discounts(self) -> List[term_discount.TermDiscount]: } -class ProjectManager(record.RecordManagerWithUniqueFieldBase[Project, str]): +class ProjectManager( + record_manager_unique_field_base.RecordManagerWithUniqueFieldBase[ + Project, + str, + ], +): env_name = "openstack.project" record_class = Project diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index c41d09a..813b675 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import partner as partner_module, project as project_module -class ProjectContact(record.RecordBase): +class ProjectContact(record_base.RecordBase): contact_type: Literal[ "primary", "billing", @@ -87,6 +87,8 @@ def project(self) -> Optional[project_module.Project]: } -class ProjectContactManager(record.RecordManagerBase[ProjectContact]): +class ProjectContactManager( + record_manager_base.RecordManagerBase[ProjectContact], +): env_name = "openstack.project_contact" record_class = ProjectContact diff --git a/openstack_odooclient/managers/record/__init__.py b/openstack_odooclient/managers/record/__init__.py deleted file mode 100644 index 8daa7df..0000000 --- a/openstack_odooclient/managers/record/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2024 Catalyst Cloud Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from .base import RecordBase -from .manager_base import RecordManagerBase -from .manager_code_base import CodedRecordManagerBase -from .manager_name_base import NamedRecordManagerBase -from .manager_unique_field_base import RecordManagerWithUniqueFieldBase - -__all__ = [ - "RecordBase", - "RecordManagerBase", - "CodedRecordManagerBase", - "NamedRecordManagerBase", - "RecordManagerWithUniqueFieldBase", -] diff --git a/openstack_odooclient/managers/record/util.py b/openstack_odooclient/managers/record/util.py deleted file mode 100644 index 1505d20..0000000 --- a/openstack_odooclient/managers/record/util.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (C) 2024 Catalyst Cloud Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from datetime import date, datetime -from typing import ( - TYPE_CHECKING, - Literal, - Type, - TypeVar, - Union, - get_args as get_type_args, - get_origin as get_type_origin, -) - -if TYPE_CHECKING: - from typing import Any, List, Mapping, Optional - -T = TypeVar("T") - - -def get_mapped_field( - field_mapping: Mapping[Optional[str], Mapping[str, str]], - odoo_version: str, - field: str, -) -> str: - """Map a field name to its representative in the given field mapping, - based on the given Odoo version. - - If a representative value is not found for the given Odoo version, - check the ``None`` mapping for all Odoo versions. - If none is found there either, return the field name as is. - - :param field_mapping: Field mapping structure - :type field_mapping: Mapping[Optional[str], Mapping[str, str]] - :param odoo_version: Odoo server version - :type odoo_version: str - :param field: Field name to map - :type field: str - :return: Mapped field name - :rtype: str - """ - - try: - return field_mapping[odoo_version][field] - except KeyError: - try: - return field_mapping[None][field] - except KeyError: - return field - - -def decode_value(annotation: Type[T], value: Any) -> T: - """Decode a raw Odoo JSON field value to its local client representation, - based on the annotation defined in the record model. - - :param annotation: The annotation to use to decode the value - :type annotation: Type[T] - :param value: The value to decode - :type value: Any - :return: The decoded value - :rtype: T - """ - - # Create a type tree, which peels back the annotation layers - # to find the basic data type that is expected. - type_tree: List[Type[Any]] = [annotation] - while get_type_origin(type_tree[-1]) is not None: - origin_type = get_type_origin(type_tree[-1]) - if origin_type is not None: - type_tree.append(origin_type) - - # The basic data types that need special handling. - if type_tree[-1] is date: - return date.fromisoformat(value) # type: ignore[return-value] - elif type_tree[-1] is datetime: - return datetime.fromisoformat(value) # type: ignore[return-value] - # When a list is expected, decode each value individually - # and return the result as a new list with the same order. - elif type_tree[-1] is list: - return [ # type: ignore[return-value] - decode_value(get_type_args(type_tree[-2])[0], v) for v in value - ] - # When a dict is expected, decode the key and the value of each - # item separately, and combine the result into a new dict. - elif type_tree[-1] is dict: - key_type, value_type = get_type_args(type_tree[-2]) - return { # type: ignore[return-value] - decode_value(key_type, k): decode_value(value_type, v) - for k, v in value.items() - } - # Basic case for handling specific union structures. - # Not suitable for handling complicated union structures. - # TODO(callumdickinson): Find a way to handle complicated - # union structures more smartly. - elif type_tree[-1] is Union: - attr_union_types = get_type_args(type_tree[-2]) - if len(attr_union_types) == 2: # noqa: PLR2004 - # Optional[T] - if type(None) in attr_union_types and value is not None: - return decode_value( - next(t for t in attr_union_types if t is not type(None)), - value, - ) - # Union[T, Literal[False]] - if Literal[False] in attr_union_types and value is not False: - return decode_value( - next( - ( - t - for t in attr_union_types - if t is not Literal[False] - ), - ), - value, - ) - - # Base case: Return the passed value unmodified. - return value diff --git a/openstack_odooclient/managers/record/base.py b/openstack_odooclient/managers/record_base.py similarity index 54% rename from openstack_odooclient/managers/record/base.py rename to openstack_odooclient/managers/record_base.py index f5c41d6..01f9cfa 100644 --- a/openstack_odooclient/managers/record/base.py +++ b/openstack_odooclient/managers/record_base.py @@ -18,7 +18,6 @@ import copy from datetime import datetime -from functools import cached_property from typing import ( TYPE_CHECKING, Any, @@ -26,20 +25,23 @@ Literal, Optional, Sequence, - get_type_hints, + Type, + Union, + get_args as get_type_args, + get_origin as get_type_origin, overload, ) -from odoorpc import ODOO # type: ignore[import] -from odoorpc.env import Environment # type: ignore[import] -from typing_extensions import Self +from typing_extensions import Annotated, Self, get_type_hints -from .util import decode_value +from .util import FieldAlias, ModelRef, decode_value, is_subclass if TYPE_CHECKING: - from ... import client - from .. import partner - from . import manager_base + from odoorpc import ODOO # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + + from .. import client + from . import record_manager_base class RecordBase: @@ -49,46 +51,34 @@ class RecordBase: create_date: datetime """The time the record was created.""" - @property - def create_uid(self) -> int: - """The ID of the partner that created this record.""" - return self._get_ref_id("create_uid") + create_uid: Annotated[int, ModelRef("create_uid")] + """The ID of the user that created this record.""" - @property - def create_name(self) -> str: - """The name of the partner that created this record.""" - return self._get_ref_name("create_uid") + create_name: Annotated[str, ModelRef("create_uid")] + """The name of the user that created this record.""" - @cached_property - def create_user(self) -> partner.Partner: - """The partner that created this record. + create_user: Annotated[user.User, ModelRef("create_uid")] + """The user that created this record. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.create_uid) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ write_date: datetime """The time the record was last modified.""" - @property - def write_uid(self) -> int: - """The ID of the partner that last modified this record.""" - return self._get_ref_id("write_uid") + write_uid: Annotated[int, ModelRef("write_uid")] + """The ID for the user that last modified this record.""" - @property - def write_name(self) -> str: - """The name of the partner that modified this record.""" - return self._get_ref_name("write_uid") + write_name: Annotated[str, ModelRef("write_uid")] + """The name of the user that last modified this record.""" - @cached_property - def write_user(self) -> partner.Partner: - """The partner that last modified this record. + write_user: Annotated[user.User, ModelRef("create_uid")] + """The user that last modified this record. - This fetches a full Partner object from Odoo once, - and caches it for subsequence access. - """ - return self._client.partners.get(self.write_uid) + This fetches the full record from Odoo once, + and caches it for subsequence access. + """ _field_mapping: Dict[Optional[str], Dict[str, str]] = {} """A dictionary structure mapping field names in the local class @@ -103,25 +93,10 @@ def write_user(self) -> partner.Partner: to their Odoo equivalent. """ - _alias_mapping: Dict[str, str] = {} - """A dictionary structure mapping aliases - (normally defined in the record class) to the corresponding field name - in Odoo. - - This is primarily used to define aliases for search filtering purposes, - to allow either e.g. ``write_uid`` or ``write_user`` to be specified, - instead of just ``write_uid``, when using the ``search`` method. - """ - - _base_alias_mapping = { - "create_user": "create_uid", - "write_user": "write_uid", - } - def __init__( self, client: client.Client, - manager: manager_base.RecordManagerBase, + manager: record_manager_base.RecordManagerBase, record: Dict[str, Any], fields: Optional[Sequence[str]], ) -> None: @@ -227,10 +202,11 @@ def _get_field(self, name: str) -> Any: @classmethod def _resolve_alias(cls, alias: str) -> str: - return cls._alias_mapping.get( - alias, - cls._base_alias_mapping.get(alias, alias), - ) + return alias + # return cls._alias_mapping.get( + # alias, + # cls._base_alias_mapping.get(alias, alias), + # ) @overload def _get_ref_id( @@ -301,20 +277,116 @@ def __getattr__(self, name: str) -> Any: # return the cached value. if name in self._values: return self._values[name] - value = self._get_field(name) - # NOTE(callumdickinson): Use the type annotation to coerce + # NOTE(callumdickinson): Use the type hint to coerce # the field value returned in the record dict into the expected type. - # If no annotation was found for the field, cache the value - # unmodified. - annotations = get_type_hints(type(self)) - self._values[name] = ( - decode_value(annotations[name], value) - if name in annotations - else value - ) - # Return the now-cached value. + type_hints = get_type_hints(type(self), include_extras=True) + # First, check if the field has a type hint defined at all. + # If not, just cache the value as is and return it. + if name not in type_hints: + self._values[name] = self._get_field(name) + return self._values[name] + # We know we have a type hint to decode for the field. + type_hint = type_hints[name] + # Check if the field is annotated. + # There are special code paths for handling fields + # with specific annotations added to them. + if get_type_origin(type_hint) is Annotated: + type_args = get_type_args(type_hint) + attr_type: Type[Any] = type_args[0] + annotations = type_args[1:] + if len(annotations) == 1: + annotation = annotations[0] + # If this field is a field alias, + # recursively fetch the value for the target field. + if isinstance(annotation, FieldAlias): + return getattr(self, annotation.field) + # If this field is a model ref, resolve the model ref + # and return the intended value. + if isinstance(annotation, ModelRef): + self._values[name] = self._getattr_model_ref( + attr_type=attr_type, + model_ref=annotation, + ) + return self._values[name] + raise ValueError( + ( + f"Unsupported annotation for field '{name}': " + f"{annotation}" + ), + ) + # Base case: Decode the value according to the field's type hint, + # cache the value, and return it. + self._values[name] = decode_value(type_hint, self._get_field(name)) return self._values[name] + def _getattr_model_ref( + self, + attr_type: Type[Any], + model_ref: ModelRef, + ) -> Any: + field_value = self._record[self._get_remote_field(model_ref.field)] + # If the expected attribute type is a list, then process the model ref + # as a list of model IDs or objects. + if get_type_origin(attr_type) is list: + value_type = get_type_args(attr_type)[0] + if is_subclass(value_type, RecordBase): + return self._client._record_manager_mapping[value_type].list( + field_value, + ) + if value_type is int: + return field_value + raise ValueError( + ( + "Unsupported field value typefor model ref list: " + f"{value_type}" + ), + ) + # The following is for decoding a singular model ref value. + # Check if the model ref is optional, and if it is, + # return the desired value for when the value is empty. + if get_type_origin(attr_type) is Union: + unsupported_union = ( + "Only unions of the format Optional[T], " + "Union[T, type(None)] or Union[T, Literal[False]] " + "are supported for singular model refs, " + f"found type hint: {attr_type}" + ) + union_types = set(get_type_args(attr_type)) + if len(union_types) > 2: # noqa: PLR2004 + raise ValueError(unsupported_union) + if type(None) in union_types: + union_types.remove(type(None)) + if not field_value: + return None + elif Literal[False] in union_types: + union_types.remove(Literal[False]) + if not field_value: + return False + if len(union_types) != 1: + raise ValueError(unsupported_union) + value_type = union_types.pop() + else: + value_type = attr_type + # The model ref is either required, or is optional but a value + # was found. Determine the appropriate value return type, + # and generate the value. + record_id: int = field_value[0] + record_name: str = field_value[1] + if value_type is int: + return record_id + if value_type is str: + return record_name + if is_subclass(value_type, RecordBase): + return self._client._record_manager_mapping[value_type].get( + record_id, + ) + raise ValueError( + ( + "Unsupported field value type for singular model ref: " + f"{value_type}" + ), + ) + def __str__(self) -> str: return ( f"{type(self).__name__}(" @@ -325,3 +397,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return str(self) + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import user # noqa: E402 diff --git a/openstack_odooclient/managers/record/manager_base.py b/openstack_odooclient/managers/record_manager_base.py similarity index 98% rename from openstack_odooclient/managers/record/manager_base.py rename to openstack_odooclient/managers/record_manager_base.py index 5ea1860..2716764 100644 --- a/openstack_odooclient/managers/record/manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -18,8 +18,8 @@ from datetime import date, datetime from typing import TYPE_CHECKING, Generic, TypeVar, overload -from ...exceptions import RecordNotFoundError -from .base import RecordBase +from ..exceptions import RecordNotFoundError +from .record_base import RecordBase from .util import get_mapped_field if TYPE_CHECKING: @@ -40,7 +40,7 @@ from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from ... import client + from .. import client Record = TypeVar("Record", bound=RecordBase) @@ -70,6 +70,7 @@ def __init__(self, client: client.Client) -> None: self.record_class._field_mapping.items() ) } + self._client._record_manager_mapping[self.record_class] = self @property def _odoo(self) -> ODOO: diff --git a/openstack_odooclient/managers/record/manager_code_base.py b/openstack_odooclient/managers/record_manager_code_base.py similarity index 98% rename from openstack_odooclient/managers/record/manager_code_base.py rename to openstack_odooclient/managers/record_manager_code_base.py index fab5e3f..d530bdc 100644 --- a/openstack_odooclient/managers/record/manager_code_base.py +++ b/openstack_odooclient/managers/record_manager_code_base.py @@ -17,7 +17,10 @@ from typing import TYPE_CHECKING, overload -from .manager_unique_field_base import Record, RecordManagerWithUniqueFieldBase +from .record_manager_unique_field_base import ( + Record, + RecordManagerWithUniqueFieldBase, +) if TYPE_CHECKING: from typing import ( diff --git a/openstack_odooclient/managers/record/manager_name_base.py b/openstack_odooclient/managers/record_manager_name_base.py similarity index 98% rename from openstack_odooclient/managers/record/manager_name_base.py rename to openstack_odooclient/managers/record_manager_name_base.py index 5b67cd7..c2a4aa3 100644 --- a/openstack_odooclient/managers/record/manager_name_base.py +++ b/openstack_odooclient/managers/record_manager_name_base.py @@ -17,7 +17,10 @@ from typing import TYPE_CHECKING, overload -from .manager_unique_field_base import Record, RecordManagerWithUniqueFieldBase +from .record_manager_unique_field_base import ( + Record, + RecordManagerWithUniqueFieldBase, +) if TYPE_CHECKING: from typing import ( diff --git a/openstack_odooclient/managers/record/manager_unique_field_base.py b/openstack_odooclient/managers/record_manager_unique_field_base.py similarity index 97% rename from openstack_odooclient/managers/record/manager_unique_field_base.py rename to openstack_odooclient/managers/record_manager_unique_field_base.py index 3a56f4b..eb70e5c 100644 --- a/openstack_odooclient/managers/record/manager_unique_field_base.py +++ b/openstack_odooclient/managers/record_manager_unique_field_base.py @@ -19,8 +19,8 @@ from typing import TYPE_CHECKING, Generic, TypeVar, overload -from ...exceptions import MultipleRecordsFoundError, RecordNotFoundError -from .manager_base import Record, RecordManagerBase +from ..exceptions import MultipleRecordsFoundError, RecordNotFoundError +from .record_manager_base import Record, RecordManagerBase if TYPE_CHECKING: from typing import ( @@ -36,7 +36,8 @@ class RecordManagerWithUniqueFieldBase( - RecordManagerBase[Record], Generic[Record, T] + RecordManagerBase[Record], + Generic[Record, T], ): @overload def _get_by_unique_field( diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index 54053cd..6f398e8 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, List -from . import record +from . import record_base, record_manager_code_base if TYPE_CHECKING: from . import credit_type, partner -class ReferralCode(record.RecordBase): +class ReferralCode(record_base.RecordBase): allowed_uses: int """The number of allowed uses of this referral code. @@ -116,6 +116,8 @@ def reward_credit_type(self) -> credit_type.CreditType: } -class ReferralCodeManager(record.CodedRecordManagerBase[ReferralCode]): +class ReferralCodeManager( + record_manager_code_base.CodedRecordManagerBase[ReferralCode], +): env_name = "openstack.referral_code" record_class = ReferralCode diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 043d9b6..5677e66 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import partner as partner_module, project, reseller_tier -class Reseller(record.RecordBase): +class Reseller(record_base.RecordBase): alternative_billing_url: Optional[str] """The URL to the cloud billing page for the reseller, if available.""" @@ -113,6 +113,6 @@ def tier(self) -> reseller_tier.ResellerTier: } -class ResellerManager(record.RecordManagerBase[Reseller]): +class ResellerManager(record_manager_base.RecordManagerBase[Reseller]): env_name = "openstack.reseller" record_class = Reseller diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 41b1b98..274771b 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import product -class ResellerTier(record.RecordBase): +class ResellerTier(record_base.RecordBase): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" @@ -92,6 +92,8 @@ def free_monthly_credit_product(self) -> product.Product: } -class ResellerTierManager(record.NamedRecordManagerBase[ResellerTier]): +class ResellerTierManager( + record_manager_name_base.NamedRecordManagerBase[ResellerTier], +): env_name = "openstack.reseller.tier" record_class = ResellerTier diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 7c52d10..412ac50 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -19,7 +19,7 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Literal, Optional, Union -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import ( @@ -30,7 +30,7 @@ ) -class SaleOrder(record.RecordBase): +class SaleOrder(record_base.RecordBase): amount_untaxed: float """The untaxed total cost of the sale order.""" @@ -194,7 +194,9 @@ def create_invoices(self) -> None: self._client.sale_orders.create_invoices(self) -class SaleOrderManager(record.NamedRecordManagerBase[SaleOrder]): +class SaleOrderManager( + record_manager_name_base.NamedRecordManagerBase[SaleOrder], +): env_name = "sale.order" record_class = SaleOrder diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index 66db698..2729da1 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -18,7 +18,7 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Literal, Optional, Union -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import ( @@ -34,7 +34,7 @@ ) -class SaleOrderLine(record.RecordBase): +class SaleOrderLine(record_base.RecordBase): @property def company_id(self) -> int: """The ID for the company this sale order line @@ -374,6 +374,8 @@ def tax(self) -> tax_module.Tax: } -class SaleOrderLineManager(record.RecordManagerBase[SaleOrderLine]): +class SaleOrderLineManager( + record_manager_base.RecordManagerBase[SaleOrderLine], +): env_name = "sale.order.line" record_class = SaleOrderLine diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index ba16008..992452d 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -19,7 +19,7 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import ( @@ -29,7 +29,7 @@ ) -class SupportSubscription(record.RecordBase): +class SupportSubscription(record_base.RecordBase): billing_type: Literal["paid", "complimentary"] """The method of billing for the support subscription. @@ -144,7 +144,7 @@ def support_subscription_type( class SupportSubscriptionManager( - record.RecordManagerBase[SupportSubscription], + record_manager_base.RecordManagerBase[SupportSubscription], ): env_name = "openstack.support_subscription" record_class = SupportSubscription diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index 7829876..7b2b65d 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -18,7 +18,7 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Literal -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import ( @@ -27,7 +27,7 @@ ) -class SupportSubscriptionType(record.RecordBase): +class SupportSubscriptionType(record_base.RecordBase): billing_type: Literal["paid", "complimentary"] """The type of support subscription.""" @@ -95,7 +95,7 @@ def support_subscriptions( class SupportSubscriptionTypeManager( - record.NamedRecordManagerBase[SupportSubscriptionType], + record_manager_name_base.NamedRecordManagerBase[SupportSubscriptionType], ): env_name = "openstack.support_subscription.type" record_class = SupportSubscriptionType diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 89dd119..1af56a1 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import company as company_module, tax_group as tax_group_module -class Tax(record.RecordBase): +class Tax(record_base.RecordBase): active: bool """Whether or not this tax is active (enabled).""" @@ -117,6 +117,6 @@ def tax_group(self) -> tax_group_module.TaxGroup: } -class TaxManager(record.NamedRecordManagerBase[Tax]): +class TaxManager(record_manager_name_base.NamedRecordManagerBase[Tax]): env_name = "account.tax" record_class = Tax diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py index 77a8db1..a529c43 100644 --- a/openstack_odooclient/managers/tax_group.py +++ b/openstack_odooclient/managers/tax_group.py @@ -15,14 +15,16 @@ from __future__ import annotations -from . import record +from . import record_base, record_manager_name_base -class TaxGroup(record.RecordBase): +class TaxGroup(record_base.RecordBase): name: str """Tax group name.""" -class TaxGroupManager(record.NamedRecordManagerBase[TaxGroup]): +class TaxGroupManager( + record_manager_name_base.NamedRecordManagerBase[TaxGroup], +): env_name = "account.tax.group" record_class = TaxGroup diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index 980bcf2..9f3a843 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -19,13 +19,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import partner as partner_module, project as project_module -class TermDiscount(record.RecordBase): +class TermDiscount(record_base.RecordBase): discount_percent: float """The maximum discount percentage for this term discount (0-100).""" @@ -135,6 +135,8 @@ def superseded_by(self) -> Optional[TermDiscount]: } -class TermDiscountManager(record.RecordManagerBase[TermDiscount]): +class TermDiscountManager( + record_manager_base.RecordManagerBase[TermDiscount], +): env_name = "openstack.term_discount" record_class = TermDiscount diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index 44c8de3..421710d 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -19,13 +19,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal, Union -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import partner as partner_module -class Trial(record.RecordBase): +class Trial(record_base.RecordBase): account_suspended_date: Union[date, Literal[False]] """The date the account was suspended, following the end of the trial.""" @@ -68,6 +68,6 @@ def partner(self) -> partner_module.Partner: } -class TrialManager(record.RecordManagerBase[Trial]): +class TrialManager(record_manager_base.RecordManagerBase[Trial]): env_name = "openstack.trial" record_class = Trial diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index 05224b2..db0ea2d 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -18,13 +18,13 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal -from . import record +from . import record_base, record_manager_base if TYPE_CHECKING: from . import uom_category -class Uom(record.RecordBase): +class Uom(record_base.RecordBase): active: bool """Whether or not this Unit of Measure is active (enabled).""" @@ -97,6 +97,6 @@ def category(self) -> uom_category.UomCategory: } -class UomManager(record.RecordManagerBase[Uom]): +class UomManager(record_manager_base.RecordManagerBase[Uom]): env_name = "uom.uom" record_class = Uom diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py index 46b1b47..7827f43 100644 --- a/openstack_odooclient/managers/uom_category.py +++ b/openstack_odooclient/managers/uom_category.py @@ -17,10 +17,10 @@ from typing import Literal -from . import record +from . import record_base, record_manager_base -class UomCategory(record.RecordBase): +class UomCategory(record_base.RecordBase): measure_type: Literal[ "unit", "weight", @@ -45,6 +45,6 @@ class UomCategory(record.RecordBase): """Unit of Measure (UoM) category name.""" -class UomCategoryManager(record.RecordManagerBase[UomCategory]): +class UomCategoryManager(record_manager_base.RecordManagerBase[UomCategory]): env_name = "uom.category" record_class = UomCategory diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index 89543a1..7cc1b95 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -15,70 +15,57 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING +from typing_extensions import Annotated -from . import record +from . import ( + company as company_module, + record_base, + record_manager_base, + util, +) -if TYPE_CHECKING: - from . import company as company_module, partner as partner_module - -class User(record.RecordBase): +class User(record_base.RecordBase): active: bool """Whether or not this user is active.""" active_partner: bool """Whether or not the partner this user is associated with is active.""" - @property - def company_id(self) -> int: - """The ID for the default company this user is logged in as.""" - return self._get_ref_id("company_id") + company_id: Annotated[int, util.ModelRef("company_id")] + """The ID for the default company this user is logged in as.""" - @property - def company_name(self) -> str: - """The name of the default company this user is logged in as.""" - return self._get_ref_name("company_id") + company_name: Annotated[str, util.ModelRef("company_id")] + """The name of the default company this user is logged in as.""" - @cached_property - def company(self) -> company_module.Company: - """The default company this user is logged in as. + company: Annotated[company_module.Company, util.ModelRef("company_id")] + """The default company this user is logged in as. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.companies.get(self.company_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ name: str """User name.""" - @property - def partner_id(self) -> int: - """The ID for the partner that this user is associated with.""" - return self._get_ref_id("partner_id") - - @property - def partner_name(self) -> str: - """The name of the partner that this user is associated with.""" - return self._get_ref_name("partner_id") + partner_id: Annotated[int, util.ModelRef("partner_id")] + """The ID for the partner that this user is associated with.""" - @cached_property - def partner(self) -> partner_module.Partner: - """The partner that this user is associated with. + partner_name: Annotated[str, util.ModelRef("partner_id")] + """The name of the partner that this user is associated with.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) + partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + """The partner that this user is associated with. - _alias_mapping = { - # Key is local alias, value is remote field name. - "company": "company_id", - "partner": "partner_id", - } + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ -class UserManager(record.RecordManagerBase[User]): +class UserManager(record_manager_base.RecordManagerBase[User]): env_name = "res.users" record_class = User + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import partner as partner_module # noqa: E402 diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py new file mode 100644 index 0000000..6217eb6 --- /dev/null +++ b/openstack_odooclient/managers/util.py @@ -0,0 +1,271 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, datetime +from typing import ( + TYPE_CHECKING, + Literal, + Tuple, + Type, + TypeVar, + Union, + get_args as get_type_args, + get_origin as get_type_origin, +) + +# from . import base + +if TYPE_CHECKING: + from typing import Any, List, Mapping, Optional + +T = TypeVar("T") + + +@dataclass(frozen=True) +class FieldAlias: + """An annotation for alias attributes to define the Odoo field name + the attribute is an alias for. + """ + + field: str + + +@dataclass(frozen=True) +class ModelRef: + """An annotation for attributes that decode an Odoo model reference, + to define the Odoo field name to be decoded. + """ + + field: str + + +def get_mapped_field( + field_mapping: Mapping[Optional[str], Mapping[str, str]], + odoo_version: str, + field: str, +) -> str: + """Map a field name to its representative in the given field mapping, + based on the given Odoo version. + + If a representative value is not found for the given Odoo version, + check the ``None`` mapping for all Odoo versions. + If none is found there either, return the field name as is. + + :param field_mapping: Field mapping structure + :type field_mapping: Mapping[Optional[str], Mapping[str, str]] + :param odoo_version: Odoo server version + :type odoo_version: str + :param field: Field name to map + :type field: str + :return: Mapped field name + :rtype: str + """ + + try: + return field_mapping[odoo_version][field] + except KeyError: + try: + return field_mapping[None][field] + except KeyError: + return field + + +def is_subclass( + type_obj: Type[Any], + classes: Union[Type[Any], Tuple[Type[Any]]], +) -> bool: + """Check whether or not the given type is a subclass of + any of the given classes (single class, or tuple of one or more classes). + + Identical to the built-in ``issubclass`` method (and uses it internally), + but returns ``False`` instead of raising ``TypeError`` when the given type + does not match. + + :param type_obj: Type object to check + :type type_obj: Type[Any] + :param classes: Classes to check the type object is a subclass of + :type classes: Union[Type[Any], Tuple[Type[Any]]] + :return: ``True`` if the type is a subclass of any of the given classes + :rtype: bool + """ + + try: + return issubclass(type_obj, classes) + except TypeError: + return False + + +def get_type_tree(type_hint: Type[Any]) -> Tuple[Type[Any], ...]: + """Generate the type tree for the given annotation. + + This function peels back the annotation layers + and generates a list containing the type hint encapsulations, + from the outermost layer to the innermost layer. + + The expected basic data type will end up at the end of the list. + + If the annotation is a union of possible values, the final two elements + will be as follows: + + >>> from typing import Literal, Union + >>> from openstack_odooclient.managers.record.util import get_type_tree + >>> get_type_tree(Union[str, Literal[False]]) + (typing.Union[str, typing.Literal[False]], typing.Union) + + ``Union[T, ...]`` can be evaluated to get the candidate types using + ``typing.get_args``. This includes ``Optional[T]``, + which is syntantic sugar for ``Union[T, type(None)]``. + + >>> from typing import Literal, Union, get_args + >>> get_args(Union[str, Literal[False]]) + (, typing.Literal[False]) + + Similarly, if the data type is a generic type such as ``list`` + or ``dict`` that take type arguments, the final two elements will be + as follows. + + >>> from typing import Dict + >>> from openstack_odooclient.managers.record.util import get_type_tree + >>> get_type_tree(Dict[int, str]) + (typing.Dict[str, int], ) + + The generic types can be retrieved using ``typing.get_args``. + + >>> from typing import Dict, get_args + >>> get_args(Dict[int, str]) + (, ) + + :param type_hint: Type hint to parse + :type type_hint: Type[Any] + :return: Type hint tree + :rtype: Tuple[Type[Any]] + """ + + type_tree: List[Type[Any]] = [type_hint] + + while get_type_origin(type_tree[-1]) is not None: + origin_type = get_type_origin(type_tree[-1]) + if origin_type is not None: + type_tree.append(origin_type) + + return tuple(type_tree) + + +def decode_value(type_hint: Type[T], value: Any) -> T: + """Decode a raw Odoo JSON field value to its local representation, + based on the given type hint from the record class. + + :param type_hint: The type hint to use to decode the value + :type type_hint: Type[T] + :param value: The value to decode + :type value: Any + :return: The decoded value + :rtype: T + """ + + type_tree = get_type_tree(type_hint) + + # The basic data types that need special handling. + if type_tree[-1] is date: + return date.fromisoformat(value) # type: ignore[return-value] + + if type_tree[-1] is datetime: + return datetime.fromisoformat(value) # type: ignore[return-value] + + # When a list is expected, decode each value individually + # and return the result as a new list with the same order. + if type_tree[-1] is list: + return [ # type: ignore[return-value] + decode_value(get_type_args(type_tree[-2])[0], v) for v in value + ] + + # When a dict is expected, decode the key and the value of each + # item separately, and combine the result into a new dict. + if type_tree[-1] is dict: + key_type, value_type = get_type_args(type_tree[-2]) + return { # type: ignore[return-value] + decode_value(key_type, k): decode_value(value_type, v) + for k, v in value.items() + } + + # Basic case for handling specific union structures. + # Not suitable for handling complicated union structures. + # TODO(callumdickinson): Find a way to handle complicated + # union structures more smartly. + if type_tree[-1] is Union: + attr_union_types = get_type_args(type_tree[-2]) + if len(attr_union_types) == 2: # noqa: PLR2004 + # Optional[T] + if type(None) in attr_union_types and value is not None: + return decode_value( + next(t for t in attr_union_types if t is not type(None)), + value, + ) + # Union[T, Literal[False]] + if Literal[False] in attr_union_types and value is not False: + return decode_value( + next( + ( + t + for t in attr_union_types + if t is not Literal[False] + ), + ), + value, + ) + + # Base case: Return the passed value unmodified. + return value + + +# def encode_create_value(annotation: Type[Any], value: Any) -> Any: +# """_summary_ + +# :param value: _description_ +# :type value: Any +# :return: _description_ +# :rtype: Any +# """ + +# type_tree = get_type_tree(annotation) +# value_type = type_tree[-1] + +# if issubclass(value_type, base.RecordBase): +# if isinstance(value, base.RecordBase): +# return value.id +# elif isinstance(value, dict): +# return { +# value_type._resolve_alias(k): encode_create_value( +# value_type.__annotations__[k], +# v, +# ) +# for k, v in value.items() +# } +# if ( +# value_type in (date, datetime) +# and isinstance(value, (date, datetime)) +# ): +# return value.isoformat() +# if ( +# value_type is list +# and isinstance(value, (list, set, tuple)) +# ): +# v_type = get_type_args(type_tree[-2])[0] +# return [encode_create_value(v_type, v) for v in value] + +# return value diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 268287c..49e8d02 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -18,10 +18,14 @@ from functools import cached_property from typing import List, Optional, Union -from . import customer_group as customer_group_module, record +from . import ( + customer_group as customer_group_module, + record_base, + record_manager_base, +) -class VolumeDiscountRange(record.RecordBase): +class VolumeDiscountRange(record_base.RecordBase): @property def customer_group_id(self) -> Optional[int]: """The ID for the customer group this volume discount range @@ -78,7 +82,7 @@ def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: class VolumeDiscountRangeManager( - record.RecordManagerBase[VolumeDiscountRange], + record_manager_base.RecordManagerBase[VolumeDiscountRange], ): env_name = "openstack.volume_discount_range" record_class = VolumeDiscountRange diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 2d51dfa..3eb23e0 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -19,7 +19,7 @@ from functools import cached_property from typing import TYPE_CHECKING, List, Literal, Optional, Union -from . import record +from . import record_base, record_manager_name_base if TYPE_CHECKING: from . import ( @@ -31,7 +31,7 @@ ) -class VoucherCode(record.RecordBase): +class VoucherCode(record_base.RecordBase): claimed: bool """Whether or not this voucher code has been claimed.""" @@ -224,6 +224,8 @@ def tags(self) -> List[partner_category.PartnerCategory]: } -class VoucherCodeManager(record.NamedRecordManagerBase[VoucherCode]): +class VoucherCodeManager( + record_manager_name_base.NamedRecordManagerBase[VoucherCode], +): env_name = "openstack.voucher_code" record_class = VoucherCode From f1ec46b8803e951bee2a38939ef0f7e0b01f3820 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 13 Jun 2024 17:55:03 +1200 Subject: [PATCH 15/87] Fix annotation parsing --- openstack_odooclient/managers/record_base.py | 10 +++++++--- openstack_odooclient/managers/util.py | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index 01f9cfa..dbd58bf 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -27,12 +27,16 @@ Sequence, Type, Union, - get_args as get_type_args, - get_origin as get_type_origin, overload, ) -from typing_extensions import Annotated, Self, get_type_hints +from typing_extensions import ( + Annotated, + Self, + get_args as get_type_args, + get_origin as get_type_origin, + get_type_hints, +) from .util import FieldAlias, ModelRef, decode_value, is_subclass diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index 6217eb6..f58cf19 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -24,6 +24,9 @@ Type, TypeVar, Union, +) + +from typing_extensions import ( get_args as get_type_args, get_origin as get_type_origin, ) From b2aa897ca635cfae7d02b7173d2f2b2c737ad6d5 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 13 Jun 2024 18:50:53 +1200 Subject: [PATCH 16/87] Convert more managers to type hinted model refs --- openstack_odooclient/managers/credit.py | 133 ++++++------- .../managers/credit_transaction.py | 41 ++-- openstack_odooclient/managers/credit_type.py | 176 +++++++++--------- .../managers/customer_group.py | 92 ++++----- openstack_odooclient/managers/grant.py | 99 +++++----- openstack_odooclient/managers/grant_type.py | 154 +++++++-------- openstack_odooclient/managers/record_base.py | 4 + 7 files changed, 313 insertions(+), 386 deletions(-) diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index 2c86a41..6d51ec3 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -16,38 +16,29 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - credit_transaction, - credit_type as credit_type_module, - voucher_code as voucher_code_module, - ) +from . import record_base, record_manager_base, util class Credit(record_base.RecordBase): - @property - def credit_type_id(self) -> int: - """The ID of the type of this credit.""" - return self._get_ref_id("credit_type") + credit_type_id: Annotated[int, util.ModelRef("credit_type")] + """The ID of the type of this credit.""" - @property - def credit_type_name(self) -> str: - """The name of thie type of this credit.""" - return self._get_ref_name("credit_type") + credit_type_name: Annotated[str, util.ModelRef("credit_type")] + """The name of thie type of this credit.""" - @cached_property - def credit_type(self) -> credit_type_module.CreditType: - """The type of this credit. + credit_type: Annotated[ + credit_type_module.CreditType, + util.ModelRef("credit_type"), + ] + """The type of this credit. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.credit_types.get(self.credit_type_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ current_balance: float """The current remaining balance on the credit.""" @@ -64,59 +55,51 @@ def credit_type(self) -> credit_type_module.CreditType: start_date: date """The start date of the credit.""" - @property - def transaction_ids(self) -> List[int]: - """A list of IDs for the transactions that have been made - using this credit. - """ - return self._get_field("transactions") - - @cached_property - def transactions(self) -> List[credit_transaction.CreditTransaction]: - """The transactions that have been made using this credit. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.credit_transactions.list(self.transaction_ids) - - @property - def voucher_code_id(self) -> Optional[int]: - """The ID of the voucher code used when applying for the credit, - if one was supplied. - """ - return self._get_ref_id("voucher_code", optional=True) - - @property - def voucher_code_name(self) -> Optional[str]: - """The name of the voucher code used when applying for the credit, - if one was supplied. - """ - return self._get_ref_name("voucher_code", optional=True) - - @cached_property - def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: - """The voucher code used when applying for the credit, - if one was supplied. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.voucher_code_id - return ( - self._client.voucher_codes.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "credit_type_id": "credit_type", - "transaction_ids": "transactions", - "voucher_code_id": "voucher_code", - } + transaction_ids: Annotated[List[int], util.ModelRef("transactions")] + """A list of IDs for the transactions that have been made + using this credit. + """ + + transactions: Annotated[ + List[credit_transaction.CreditTransaction], + util.ModelRef("transactions"), + ] + """The transactions that have been made using this credit. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + voucher_code_id: Annotated[Optional[int], util.ModelRef("voucher_code")] + """The ID of the voucher code used when applying for the credit, + if one was supplied. + """ + + voucher_code_name: Annotated[Optional[str], util.ModelRef("voucher_code")] + """The name of the voucher code used when applying for the credit, + if one was supplied. + """ + + voucher_code: Annotated[ + Optional[voucher_code_module.VoucherCode], + util.ModelRef("voucher_code"), + ] + """The voucher code used when applying for the credit, + if one was supplied. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class CreditManager(record_manager_base.RecordManagerBase[Credit]): env_name = "openstack.credit" record_class = Credit + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + credit_transaction, + credit_type as credit_type_module, + voucher_code as voucher_code_module, +) diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index ed995d6..ea5abfe 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -15,34 +15,24 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING +from typing_extensions import Annotated -from . import record_base, record_manager_base - -if TYPE_CHECKING: - from . import credit as credit_module +from . import record_base, record_manager_base, util class CreditTransaction(record_base.RecordBase): - @property - def credit_id(self) -> int: - """The ID of the credit this transaction was made against.""" - return self._get_ref_id("credit") + credit_id: Annotated[int, util.ModelRef("credit")] + """The ID of the credit this transaction was made against.""" - @property - def credit_name(self) -> str: - """The name of the credit this transaction was made against.""" - return self._get_ref_name("credit") + credit_name: Annotated[str, util.ModelRef("credit")] + """The name of the credit this transaction was made against.""" - @cached_property - def credit(self) -> credit_module.Credit: - """The credit this transaction was made against. + credit: Annotated[credit_module.Credit, util.ModelRef("credit")] + """The credit this transaction was made against. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.credits.get(self.credit_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ description: str """A description of this credit transaction.""" @@ -50,14 +40,13 @@ def credit(self) -> credit_module.Credit: value: float """The value of the credit transaction.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "credit_id": "credit", - } - class CreditTransactionManager( record_manager_base.RecordManagerBase[CreditTransaction], ): env_name = "openstack.credit.transaction" record_class = CreditTransaction + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import credit as credit_module # noqa: E402 diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index e23e074..c09dcc5 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -15,116 +15,108 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List +from typing import List -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import credit, product as product_module, product_category +from . import ( + product as product_module, + product_category, + record_base, + record_manager_name_base, + util, +) class CreditType(record_base.RecordBase): - @property - def credit_ids(self) -> List[int]: - """A list of IDs for the credits which are of this credit type.""" - return self._get_field("credits") + credit_ids: Annotated[List[int], util.ModelRef("credits")] + """A list of IDs for the credits which are of this credit type.""" - @cached_property - def credits(self) -> List[credit.Credit]: - """A list of credits which are of this credit type. + credits: Annotated[List[credit.Credit], util.ModelRef("credits")] + """A list of credits which are of this credit type. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.credits.list(self.credit_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ name: str """Name of the Credit Type.""" - @property - def only_for_product_ids(self) -> List[int]: - """A list of IDs for the products this credit applies to. - - Mutually exclusive with ``only_for_product_category_ids``. - If neither are specified, the credit applies to all products. - """ - return self._get_field("only_for_products") - - @cached_property - def only_for_products(self) -> List[product_module.Product]: - """A list of products which this credit applies to. - - Mutually exclusive with ``only_for_product_categories``. - If neither are specified, the credit applies to all products. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.products.list(self.only_for_product_ids) - - @property - def only_for_product_category_ids(self) -> List[int]: - """A list of IDs for the product categories this credit applies to. - - Mutually exclusive with ``only_for_product_ids``. - If neither are specified, the credit applies to all product - categories. - """ - return self._get_field("only_for_product_categories") - - @cached_property - def only_for_product_categories( - self, - ) -> List[product_category.ProductCategory]: - """A list of product categories which this credit applies to. - - Mutually exclusive with ``only_for_products``. - If neither are specified, the credit applies to all product - categories. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.product_categories.list(self.only_for_product_ids) - - @property - def product_id(self) -> int: - """The ID of the product to use when applying - the credit to invoices. - """ - return self._get_ref_id("product") - - @property - def product_name(self) -> str: - """The name of the product to use when applying - the credit to invoices. - """ - return self._get_ref_name("product") - - @cached_property - def product(self) -> product_module.Product: - """The product to use when applying the credit to invoices. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.product_id) + only_for_product_ids: Annotated[ + List[int], + util.ModelRef("only_for_products"), + ] + """A list of IDs for the products this credit applies to. + + Mutually exclusive with ``only_for_product_category_ids``. + If neither are specified, the credit applies to all products. + """ + + only_for_products: Annotated[ + List[product_module.Product], + util.ModelRef("only_for_products"), + ] + """A list of products which this credit applies to. + + Mutually exclusive with ``only_for_product_categories``. + If neither are specified, the credit applies to all products. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + only_for_product_category_ids: Annotated[ + List[int], + util.ModelRef("only_for_product_categories"), + ] + """A list of IDs for the product categories this credit applies to. + + Mutually exclusive with ``only_for_product_ids``. + If neither are specified, the credit applies to all product + categories. + """ + + only_for_product_categories: Annotated[ + List[product_category.ProductCategory], + util.ModelRef("only_for_product_categories"), + ] + """A list of product categories which this credit applies to. + + Mutually exclusive with ``only_for_products``. + If neither are specified, the credit applies to all product + categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + product_id: Annotated[int, util.ModelRef("product")] + """The ID of the product to use when applying + the credit to invoices. + """ + + product_name: Annotated[str, util.ModelRef("product")] + """The name of the product to use when applying + the credit to invoices. + """ + + product: Annotated[product_module.Product, util.ModelRef("product")] + """The product to use when applying the credit to invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ refundable: bool """Whether or not the credit is refundable.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "credit_ids": "credits", - "only_for_product_ids": "only_for_products", - "only_for_product_category_ids": "only_for_product_categories", - "product_id": "product", - } - class CreditTypeManager( record_manager_name_base.NamedRecordManagerBase[CreditType], ): env_name = "openstack.credit.type" record_class = CreditType + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import credit # noqa: E402 diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index e29a7e2..0c82fd6 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -15,68 +15,48 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Optional +from typing import List, Optional -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner, pricelist as pricelist_module +from . import record_base, record_manager_name_base, util class CustomerGroup(record_base.RecordBase): name: str """The name of the customer group.""" - @property - def partner_ids(self) -> List[int]: - """A list of IDs for the partners that are part - of this customer group. - """ - return self._get_field("partners") - - @cached_property - def partners(self) -> List[partner.Partner]: - """The partners that are part of this customer group. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.partners.list(self.partner_ids) - - @property - def pricelist_id(self) -> Optional[int]: - """The ID for the pricelist this customer group uses, - if not the default one. - """ - return self._get_ref_id("pricelist", optional=True) - - @property - def pricelist_name(self) -> Optional[str]: - """The name of the pricelist this customer group uses, - if not the default one. - """ - return self._get_ref_name("pricelist", optional=True) - - @cached_property - def pricelist(self) -> Optional[pricelist_module.Pricelist]: - """The pricelist this customer group uses, if not the default one. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.pricelist_id - return ( - self._client.pricelists.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "partner_ids": "partners", - "pricelist_id": "pricelist", - } + partner_ids: Annotated[List[int], util.ModelRef("partners")] + """A list of IDs for the partners that are part + of this customer group. + """ + + partners: Annotated[List[partner.Partner], util.ModelRef("partners")] + """The partners that are part of this customer group. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + pricelist_id: Annotated[Optional[int], util.ModelRef("pricelist")] + """The ID for the pricelist this customer group uses, + if not the default one. + """ + + pricelist_name: Annotated[Optional[str], util.ModelRef("pricelist")] + """The name of the pricelist this customer group uses, + if not the default one. + """ + + pricelist: Annotated[ + Optional[pricelist_module.Pricelist], + util.ModelRef("pricelist"), + ] + """The pricelist this customer group uses, if not the default one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class CustomerGroupManager( @@ -84,3 +64,7 @@ class CustomerGroupManager( ): env_name = "openstack.customer_group" record_class = CustomerGroup + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import partner, pricelist as pricelist_module # noqa: E402 diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index b0c53f9..05c7a0d 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -16,40 +16,32 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, Optional +from typing import Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - grant_type as grant_type_module, - voucher_code as voucher_code_module, - ) +from . import record_base, record_manager_base, util class Grant(record_base.RecordBase): expiry_date: date """The date the grant expires.""" - @property - def grant_type_id(self) -> int: - """The ID of the type of this grant.""" - return self._get_ref_id("grant_type") + grant_type_id: Annotated[int, util.ModelRef("grant_type")] + """The ID of the type of this grant.""" - @property - def grant_type_name(self) -> str: - """The name of thie type of this grant.""" - return self._get_ref_name("grant_type") + grant_type_name: Annotated[str, util.ModelRef("grant_type")] + """The name of thie type of this grant.""" - @cached_property - def grant_type(self) -> grant_type_module.GrantType: - """The type of this grant. + grant_type: Annotated[ + grant_type_module.GrantType, + util.ModelRef("grant_type"), + ] + """The type of this grant. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.grant_types.get(self.grant_type_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ name: str """The automatically generated name of the grant.""" @@ -60,42 +52,35 @@ def grant_type(self) -> grant_type_module.GrantType: value: float """The value of the grant.""" - @property - def voucher_code_id(self) -> Optional[int]: - """The ID of the voucher code used when applying for the grant, - if one was supplied. - """ - return self._get_ref_id("voucher_code", optional=True) - - @property - def voucher_code_name(self) -> Optional[str]: - """The name of the voucher code used when applying for the grant, - if one was supplied. - """ - return self._get_ref_name("voucher_code", optional=True) - - @cached_property - def voucher_code(self) -> Optional[voucher_code_module.VoucherCode]: - """The voucher code used when applying for the grant, - if one was supplied. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.voucher_code_id - return ( - self._client.voucher_codes.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "grant_type_id": "grant_type", - "voucher_code_id": "voucher_code", - } + voucher_code_id: Annotated[Optional[int], util.ModelRef("voucher_code")] + """The ID of the voucher code used when applying for the grant, + if one was supplied. + """ + + voucher_code_name: Annotated[Optional[str], util.ModelRef("voucher_code")] + """The name of the voucher code used when applying for the grant, + if one was supplied. + """ + + voucher_code: Annotated[ + Optional[voucher_code_module.VoucherCode], + util.ModelRef("voucher_code"), + ] + """The voucher code used when applying for the grant, + if one was supplied. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class GrantManager(record_manager_base.RecordManagerBase[Grant]): env_name = "openstack.grant" record_class = Grant + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + grant_type as grant_type_module, + voucher_code as voucher_code_module, +) diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 7f4a3f2..63271fa 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -15,114 +15,96 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List +from typing import List -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import grant, product as product_module, product_category +from . import record_base, record_manager_name_base, util class GrantType(record_base.RecordBase): - @property - def grant_ids(self) -> List[int]: - """A list of IDs for the grants which are of this grant type.""" - return self._get_field("grants") + grant_ids: Annotated[List[int], util.ModelRef("grants")] + """A list of IDs for the grants which are of this grant type.""" - @cached_property - def grants(self) -> List[grant.Grant]: - """A list of grants which are of this grant type. + grants: Annotated[List[grant.Grant], util.ModelRef("grants")] + """A list of grants which are of this grant type. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.grants.list(self.grant_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ name: str """Name of the Grant Type.""" - @property - def only_for_product_ids(self) -> List[int]: - """A list of IDs for the products this grant applies to. + only_for_product_ids: Annotated[ + List[int], + util.ModelRef("only_for_products"), + ] + """A list of IDs for the products this grant applies to. - Mutually exclusive with ``only_for_product_category_ids``. - If neither are specified, the grant applies to all products. - """ - return self._get_field("only_for_products") + Mutually exclusive with ``only_for_product_category_ids``. + If neither are specified, the grant applies to all products. + """ - @cached_property - def only_for_products(self) -> List[product_module.Product]: - """A list of products which this grant applies to. + only_for_products: Annotated[ + List[product_module.Product], + util.ModelRef("only_for_products"), + ] + """A list of products which this grant applies to. - Mutually exclusive with ``only_for_product_categories``. - If neither are specified, the grant applies to all products. + Mutually exclusive with ``only_for_product_categories``. + If neither are specified, the grant applies to all products. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.products.list(self.only_for_product_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ - @property - def only_for_product_category_ids(self) -> List[int]: - """A list of IDs for the product categories this grant applies to. + only_for_product_category_ids: Annotated[ + List[int], + util.ModelRef("only_for_product_categories"), + ] + """A list of IDs for the product categories this grant applies to. - Mutually exclusive with ``only_for_product_ids``. - If neither are specified, the grant applies to all product - categories. - """ - return self._get_field("only_for_product_categories") + Mutually exclusive with ``only_for_product_ids``. + If neither are specified, the grant applies to all product + categories. + """ - @cached_property - def only_for_product_categories( - self, - ) -> List[product_category.ProductCategory]: - """A list of product categories which this grant applies to. + only_for_product_categories: Annotated[ + List[product_category.ProductCategory], + util.ModelRef("only_for_product_categories"), + ] + """A list of product categories which this grant applies to. - Mutually exclusive with ``only_for_products``. - If neither are specified, the grant applies to all product - categories. + Mutually exclusive with ``only_for_products``. + If neither are specified, the grant applies to all product + categories. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.product_categories.list(self.only_for_product_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ only_on_group_root: bool """When set to ``True``, this grant type is only allowed to be part of an invoice grouping if it is on the group root project. """ - @property - def product_id(self) -> int: - """The ID of the product to use when applying - the grant to invoices. - """ - return self._get_ref_id("product") - - @property - def product_name(self) -> str: - """The name of the product to use when applying - the grant to invoices. - """ - return self._get_ref_name("product") - - @cached_property - def product(self) -> product_module.Product: - """The product to use when applying the grant to invoices. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.product_id) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "grant_ids": "grants", - "only_for_product_ids": "only_for_products", - "only_for_product_category_ids": "only_for_product_categories", - "product_id": "product", - } + product_id: Annotated[int, util.ModelRef("product")] + """The ID of the product to use when applying + the grant to invoices. + """ + + product_name: Annotated[str, util.ModelRef("product")] + """The name of the product to use when applying + the grant to invoices. + """ + + product: Annotated[product_module.Product, util.ModelRef("product")] + """The product to use when applying the grant to invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class GrantTypeManager( @@ -130,3 +112,11 @@ class GrantTypeManager( ): env_name = "openstack.grant.type" record_class = GrantType + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + grant, + product as product_module, + product_category, +) diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index dbd58bf..63a04dc 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -333,10 +333,14 @@ def _getattr_model_ref( # as a list of model IDs or objects. if get_type_origin(attr_type) is list: value_type = get_type_args(attr_type)[0] + # List of model objects. Fetch the objects from Odoo, + # and return the results. if is_subclass(value_type, RecordBase): return self._client._record_manager_mapping[value_type].list( field_value, ) + # List of model IDs. The raw field value is already this format, + # so just return it as is. if value_type is int: return field_value raise ValueError( From 6b8f1a7cbe5c37689dbb48aff90734e5e76bc23c Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 13:28:52 +1200 Subject: [PATCH 17/87] FInish switching to type hint-based model refs, add type hint model ref support in create() --- docs/managers/product-category.md | 11 +- docs/managers/sale-order-line.md | 4 +- openstack_odooclient/managers/credit.py | 2 +- openstack_odooclient/managers/grant.py | 2 +- .../managers/partner_category.py | 109 ++--- openstack_odooclient/managers/pricelist.py | 85 ++-- openstack_odooclient/managers/product.py | 143 +++---- .../managers/product_category.py | 81 ++-- openstack_odooclient/managers/project.py | 236 +++++------ .../managers/project_contact.py | 91 ++-- openstack_odooclient/managers/record_base.py | 68 +-- .../managers/record_manager_base.py | 174 +++++++- .../managers/referral_code.py | 110 +++-- openstack_odooclient/managers/reseller.py | 105 ++--- .../managers/reseller_tier.py | 94 ++--- openstack_odooclient/managers/sale_order.py | 151 +++---- .../managers/sale_order_line.py | 399 ++++++++---------- .../managers/support_subscription.py | 186 ++++---- .../managers/support_subscription_type.py | 107 ++--- openstack_odooclient/managers/tax.py | 72 ++-- .../managers/term_discount.py | 171 ++++---- openstack_odooclient/managers/trial.py | 41 +- openstack_odooclient/managers/uom.py | 45 +- openstack_odooclient/managers/util.py | 38 -- .../managers/volume_discount_range.py | 66 ++- openstack_odooclient/managers/voucher_code.py | 255 +++++------ 26 files changed, 1263 insertions(+), 1583 deletions(-) diff --git a/docs/managers/product-category.md b/docs/managers/product-category.md index 87410f2..7ccaa6e 100644 --- a/docs/managers/product-category.md +++ b/docs/managers/product-category.md @@ -45,14 +45,21 @@ from openstack_odooclient import ProductCategory The record class currently implements the following fields and methods. +### `child_id` + +```python +child_id: list[int] +``` + +A list of IDs for the child categories. + ### `child_ids` ```python child_ids: list[int] ``` -A list of IDs for the child categories. -return self._get_field("child_id") +An alias for [`child_id`](#child_id). ### `children` diff --git a/docs/managers/sale-order-line.md b/docs/managers/sale-order-line.md index e0328c3..b5d48b2 100644 --- a/docs/managers/sale-order-line.md +++ b/docs/managers/sale-order-line.md @@ -112,7 +112,7 @@ Display name for the sale order line in the sale order. invoice_line_ids: list[int] ``` -A list of IDs for the invoice (account move) lines created +A list of IDs for the [account move (invoice) lines](account-move-line.md) created from this sale order line. ### `invoice_lines` @@ -121,7 +121,7 @@ from this sale order line. invoice_lines: list[AccountMoveLine] ``` -The invoice (account move) lines created +The [account move (invoice) lines](account-move-line.md) created from this sale order line. This fetches the full records from Odoo once, diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index 6d51ec3..6c944cd 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -28,7 +28,7 @@ class Credit(record_base.RecordBase): """The ID of the type of this credit.""" credit_type_name: Annotated[str, util.ModelRef("credit_type")] - """The name of thie type of this credit.""" + """The name of the type of this credit.""" credit_type: Annotated[ credit_type_module.CreditType, diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index 05c7a0d..244bada 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -31,7 +31,7 @@ class Grant(record_base.RecordBase): """The ID of the type of this grant.""" grant_type_name: Annotated[str, util.ModelRef("grant_type")] - """The name of thie type of this grant.""" + """The name of the type of this grant.""" grant_type: Annotated[ grant_type_module.GrantType, diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index 6f6d549..9745b9b 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -15,99 +15,66 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner +from . import record_base, record_manager_name_base, util class PartnerCategory(record_base.RecordBase): active: bool """Whether or not the partner category is active (enabled).""" - @property - def child_ids(self) -> List[int]: - """A list of IDs for the child categories.""" - return self._get_field("child_id") + child_ids: Annotated[List[int], util.ModelRef("child_id")] + """A list of IDs for the child categories.""" - @cached_property - def children(self) -> List[PartnerCategory]: - """The list of child categories. + children: Annotated[List[PartnerCategory], util.ModelRef("child_id")] + """The list of child categories. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.partner_categories.list(self.child_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ color: int """Colour index for the partner category.""" - @property - def colour(self) -> int: - """Alias for ``color``.""" - return self.color + colour: Annotated[int, util.FieldAlias("color")] + """Alias for ``color``.""" name: str """The name of the partner category.""" - @property - def parent_id(self) -> Optional[int]: - """The ID for the parent partner category, if this category - is the child of another category. - """ - return self._get_ref_id("parent_id", optional=True) - - @property - def parent_name(self) -> Optional[str]: - """The name of the parent partner category, if this category - is the child of another category. - """ - return self._get_ref_name("parent_id", optional=True) - - @cached_property - def parent(self) -> Optional[PartnerCategory]: - """The parent partner category, if this category - is the child of another category. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.parent_id - return ( - self._client.partner_categories.get(record_id) - if record_id is not None - else None - ) + parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + """The ID for the parent partner category, if this category + is the child of another category. + """ + + parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + """The name of the parent partner category, if this category + is the child of another category. + """ + + parent: Annotated[Optional[PartnerCategory], util.ModelRef("parent_id")] + """The parent partner category, if this category + is the child of another category. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ parent_path: Union[str, Literal[False]] """The path of the parent partner category, if there is a parent.""" - @property - def partner_ids(self) -> List[int]: - """A list of IDs for the partners in this category.""" - return self._get_field("partner_id") + partner_ids: Annotated[List[int], util.ModelRef("partner_id")] + """A list of IDs for the partners in this category.""" - @cached_property - def partners(self) -> List[partner.Partner]: - """The list of partners in this category. + partners: Annotated[List[partner.Partner], util.ModelRef("partner_id")] + """The list of partners in this category. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.partners.list(self.partner_ids) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "child_ids": "child_id", - "children": "child_id", - "colour": "color", - "parent": "parent_id", - "partner_ids": "partner_id", - "partners": "partner_id", - } + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ class PartnerCategoryManager( @@ -115,3 +82,7 @@ class PartnerCategoryManager( ): env_name = "res.partner.category" record_class = PartnerCategory + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import partner # noqa: E402 diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 0174dc1..16509dd 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -15,61 +15,53 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Literal, Optional, Union +from typing import Literal, Optional, Union -from . import product as product_module, record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import company as company_module, currency as currency_module +from . import ( + product as product_module, + record_base, + record_manager_name_base, + util, +) class Pricelist(record_base.RecordBase): active: bool """Whether or not the pricelist is active.""" - @property - def company_id(self) -> Optional[int]: - """The ID for the company for this pricelist, if set.""" - return self._get_ref_id("company_id", optional=True) + company_id: Annotated[Optional[int], util.ModelRef("company_id")] + """The ID for the company for this pricelist, if set.""" - @property - def company_name(self) -> Optional[str]: - """The name of the company for this pricelist, if set.""" - return self._get_ref_name("company_id", optional=True) + company_name: Annotated[Optional[str], util.ModelRef("company_id")] + """The name of the company for this pricelist, if set.""" - @cached_property - def company(self) -> Optional[company_module.Company]: - """The company for this pricelist, if set. + company: Annotated[ + Optional[company_module.Company], + util.ModelRef("company_id"), + ] + """The company for this pricelist, if set. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.company_id - return ( - self._client.companies.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - @property - def currency_id(self) -> int: - """The ID for the currency used in this pricelist.""" - return self._get_ref_id("currency_id") + currency_id: Annotated[int, util.ModelRef("currency_id")] + """The ID for the currency used in this pricelist.""" - @property - def currency_name(self) -> str: - """The name of the currency used in this pricelist.""" - return self._get_ref_name("currency_id") + currency_name: Annotated[str, util.ModelRef("currency_id")] + """The name of the currency used in this pricelist.""" - @cached_property - def currency(self) -> currency_module.Currency: - """The currency used in this pricelist. + currency: Annotated[ + currency_module.Currency, + util.ModelRef("currency_id"), + ] + """The currency used in this pricelist. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.currencies.get(self.currency_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ discount_policy: Literal["with_discount", "without_discount"] """Discount policy for the pricelist. @@ -83,12 +75,6 @@ def currency(self) -> currency_module.Currency: name: str """The name of this pricelist.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "company": "company_id", - "currency": "currency_id", - } - def get_price( self, product: Union[int, product_module.Product], @@ -147,3 +133,10 @@ def get_price( max(qty, 0), )[str(pricelist_id)] return price if qty >= 0 else -price + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + company as company_module, + currency as currency_module, +) diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index 67a8cd6..d8aaefb 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -15,9 +15,7 @@ from __future__ import annotations -from functools import cached_property from typing import ( - TYPE_CHECKING, Any, Dict, Iterable, @@ -28,55 +26,43 @@ overload, ) -from . import record_base, record_manager_unique_field_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import company, product_category, uom as uom_module +from . import record_base, record_manager_unique_field_base, util class Product(record_base.RecordBase): - @property - def categ_id(self) -> int: - """The ID for the category this product is under.""" - return self._get_ref_id("categ_id") - - @property - def categ_name(self) -> str: - """The name of the category this product is under.""" - return self._get_ref_name("categ_id") - - @cached_property - def categ(self) -> product_category.ProductCategory: - """The category this product is under. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.product_categories.get(self.categ_id) + categ_id: Annotated[int, util.ModelRef("categ_id")] + """The ID for the category this product is under.""" - @property - def company_id(self) -> Optional[int]: - """The ID for the company that owns this product, if set.""" - return self._get_ref_id("company_id", optional=True) + categ_name: Annotated[str, util.ModelRef("categ_id")] + """The name of the category this product is under.""" - @property - def company_name(self) -> Optional[str]: - """The name of the company that owns this product, if set.""" - return self._get_ref_name("company_id", optional=True) + categ: Annotated[ + product_category.ProductCategory, + util.ModelRef("categ_id"), + ] + """The category this product is under. - @cached_property - def company(self) -> Optional[company.Company]: - """The company that owns this product, if set. + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.company_id - return ( - self._client.companies.get(record_id) - if record_id is not None - else None - ) + company_id: Annotated[Optional[int], util.ModelRef("company_id")] + """The ID for the company that owns this product, if set.""" + + company_name: Annotated[Optional[str], util.ModelRef("company_id")] + """The name of the company that owns this product, if set.""" + + company: Annotated[ + Optional[company_module.Company], + util.ModelRef("company_id"), + ] + """The company that owns this product, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ default_code: str """The Default Code for this product. @@ -100,31 +86,18 @@ def company(self) -> Optional[company.Company]: name: str """The name of the product.""" - @property - def uom_id(self) -> int: - """The ID for the Unit of Measure for this product.""" - return self._get_ref_id("uom_id") - - @property - def uom_name(self) -> str: - """The name of the Unit of Measure for this product.""" - return self._get_ref_name("uom_id") + uom_id: Annotated[int, util.ModelRef("uom_id")] + """The ID for the Unit of Measure for this product.""" - @cached_property - def uom(self) -> uom_module.Uom: - """The Unit of Measure for this product. + uom_name: Annotated[str, util.ModelRef("uom_id")] + """The name of the Unit of Measure for this product.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.uoms.get(self.uom_id) + uom: Annotated[uom_module.Uom, util.ModelRef("uom_id")] + """The Unit of Measure for this product. - _alias_mapping = { - # Key is local alias, value is remote field name. - "categ": "categ_id", - "company": "company_id", - "uom": "uom_id", - } + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class ProductManager( @@ -139,7 +112,7 @@ class ProductManager( @overload def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -150,7 +123,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -161,7 +134,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -172,7 +145,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -183,7 +156,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -193,7 +166,7 @@ def get_sellable_company_products( def get_sellable_company_products( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], fields: Optional[Iterable[str]] = None, order: Optional[str] = None, as_id: bool = False, @@ -229,7 +202,7 @@ def get_sellable_company_products( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -241,7 +214,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -253,7 +226,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -265,7 +238,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -277,7 +250,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -289,7 +262,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -301,7 +274,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -313,7 +286,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -325,7 +298,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -336,7 +309,7 @@ def get_sellable_company_product_by_name( def get_sellable_company_product_by_name( self, - company: Union[int, company.Company], + company: Union[int, company_module.Company], name: str, fields: Optional[Iterable[str]] = None, as_id: bool = False, @@ -391,3 +364,11 @@ def get_sellable_company_product_by_name( as_dict=as_dict, optional=optional, ) + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + company as company_module, + product_category, + uom as uom_module, +) diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index f2d1b53..3c3dffb 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -15,26 +15,29 @@ from __future__ import annotations -from functools import cached_property from typing import List, Literal, Optional, Union -from . import record_base, record_manager_name_base +from typing_extensions import Annotated + +from . import record_base, record_manager_name_base, util class ProductCategory(record_base.RecordBase): - @property - def child_ids(self) -> List[int]: - """A list of IDs for the child categories.""" - return self._get_field("child_id") + child_id: Annotated[List[int], util.ModelRef("child_id")] + """A list of IDs for the child categories.""" + + child_ids: Annotated[List[int], util.FieldAlias("child_id")] + """An alias for ``child_id``.""" - @cached_property - def children(self) -> List[ProductCategory]: - """The list of child categories. + children: Annotated[ + List[ProductCategory], + util.ModelRef("child_id"), + ] + """The list of child categories. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.product_categories.list(self.child_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ complete_name: str """The complete product category tree.""" @@ -42,34 +45,23 @@ def children(self) -> List[ProductCategory]: name: str """Name of the product category.""" - @property - def parent_id(self) -> Optional[int]: - """The ID for the parent product category, if this category - is the child of another category. - """ - return self._get_ref_id("parent_id", optional=True) - - @property - def parent_name(self) -> Optional[str]: - """The name of the parent product category, if this category - is the child of another category. - """ - return self._get_ref_name("parent_id", optional=True) - - @cached_property - def parent(self) -> Optional[ProductCategory]: - """The parent product category, if this category - is the child of another category. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.parent_id - return ( - self._client.product_categories.get(record_id) - if record_id is not None - else None - ) + parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + """The ID for the parent product category, if this category + is the child of another category. + """ + + parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + """The name of the parent product category, if this category + is the child of another category. + """ + + parent: Annotated[Optional[ProductCategory], util.ModelRef("parent_id")] + """The parent product category, if this category + is the child of another category. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ parent_path: Union[str, Literal[False]] """The path of the parent product category, if there is a parent.""" @@ -77,13 +69,6 @@ def parent(self) -> Optional[ProductCategory]: product_count: int """The number of products under this category.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "child_ids": "child_id", - "children": "child_id", - "parent": "parent_id", - } - class ProductCategoryManager( record_manager_name_base.NamedRecordManagerBase[ProductCategory], diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index da4d9d6..652618c 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -15,9 +15,7 @@ from __future__ import annotations -from functools import cached_property from typing import ( - TYPE_CHECKING, Any, Dict, Iterable, @@ -28,17 +26,9 @@ overload, ) -from . import record_base, record_manager_unique_field_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - credit, - grant, - partner as partner_module, - project_contact, - support_subscription as support_subscription_module, - term_discount, - ) +from . import record_base, record_manager_unique_field_base, util class Project(record_base.RecordBase): @@ -71,53 +61,36 @@ class Project(record_base.RecordBase): set on this Project. """ - @property - def owner_id(self) -> int: - """The ID for the partner that owns this project.""" - return self._get_ref_id("owner") + owner_id: Annotated[int, util.ModelRef("owner")] + """The ID for the partner that owns this project.""" - @property - def owner_name(self) -> str: - """The name of the partner that owns this project.""" - return self._get_ref_name("owner") + owner_name: Annotated[str, util.ModelRef("owner")] + """The name of the partner that owns this project.""" - @cached_property - def owner(self) -> partner_module.Partner: - """The partner that owns this project. + owner: Annotated[partner_module.Partner, util.ModelRef("owner")] + """The partner that owns this project. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.owner_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - @property - def parent_id(self) -> Optional[int]: - """The ID for the parent project, if this project - is the child of another project. - """ - return self._get_ref_id("parent", optional=True) + parent_id: Annotated[Optional[int], util.ModelRef("parent")] + """The ID for the parent project, if this project + is the child of another project. + """ - @property - def parent_name(self) -> Optional[str]: - """The name of the parent project, if this project - is the child of another project. - """ - return self._get_ref_name("parent", optional=True) + parent_name: Annotated[Optional[str], util.ModelRef("parent")] + """The name of the parent project, if this project + is the child of another project. + """ - @cached_property - def parent(self) -> Optional[Project]: - """The parent project, if this project - is the child of another project. + parent: Annotated[Optional[Project], util.ModelRef("parent")] + """The parent project, if this project + is the child of another project. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.parent_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ payment_method: Literal["invoice", "credit_card"] """Payment method configured on the project. @@ -131,47 +104,47 @@ def parent(self) -> Optional[Project]: po_number: Union[str, Literal[False]] """The PO number set for this specific Project (if set).""" - @property - def project_contact_ids(self) -> List[int]: - """A list of IDs for the contacts for this project.""" - return self._get_field("project_contacts") + project_contact_ids: Annotated[ + List[int], + util.ModelRef("project_contacts"), + ] + """A list of IDs for the contacts for this project.""" - @cached_property - def project_contacts(self) -> List[project_contact.ProjectContact]: - """The contacts for this project. + project_contacts: Annotated[ + List[project_contact.ProjectContact], + util.ModelRef("project_contacts"), + ] + """The contacts for this project. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.project_contacts.list(self.project_contact_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ - @property - def project_credit_ids(self) -> List[int]: - """A list of IDs for the credits that apply to this project.""" - return self._get_field("project_credits") + project_credit_ids: Annotated[List[int], util.ModelRef("project_credits")] + """A list of IDs for the credits that apply to this project.""" - @cached_property - def project_credits(self) -> List[credit.Credit]: - """The credits that apply to this project. + project_credits: Annotated[ + List[credit.Credit], + util.ModelRef("project_credits"), + ] + """The credits that apply to this project. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.credits.list(self.project_credit_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ - @property - def project_grant_ids(self) -> List[int]: - """A list of IDs for the grants that apply to this project.""" - return self._get_field("project_grants") + project_grant_ids: Annotated[List[int], util.ModelRef("project_grants")] + """A list of IDs for the grants that apply to this project.""" - @cached_property - def project_grants(self) -> List[grant.Grant]: - """The grants that apply to this project. + project_grants: Annotated[ + List[grant.Grant], + util.ModelRef("project_grants"), + ] + """The grants that apply to this project. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.grants.list(self.project_grant_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ stripe_card_id: Union[str, Literal[False]] """The card ID used for credit card payments on this project @@ -181,61 +154,45 @@ def project_grants(self) -> List[grant.Grant]: this field will be set to ``False``. """ - @property - def support_subscription_id(self) -> Optional[int]: - """The ID for the support subscription for this project, - if the project has one. - """ - return self._get_ref_id("support_subscription", optional=True) - - @property - def support_subscription_name(self) -> Optional[str]: - """The name of the support subscription for this project, - if the project has one. - """ - return self._get_ref_name("support_subscription", optional=True) + support_subscription_id: Annotated[ + Optional[int], + util.ModelRef("support_subscription"), + ] + """The ID for the support subscription for this project, + if the project has one. + """ - @cached_property - def support_subscription( - self, - ) -> Optional[support_subscription_module.SupportSubscription]: - """The support subscription for this project, - if the project has one. + support_subscription_name: Annotated[ + Optional[str], + util.ModelRef("support_subscription"), + ] + """The name of the support subscription for this project, + if the project has one. + """ - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.support_subscription_id - return ( - self._client.support_subscriptions.get(record_id) - if record_id is not None - else None - ) + support_subscription: Annotated[ + Optional[support_subscription_module.SupportSubscription], + util.ModelRef("support_subscription"), + ] + """The support subscription for this project, + if the project has one. - @property - def term_discount_ids(self) -> List[int]: - """A list of IDs for the term discounts that apply to this project.""" - return self._get_field("term_discounts") + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - @cached_property - def term_discounts(self) -> List[term_discount.TermDiscount]: - """The term discounts that apply to this project. + term_discount_ids: Annotated[List[int], util.ModelRef("term_discounts")] + """A list of IDs for the term discounts that apply to this project.""" - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.term_discounts.list(self.term_discount_ids) + term_discounts: Annotated[ + List[term_discount.TermDiscount], + util.ModelRef("term_discounts"), + ] + """The term discounts that apply to this project. - _alias_mapping = { - # Key is local alias, value is remote field name. - "owner_id": "owner", - "parent_id": "parent", - "project_contact_ids": "project_contacts", - "project_credit_ids": "project_credits", - "project_grant_ids": "project_grants", - "support_subcription_id": "support_subscription", - "term_discount_ids": "term_discounts", - } + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ class ProjectManager( @@ -382,3 +339,14 @@ def get_by_os_id( as_dict=as_dict, optional=optional, ) + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + credit, + grant, + partner as partner_module, + project_contact, + support_subscription as support_subscription_module, + term_discount, +) diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index 813b675..7acf936 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -15,13 +15,11 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Literal, Optional +from typing import Literal, Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner as partner_module, project as project_module +from . import record_base, record_manager_base, util class ProjectContact(record_base.RecordBase): @@ -37,54 +35,34 @@ class ProjectContact(record_base.RecordBase): inherit: bool """Whether or not this contact should be inherited by child projects.""" - @property - def partner_id(self) -> int: - """The ID for the partner linked to this project contact.""" - return self._get_ref_id("partner") - - @property - def partner_name(self) -> str: - """The name of the partner linked to this project contact.""" - return self._get_ref_name("partner") - - @cached_property - def partner(self) -> partner_module.Partner: - """The partner linked to this project contact. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) - - @property - def project_id(self) -> Optional[int]: - """The ID for the project this contact is linked to, if set.""" - return self._get_ref_id("project", optional=True) - - @property - def project_name(self) -> Optional[str]: - """The name of the project this contact is linked to, if set.""" - return self._get_ref_name("project", optional=True) - - @cached_property - def project(self) -> Optional[project_module.Project]: - """The project this contact is linked to, if set. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "partner_id": "partner", - "project_id": "project", - } + partner_id: Annotated[int, util.ModelRef("partner")] + """The ID for the partner linked to this project contact.""" + + partner_name: Annotated[str, util.ModelRef("partner")] + """The name of the partner linked to this project contact.""" + + partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + """The partner linked to this project contact. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + project_id: Annotated[Optional[int], util.ModelRef("project")] + """The ID for the project this contact is linked to, if set.""" + + project_name: Annotated[Optional[str], util.ModelRef("project")] + """The name of the project this contact is linked to, if set.""" + + project: Annotated[ + Optional[project_module.Project], + util.ModelRef("project"), + ] + """The project this contact is linked to, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class ProjectContactManager( @@ -92,3 +70,10 @@ class ProjectContactManager( ): env_name = "openstack.project_contact" record_class = ProjectContact + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from . import ( # noqa: E402 + partner as partner_module, + project as project_module, +) diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index 63a04dc..fb569f9 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -27,7 +27,6 @@ Sequence, Type, Union, - overload, ) from typing_extensions import ( @@ -212,70 +211,6 @@ def _resolve_alias(cls, alias: str) -> str: # cls._base_alias_mapping.get(alias, alias), # ) - @overload - def _get_ref_id( - self, - name: str, - optional: Literal[False] = ..., - ) -> int: ... - - @overload - def _get_ref_id( - self, - name: str, - optional: Literal[True], - ) -> Optional[int]: ... - - @overload - def _get_ref_id( - self, name: str, optional: bool = ... - ) -> Optional[int]: ... - - def _get_ref_id(self, name: str, optional: bool = False) -> Optional[int]: - # NOTE(callumdickinson): This method intentionally does not test - # for field existence, so an error is raised if the field is not - # actually selected in the query. - # If an optional ref is selected in a query but a ref is not set, - # ``False`` is returned instead of the expected 2-element list. - ref = self._get_field(name) - if not optional: - return ref[0] - return ref[0] if ref else None - - @overload - def _get_ref_name( - self, - name: str, - optional: Literal[False] = ..., - ) -> str: ... - - @overload - def _get_ref_name( - self, - name: str, - optional: Literal[True], - ) -> Optional[str]: ... - - @overload - def _get_ref_name( - self, name: str, optional: bool = ... - ) -> Optional[str]: ... - - def _get_ref_name( - self, - name: str, - optional: bool = False, - ) -> Optional[str]: - # NOTE(callumdickinson): This method intentionally does not test - # for field existence, so an error is raised if the field is not - # actually selected in the query. - # If an optional ref is selected in a query but a ref is not set, - # ``False`` is returned instead of the expected 2-element list. - ref = self._get_field(name) - if not optional: - return ref[1] - return ref[1] if ref else None - def __getattr__(self, name: str) -> Any: # If the field value has already been decoded, # return the cached value. @@ -303,7 +238,8 @@ def __getattr__(self, name: str) -> Any: # If this field is a field alias, # recursively fetch the value for the target field. if isinstance(annotation, FieldAlias): - return getattr(self, annotation.field) + self._values[name] = getattr(self, annotation.field) + return self._values[name] # If this field is a model ref, resolve the model ref # and return the intended value. if isinstance(annotation, ModelRef): diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 2716764..18db5c6 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -18,9 +18,16 @@ from datetime import date, datetime from typing import TYPE_CHECKING, Generic, TypeVar, overload +from typing_extensions import ( + Annotated, + get_args as get_type_args, + get_origin as get_type_origin, + get_type_hints, +) + from ..exceptions import RecordNotFoundError from .record_base import RecordBase -from .util import get_mapped_field +from .util import FieldAlias, ModelRef, get_mapped_field if TYPE_CHECKING: from typing import ( @@ -33,6 +40,7 @@ Optional, Sequence, Set, + Tuple, Type, Union, ) @@ -382,15 +390,7 @@ def create(self, **fields) -> int: :return: The ID of the newly created record :rtype: int """ - return self._env.create( - { - # TODO(callumdickinson): Handle nested model object - # encoding properly using type hints, - # e.g. sale order lines defined in sale orders. - self._encode_field(field): self._encode_value(value) - for field, value in fields.items() - }, - ) + return self._env.create(self._encode_create_fields(fields)) def create_multi(self, *records: Mapping[str, Any]) -> List[int]: """Create one or more new records in a single request, @@ -404,18 +404,156 @@ def create_multi(self, *records: Mapping[str, Any]) -> List[int]: :rtype: List[int] """ res: Union[int, List[int]] = self._env.create( - [ - { - self._encode_field(field): self._encode_value(value) - for field, value in record.items() - } - for record in records - ], + [self._encode_create_fields(record) for record in records], ) if isinstance(res, int): return [res] return res + def _encode_create_fields( + self, + fields: Mapping[str, Any], + ) -> Dict[str, Any]: + create_fields: Dict[str, Any] = {} + field_remote_mapping: Dict[str, str] = {} + type_hints = get_type_hints(self.record_class, include_extras=True) + for field, value in fields.items(): + remote_field, remote_value = self._encode_create_field( + type_hints=type_hints, + field=field, + value=value, + ) + if remote_field in field_remote_mapping: + raise ValueError( + ( + "Conflicting field keys found that resolve to the " + "same remote field when creating record from " + f"mapping: {fields} (conflicting keys: " + f"{field_remote_mapping[remote_field]}, {field})" + ), + ) + field_remote_mapping[remote_field] = field + create_fields[remote_field] = remote_value + return create_fields + + def _encode_create_field( + self, + type_hints: Mapping[str, Type[Any]], + field: str, + value: Any, + ) -> Tuple[str, Any]: + # Fetch the local and remote representations of the given field. + local_field = self._get_local_field(field) + remote_field = self._get_remote_field(field) + # If there is no type hint for the given field, map the value + # to the field unchanged. + if local_field not in type_hints: + return (remote_field, value) + # Fetch the type hint for parsing. + type_hint = type_hints[local_field] + # Perform special handling of annotated fields. + if get_type_origin(type_hint) is Annotated: + type_args = get_type_args(type_hint) + attr_type: Type[Any] = type_args[0] + annotations = type_args[1:] + if len(annotations) == 1: + annotation = annotations[0] + # If this field is a field alias, + # recursively encode the field as the target field. + if isinstance(annotation, FieldAlias): + return self._encode_create_field( + type_hints=type_hints, + field=annotation.field, + value=value, + ) + # If this field is a model ref, encode the model ref + # according to the given value's type, and map the result + # to the Odoo model's ref field name. + if isinstance(annotation, ModelRef): + model_ref_field = self._get_remote_field(annotation.field) + # If the field is a list of multiple model refs, + # iterate over the given value and decode the elements + # appropriately. + if get_type_origin(attr_type) is list: + if not value: + return (model_ref_field, []) + remote_values: List[Union[int, Dict[str, Any]]] = [] + for v in value: + if isinstance(v, int): + remote_values.append(v) + elif isinstance(v, RecordBase): + remote_values.append(v.id) + elif isinstance(v, dict): + manager = self._client._record_manager_mapping[ + attr_type + ] + remote_values.append( + manager._encode_create_fields(value), + ) + else: + raise ValueError( + ( + "Unsupported element value for model " + f"ref list field '{field}' " + f"when creating record: {v}" + ), + ) + return (model_ref_field, remote_values) + # If the value type is an integer, + # treat it as a record ID and assign i to the field. + if isinstance(value, int): + return (model_ref_field, value) + # If the value type is a record object, + # then treat it as if it already exists on Odoo, + # and return the record ID to assign to the field. + if isinstance(value, RecordBase): + return (model_ref_field, value.id) + # If the value type is a dictionary, + # then treat it as a nested record to be created + # alongside the parent record. + # Encode the contents of the dict recursively + # using the record class's manager object, + # and assign it to the parent record so they can + # both be created. + if isinstance(value, dict): + return ( + model_ref_field, + ( + self._client._record_manager_mapping[ + attr_type + ]._encode_create_fields(value) + ), + ) + raise ValueError( + ( + f"Unsupported value for model ref field '{field}' " + f"when creating record: {value}" + ), + ) + raise ValueError( + ( + f"Unsupported annotation for field '{field}': " + f"{annotation}" + ), + ) + # For non-annotated fields, encode the value based on its type hint. + return ( + remote_field, + self._encode_create_value(type_hint=type_hint, value=value), + ) + + def _encode_create_value(self, type_hint: Type[Any], value: Any) -> Any: + value_type = get_type_origin(type_hint) + if value_type in (date, datetime) and isinstance( + value, + (date, datetime), + ): + return value.isoformat() + if value_type is list and isinstance(value, (list, set, tuple)): + v_type = get_type_args(type_hint)[0] + return [self._encode_create_value(v_type, v) for v in value] + return value + def unlink( self, *records: Union[Record, int, Iterable[Union[Record, int]]], @@ -488,6 +626,8 @@ def _encode_value(self, value: Any) -> Any: return value def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: + # TODO(callumdickinson): Parse nested field references + # (e.g. "product.categ_id") in filters. _filters: List[Any] = [] for f in filters: if isinstance(f, tuple): diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index 6f398e8..e5ff44a 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -15,13 +15,11 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List +from typing import List -from . import record_base, record_manager_code_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import credit_type, partner +from . import record_base, record_manager_code_base, util class ReferralCode(record_base.RecordBase): @@ -42,21 +40,17 @@ class ReferralCode(record_base.RecordBase): name: str """Automatically generated name for the referral code.""" - @property - def referral_ids(self) -> List[int]: - """A list of IDs for the partners that signed up - using this referral code. - """ - return self._get_field("referrals") + referral_ids: Annotated[List[int], util.ModelRef("referrals")] + """A list of IDs for the partners that signed up + using this referral code. + """ - @cached_property - def referrals(self) -> List[partner.Partner]: - """The partners that signed up using this referral code. + referrals: Annotated[List[partner.Partner], util.ModelRef("referrals")] + """The partners that signed up using this referral code. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.partners.list(self.referral_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ referral_credit_amount: float """Initial balance for the referral credit.""" @@ -64,24 +58,27 @@ def referrals(self) -> List[partner.Partner]: referral_credit_duration: int """Duration of the referral credit, in days.""" - @property - def referral_credit_type_id(self) -> int: - """The ID of the credit type to use for the referral credit.""" - return self._get_ref_id("referral_credit_type") - - @property - def referral_credit_type_name(self) -> str: - """The name of the credit type to use for the referral credit.""" - return self._get_ref_name("referral_credit_type") - - @cached_property - def referral_credit_type(self) -> credit_type.CreditType: - """The credit type to use for the referral credit. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.credit_types.get(self.referral_credit_type_id) + referral_credit_type_id: Annotated[ + int, + util.ModelRef("referral_credit_type"), + ] + """The ID of the credit type to use for the referral credit.""" + + referral_credit_type_name: Annotated[ + str, + util.ModelRef("referral_credit_type"), + ] + """The name of the credit type to use for the referral credit.""" + + referral_credit_type: Annotated[ + credit_type.CreditType, + util.ModelRef("referral_credit_type"), + ] + """The credit type to use for the referral credit. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ reward_credit_amount: float """Initial balance for the reward credit.""" @@ -89,31 +86,24 @@ def referral_credit_type(self) -> credit_type.CreditType: reward_credit_duration: int """Duration of the reward credit, in days.""" - @property - def reward_credit_type_id(self) -> int: - """The ID of the credit type to use for the reward credit.""" - return self._get_ref_id("reward_credit_type") - - @property - def reward_credit_type_name(self) -> str: - """The name of the credit type to use for the reward credit.""" - return self._get_ref_name("reward_credit_type") + reward_credit_type_id: Annotated[int, util.ModelRef("reward_credit_type")] + """The ID of the credit type to use for the reward credit.""" - @cached_property - def reward_credit_type(self) -> credit_type.CreditType: - """The credit type to use for the reward credit. + reward_credit_type_name: Annotated[ + str, + util.ModelRef("reward_credit_type"), + ] + """The name of the credit type to use for the reward credit.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.credit_types.get(self.reward_credit_type_id) + reward_credit_type: Annotated[ + credit_type.CreditType, + util.ModelRef("reward_credit_type"), + ] + """The credit type to use for the reward credit. - _alias_mapping = { - # Key is local alias, value is remote field name. - "referral_ids": "referrals", - "referral_credit_type_id": "referral_credit_type", - "reward_credit_type_id": "reward_credit_type", - } + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class ReferralCodeManager( @@ -121,3 +111,7 @@ class ReferralCodeManager( ): env_name = "openstack.referral_code" record_class = ReferralCode + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import credit_type, partner # noqa: E402 diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 5677e66..6098bf1 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -15,13 +15,11 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Optional +from typing import Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner as partner_module, project, reseller_tier +from . import record_base, record_manager_base, util class Reseller(record_base.RecordBase): @@ -31,29 +29,21 @@ class Reseller(record_base.RecordBase): alternative_support_url: Optional[str] """The URL to the cloud support centre for the reseller, if available.""" - @property - def demo_project_id(self) -> Optional[int]: - """The ID for the optional demo project belonging to the reseller.""" - return self._get_ref_id("project_demo", optional=True) - - @property - def demo_project_name(self) -> Optional[str]: - """The name of the optional demo project belonging to the reseller.""" - return self._get_ref_name("project_demo", optional=True) - - @cached_property - def demo_project(self) -> Optional[project.Project]: - """An optional demo project belonging to the reseller. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + demo_project_id: Annotated[Optional[int], util.ModelRef("demo_project")] + """The ID for the optional demo project belonging to the reseller.""" + + demo_project_name: Annotated[Optional[str], util.ModelRef("demo_project")] + """The name of the optional demo project belonging to the reseller.""" + + demo_project: Annotated[ + Optional[project.Project], + util.ModelRef("demo_project"), + ] + """An optional demo project belonging to the reseller. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ hide_billing: bool """Whether or not the billing URL should be hidden.""" @@ -67,52 +57,37 @@ def demo_project(self) -> Optional[project.Project]: This is set to the reseller partner's name. """ - @property - def partner_id(self) -> int: - """The ID for the reseller partner.""" - return self._get_ref_id("partner") - - @property - def partner_name(self) -> str: - """The name of the reseller partner.""" - return self._get_ref_name("partner") + partner_id: Annotated[int, util.ModelRef("partner")] + """The ID for the reseller partner.""" - @cached_property - def partner(self) -> partner_module.Partner: - """The reseller partner. + partner_name: Annotated[str, util.ModelRef("partner")] + """The name of the reseller partner.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) + partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + """The reseller partner. - @property - def tier_id(self) -> int: - """The ID for the tier this reseller is under.""" - return self._get_ref_id("tier") + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - @property - def tier_name(self) -> str: - """The name of the tier this reseller is under.""" - return self._get_ref_name("tier") + tier_id: Annotated[int, util.ModelRef("tier")] + """The ID for the tier this reseller is under.""" - @cached_property - def tier(self) -> reseller_tier.ResellerTier: - """The tier this reseller is under. + tier_name: Annotated[str, util.ModelRef("tier")] + """The name of the tier this reseller is under.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.reseller_tiers.get(self.tier_id) + tier: Annotated[reseller_tier.ResellerTier, util.ModelRef("tier")] + """The tier this reseller is under. - _alias_mapping = { - # Key is local alias, value is remote field name. - "demo_project_id": "demo_project", - "partner_id": "partner", - "tier_id": "tier", - } + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class ResellerManager(record_manager_base.RecordManagerBase[Reseller]): env_name = "openstack.reseller" record_class = Reseller + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import partner as partner_module, project, reseller_tier # noqa: E402 diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 274771b..8b805cb 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -15,64 +15,60 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING +from typing_extensions import Annotated -from . import record_base, record_manager_name_base - -if TYPE_CHECKING: - from . import product +from . import record_base, record_manager_name_base, util class ResellerTier(record_base.RecordBase): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" - @property - def discount_product_id(self) -> int: - """The ID of the discount product for the reseller tier.""" - return self._get_ref_id("discount_product") + discount_product_id: Annotated[int, util.ModelRef("discount_product")] + """The ID of the discount product for the reseller tier.""" - @property - def discount_product_name(self) -> str: - """The name of the discount product for the reseller tier.""" - return self._get_ref_name("discount_product") + discount_product_name: Annotated[str, util.ModelRef("discount_product")] + """The name of the discount product for the reseller tier.""" - @cached_property - def discount_product(self) -> product.Product: - """The discount product for the reseller tier. + discount_product: Annotated[ + product.Product, + util.ModelRef("discount_product"), + ] + """The discount product for the reseller tier. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.discount_product_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ free_monthly_credit: float """The amount the reseller gets monthly in credit for demo projects.""" - @property - def free_monthly_credit_product_id(self) -> int: - """The ID of the product to use when adding the free monthly credit - to demo project invoices. - """ - return self._get_ref_id("free_monthly_credit_product") - - @property - def free_monthly_credit_product_name(self) -> str: - """The name of the product to use when adding the free monthly credit - to demo project invoices. - """ - return self._get_ref_name("free_monthly_credit_product") - - @cached_property - def free_monthly_credit_product(self) -> product.Product: - """The product to use when adding the free monthly credit - to demo project invoices. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.free_monthly_credit_product_id) + free_monthly_credit_product_id: Annotated[ + int, + util.ModelRef("free_monthly_credit_product"), + ] + """The ID of the product to use when adding the free monthly credit + to demo project invoices. + """ + + free_monthly_credit_product_name: Annotated[ + str, + util.ModelRef("free_monthly_credit_product"), + ] + """The name of the product to use when adding the free monthly credit + to demo project invoices. + """ + + free_monthly_credit_product: Annotated[ + product.Product, + util.ModelRef("free_monthly_credit_product"), + ] + """The product to use when adding the free monthly credit + to demo project invoices. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ free_support_hours: int """The amount of free support hours the reseller is entitled to @@ -85,15 +81,13 @@ def free_monthly_credit_product(self) -> product.Product: min_usage_threshold: float """The minimum required usage amount for the reseller tier.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "discount_product_id": "discount_product", - "free_monthly_credit_product_id": "free_monthly_credit_product", - } - class ResellerTierManager( record_manager_name_base.NamedRecordManagerBase[ResellerTier], ): env_name = "openstack.reseller.tier" record_class = ResellerTier + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import product # noqa: E402 diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 412ac50..ff2f6e5 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -16,18 +16,11 @@ from __future__ import annotations from datetime import date, datetime -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - currency as currency_module, - partner as partner_module, - project, - sale_order_line, - ) +from . import record_base, record_manager_name_base, util class SaleOrder(record_base.RecordBase): @@ -43,24 +36,18 @@ class SaleOrder(record_base.RecordBase): client_order_ref: Union[str, Literal[False]] """The customer reference for this sale order, if defined.""" - @property - def currency_id(self) -> int: - """The ID for the currency used in this sale order.""" - return self._get_ref_id("currency_id") + currency_id: Annotated[int, util.ModelRef("currency_id")] + """The ID for the currency used in this sale order.""" - @property - def currency_name(self) -> str: - """The name of the currency used in this sale order.""" - return self._get_ref_name("currency_id") + currency_name: Annotated[str, util.ModelRef("currency_id")] + """The name of the currency used in this sale order.""" - @cached_property - def currency(self) -> currency_module.Currency: - """The currency used in this sale order. + currency: Annotated[currency_module.Currency, util.ModelRef("currency_id")] + """The currency used in this sale order. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.currencies.get(self.currency_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ date_order: datetime """The time the sale order was created.""" @@ -88,24 +75,24 @@ def currency(self) -> currency_module.Currency: Generally used for terms and conditions. """ - @property - def order_line_ids(self) -> List[int]: - """A list of IDs for the lines added to the sale order.""" - return self._get_field("order_line") + order_line_ids: Annotated[List[int], util.ModelRef("order_line")] + """A list of IDs for the lines added to the sale order.""" - @cached_property - def order_line(self) -> List[sale_order_line.SaleOrderLine]: - """The lines added to the sale order. + order_line: Annotated[ + List[sale_order_line.SaleOrderLine], + util.ModelRef("order_line"), + ] + """The lines added to the sale order. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.sale_order_lines.list(self.order_line_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ - @property - def order_lines(self) -> List[sale_order_line.SaleOrderLine]: - """An alias for ``order_line``.""" - return self.order_line + order_lines: Annotated[ + List[sale_order_line.SaleOrderLine], + util.FieldAlias("order_line"), + ] + """An alias for ``order_line``.""" os_invoice_date: date """The invoicing date for the invoice that is created @@ -117,53 +104,39 @@ def order_lines(self) -> List[sale_order_line.SaleOrderLine]: from the sale order. """ - @property - def os_project_id(self) -> Optional[int]: - """The ID for the the OpenStack project this sale order was - was generated for. - """ - return self._get_ref_id("os_project", optional=True) + os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + """The ID for the the OpenStack project this sale order was + was generated for. + """ - @property - def os_project_name(self) -> Optional[str]: - """The name of the the OpenStack project this sale order was - was generated for. - """ - return self._get_ref_name("os_project", optional=True) + os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + """The name of the the OpenStack project this sale order was + was generated for. + """ - @cached_property - def os_project(self) -> Optional[project.Project]: - """The OpenStack project this sale order was - was generated for. + os_project: Annotated[ + Optional[project.Project], + util.ModelRef("os_project"), + ] + """The OpenStack project this sale order was + was generated for. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ - @property - def partner_id(self) -> int: - """The ID for the recipient partner for the sale order.""" - return self._get_ref_id("partner_id") + partner_id: Annotated[int, util.ModelRef("partner_id")] + """The ID for the recipient partner for the sale order.""" - @property - def partner_name(self) -> str: - """The name of the recipient partner for the sale order.""" - return self._get_ref_name("partner_id") + partner_name: Annotated[str, util.ModelRef("partner_id")] + """The name of the recipient partner for the sale order.""" - @cached_property - def partner(self) -> partner_module.Partner: - """The recipient partner for the sale order. + partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + """The recipient partner for the sale order. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ state: Literal["draft", "sale", "done", "cancel"] """State of the sale order. @@ -176,15 +149,6 @@ def partner(self) -> partner_module.Partner: * ``cancel`` - Cancelled sale order, can be deleted in most cases """ - _alias_mapping = { - # Key is local alias, value is remote field name. - "currency": "currency_id", - "order_line_ids": "order_line", - "order_lines": "order_line", - "os_project_id": "os_project", - "partner": "partner_id", - } - def action_confirm(self) -> None: """Confirm the sale order.""" self._client.sale_orders.action_confirm(self) @@ -227,3 +191,12 @@ def create_invoices(self, sale_order: Union[int, SaleOrder]) -> None: else sale_order ), ) + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa: E402 + currency as currency_module, + partner as partner_module, + project, + sale_order_line, +) diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index 2729da1..1ad60e7 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -15,68 +15,47 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - account_move_line, - company as company_module, - currency as currency_module, - partner, - product as product_module, - project, - sale_order, - tax as tax_module, - uom, - ) +from . import record_base, record_manager_base, util class SaleOrderLine(record_base.RecordBase): - @property - def company_id(self) -> int: - """The ID for the company this sale order line - was generated for. - """ - return self._get_ref_id("company_id") - - @property - def company_name(self) -> str: - """The name of the company this sale order line - was generated for. - """ - return self._get_ref_name("company_id") - - @cached_property - def company(self) -> company_module.Company: - """The company this sale order line - was generated for. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.companies.get(self.company_id) - - @property - def currency_id(self) -> int: - """The ID for the currency used in this sale order line.""" - return self._get_ref_id("currency_id") - - @property - def currency_name(self) -> str: - """The name of the currency used in this sale order line.""" - return self._get_ref_name("currency_id") - - @cached_property - def currency(self) -> currency_module.Currency: - """The currency used in this sale order line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.currencies.get(self.currency_id) + company_id: Annotated[int, util.ModelRef("company_id")] + """The ID for the company this sale order line + was generated for. + """ + + company_name: Annotated[str, util.ModelRef("company_id")] + """The name of the company this sale order line + was generated for. + """ + + company: Annotated[company_module.Company, util.ModelRef("company_id")] + """The company this sale order line + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + currency_id: Annotated[int, util.ModelRef("currency_id")] + """The ID for the currency used in this sale order line.""" + + currency_name: Annotated[str, util.ModelRef("currency_id")] + """The name of the currency used in this sale order line.""" + + currency: Annotated[ + currency_module.Currency, + util.ModelRef("currency_id"), + ] + """The currency used in this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ discount: float """Discount percentage on the sale order line (0-100).""" @@ -84,22 +63,21 @@ def currency(self) -> currency_module.Currency: display_name: str """Display name for the sale order line in the sale order.""" - @property - def invoice_line_ids(self) -> List[int]: - """A list of IDs for the invoice (account move) lines created - from this sale order line. - """ - return self._get_field("invoice_lines") + invoice_line_ids: Annotated[List[int], util.ModelRef("invoice_lines")] + """A list of IDs for the account move (invoice) lines created + from this sale order line. + """ - @cached_property - def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: - """The invoice (account move) lines created - from this sale order line. + invoice_lines: Annotated[ + List[account_move_line.AccountMoveLine], + util.ModelRef("invoice_lines"), + ] + """The account move (invoice) lines created + from this sale order line. - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.account_move_lines.list(self.invoice_line_ids) + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ invoice_status: Literal["no", "to invoice", "invoiced", "upselling"] """The current invoicing status of this sale order line. @@ -126,72 +104,55 @@ def invoice_lines(self) -> List[account_move_line.AccountMoveLine]: the resource's name. """ - @property - def order_id(self) -> int: - """The ID for the sale order this line is linked to.""" - return self._get_ref_id("order_id") - - @property - def order_name(self) -> str: - """The name of the sale order this line is linked to.""" - return self._get_ref_name("order_id") - - @cached_property - def order(self) -> sale_order.SaleOrder: - """The sale order this line is linked to. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.sale_orders.get(self.order_id) - - @property - def order_partner_id(self) -> int: - """The ID for the recipient partner for the sale order.""" - return self._get_ref_id("order_partner_id") - - @property - def order_partner_name(self) -> str: - """The name of the recipient partner for the sale order.""" - return self._get_ref_name("order_partner_id") - - @cached_property - def order_partner(self) -> partner.Partner: - """The recipient partner for the sale order. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.order_partner_id) - - @property - def os_project_id(self) -> Optional[int]: - """The ID for the the OpenStack project this sale order line was - was generated for. - """ - return self._get_ref_id("os_project", optional=True) - - @property - def os_project_name(self) -> Optional[str]: - """The name of the the OpenStack project this sale order line was - was generated for. - """ - return self._get_ref_name("os_project", optional=True) - - @cached_property - def os_project(self) -> Optional[project.Project]: - """The OpenStack project this sale order line was - was generated for. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.os_project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + order_id: Annotated[int, util.ModelRef("order_id")] + """The ID for the sale order this line is linked to.""" + + order_name: Annotated[str, util.ModelRef("order_id")] + """The name of the sale order this line is linked to.""" + + order: Annotated[sale_order.SaleOrder, util.ModelRef("order_id")] + """The sale order this line is linked to. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + order_partner_id: Annotated[int, util.ModelRef("order_partner_id")] + """The ID for the recipient partner for the sale order.""" + + order_partner_name: Annotated[str, util.ModelRef("order_partner_id")] + """The name of the recipient partner for the sale order.""" + + order_partner: Annotated[ + partner.Partner, + util.ModelRef("order_partner_id"), + ] + """The recipient partner for the sale order. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + """The ID for the the OpenStack project this sale order line was + was generated for. + """ + + os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + """The name of the the OpenStack project this sale order line was + was generated for. + """ + + os_project: Annotated[ + Optional[project.Project], + util.ModelRef("os_project"), + ] + """The OpenStack project this sale order line was + was generated for. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ os_region: Union[str, Literal[False]] """The OpenStack region the sale order line was created from.""" @@ -235,48 +196,36 @@ def os_project(self) -> Optional[project.Project]: price_unit: float """Base unit price, excluding tax, before any discounts.""" - @property - def product_id(self) -> int: - """The ID of the product charged on this sale order line.""" - return self._get_ref_id("product_id") - - @property - def product_name(self) -> str: - """The name of the product charged on this sale order line.""" - return self._get_ref_name("product_id") - - @cached_property - def product(self) -> product_module.Product: - """The product charged on this sale order line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.product_id) - - @property - def product_uom_id(self) -> int: - """The ID for the Unit of Measure for the product being charged in - this sale order line. - """ - return self._get_ref_id("product_uom") - - @property - def product_uom_name(self) -> str: - """The name of the Unit of Measure for the product being charged in - this sale order line. - """ - return self._get_ref_name("product_uom") - - @cached_property - def product_uom(self) -> uom.Uom: - """The Unit of Measure for the product being charged in - this sale order line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.uoms.get(self.product_uom_id) + product_id: Annotated[int, util.ModelRef("product_id")] + """The ID of the product charged on this sale order line.""" + + product_name: Annotated[str, util.ModelRef("product_id")] + """The name of the product charged on this sale order line.""" + + product: Annotated[product_module.Product, util.ModelRef("product_id")] + """The product charged on this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + product_uom_id: Annotated[int, util.ModelRef("product_uom")] + """The ID for the Unit of Measure for the product being charged in + this sale order line. + """ + + product_uom_name: Annotated[str, util.ModelRef("product_uom")] + """The name of the Unit of Measure for the product being charged in + this sale order line. + """ + + product_uom: Annotated[uom.Uom, util.ModelRef("product_uom")] + """The Unit of Measure for the product being charged in + this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ product_uom_qty: float """The product quantity on the sale order line.""" @@ -295,29 +244,23 @@ def product_uom(self) -> uom.Uom: qty_to_invoice: float """The product quantity that still needs to be invoiced.""" - @property - def salesman_id(self) -> int: - """The ID for the salesperson partner assigned - to this sale order line. - """ - return self._get_ref_id("salesman_id") - - @property - def salesman_name(self) -> str: - """The name of the salesperson partner assigned - to this sale order line. - """ - return self._get_ref_name("salesman_id") - - @cached_property - def salesman(self) -> partner.Partner: - """The salesperson partner assigned - to this sale order line. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.salesman_id) + salesman_id: Annotated[int, util.ModelRef("salesman_id")] + """The ID for the salesperson partner assigned + to this sale order line. + """ + + salesman_name: Annotated[str, util.ModelRef("salesman_id")] + """The name of the salesperson partner assigned + to this sale order line. + """ + + salesman: Annotated[partner.Partner, util.ModelRef("salesman_id")] + """The salesperson partner assigned + to this sale order line. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ state: Literal["draft", "sale", "done", "cancel"] """State of the sale order. @@ -330,24 +273,18 @@ def salesman(self) -> partner.Partner: * ``cancel`` - Cancelled sale order, can be deleted """ - @property - def tax_id(self) -> int: - """The ID for the tax used on this sale order line.""" - return self._get_ref_id("tax_id") + tax_id: Annotated[int, util.ModelRef("tax_id")] + """The ID for the tax used on this sale order line.""" - @property - def tax_name(self) -> str: - """The name of the tax used on this sale order line.""" - return self._get_ref_name("tax_id") + tax_name: Annotated[str, util.ModelRef("tax_id")] + """The name of the tax used on this sale order line.""" - @cached_property - def tax(self) -> tax_module.Tax: - """The tax used on this sale order line. + tax: Annotated[tax_module.Tax, util.ModelRef("tax_id")] + """The tax used on this sale order line. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.taxes.get(self.tax_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ untaxed_amount_invoiced: float """The balance, excluding tax, on the sale order line that @@ -359,23 +296,23 @@ def tax(self) -> tax_module.Tax: still needs to be invoiced. """ - _alias_mapping = { - # Key is local alias, value is remote field name. - "company": "company_id", - "currency": "currency_id", - "os_project_id": "os_project", - "invoice_line_ids": "invoice_lines", - "order": "order_id", - "order_partner": "order_partner_id", - "product": "product_id", - "product_uom_id": "product_uom", - "salesman": "salesman_id", - "tax": "tax_id", - } - class SaleOrderLineManager( record_manager_base.RecordManagerBase[SaleOrderLine], ): env_name = "sale.order.line" record_class = SaleOrderLine + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa: E402 + account_move_line, + company as company_module, + currency as currency_module, + partner, + product as product_module, + project, + sale_order, + tax as tax_module, + uom, +) diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index 992452d..19cb2e3 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -16,17 +16,11 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, Literal, Optional +from typing import Literal, Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - partner as partner_module, - project as project_module, - support_subscription_type as support_subscription_type_module, - ) +from . import record_base, record_manager_base, util class SupportSubscription(record_base.RecordBase): @@ -42,105 +36,81 @@ class SupportSubscription(record_base.RecordBase): end_date: date """The end date of the credit.""" - @property - def partner_id(self) -> Optional[int]: - """The ID for the partner linked to this support subscription, - if it is linked to a partner. - - Support subscriptions linked to a partner - cover all projects the partner owns. - """ - return self._get_ref_id("partner", optional=True) - - @property - def partner_name(self) -> Optional[str]: - """The name of thepartner linked to this support subscription, - if it is linked to a partner. - - Support subscriptions linked to a partner - cover all projects the partner owns. - """ - return self._get_ref_name("partner", optional=True) - - @cached_property - def partner(self) -> Optional[partner_module.Partner]: - """The partner linked to this support subscription, - if it is linked to a partner. - - Support subscriptions linked to a partner - cover all projects the partner owns. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.partner_id - return ( - self._client.partners.get(record_id) - if record_id is not None - else None - ) - - @property - def project_id(self) -> Optional[int]: - """The ID of the project this support subscription is for, - if it is linked to a specific project. - """ - return self._get_ref_id("project", optional=True) - - @property - def project_name(self) -> Optional[str]: - """The name of the project this support subscription is for, - if it is linked to a specific project. - """ - return self._get_ref_name("project", optional=True) - - @cached_property - def project(self) -> Optional[project_module.Project]: - """The project this support subscription is for, - if it is linked to a specific project. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + partner_id: Annotated[Optional[int], util.ModelRef("partner")] + """The ID for the partner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + """ + + partner_name: Annotated[Optional[str], util.ModelRef("partner")] + """The name of thepartner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + """ + + partner: Annotated[ + Optional[partner_module.Partner], + util.ModelRef("partner"), + ] + """The partner linked to this support subscription, + if it is linked to a partner. + + Support subscriptions linked to a partner + cover all projects the partner owns. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + project_id: Annotated[Optional[int], util.ModelRef("project")] + """The ID of the project this support subscription is for, + if it is linked to a specific project. + """ + + project_name: Annotated[Optional[str], util.ModelRef("project")] + """The name of the project this support subscription is for, + if it is linked to a specific project. + """ + + project: Annotated[ + Optional[project_module.Project], + util.ModelRef("project"), + ] + """The project this support subscription is for, + if it is linked to a specific project. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ start_date: date """The start date of the credit.""" - @property - def support_subscription_type_id(self) -> int: - """The ID of the type of the support subscription.""" - return self._get_ref_id("support_subscription_type") - - @property - def support_subscription_type_name(self) -> str: - """The name of the type of the support subscription.""" - return self._get_ref_name("support_subscription_type") - - @cached_property - def support_subscription_type( - self, - ) -> support_subscription_type_module.SupportSubscriptionType: - """The type of the support subscription. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.support_subscription_types.get( - self.support_subscription_type_id, - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "partner_id": "partner", - "project_id": "project", - "support_subscription_type_id": "support_subscription_type", - } + support_subscription_type_id: Annotated[ + int, + util.ModelRef("support_subscription_type"), + ] + """The ID of the type of the support subscription.""" + + support_subscription_type_name: Annotated[ + str, + util.ModelRef("support_subscription_type"), + ] + """The name of the type of the support subscription.""" + + support_subscription_type: Annotated[ + support_subscription_type_module.SupportSubscriptionType, + util.ModelRef("support_subscription_type"), + ] + """The type of the support subscription. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class SupportSubscriptionManager( @@ -148,3 +118,11 @@ class SupportSubscriptionManager( ): env_name = "openstack.support_subscription" record_class = SupportSubscription + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa: E402 + partner as partner_module, + project as project_module, + support_subscription_type as support_subscription_type_module, +) diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index 7b2b65d..406bafe 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -15,16 +15,11 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal +from typing import List, Literal -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - product as product_module, - support_subscription as support_subscription_type, - ) +from . import record_base, record_manager_name_base, util class SupportSubscriptionType(record_base.RecordBase): @@ -34,64 +29,45 @@ class SupportSubscriptionType(record_base.RecordBase): name: str """The name of the support subscription type.""" - @property - def product_id(self) -> int: - """The ID for the product to use to invoice - the support subscription. - """ - return self._get_ref_id("product") - - @property - def product_name(self) -> str: - """The name of the product to use to invoice - the support subscription. - """ - return self._get_ref_name("product") - - @cached_property - def product(self) -> product_module.Product: - """The product to use to invoice - the support subscription. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.products.get(self.product_id) + product_id: Annotated[int, util.ModelRef("product")] + """The ID for the product to use to invoice + the support subscription. + """ + + product_name: Annotated[str, util.ModelRef("product")] + """The name of the product to use to invoice + the support subscription. + """ + + product: Annotated[product_module.Product, util.ModelRef("product")] + """The product to use to invoice + the support subscription. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ usage_percent: float """Percentage of usage compared to price (0-100).""" - @property - def support_subscription_ids(self) -> List[int]: - """A list of IDs for the support subscriptions of this type.""" - return self._get_field("support_subscription") - - @cached_property - def support_subscription( - self, - ) -> List[support_subscription_type.SupportSubscription]: - """The list of support subscriptions of this type. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.support_subscriptions.list( - self.support_subscription_ids, - ) - - @cached_property - def support_subscriptions( - self, - ) -> List[support_subscription_type.SupportSubscription]: - """An alias for ``support_subscription``.""" - return self.support_subscription - - _alias_mapping = { - # Key is local alias, value is remote field name. - "product": "product_id", - "support_subscription_ids": "support_subscription", - "support_subscriptions": "support_subscription", - } + support_subscription_ids: Annotated[List[int], util.ModelRef("product")] + """A list of IDs for the support subscriptions of this type.""" + + support_subscription: Annotated[ + List[support_subscription_type.SupportSubscription], + util.ModelRef("product"), + ] + """The list of support subscriptions of this type. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + support_subscriptions: Annotated[ + List[support_subscription_type.SupportSubscription], + util.FieldAlias("support_subscription"), + ] + """An alias for ``support_subscription``.""" class SupportSubscriptionTypeManager( @@ -99,3 +75,10 @@ class SupportSubscriptionTypeManager( ): env_name = "openstack.support_subscription.type" record_class = SupportSubscriptionType + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa :E402 + product as product_module, + support_subscription as support_subscription_type, +) diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 1af56a1..f64d14b 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -15,13 +15,11 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Literal +from typing import Literal -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import company as company_module, tax_group as tax_group_module +from . import record_base, record_manager_name_base, util class Tax(record_base.RecordBase): @@ -46,24 +44,18 @@ class Tax(record_base.RecordBase): to the same analytic account as the invoice line (if any). """ - @property - def company_id(self) -> int: - """The ID for the company this tax is owned by.""" - return self._get_ref_id("company_id") + company_id: Annotated[int, util.ModelRef("company_id")] + """The ID for the company this tax is owned by.""" - @property - def company_name(self) -> str: - """The name of the company this tax is owned by.""" - return self._get_ref_name("company_id") + company_name: Annotated[str, util.ModelRef("company_id")] + """The name of the company this tax is owned by.""" - @cached_property - def company(self) -> company_module.Company: - """The company this tax is owned by. + company: Annotated[company_module.Company, util.ModelRef("company_id")] + """The company this tax is owned by. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.companies.get(self.company_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ country_code: str """The country code for this tax.""" @@ -91,32 +83,30 @@ def company(self) -> company_module.Company: * ``on_payment`` - Due as soon as payment of the invoice is received """ - @property - def tax_group_id(self) -> int: - """The ID for the company partner this tax is owned by.""" - return self._get_ref_id("tax_group_id") - - @property - def tax_group_name(self) -> str: - """The name of the tax_group partner this tax is owned by.""" - return self._get_ref_name("tax_group_id") + tax_group_id: Annotated[int, util.ModelRef("tax_group_id")] + """The ID for the tax group this tax is categorised under.""" - @cached_property - def tax_group(self) -> tax_group_module.TaxGroup: - """The tax_group partner this tax is owned by. + tax_group_name: Annotated[str, util.ModelRef("tax_group_id")] + """The name of the tax group this tax is categorised under.""" - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.tax_groups.get(self.tax_group_id) + tax_group: Annotated[ + tax_group_module.TaxGroup, + util.ModelRef("tax_group_id"), + ] + """The tax group this tax is categorised under. - _alias_mapping = { - # Key is local alias, value is remote field name. - "company": "company_id", - "tax_group": "tax_group_id", - } + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class TaxManager(record_manager_name_base.NamedRecordManagerBase[Tax]): env_name = "account.tax" record_class = Tax + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa: E402 + company as company_module, + tax_group as tax_group_module, +) diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index 9f3a843..1c023d6 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -16,13 +16,11 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, Optional +from typing import Optional -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner as partner_module, project as project_module +from . import record_base, record_manager_base, util class TermDiscount(record_base.RecordBase): @@ -38,101 +36,75 @@ class TermDiscount(record_base.RecordBase): min_commit: float """The minimum commitment for this term discount to apply.""" - @property - def partner_id(self) -> int: - """The ID for the partner that receives this term discount.""" - return self._get_ref_id("partner_id") - - @property - def partner_name(self) -> str: - """The name of the partner that receives this term discount.""" - return self._get_ref_name("partner_id") - - @cached_property - def partner(self) -> partner_module.Partner: - """The partner that receives this term discount. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) - - @property - def project_id(self) -> Optional[int]: - """The ID for the project this term discount applies to, - if it is a project-specific term discount. - - If not set, the term discount applies to all projects - the partner owns. - """ - return self._get_ref_id("project", optional=True) - - @property - def project_name(self) -> Optional[str]: - """The name of the project this term discount applies to, - if it is a project-specific term discount. - - If not set, the term discount applies to all projects - the partner owns. - """ - return self._get_ref_name("project", optional=True) - - @cached_property - def project(self) -> Optional[project_module.Project]: - """The project this term discount applies to, - if it is a project-specific term discount. - - If not set, the term discount applies to all projects - the partner owns. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.project_id - return ( - self._client.projects.get(record_id) - if record_id is not None - else None - ) + partner_id: Annotated[int, util.ModelRef("partner")] + """The ID for the partner that receives this term discount.""" + + partner_name: Annotated[str, util.ModelRef("partner")] + """The name of the partner that receives this term discount.""" + + partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + """The partner that receives this term discount. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + project_id: Annotated[Optional[int], util.ModelRef("project")] + """The ID for the project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + """ + + project_name: Annotated[Optional[str], util.ModelRef("project")] + """The name of the project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + """ + + project: Annotated[ + Optional[project_module.Project], + util.ModelRef("project"), + ] + """The project this term discount applies to, + if it is a project-specific term discount. + + If not set, the term discount applies to all projects + the partner owns. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ start_date: date """The date from which this term discount starts.""" - @property - def superseded_by_id(self) -> Optional[int]: - """The ID for the term discount that supersedes this one, - if superseded. - """ - return self._get_ref_id("superseded_by", optional=True) - - @property - def superseded_by_name(self) -> Optional[str]: - """The name of the term discount that supersedes this one, - if superseded. - """ - return self._get_ref_name("superseded_by", optional=True) - - @cached_property - def superseded_by(self) -> Optional[TermDiscount]: - """The term discount that supersedes this one, - if superseded. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.superseded_by_id - return ( - self._client.term_discounts.get(record_id) - if record_id is not None - else None - ) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "partner_id": "partner", - "project_id": "project", - "superseded_by_id": "superseded_by", - } + superseded_by_id: Annotated[Optional[int], util.ModelRef("superseded_by")] + """The ID for the term discount that supersedes this one, + if superseded. + """ + + superseded_by_name: Annotated[ + Optional[str], + util.ModelRef("superseded_by"), + ] + """The name of the term discount that supersedes this one, + if superseded. + """ + + superseded_by: Annotated[ + Optional[TermDiscount], + util.ModelRef("superseded_by"), + ] + """The term discount that supersedes this one, + if superseded. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ class TermDiscountManager( @@ -140,3 +112,10 @@ class TermDiscountManager( ): env_name = "openstack.term_discount" record_class = TermDiscount + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa :E402 + partner as partner_module, + project as project_module, +) diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index 421710d..ffbe254 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -16,13 +16,11 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, Literal, Union +from typing import Literal, Union -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import partner as partner_module +from . import record_base, record_manager_base, util class Trial(record_base.RecordBase): @@ -40,34 +38,27 @@ class Trial(record_base.RecordBase): end_date: date """The end date of this trial.""" - @property - def partner_id(self) -> int: - """The ID for the target partner for this trial.""" - return self._get_ref_id("partner") + partner_id: Annotated[int, util.ModelRef("partner")] + """The ID for the target partner for this trial.""" - @property - def partner_name(self) -> str: - """The name of the target partner for this trial.""" - return self._get_ref_name("partner") + partner_name: Annotated[str, util.ModelRef("partner")] + """The name of the target partner for this trial.""" - @cached_property - def partner(self) -> partner_module.Partner: - """The target partner for this trial. + partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + """The target partner for this trial. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.partners.get(self.partner_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ start_date: date """The start date of this trial.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "partner_id": "partner", - } - class TrialManager(record_manager_base.RecordManagerBase[Trial]): env_name = "openstack.trial" record_class = Trial + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import partner as partner_module # noqa: E402 diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index db0ea2d..1b56269 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -15,37 +15,32 @@ from __future__ import annotations -from functools import cached_property -from typing import TYPE_CHECKING, Literal +from typing import Literal -from . import record_base, record_manager_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import uom_category +from . import record_base, record_manager_base, util class Uom(record_base.RecordBase): active: bool """Whether or not this Unit of Measure is active (enabled).""" - @property - def category_id(self) -> int: - """The ID for the category this Unit of Measure is classified as.""" - return self._get_ref_id("category_id") + category_id: Annotated[int, util.ModelRef("category_id")] + """The ID for the category this Unit of Measure is classified as.""" - @property - def category_name(self) -> str: - """The name of the category this Unit of Measure is classified as.""" - return self._get_ref_name("category_id") + category_name: Annotated[str, util.ModelRef("category_id")] + """The name of the category this Unit of Measure is classified as.""" - @cached_property - def category(self) -> uom_category.UomCategory: - """The category this Unit of Measure is classified as. + category: Annotated[ + uom_category.UomCategory, + util.ModelRef("category_id"), + ] + """The category this Unit of Measure is classified as. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - return self._client.uom_categories.get(self.category_id) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ factor: float """How much bigger or smaller this unit is compared to the reference @@ -82,6 +77,7 @@ def category(self) -> uom_category.UomCategory: uom_type: Literal["bigger", "reference", "smaller"] """The type of the Unit of Measure (UoM). + This determines its relationship with other UoMs in the same category. Values: @@ -91,12 +87,11 @@ def category(self) -> uom_category.UomCategory: * ``smaller`` - Smaller than the reference Unit of Measure """ - _alias_mapping = { - # Key is local alias, value is remote field name. - "category": "category_id", - } - class UomManager(record_manager_base.RecordManagerBase[Uom]): env_name = "uom.uom" record_class = Uom + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import uom_category # noqa: E402 diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index f58cf19..c274892 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -234,41 +234,3 @@ def decode_value(type_hint: Type[T], value: Any) -> T: # Base case: Return the passed value unmodified. return value - - -# def encode_create_value(annotation: Type[Any], value: Any) -> Any: -# """_summary_ - -# :param value: _description_ -# :type value: Any -# :return: _description_ -# :rtype: Any -# """ - -# type_tree = get_type_tree(annotation) -# value_type = type_tree[-1] - -# if issubclass(value_type, base.RecordBase): -# if isinstance(value, base.RecordBase): -# return value.id -# elif isinstance(value, dict): -# return { -# value_type._resolve_alias(k): encode_create_value( -# value_type.__annotations__[k], -# v, -# ) -# for k, v in value.items() -# } -# if ( -# value_type in (date, datetime) -# and isinstance(value, (date, datetime)) -# ): -# return value.isoformat() -# if ( -# value_type is list -# and isinstance(value, (list, set, tuple)) -# ): -# v_type = get_type_args(type_tree[-2])[0] -# return [encode_create_value(v_type, v) for v in value] - -# return value diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 49e8d02..2298784 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -15,45 +15,40 @@ from __future__ import annotations -from functools import cached_property from typing import List, Optional, Union -from . import ( - customer_group as customer_group_module, - record_base, - record_manager_base, -) +from typing_extensions import Annotated + +from . import record_base, record_manager_base, util class VolumeDiscountRange(record_base.RecordBase): - @property - def customer_group_id(self) -> Optional[int]: - """The ID for the customer group this volume discount range - applies to, if a specific customer group is set. - """ - return self._get_ref_id("customer_group", optional=True) + customer_group_id: Annotated[ + Optional[int], + util.ModelRef("customer_group"), + ] + """The ID for the customer group this volume discount range + applies to, if a specific customer group is set. + """ - @property - def customer_group_name(self) -> Optional[str]: - """The name of the customer group this volume discount range - applies to, if a specific customer group is set. - """ - return self._get_ref_name("customer_group", optional=True) + customer_group_name: Annotated[ + Optional[str], + util.ModelRef("customer_group"), + ] + """The name of the customer group this volume discount range + applies to, if a specific customer group is set. + """ - @cached_property - def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: - """The customer group this volume discount range - applies to, if a specific customer group is set. + customer_group: Annotated[ + Optional[customer_group_module.CustomerGroup], + util.ModelRef("customer_group"), + ] + """The customer group this volume discount range + applies to, if a specific customer group is set. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.customer_group_id - return ( - self._client.customer_groups.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ discount_percent: float """Discount percentage of this volume discount range (0-100).""" @@ -75,11 +70,6 @@ def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: use_max: bool """Use the ``max`` field, if defined.""" - _alias_mapping = { - # Key is local alias, value is remote field name. - "customer_group_id": "customer_group", - } - class VolumeDiscountRangeManager( record_manager_base.RecordManagerBase[VolumeDiscountRange], @@ -138,3 +128,7 @@ def get_for_charge( if not found_ranges: return None return sorted(found_ranges, key=lambda r: r.discount_percent)[-1] + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import customer_group as customer_group_module # noqa: E402 diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 3eb23e0..02ce983 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -16,19 +16,11 @@ from __future__ import annotations from datetime import date -from functools import cached_property -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union -from . import record_base, record_manager_name_base +from typing_extensions import Annotated -if TYPE_CHECKING: - from . import ( - credit_type as credit_type_module, - customer_group as customer_group_module, - grant_type as grant_type_module, - partner, - partner_category, - ) +from . import record_base, record_manager_name_base, util class VoucherCode(record_base.RecordBase): @@ -43,71 +35,61 @@ class VoucherCode(record_base.RecordBase): created by the voucher code. """ - @property - def credit_type_id(self) -> Optional[int]: - """The ID of the credit type to use, if a credit is to be - created by this voucher code. - """ - return self._get_ref_id("credit_type", optional=True) - - @property - def credit_type_name(self) -> Optional[str]: - """The name of the credit type to use, if a credit is to be - created by this voucher code. - """ - return self._get_ref_name("credit_type", optional=True) - - @cached_property - def credit_type(self) -> Optional[credit_type_module.CreditType]: - """The credit type to use, if a credit is to be - created by this voucher code. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.credit_type_id - return ( - self._client.credit_types.get(record_id) - if record_id is not None - else None - ) + credit_type_id: Annotated[Optional[int], util.ModelRef("credit_type")] + """The ID of the credit type to use, if a credit is to be + created by this voucher code. + """ + + credit_type_name: Annotated[Optional[str], util.ModelRef("credit_type")] + """The name of the credit type to use, if a credit is to be + created by this voucher code. + """ + + credit_type: Annotated[ + Optional[credit_type_module.CreditType], + util.ModelRef("credit_type"), + ] + """The credit type to use, if a credit is to be + created by this voucher code. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ credit_duration: int """The duration of the credit, in days, if a credit is to be created by the voucher code. """ - @property - def customer_group_id(self) -> Optional[int]: - """The ID of the customer group this voucher code is available to. + customer_group_id: Annotated[ + Optional[int], + util.ModelRef("customer_group"), + ] + """The ID of the customer group this voucher code is available to. - If not set, the voucher code is available to all customers. - """ - return self._get_ref_id("customer_group", optional=True) + If not set, the voucher code is available to all customers. + """ - @property - def customer_group_name(self) -> Optional[str]: - """The name of the customer group this voucher code is available to. + customer_group_name: Annotated[ + Optional[str], + util.ModelRef("customer_group"), + ] + """The name of the customer group this voucher code is available to. - If not set, the voucher code is available to all customers. - """ - return self._get_ref_name("customer_group", optional=True) + If not set, the voucher code is available to all customers. + """ - @cached_property - def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: - """The customer group this voucher code is available to. + customer_group: Annotated[ + Optional[customer_group_module.CustomerGroup], + util.ModelRef("customer_group"), + ] + """The customer group this voucher code is available to. - If not set, the voucher code is available to all customers. + If not set, the voucher code is available to all customers. - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.customer_group_id - return ( - self._client.customer_groups.get(record_id) - if record_id is not None - else None - ) + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ expiry_date: date """The date the voucher code expires.""" @@ -117,34 +99,26 @@ def customer_group(self) -> Optional[customer_group_module.CustomerGroup]: created by the voucher code. """ - @property - def grant_type_id(self) -> Optional[int]: - """The ID of the grant type to use, if a grant is to be - created by this voucher code. - """ - return self._get_ref_id("grant_type", optional=True) - - @property - def grant_type_name(self) -> Optional[str]: - """The name of the grant type to use, if a grant is to be - created by this voucher code. - """ - return self._get_ref_name("grant_type", optional=True) - - @cached_property - def grant_type(self) -> Optional[grant_type_module.GrantType]: - """The grant type to use, if a grant is to be - created by this voucher code. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.grant_type_id - return ( - self._client.grant_types.get(record_id) - if record_id is not None - else None - ) + grant_type_id: Annotated[Optional[int], util.ModelRef("grant_type")] + """The ID of the grant type to use, if a grant is to be + created by this voucher code. + """ + + grant_type_name: Annotated[Optional[str], util.ModelRef("grant_type")] + """The name of the grant type to use, if a grant is to be + created by this voucher code. + """ + + grant_type: Annotated[ + Optional[grant_type_module.GrantType], + util.ModelRef("grant_type"), + ] + """The grant type to use, if a grant is to be + created by this voucher code. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ grant_duration: int """The duration of the grant, in days, if a grant is to be @@ -171,57 +145,42 @@ def grant_type(self) -> Optional[grant_type_module.GrantType]: If unset, use the default quota size. """ - @property - def sales_person_id(self) -> Optional[int]: - """The ID for the salesperson responsible for this - voucher code, if assigned. - """ - return self._get_ref_id("sales_person", optional=True) - - @property - def sales_person_name(self) -> Optional[str]: - """The name of the salesperson responsible for this - voucher code, if assigned. - """ - return self._get_ref_name("sales_person", optional=True) - - @cached_property - def sales_person(self) -> Optional[partner.Partner]: - """The salesperson responsible for this - voucher code, if assigned. - - This fetches the full record from Odoo once, - and caches it for subsequent accesses. - """ - record_id = self.sales_person_id - return ( - self._client.partners.get(record_id) - if record_id is not None - else None - ) - - @property - def tag_ids(self) -> List[int]: - """A list of IDs for the tags (partner categories) to assign - to partners for new accounts that signed up using this voucher code. - """ - return self._get_field("tags") - - @cached_property - def tags(self) -> List[partner_category.PartnerCategory]: - """The list of tags (partner categories) to assign - to partners for new accounts that signed up using this voucher code. - - This fetches the full records from Odoo once, - and caches them for subsequent accesses. - """ - return self._client.partner_categories.list(self.tag_ids) - - _alias_mapping = { - # Key is local alias, value is remote field name. - "sales_person_id": "sales_person", - "tag_ids": "tags", - } + sales_person_id: Annotated[Optional[int], util.ModelRef("sales_person")] + """The ID for the salesperson responsible for this + voucher code, if assigned. + """ + + sales_person_name: Annotated[Optional[str], util.ModelRef("sales_person")] + """The name of the salesperson responsible for this + voucher code, if assigned. + """ + + sales_person: Annotated[ + Optional[partner.Partner], + util.ModelRef("sales_person"), + ] + """The salesperson responsible for this + voucher code, if assigned. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + tag_ids: Annotated[List[int], util.ModelRef("tags")] + """A list of IDs for the tags (partner categories) to assign + to partners for new accounts that signed up using this voucher code. + """ + + tags: Annotated[ + List[partner_category.PartnerCategory], + util.ModelRef("tags"), + ] + """The list of tags (partner categories) to assign + to partners for new accounts that signed up using this voucher code. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ class VoucherCodeManager( @@ -229,3 +188,13 @@ class VoucherCodeManager( ): env_name = "openstack.voucher_code" record_class = VoucherCode + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from . import ( # noqa: E402 + credit_type as credit_type_module, + customer_group as customer_group_module, + grant_type as grant_type_module, + partner, + partner_category, +) From 4ae22bf4d4eaa931e6c7fab23e56c1be66dc69dc Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 13:43:25 +1200 Subject: [PATCH 18/87] Comment --- openstack_odooclient/managers/record_manager_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 18db5c6..fe7c35a 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -536,7 +536,7 @@ def _encode_create_field( f"{annotation}" ), ) - # For non-annotated fields, encode the value based on its type hint. + # For regular fields, encode the value based on its type hint. return ( remote_field, self._encode_create_value(type_hint=type_hint, value=value), From 98fbb7d8e5d3effb2a8ca07f58bd8c5c28b38116 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 17:04:59 +1200 Subject: [PATCH 19/87] Resolve aliases before the local and remote fields --- openstack_odooclient/managers/record_base.py | 13 +++++++++---- .../managers/record_manager_base.py | 16 +++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index fb569f9..aa9e5cd 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -205,11 +205,16 @@ def _get_field(self, name: str) -> Any: @classmethod def _resolve_alias(cls, alias: str) -> str: + type_hints = get_type_hints(cls, include_extras=True) + if alias not in type_hints: + return alias + type_hint = type_hints[alias] + if get_type_origin(type_hint) is not Annotated: + return alias + for annotation in get_type_args(type_hint)[1:]: + if isinstance(annotation, FieldAlias): + return annotation.field return alias - # return cls._alias_mapping.get( - # alias, - # cls._base_alias_mapping.get(alias, alias), - # ) def __getattr__(self, name: str) -> Any: # If the field value has already been decoded, diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index fe7c35a..e6f397e 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -27,7 +27,7 @@ from ..exceptions import RecordNotFoundError from .record_base import RecordBase -from .util import FieldAlias, ModelRef, get_mapped_field +from .util import ModelRef, get_mapped_field if TYPE_CHECKING: from typing import ( @@ -443,8 +443,10 @@ def _encode_create_field( value: Any, ) -> Tuple[str, Any]: # Fetch the local and remote representations of the given field. - local_field = self._get_local_field(field) - remote_field = self._get_remote_field(field) + # Field aliases are resolved at this point. + orig_field = self._resolve_alias(field) + local_field = self._get_local_field(orig_field) + remote_field = self._get_remote_field(orig_field) # If there is no type hint for the given field, map the value # to the field unchanged. if local_field not in type_hints: @@ -458,14 +460,6 @@ def _encode_create_field( annotations = type_args[1:] if len(annotations) == 1: annotation = annotations[0] - # If this field is a field alias, - # recursively encode the field as the target field. - if isinstance(annotation, FieldAlias): - return self._encode_create_field( - type_hints=type_hints, - field=annotation.field, - value=value, - ) # If this field is a model ref, encode the model ref # according to the given value's type, and map the result # to the Odoo model's ref field name. From 2b0d2ca5b5d0986de3c248343246d881a8a16857 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 17:57:45 +1200 Subject: [PATCH 20/87] Fix encoding model ref lists and creating new sub-models, improve encoding dates/times/datetimes --- .../managers/record_manager_base.py | 75 ++++++++++++++----- openstack_odooclient/managers/util.py | 7 ++ 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index e6f397e..4bbe152 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -15,7 +15,7 @@ from __future__ import annotations -from datetime import date, datetime +from datetime import date, datetime, time from typing import TYPE_CHECKING, Generic, TypeVar, overload from typing_extensions import ( @@ -27,7 +27,13 @@ from ..exceptions import RecordNotFoundError from .record_base import RecordBase -from .util import ModelRef, get_mapped_field +from .util import ( + DEFAULT_SERVER_DATE_FORMAT, + DEFAULT_SERVER_DATETIME_FORMAT, + DEFAULT_SERVER_TIME_FORMAT, + ModelRef, + get_mapped_field, +) if TYPE_CHECKING: from typing import ( @@ -464,6 +470,26 @@ def _encode_create_field( # according to the given value's type, and map the result # to the Odoo model's ref field name. if isinstance(annotation, ModelRef): + # NOTE(callumdickinson): JSON RPC API model link + # definition reference. + # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#odoo.models.Model.write + # * (0, 0, {values}) - Link to a new record that needs to + # be created with the given values dictionary. + # * (1, id, {values}) - Update the linked record *id* + # (write *values* to it). + # * (2, id) - Remove and delete the linked record *id*. + # Calls ``unlink`` on ID, deleting the object + # completely, and the link to it as well. + # * (3, id) - Cut the link to the linked record *id*. + # Deletes the relationship between the two objects, + # but does not delete the target object itself. + # * (4, id) - Link to existing record *id* + # (adds a relationship). + # * (5) - Unlink all record links. + # Functions like using (3,ID) for all linked records. + # * (6, 0, [ids]) - Replace the list of linked IDs + # with *ids*. Functions like using (5), then (4, id) + # for each ID in the list of IDs. model_ref_field = self._get_remote_field(annotation.field) # If the field is a list of multiple model refs, # iterate over the given value and decode the elements @@ -471,18 +497,27 @@ def _encode_create_field( if get_type_origin(attr_type) is list: if not value: return (model_ref_field, []) - remote_values: List[Union[int, Dict[str, Any]]] = [] + remote_values: List[ + Union[ + Tuple[int, int], + Tuple[int, int, Dict[str, Any]], + ], + ] = [] for v in value: if isinstance(v, int): - remote_values.append(v) + remote_values.append((4, v)) elif isinstance(v, RecordBase): - remote_values.append(v.id) + remote_values.append((4, v.id)) elif isinstance(v, dict): manager = self._client._record_manager_mapping[ attr_type ] remote_values.append( - manager._encode_create_fields(value), + ( + 0, + 0, + manager._encode_create_fields(value), + ), ) else: raise ValueError( @@ -509,14 +544,19 @@ def _encode_create_field( # using the record class's manager object, # and assign it to the parent record so they can # both be created. + # TODO(callumdickinson): Check that this works. if isinstance(value, dict): return ( model_ref_field, - ( - self._client._record_manager_mapping[ - attr_type - ]._encode_create_fields(value) - ), + [ + ( + 0, + 0, + self._client._record_manager_mapping[ + attr_type + ]._encode_create_fields(value), + ), + ], ) raise ValueError( ( @@ -538,11 +578,12 @@ def _encode_create_field( def _encode_create_value(self, type_hint: Type[Any], value: Any) -> Any: value_type = get_type_origin(type_hint) - if value_type in (date, datetime) and isinstance( - value, - (date, datetime), - ): - return value.isoformat() + if value_type is date and isinstance(value, date): + return value.strftime(DEFAULT_SERVER_DATE_FORMAT) + if value_type is time and isinstance(value, time): + return value.strftime(DEFAULT_SERVER_TIME_FORMAT) + if value_type is datetime and isinstance(value, datetime): + return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if value_type is list and isinstance(value, (list, set, tuple)): v_type = get_type_args(type_hint)[0] return [self._encode_create_value(v_type, v) for v in value] @@ -613,7 +654,7 @@ def _encode_field(self, field: str) -> str: def _encode_value(self, value: Any) -> Any: if isinstance(value, RecordBase): return value.id - if isinstance(value, (date, datetime)): + if isinstance(value, (date, time, datetime)): return value.isoformat() if isinstance(value, (list, set, tuple)): return [self._encode_value(v) for v in value] diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index c274892..28e6eee 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -38,6 +38,13 @@ T = TypeVar("T") +# Same values as defined in odoo.tools.misc. +DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" +DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S" +DEFAULT_SERVER_DATETIME_FORMAT = ( + f"{DEFAULT_SERVER_DATE_FORMAT} {DEFAULT_SERVER_TIME_FORMAT}" +) + @dataclass(frozen=True) class FieldAlias: From 52026c32e531aa815e0cf44e62252bae4ded8151 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 18:02:51 +1200 Subject: [PATCH 21/87] Parse list value type to pass to record manager mapping --- openstack_odooclient/managers/record_manager_base.py | 3 ++- openstack_odooclient/managers/util.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 4bbe152..5d42fac 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -495,6 +495,7 @@ def _encode_create_field( # iterate over the given value and decode the elements # appropriately. if get_type_origin(attr_type) is list: + value_type = get_type_args(attr_type)[0] if not value: return (model_ref_field, []) remote_values: List[ @@ -510,7 +511,7 @@ def _encode_create_field( remote_values.append((4, v.id)) elif isinstance(v, dict): manager = self._client._record_manager_mapping[ - attr_type + value_type ] remote_values.append( ( diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index 28e6eee..620a912 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -31,8 +31,6 @@ get_origin as get_type_origin, ) -# from . import base - if TYPE_CHECKING: from typing import Any, List, Mapping, Optional From 0938bee01982acf7c1888d5381c4b31bf930bf8d Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 18:05:29 +1200 Subject: [PATCH 22/87] Pass correct value --- openstack_odooclient/managers/record_manager_base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 5d42fac..e5cd580 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -514,11 +514,7 @@ def _encode_create_field( value_type ] remote_values.append( - ( - 0, - 0, - manager._encode_create_fields(value), - ), + (0, 0, manager._encode_create_fields(v)), ) else: raise ValueError( From e2e87e5387d4dd953c8befbd3085779275e8d0df Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 18:17:38 +1200 Subject: [PATCH 23/87] Organise code, add TODOs --- .../managers/record_manager_base.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index e5cd580..944393f 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -386,6 +386,25 @@ def search( return self.list(ids, fields=fields, as_dict=as_dict) return [] # type: ignore[return-value] + def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: + # TODO(callumdickinson): Parse nested field references + # (e.g. "product.categ_id") in filters. + # TODO(callumdickinson): Replace usage of _encode_value + # with _encode_create_value, and rename _encode_create_value + # to _encode_value. + _filters: List[Any] = [] + for f in filters: + if isinstance(f, tuple): + _filter = ( + self._encode_field(f[0]), # Field name. + f[1], # Filter operator (=, >=, in, etc). + self._encode_value(f[2]), # Possible value(s). + ) + else: + _filter = f + _filters.append(_filter) + return _filters + def create(self, **fields) -> int: """Create a new record, using the specified keyword arguments as input fields. @@ -450,9 +469,8 @@ def _encode_create_field( ) -> Tuple[str, Any]: # Fetch the local and remote representations of the given field. # Field aliases are resolved at this point. - orig_field = self._resolve_alias(field) - local_field = self._get_local_field(orig_field) - remote_field = self._get_remote_field(orig_field) + local_field = self._decode_field(field) + remote_field = self._encode_field(field) # If there is no type hint for the given field, map the value # to the field unchanged. if local_field not in type_hints: @@ -505,6 +523,7 @@ def _encode_create_field( ], ] = [] for v in value: + # TODO(callumdickinson): Check if this works. if isinstance(v, int): remote_values.append((4, v)) elif isinstance(v, RecordBase): @@ -568,24 +587,12 @@ def _encode_create_field( ), ) # For regular fields, encode the value based on its type hint. + # TODO(callumdickinson): Rename _encode_create_value to _encode_value. return ( remote_field, self._encode_create_value(type_hint=type_hint, value=value), ) - def _encode_create_value(self, type_hint: Type[Any], value: Any) -> Any: - value_type = get_type_origin(type_hint) - if value_type is date and isinstance(value, date): - return value.strftime(DEFAULT_SERVER_DATE_FORMAT) - if value_type is time and isinstance(value, time): - return value.strftime(DEFAULT_SERVER_TIME_FORMAT) - if value_type is datetime and isinstance(value, datetime): - return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - if value_type is list and isinstance(value, (list, set, tuple)): - v_type = get_type_args(type_hint)[0] - return [self._encode_create_value(v_type, v) for v in value] - return value - def unlink( self, *records: Union[Record, int, Iterable[Union[Record, int]]], @@ -645,10 +652,15 @@ def _get_local_field(self, field: str) -> str: def _resolve_alias(self, alias: str) -> str: return self.record_class._resolve_alias(alias) + def _decode_field(self, field: str) -> str: + return self._get_local_field(self._resolve_alias(field)) + def _encode_field(self, field: str) -> str: return self._get_remote_field(self._resolve_alias(field)) def _encode_value(self, value: Any) -> Any: + # TODO: Replace with _encode_create_value, + # and rename _encode_create_value to _encode_value. if isinstance(value, RecordBase): return value.id if isinstance(value, (date, time, datetime)): @@ -657,18 +669,15 @@ def _encode_value(self, value: Any) -> Any: return [self._encode_value(v) for v in value] return value - def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: - # TODO(callumdickinson): Parse nested field references - # (e.g. "product.categ_id") in filters. - _filters: List[Any] = [] - for f in filters: - if isinstance(f, tuple): - _filter = ( - self._encode_field(f[0]), # Field name. - f[1], # Filter operator (=, >=, in, etc). - self._encode_value(f[2]), # Possible value(s). - ) - else: - _filter = f - _filters.append(_filter) - return _filters + def _encode_create_value(self, type_hint: Type[Any], value: Any) -> Any: + value_type = get_type_origin(type_hint) + if value_type is date and isinstance(value, date): + return value.strftime(DEFAULT_SERVER_DATE_FORMAT) + if value_type is time and isinstance(value, time): + return value.strftime(DEFAULT_SERVER_TIME_FORMAT) + if value_type is datetime and isinstance(value, datetime): + return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + if value_type is list and isinstance(value, (list, set, tuple)): + v_type = get_type_args(type_hint)[0] + return [self._encode_create_value(v_type, v) for v in value] + return value From 732bea4a682f131d09ea0cb9175f09c1e85b1161 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 14 Jun 2024 18:29:00 +1200 Subject: [PATCH 24/87] Add error handler for unexpected number of annotations --- openstack_odooclient/managers/record_manager_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 944393f..5689692 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -586,6 +586,13 @@ def _encode_create_field( f"{annotation}" ), ) + raise ValueError( + ( + f"More annotations than expected on field '{field}' " + f"(found {len(annotations)}, expected 1): " + f"{annotations}" + ), + ) # For regular fields, encode the value based on its type hint. # TODO(callumdickinson): Rename _encode_create_value to _encode_value. return ( From 4ae947f5a2f43e82125ebef85893150797bc8e33 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 10:50:21 +1200 Subject: [PATCH 25/87] Implement recursive filter field and value encoding --- openstack_odooclient/client.py | 2 +- openstack_odooclient/managers/record_base.py | 39 ++- .../managers/record_manager_base.py | 310 ++++++++++-------- openstack_odooclient/managers/util.py | 57 +++- 4 files changed, 245 insertions(+), 163 deletions(-) diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 4311c3e..24882f6 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -192,7 +192,7 @@ def __init__( opener=opener, ) self._odoo.login(database, username, password) - # Create aninternal mapping between record classes and their managers. + # Create an internal mapping between record classes and their managers. # This is populated by the manager classes themselves when created, # and used when converting model references on record objects into # # new record objects. diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index aa9e5cd..e38fd33 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -22,9 +22,11 @@ TYPE_CHECKING, Any, Dict, + List, Literal, Optional, Sequence, + Set, Type, Union, ) @@ -204,17 +206,34 @@ def _get_field(self, name: str) -> Any: raise AttributeError(str(err)) from None @classmethod - def _resolve_alias(cls, alias: str) -> str: + def _resolve_alias(cls, field: str) -> str: type_hints = get_type_hints(cls, include_extras=True) - if alias not in type_hints: - return alias - type_hint = type_hints[alias] - if get_type_origin(type_hint) is not Annotated: - return alias - for annotation in get_type_args(type_hint)[1:]: - if isinstance(annotation, FieldAlias): - return annotation.field - return alias + if field not in type_hints: + return field + # NOTE(callumdickinson): Continually resolve field aliases + # until we get to a field that is not an alias. + resolved_aliases: Set[str] = set() + alias_chain: List[str] = [] + annotation = FieldAlias.get(type_hints[field]) + while annotation: + # Check if field aliases loop back on each other. + if field in resolved_aliases: + raise ValueError( + ( + "Found recursive field alias definitions " + f"on {cls.__name__}: {' -> '.join(alias_chain)}" + ), + ) + resolved_aliases.add(field) + alias_chain.append(field) + # Resolve the target field from the alias annotation, + # and try to fetch the target field's annotation to check + # if it is also an alias. + field = annotation.field + if field not in type_hints: + break + annotation = FieldAlias.get(type_hints[field]) + return field def __getattr__(self, name: str) -> Any: # If the field value has already been decoded, diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 5689692..5594caf 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -33,6 +33,7 @@ DEFAULT_SERVER_TIME_FORMAT, ModelRef, get_mapped_field, + is_subclass, ) if TYPE_CHECKING: @@ -387,24 +388,73 @@ def search( return [] # type: ignore[return-value] def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: - # TODO(callumdickinson): Parse nested field references - # (e.g. "product.categ_id") in filters. - # TODO(callumdickinson): Replace usage of _encode_value - # with _encode_create_value, and rename _encode_create_value - # to _encode_value. _filters: List[Any] = [] + type_hints = get_type_hints(self.record_class, include_extras=True) for f in filters: if isinstance(f, tuple): - _filter = ( - self._encode_field(f[0]), # Field name. - f[1], # Filter operator (=, >=, in, etc). - self._encode_value(f[2]), # Possible value(s). + field_type, field_name = self._encode_filter_field( + type_hints=type_hints, + field=f[0], ) + operator = f[1] + value = self._encode_value(type_hint=field_type, value=f[2]) + _filter = (field_name, operator, value) else: _filter = f _filters.append(_filter) return _filters + def _encode_filter_field( + self, + type_hints: Mapping[str, Any], + field: str, + ) -> Tuple[Any, str]: + # The field reference in a filter may be nested. + # Split the reference by the delimiter (.), + # so we can perform a recursive lookup of the correct field + # to return. + field_refs = field.split(".") + # Handle nested field references. + # These are mapped to model refs in the Odoo client, + # with the remote equivalent of the field names encoded + # in the return value. + # If no model ref is found for the given field reference, + # the type will be set to Any. + if len(field_refs) > 1: + local_field = self._decode_field(field_refs[0]) + remote_field = self._encode_field(field_refs[0]) + if local_field not in type_hints: + return (Any, f"{remote_field}.{'.'.join(field_refs[1:])}") + type_hint: Any = type_hints[local_field] + model_ref = ModelRef.get(type_hint) + if model_ref: + value_type = get_type_args(type_hint)[0] + type_hint, remote_field_refs = ( + self._client._record_manager_mapping[ + value_type + ]._encode_filter_field( + type_hints=get_type_hints(value_type), + field=".".join(field_refs[1:]), + ) + ) + return (type_hint, f"{remote_field}.{remote_field_refs}") + return (Any, f"{remote_field}.{'.'.join(field_refs[1:])}") + # Base base: The field reference is not nested + # (references a local field on this manager's record class.) + # Fetch the local and remote representations of the given field. + # Field aliases are resolved at this point. + local_field = self._decode_field(field) + remote_field = self._encode_field(field) + # If there is no type hint defined for the given field, + # return the Any type to denote that no processing should be done. + if local_field not in type_hints: + return (Any, remote_field) + type_hint = type_hints[local_field] + # If the type hint is annotated, get the original data type. + if get_type_origin(type_hint) is Annotated: + return (get_type_args(type_hint)[0], remote_field) + return (type_hint, remote_field) + def create(self, **fields) -> int: """Create a new record, using the specified keyword arguments as input fields. @@ -463,7 +513,7 @@ def _encode_create_fields( def _encode_create_field( self, - type_hints: Mapping[str, Type[Any]], + type_hints: Mapping[str, Any], field: str, value: Any, ) -> Tuple[str, Any]: @@ -477,127 +527,105 @@ def _encode_create_field( return (remote_field, value) # Fetch the type hint for parsing. type_hint = type_hints[local_field] - # Perform special handling of annotated fields. - if get_type_origin(type_hint) is Annotated: - type_args = get_type_args(type_hint) - attr_type: Type[Any] = type_args[0] - annotations = type_args[1:] - if len(annotations) == 1: - annotation = annotations[0] - # If this field is a model ref, encode the model ref - # according to the given value's type, and map the result - # to the Odoo model's ref field name. - if isinstance(annotation, ModelRef): - # NOTE(callumdickinson): JSON RPC API model link - # definition reference. - # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#odoo.models.Model.write - # * (0, 0, {values}) - Link to a new record that needs to - # be created with the given values dictionary. - # * (1, id, {values}) - Update the linked record *id* - # (write *values* to it). - # * (2, id) - Remove and delete the linked record *id*. - # Calls ``unlink`` on ID, deleting the object - # completely, and the link to it as well. - # * (3, id) - Cut the link to the linked record *id*. - # Deletes the relationship between the two objects, - # but does not delete the target object itself. - # * (4, id) - Link to existing record *id* - # (adds a relationship). - # * (5) - Unlink all record links. - # Functions like using (3,ID) for all linked records. - # * (6, 0, [ids]) - Replace the list of linked IDs - # with *ids*. Functions like using (5), then (4, id) - # for each ID in the list of IDs. - model_ref_field = self._get_remote_field(annotation.field) - # If the field is a list of multiple model refs, - # iterate over the given value and decode the elements - # appropriately. - if get_type_origin(attr_type) is list: - value_type = get_type_args(attr_type)[0] - if not value: - return (model_ref_field, []) - remote_values: List[ - Union[ - Tuple[int, int], - Tuple[int, int, Dict[str, Any]], - ], - ] = [] - for v in value: - # TODO(callumdickinson): Check if this works. - if isinstance(v, int): - remote_values.append((4, v)) - elif isinstance(v, RecordBase): - remote_values.append((4, v.id)) - elif isinstance(v, dict): - manager = self._client._record_manager_mapping[ - value_type - ] - remote_values.append( - (0, 0, manager._encode_create_fields(v)), - ) - else: - raise ValueError( - ( - "Unsupported element value for model " - f"ref list field '{field}' " - f"when creating record: {v}" - ), - ) - return (model_ref_field, remote_values) - # If the value type is an integer, - # treat it as a record ID and assign i to the field. - if isinstance(value, int): - return (model_ref_field, value) - # If the value type is a record object, - # then treat it as if it already exists on Odoo, - # and return the record ID to assign to the field. - if isinstance(value, RecordBase): - return (model_ref_field, value.id) - # If the value type is a dictionary, - # then treat it as a nested record to be created - # alongside the parent record. - # Encode the contents of the dict recursively - # using the record class's manager object, - # and assign it to the parent record so they can - # both be created. - # TODO(callumdickinson): Check that this works. - if isinstance(value, dict): - return ( - model_ref_field, - [ - ( - 0, - 0, - self._client._record_manager_mapping[ - attr_type - ]._encode_create_fields(value), - ), - ], + # If this field is a model ref, encode the model ref + # according to the given value's type, and map the result + # to the Odoo model's ref field name. + model_ref = ModelRef.get(type_hint) + if model_ref: + attr_type: Any = get_type_args(type_hint)[0] + # NOTE(callumdickinson): JSON RPC API model link reference. + # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#odoo.models.Model.write + # * (0, 0, {values}) - Link to a new record that needs to + # be created with the given values dictionary. + # * (1, id, {values}) - Update the linked record *id* + # (write *values* to it). + # * (2, id) - Remove and delete the linked record *id*. + # Calls ``unlink`` on ID, deleting the object + # completely, and the link to it as well. + # * (3, id) - Cut the link to the linked record *id*. + # Deletes the relationship between the two objects, + # but does not delete the target object itself. + # * (4, id) - Link to existing record *id* + # (adds a relationship). + # * (5) - Unlink all record links. + # Functions like using (3,ID) for all linked records. + # * (6, 0, [ids]) - Replace the list of linked IDs + # with *ids*. Functions like using (5), then (4, id) + # for each ID in the list of IDs. + model_ref_field = self._get_remote_field(model_ref.field) + # If the field is a list of multiple model refs, + # iterate over the given value and decode the elements + # appropriately. + if get_type_origin(attr_type) is list: + value_type = get_type_args(attr_type)[0] + if not value: + return (model_ref_field, []) + remote_values: List[ + Union[ + Tuple[int, int], + Tuple[int, int, Dict[str, Any]], + ], + ] = [] + for v in value: + # TODO(callumdickinson): Check if this works. + if isinstance(v, int): + remote_values.append((4, v)) + elif isinstance(v, RecordBase): + remote_values.append((4, v.id)) + elif isinstance(v, dict): + manager = self._client._record_manager_mapping[ + value_type + ] + remote_values.append( + (0, 0, manager._encode_create_fields(v)), ) - raise ValueError( + else: + raise ValueError( + ( + "Unsupported element value for model " + f"ref list field '{field}' " + f"when creating record: {v}" + ), + ) + return (model_ref_field, remote_values) + # If the value type is an integer, treat it as a record ID + # and assign i to the field. + if isinstance(value, int): + return (model_ref_field, value) + # If the value type is a record object, then treat it as if + # it already exists on Odoo, and return the record ID to assign + # to the field. + if isinstance(value, RecordBase): + return (model_ref_field, value.id) + # If the value type is a dictionary, then treat it as a nested + # record to be created alongside the parent record. + # Encode the contents of the dict recursively using the + # record class's manager object, and assign it to the + # parent record so they can both be created. + # TODO(callumdickinson): Check that this works. + if isinstance(value, dict): + return ( + model_ref_field, + [ ( - f"Unsupported value for model ref field '{field}' " - f"when creating record: {value}" + 0, + 0, + self._client._record_manager_mapping[ + attr_type + ]._encode_create_fields(value), ), - ) - raise ValueError( - ( - f"Unsupported annotation for field '{field}': " - f"{annotation}" - ), + ], ) raise ValueError( ( - f"More annotations than expected on field '{field}' " - f"(found {len(annotations)}, expected 1): " - f"{annotations}" + f"Unsupported value for model ref field '{field}' " + f"when creating record: {value}" ), ) # For regular fields, encode the value based on its type hint. - # TODO(callumdickinson): Rename _encode_create_value to _encode_value. return ( remote_field, - self._encode_create_value(type_hint=type_hint, value=value), + self._encode_value(type_hint=type_hint, value=value), ) def unlink( @@ -665,26 +693,24 @@ def _decode_field(self, field: str) -> str: def _encode_field(self, field: str) -> str: return self._get_remote_field(self._resolve_alias(field)) - def _encode_value(self, value: Any) -> Any: - # TODO: Replace with _encode_create_value, - # and rename _encode_create_value to _encode_value. - if isinstance(value, RecordBase): - return value.id - if isinstance(value, (date, time, datetime)): - return value.isoformat() - if isinstance(value, (list, set, tuple)): - return [self._encode_value(v) for v in value] - return value - - def _encode_create_value(self, type_hint: Type[Any], value: Any) -> Any: - value_type = get_type_origin(type_hint) - if value_type is date and isinstance(value, date): - return value.strftime(DEFAULT_SERVER_DATE_FORMAT) - if value_type is time and isinstance(value, time): - return value.strftime(DEFAULT_SERVER_TIME_FORMAT) - if value_type is datetime and isinstance(value, datetime): - return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - if value_type is list and isinstance(value, (list, set, tuple)): - v_type = get_type_args(type_hint)[0] - return [self._encode_create_value(v_type, v) for v in value] + def _encode_value(self, type_hint: Any, value: Any) -> Any: + type_origin = get_type_origin(type_hint) + value_types = ( + get_type_args(type_hint) if type_origin is Union else [type_origin] + ) + for value_type in value_types: + if is_subclass(value_type, RecordBase) and isinstance( + value, + RecordBase, + ): + return value.id + if value_type is date and isinstance(value, date): + return value.strftime(DEFAULT_SERVER_DATE_FORMAT) + if value_type is time and isinstance(value, time): + return value.strftime(DEFAULT_SERVER_TIME_FORMAT) + if value_type is datetime and isinstance(value, datetime): + return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + if value_type is list and isinstance(value, (list, set, tuple)): + v_type = get_type_args(type_hint)[0] + return [self._encode_value(v_type, v) for v in value] return value diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index 620a912..1e46632 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -18,8 +18,11 @@ from dataclasses import dataclass from datetime import date, datetime from typing import ( - TYPE_CHECKING, + Any, + List, Literal, + Mapping, + Optional, Tuple, Type, TypeVar, @@ -27,13 +30,12 @@ ) from typing_extensions import ( + Annotated, + Self, get_args as get_type_args, get_origin as get_type_origin, ) -if TYPE_CHECKING: - from typing import Any, List, Mapping, Optional - T = TypeVar("T") # Same values as defined in odoo.tools.misc. @@ -44,8 +46,43 @@ ) +class AnnotationBase: + @classmethod + def get(cls, type_hint: Any) -> Optional[Self]: + """Return the annotation applied to the given type hint, + if the type hint is annotated with this type of annotation. + + If multiple matching annotations are found, the last occurrence + is returned. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: Applied annotation, or ``None`` if no annotation was found + :rtype: Optional[Self] + """ + if get_type_origin(type_hint) is not Annotated: + return None + matching_annotation: Optional[Self] = None + for annotation in get_type_args(type_hint)[1:]: + if isinstance(annotation, cls): + matching_annotation = annotation + return matching_annotation + + @classmethod + def is_annotated(cls, type_hint: Any) -> bool: + """Checks whether or not the given type hint is annotated + with an annotation of this type. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: ``True`` if annotated, otherwise ``False`` + :rtype: bool + """ + return bool(cls.get(type_hint)) + + @dataclass(frozen=True) -class FieldAlias: +class FieldAlias(AnnotationBase): """An annotation for alias attributes to define the Odoo field name the attribute is an alias for. """ @@ -54,7 +91,7 @@ class FieldAlias: @dataclass(frozen=True) -class ModelRef: +class ModelRef(AnnotationBase): """An annotation for attributes that decode an Odoo model reference, to define the Odoo field name to be decoded. """ @@ -118,7 +155,7 @@ def is_subclass( return False -def get_type_tree(type_hint: Type[Any]) -> Tuple[Type[Any], ...]: +def get_type_tree(type_hint: Any) -> Tuple[Any, ...]: """Generate the type tree for the given annotation. This function peels back the annotation layers @@ -159,12 +196,12 @@ def get_type_tree(type_hint: Type[Any]) -> Tuple[Type[Any], ...]: (, ) :param type_hint: Type hint to parse - :type type_hint: Type[Any] + :type type_hint: Any :return: Type hint tree - :rtype: Tuple[Type[Any]] + :rtype: Tuple[Any] """ - type_tree: List[Type[Any]] = [type_hint] + type_tree: List[Any] = [type_hint] while get_type_origin(type_tree[-1]) is not None: origin_type = get_type_origin(type_tree[-1]) From cd14ec79efa9b64bf0fb8061d25b1ceecc6d695e Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 10:55:34 +1200 Subject: [PATCH 26/87] Replace get_type_tree with get_type_origin/get_type_args Turns out `get_type_tree` is not actually needed for this application of type hint reflection --- openstack_odooclient/managers/util.py | 75 ++++----------------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index 1e46632..73a6e1f 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -19,7 +19,6 @@ from datetime import date, datetime from typing import ( Any, - List, Literal, Mapping, Optional, @@ -155,62 +154,6 @@ def is_subclass( return False -def get_type_tree(type_hint: Any) -> Tuple[Any, ...]: - """Generate the type tree for the given annotation. - - This function peels back the annotation layers - and generates a list containing the type hint encapsulations, - from the outermost layer to the innermost layer. - - The expected basic data type will end up at the end of the list. - - If the annotation is a union of possible values, the final two elements - will be as follows: - - >>> from typing import Literal, Union - >>> from openstack_odooclient.managers.record.util import get_type_tree - >>> get_type_tree(Union[str, Literal[False]]) - (typing.Union[str, typing.Literal[False]], typing.Union) - - ``Union[T, ...]`` can be evaluated to get the candidate types using - ``typing.get_args``. This includes ``Optional[T]``, - which is syntantic sugar for ``Union[T, type(None)]``. - - >>> from typing import Literal, Union, get_args - >>> get_args(Union[str, Literal[False]]) - (, typing.Literal[False]) - - Similarly, if the data type is a generic type such as ``list`` - or ``dict`` that take type arguments, the final two elements will be - as follows. - - >>> from typing import Dict - >>> from openstack_odooclient.managers.record.util import get_type_tree - >>> get_type_tree(Dict[int, str]) - (typing.Dict[str, int], ) - - The generic types can be retrieved using ``typing.get_args``. - - >>> from typing import Dict, get_args - >>> get_args(Dict[int, str]) - (, ) - - :param type_hint: Type hint to parse - :type type_hint: Any - :return: Type hint tree - :rtype: Tuple[Any] - """ - - type_tree: List[Any] = [type_hint] - - while get_type_origin(type_tree[-1]) is not None: - origin_type = get_type_origin(type_tree[-1]) - if origin_type is not None: - type_tree.append(origin_type) - - return tuple(type_tree) - - def decode_value(type_hint: Type[T], value: Any) -> T: """Decode a raw Odoo JSON field value to its local representation, based on the given type hint from the record class. @@ -223,26 +166,26 @@ def decode_value(type_hint: Type[T], value: Any) -> T: :rtype: T """ - type_tree = get_type_tree(type_hint) + value_type = get_type_origin(type_hint) # The basic data types that need special handling. - if type_tree[-1] is date: + if value_type is date: return date.fromisoformat(value) # type: ignore[return-value] - if type_tree[-1] is datetime: + if value_type is datetime: return datetime.fromisoformat(value) # type: ignore[return-value] # When a list is expected, decode each value individually # and return the result as a new list with the same order. - if type_tree[-1] is list: + if value_type is list: return [ # type: ignore[return-value] - decode_value(get_type_args(type_tree[-2])[0], v) for v in value + decode_value(get_type_args(type_hint)[0], v) for v in value ] # When a dict is expected, decode the key and the value of each # item separately, and combine the result into a new dict. - if type_tree[-1] is dict: - key_type, value_type = get_type_args(type_tree[-2]) + if value_type is dict: + key_type, value_type = get_type_args(type_hint) return { # type: ignore[return-value] decode_value(key_type, k): decode_value(value_type, v) for k, v in value.items() @@ -252,8 +195,8 @@ def decode_value(type_hint: Type[T], value: Any) -> T: # Not suitable for handling complicated union structures. # TODO(callumdickinson): Find a way to handle complicated # union structures more smartly. - if type_tree[-1] is Union: - attr_union_types = get_type_args(type_tree[-2]) + if value_type is Union: + attr_union_types = get_type_args(type_hint) if len(attr_union_types) == 2: # noqa: PLR2004 # Optional[T] if type(None) in attr_union_types and value is not None: From e599d3cd1d0405d716a60c03f7f6bdd6cdc5e7de Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 11:01:00 +1200 Subject: [PATCH 27/87] Import type hints during runtime as well --- .../managers/record_manager_base.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 5594caf..3a88757 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -16,7 +16,24 @@ from __future__ import annotations from datetime import date, datetime, time -from typing import TYPE_CHECKING, Generic, TypeVar, overload +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + Iterable, + List, + Literal, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + overload, +) from typing_extensions import ( Annotated, @@ -37,21 +54,6 @@ ) if TYPE_CHECKING: - from typing import ( - Any, - Dict, - Iterable, - List, - Literal, - Mapping, - Optional, - Sequence, - Set, - Tuple, - Type, - Union, - ) - from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] From 133f57329a2e4c2c9fc2a3aa35b2abbe7cad4a52 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 13:08:37 +1200 Subject: [PATCH 28/87] Start defining record class in model refs --- openstack_odooclient/__init__.py | 3 +- openstack_odooclient/managers/account_move.py | 35 ++++++-- .../managers/account_move_line.py | 55 +++++++++--- openstack_odooclient/managers/company.py | 41 ++++++--- openstack_odooclient/managers/credit.py | 39 +++++++-- .../managers/credit_transaction.py | 17 +++- openstack_odooclient/managers/credit_type.py | 40 ++++++--- .../managers/customer_group.py | 24 ++++-- openstack_odooclient/managers/grant.py | 26 ++++-- openstack_odooclient/managers/grant_type.py | 32 ++++--- .../managers/partner_category.py | 32 ++++--- openstack_odooclient/managers/record_base.py | 84 ++++++++++++++++--- .../managers/record_manager_base.py | 17 ++-- openstack_odooclient/managers/user.py | 31 +++++-- openstack_odooclient/managers/util.py | 56 ------------- 15 files changed, 363 insertions(+), 169 deletions(-) diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index fadf35e..70c81f1 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -38,7 +38,7 @@ from .managers.product_category import ProductCategory from .managers.project import Project from .managers.project_contact import ProjectContact -from .managers.record_base import RecordBase +from .managers.record_base import FieldAlias, ModelRef, RecordBase from .managers.record_manager_base import RecordManagerBase from .managers.record_manager_code_base import CodedRecordManagerBase from .managers.record_manager_name_base import NamedRecordManagerBase @@ -59,7 +59,6 @@ from .managers.uom import Uom from .managers.uom_category import UomCategory from .managers.user import User -from .managers.util import FieldAlias, ModelRef from .managers.volume_discount_range import VolumeDiscountRange from .managers.voucher_code import VoucherCode diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index a367d7f..a175d79 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -25,7 +25,6 @@ project, record_base, record_manager_name_base, - util, ) @@ -36,15 +35,21 @@ class AccountMove(record_base.RecordBase): amount_untaxed: float """Total (untaxed) amount charged on the account move (invoice).""" - currency_id: Annotated[int, util.ModelRef("currency_id")] + currency_id: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The ID for the currency used in this account move (invoice).""" - currency_name: Annotated[str, util.ModelRef("currency_id")] + currency_name: Annotated[ + str, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The name of the currency used in this account move (invoice).""" currency: Annotated[ currency_module.Currency, - util.ModelRef("currency_id"), + record_base.ModelRef("currency_id", currency_module.Currency), ] """The currency used in this account move (invoice). @@ -57,7 +62,10 @@ class AccountMove(record_base.RecordBase): invoice_line_ids: Annotated[ List[int], - record_base.ModelRef("invoice_line_ids"), + record_base.ModelRef( + "invoice_line_ids", + account_move_line.AccountMoveLine, + ), ] """The list of the IDs for the account move (invoice) lines that comprise this account move (invoice). @@ -65,7 +73,10 @@ class AccountMove(record_base.RecordBase): invoice_lines: Annotated[ List[account_move_line.AccountMoveLine], - record_base.ModelRef("invoice_line_ids"), + record_base.ModelRef( + "invoice_line_ids", + account_move_line.AccountMoveLine, + ), ] """A list of account move (invoice) lines that comprise this account move (invoice). @@ -102,19 +113,25 @@ class AccountMove(record_base.RecordBase): name: Union[str, Literal[False]] """Name assigned to the account move (invoice), if posted.""" - os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + os_project_id: Annotated[ + Optional[int], + record_base.ModelRef("os_project", project.Project), + ] """The ID of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. """ - os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + os_project_name: Annotated[ + Optional[str], + record_base.ModelRef("os_project", project.Project), + ] """The name of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. """ os_project: Annotated[ Optional[project.Project], - util.ModelRef("os_project"), + record_base.ModelRef("os_project", project.Project), ] """The OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index e467096..8276bc5 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -25,24 +25,29 @@ project, record_base, record_manager_base, - util, ) class AccountMoveLine(record_base.RecordBase): - currency_id: Annotated[int, util.ModelRef("currency_id")] + currency_id: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The ID for the currency used in this account move (invoice) line. """ - currency_name: Annotated[str, util.ModelRef("currency_id")] + currency_name: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The name of the currency used in this account move (invoice) line. """ currency: Annotated[ currency_module.Currency, - util.ModelRef("currency_id"), + record_base.ModelRef("currency_id", currency_module.Currency), ] """The currency used in this account move (invoice) line. @@ -54,13 +59,22 @@ class AccountMoveLine(record_base.RecordBase): line_tax_amount: float """Amount charged in tax on the account move (invoice) line.""" - move_id: Annotated[int, util.ModelRef("move_id")] + move_id: Annotated[ + int, + record_base.ModelRef("move_id", account_move.AccountMove), + ] """The ID for the account move (invoice) this line is part of.""" - move_name: Annotated[str, util.ModelRef("move_id")] + move_name: Annotated[ + str, + record_base.ModelRef("move_id", account_move.AccountMove), + ] """The name of the account move (invoice) this line is part of.""" - move: Annotated[account_move.AccountMove, util.ModelRef("move_id")] + move: Annotated[ + account_move.AccountMove, + record_base.ModelRef("move_id", account_move.AccountMove), + ] """The account move (invoice) this line is part of. This fetches the full record from Odoo once, @@ -70,19 +84,25 @@ class AccountMoveLine(record_base.RecordBase): name: str """Name of the product charged on the account move (invoice) line.""" - os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + os_project_id: Annotated[ + Optional[int], + record_base.ModelRef("os_project", project.Project), + ] """The ID for the OpenStack project this account move (invoice) line was generated for. """ - os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + os_project_name: Annotated[ + Optional[str], + record_base.ModelRef("os_project", project.Project), + ] """The name of the OpenStack project this account move (invoice) line was generated for. """ os_project: Annotated[ Optional[project.Project], - util.ModelRef("os_project"), + record_base.ModelRef("os_project", project.Project), ] """The OpenStack project this account move (invoice) line was generated for. @@ -122,17 +142,26 @@ class AccountMoveLine(record_base.RecordBase): price_unit: float """Unit price for the product used on the account move (invoice) line.""" - product_id: Annotated[int, util.ModelRef("product_id")] + product_id: Annotated[ + int, + record_base.ModelRef("product_id", product_module.Product), + ] """The ID for the product charged on the account move (invoice) line. """ - product_name: Annotated[str, util.ModelRef("product_id")] + product_name: Annotated[ + str, + record_base.ModelRef("product_id", product_module.Product), + ] """The name of the product charged on the account move (invoice) line. """ - product: Annotated[product_module.Product, util.ModelRef("product_id")] + product: Annotated[ + product_module.Product, + record_base.ModelRef("product_id", product_module.Product), + ] """The product charged on the account move (invoice) line. diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py index 8057a98..28c6b85 100644 --- a/openstack_odooclient/managers/company.py +++ b/openstack_odooclient/managers/company.py @@ -17,19 +17,25 @@ from typing import List, Literal, Optional, Union -from typing_extensions import Annotated +from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class Company(record_base.RecordBase): active: bool """Whether or not this company is active (enabled).""" - child_ids: Annotated[List[int], util.ModelRef("child_ids")] + child_ids: Annotated[ + List[int], + record_base.ModelRef("child_ids", Self), + ] """A list of IDs for the child companies.""" - children: Annotated[List[Company], util.ModelRef("child_ids")] + children: Annotated[ + List[Self], + record_base.ModelRef("child_ids", Self), + ] """The list of child companies. This fetches the full records from Odoo once, @@ -39,17 +45,23 @@ class Company(record_base.RecordBase): name: str """Company name, set from the partner name.""" - parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + parent_id: Annotated[ + Optional[int], + record_base.ModelRef("parent_id", Self), + ] """The ID for the parent company, if this company is the child of another company. """ - parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + parent_name: Annotated[ + Optional[str], + record_base.ModelRef("parent_id", Self), + ] """The name of the parent company, if this company is the child of another company. """ - parent: Annotated[Optional[Company], util.ModelRef("parent_id")] + parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] """The parent company, if this company is the child of another company. @@ -60,13 +72,22 @@ class Company(record_base.RecordBase): parent_path: Union[str, Literal[False]] """The path of the parent company, if there is a parent.""" - partner_id: Annotated[int, util.ModelRef("partner_id")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The ID for the partner for the company.""" - partner_name: Annotated[str, util.ModelRef("partner_id")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The name of the partner for the company.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The partner for the company. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index 6c944cd..df2d044 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -20,19 +20,25 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class Credit(record_base.RecordBase): - credit_type_id: Annotated[int, util.ModelRef("credit_type")] + credit_type_id: Annotated[ + int, + record_base.ModelRef("credit_type", credit_type_module.CreditType), + ] """The ID of the type of this credit.""" - credit_type_name: Annotated[str, util.ModelRef("credit_type")] + credit_type_name: Annotated[ + str, + record_base.ModelRef("credit_type", credit_type_module.CreditType), + ] """The name of the type of this credit.""" credit_type: Annotated[ credit_type_module.CreditType, - util.ModelRef("credit_type"), + record_base.ModelRef("credit_type", credit_type_module.CreditType), ] """The type of this credit. @@ -55,14 +61,23 @@ class Credit(record_base.RecordBase): start_date: date """The start date of the credit.""" - transaction_ids: Annotated[List[int], util.ModelRef("transactions")] + transaction_ids: Annotated[ + List[int], + record_base.ModelRef( + "transactions", + credit_transaction.CreditTransaction, + ), + ] """A list of IDs for the transactions that have been made using this credit. """ transactions: Annotated[ List[credit_transaction.CreditTransaction], - util.ModelRef("transactions"), + record_base.ModelRef( + "transactions", + credit_transaction.CreditTransaction, + ), ] """The transactions that have been made using this credit. @@ -70,19 +85,25 @@ class Credit(record_base.RecordBase): and caches them for subsequent accesses. """ - voucher_code_id: Annotated[Optional[int], util.ModelRef("voucher_code")] + voucher_code_id: Annotated[ + Optional[int], + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ] """The ID of the voucher code used when applying for the credit, if one was supplied. """ - voucher_code_name: Annotated[Optional[str], util.ModelRef("voucher_code")] + voucher_code_name: Annotated[ + Optional[str], + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ] """The name of the voucher code used when applying for the credit, if one was supplied. """ voucher_code: Annotated[ Optional[voucher_code_module.VoucherCode], - util.ModelRef("voucher_code"), + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), ] """The voucher code used when applying for the credit, if one was supplied. diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index ea5abfe..a142228 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -17,17 +17,26 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class CreditTransaction(record_base.RecordBase): - credit_id: Annotated[int, util.ModelRef("credit")] + credit_id: Annotated[ + int, + record_base.ModelRef("credit", credit_module.Credit), + ] """The ID of the credit this transaction was made against.""" - credit_name: Annotated[str, util.ModelRef("credit")] + credit_name: Annotated[ + str, + record_base.ModelRef("credit", credit_module.Credit), + ] """The name of the credit this transaction was made against.""" - credit: Annotated[credit_module.Credit, util.ModelRef("credit")] + credit: Annotated[ + credit_module.Credit, + record_base.ModelRef("credit", credit_module.Credit), + ] """The credit this transaction was made against. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index c09dcc5..3803aef 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -24,15 +24,20 @@ product_category, record_base, record_manager_name_base, - util, ) class CreditType(record_base.RecordBase): - credit_ids: Annotated[List[int], util.ModelRef("credits")] + credit_ids: Annotated[ + List[int], + record_base.ModelRef("credits", credit.Credit), + ] """A list of IDs for the credits which are of this credit type.""" - credits: Annotated[List[credit.Credit], util.ModelRef("credits")] + credits: Annotated[ + List[credit.Credit], + record_base.ModelRef("credits", credit.Credit), + ] """A list of credits which are of this credit type. This fetches the full records from Odoo once, @@ -44,7 +49,7 @@ class CreditType(record_base.RecordBase): only_for_product_ids: Annotated[ List[int], - util.ModelRef("only_for_products"), + record_base.ModelRef("only_for_products", product_module.Product), ] """A list of IDs for the products this credit applies to. @@ -54,7 +59,7 @@ class CreditType(record_base.RecordBase): only_for_products: Annotated[ List[product_module.Product], - util.ModelRef("only_for_products"), + record_base.ModelRef("only_for_products", product_module.Product), ] """A list of products which this credit applies to. @@ -67,7 +72,10 @@ class CreditType(record_base.RecordBase): only_for_product_category_ids: Annotated[ List[int], - util.ModelRef("only_for_product_categories"), + record_base.ModelRef( + "only_for_product_categories", + product_category.ProductCategory, + ), ] """A list of IDs for the product categories this credit applies to. @@ -78,7 +86,10 @@ class CreditType(record_base.RecordBase): only_for_product_categories: Annotated[ List[product_category.ProductCategory], - util.ModelRef("only_for_product_categories"), + record_base.ModelRef( + "only_for_product_categories", + product_category.ProductCategory, + ), ] """A list of product categories which this credit applies to. @@ -90,17 +101,26 @@ class CreditType(record_base.RecordBase): and caches them for subsequent accesses. """ - product_id: Annotated[int, util.ModelRef("product")] + product_id: Annotated[ + int, + record_base.ModelRef("product", product_module.Product), + ] """The ID of the product to use when applying the credit to invoices. """ - product_name: Annotated[str, util.ModelRef("product")] + product_name: Annotated[ + str, + record_base.ModelRef("product", product_module.Product), + ] """The name of the product to use when applying the credit to invoices. """ - product: Annotated[product_module.Product, util.ModelRef("product")] + product: Annotated[ + product_module.Product, + record_base.ModelRef("product", product_module.Product), + ] """The product to use when applying the credit to invoices. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index 0c82fd6..810de23 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -19,38 +19,50 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class CustomerGroup(record_base.RecordBase): name: str """The name of the customer group.""" - partner_ids: Annotated[List[int], util.ModelRef("partners")] + partner_ids: Annotated[ + List[int], + record_base.ModelRef("partners", partner.Partner), + ] """A list of IDs for the partners that are part of this customer group. """ - partners: Annotated[List[partner.Partner], util.ModelRef("partners")] + partners: Annotated[ + List[partner.Partner], + record_base.ModelRef("partners", partner.Partner), + ] """The partners that are part of this customer group. This fetches the full records from Odoo once, and caches them for subsequent accesses. """ - pricelist_id: Annotated[Optional[int], util.ModelRef("pricelist")] + pricelist_id: Annotated[ + Optional[int], + record_base.ModelRef("pricelist", pricelist_module.Pricelist), + ] """The ID for the pricelist this customer group uses, if not the default one. """ - pricelist_name: Annotated[Optional[str], util.ModelRef("pricelist")] + pricelist_name: Annotated[ + Optional[str], + record_base.ModelRef("pricelist", pricelist_module.Pricelist), + ] """The name of the pricelist this customer group uses, if not the default one. """ pricelist: Annotated[ Optional[pricelist_module.Pricelist], - util.ModelRef("pricelist"), + record_base.ModelRef("pricelist", pricelist_module.Pricelist), ] """The pricelist this customer group uses, if not the default one. diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index 244bada..b256210 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -20,22 +20,28 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class Grant(record_base.RecordBase): expiry_date: date """The date the grant expires.""" - grant_type_id: Annotated[int, util.ModelRef("grant_type")] + grant_type_id: Annotated[ + int, + record_base.ModelRef("grant_type", grant_type_module.GrantType), + ] """The ID of the type of this grant.""" - grant_type_name: Annotated[str, util.ModelRef("grant_type")] + grant_type_name: Annotated[ + str, + record_base.ModelRef("grant_type", grant_type_module.GrantType), + ] """The name of the type of this grant.""" grant_type: Annotated[ grant_type_module.GrantType, - util.ModelRef("grant_type"), + record_base.ModelRef("grant_type", grant_type_module.GrantType), ] """The type of this grant. @@ -52,19 +58,25 @@ class Grant(record_base.RecordBase): value: float """The value of the grant.""" - voucher_code_id: Annotated[Optional[int], util.ModelRef("voucher_code")] + voucher_code_id: Annotated[ + Optional[int], + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ] """The ID of the voucher code used when applying for the grant, if one was supplied. """ - voucher_code_name: Annotated[Optional[str], util.ModelRef("voucher_code")] + voucher_code_name: Annotated[ + Optional[str], + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ] """The name of the voucher code used when applying for the grant, if one was supplied. """ voucher_code: Annotated[ Optional[voucher_code_module.VoucherCode], - util.ModelRef("voucher_code"), + record_base.ModelRef("voucher_code"), ] """The voucher code used when applying for the grant, if one was supplied. diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 63271fa..4558038 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -19,14 +19,20 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class GrantType(record_base.RecordBase): - grant_ids: Annotated[List[int], util.ModelRef("grants")] + grant_ids: Annotated[ + List[int], + record_base.ModelRef("grants", grant.Grant), + ] """A list of IDs for the grants which are of this grant type.""" - grants: Annotated[List[grant.Grant], util.ModelRef("grants")] + grants: Annotated[ + List[grant.Grant], + record_base.ModelRef("grants", grant.Grant), + ] """A list of grants which are of this grant type. This fetches the full records from Odoo once, @@ -38,7 +44,7 @@ class GrantType(record_base.RecordBase): only_for_product_ids: Annotated[ List[int], - util.ModelRef("only_for_products"), + record_base.ModelRef("only_for_products", product_module.Product), ] """A list of IDs for the products this grant applies to. @@ -48,7 +54,7 @@ class GrantType(record_base.RecordBase): only_for_products: Annotated[ List[product_module.Product], - util.ModelRef("only_for_products"), + record_base.ModelRef("only_for_products", product_module.Product), ] """A list of products which this grant applies to. @@ -61,7 +67,10 @@ class GrantType(record_base.RecordBase): only_for_product_category_ids: Annotated[ List[int], - util.ModelRef("only_for_product_categories"), + record_base.ModelRef( + "only_for_product_categories", + product_category.ProductCategory, + ), ] """A list of IDs for the product categories this grant applies to. @@ -72,7 +81,10 @@ class GrantType(record_base.RecordBase): only_for_product_categories: Annotated[ List[product_category.ProductCategory], - util.ModelRef("only_for_product_categories"), + record_base.ModelRef( + "only_for_product_categories", + product_category.ProductCategory, + ), ] """A list of product categories which this grant applies to. @@ -89,17 +101,17 @@ class GrantType(record_base.RecordBase): part of an invoice grouping if it is on the group root project. """ - product_id: Annotated[int, util.ModelRef("product")] + product_id: Annotated[int, record_base.ModelRef("product")] """The ID of the product to use when applying the grant to invoices. """ - product_name: Annotated[str, util.ModelRef("product")] + product_name: Annotated[str, record_base.ModelRef("product")] """The name of the product to use when applying the grant to invoices. """ - product: Annotated[product_module.Product, util.ModelRef("product")] + product: Annotated[product_module.Product, record_base.ModelRef("product")] """The product to use when applying the grant to invoices. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index 9745b9b..a18064b 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -17,19 +17,19 @@ from typing import List, Literal, Optional, Union -from typing_extensions import Annotated +from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class PartnerCategory(record_base.RecordBase): active: bool """Whether or not the partner category is active (enabled).""" - child_ids: Annotated[List[int], util.ModelRef("child_id")] + child_ids: Annotated[List[int], record_base.ModelRef("child_id", Self)] """A list of IDs for the child categories.""" - children: Annotated[List[PartnerCategory], util.ModelRef("child_id")] + children: Annotated[List[Self], record_base.ModelRef("child_id", Self)] """The list of child categories. This fetches the full records from Odoo once, @@ -39,23 +39,29 @@ class PartnerCategory(record_base.RecordBase): color: int """Colour index for the partner category.""" - colour: Annotated[int, util.FieldAlias("color")] + colour: Annotated[int, record_base.FieldAlias("color")] """Alias for ``color``.""" name: str """The name of the partner category.""" - parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + parent_id: Annotated[ + Optional[int], + record_base.ModelRef("parent_id", Self), + ] """The ID for the parent partner category, if this category is the child of another category. """ - parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + parent_name: Annotated[ + Optional[str], + record_base.ModelRef("parent_id", Self), + ] """The name of the parent partner category, if this category is the child of another category. """ - parent: Annotated[Optional[PartnerCategory], util.ModelRef("parent_id")] + parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] """The parent partner category, if this category is the child of another category. @@ -66,10 +72,16 @@ class PartnerCategory(record_base.RecordBase): parent_path: Union[str, Literal[False]] """The path of the parent partner category, if there is a parent.""" - partner_ids: Annotated[List[int], util.ModelRef("partner_id")] + partner_ids: Annotated[ + List[int], + record_base.ModelRef("partner_id", partner.Partner), + ] """A list of IDs for the partners in this category.""" - partners: Annotated[List[partner.Partner], util.ModelRef("partner_id")] + partners: Annotated[ + List[partner.Partner], + record_base.ModelRef("partner_id", partner.Partner), + ] """The list of partners in this category. This fetches the full records from Odoo once, diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index e38fd33..fb39846 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -17,6 +17,7 @@ import copy +from dataclasses import dataclass from datetime import datetime from typing import ( TYPE_CHECKING, @@ -39,7 +40,7 @@ get_type_hints, ) -from .util import FieldAlias, ModelRef, decode_value, is_subclass +from .util import decode_value, is_subclass if TYPE_CHECKING: from odoorpc import ODOO # type: ignore[import] @@ -49,6 +50,60 @@ from . import record_manager_base +class AnnotationBase: + @classmethod + def get(cls, type_hint: Any) -> Optional[Self]: + """Return the annotation applied to the given type hint, + if the type hint is annotated with this type of annotation. + + If multiple matching annotations are found, the last occurrence + is returned. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: Applied annotation, or ``None`` if no annotation was found + :rtype: Optional[Self] + """ + if get_type_origin(type_hint) is not Annotated: + return None + matching_annotation: Optional[Self] = None + for annotation in get_type_args(type_hint)[1:]: + if isinstance(annotation, cls): + matching_annotation = annotation + return matching_annotation + + @classmethod + def is_annotated(cls, type_hint: Any) -> bool: + """Checks whether or not the given type hint is annotated + with an annotation of this type. + + :param type_hint: The type hint to parse + :type type_hint: Any + :return: ``True`` if annotated, otherwise ``False`` + :rtype: bool + """ + return bool(cls.get(type_hint)) + + +@dataclass(frozen=True) +class FieldAlias(AnnotationBase): + """An annotation for alias attributes to define the Odoo field name + the attribute is an alias for. + """ + + field: str + + +@dataclass(frozen=True) +class ModelRef(AnnotationBase): + """An annotation for attributes that decode an Odoo model reference, + to define the Odoo field name to be decoded. + """ + + field: str + record_class: Any + + class RecordBase: id: int """The record's ID in Odoo.""" @@ -56,13 +111,13 @@ class RecordBase: create_date: datetime """The time the record was created.""" - create_uid: Annotated[int, ModelRef("create_uid")] + create_uid: Annotated[int, ModelRef("create_uid", user.User)] """The ID of the user that created this record.""" - create_name: Annotated[str, ModelRef("create_uid")] + create_name: Annotated[str, ModelRef("create_uid", user.User)] """The name of the user that created this record.""" - create_user: Annotated[user.User, ModelRef("create_uid")] + create_user: Annotated[user.User, ModelRef("create_uid", user.User)] """The user that created this record. This fetches the full record from Odoo once, @@ -72,13 +127,13 @@ class RecordBase: write_date: datetime """The time the record was last modified.""" - write_uid: Annotated[int, ModelRef("write_uid")] + write_uid: Annotated[int, ModelRef("write_uid", user.User)] """The ID for the user that last modified this record.""" - write_name: Annotated[str, ModelRef("write_uid")] + write_name: Annotated[str, ModelRef("write_uid", user.User)] """The name of the user that last modified this record.""" - write_user: Annotated[user.User, ModelRef("create_uid")] + write_user: Annotated[user.User, ModelRef("write_uid", user.User)] """The user that last modified this record. This fetches the full record from Odoo once, @@ -293,6 +348,11 @@ def _getattr_model_ref( # as a list of model IDs or objects. if get_type_origin(attr_type) is list: value_type = get_type_args(attr_type)[0] + # Handle a model ref list with the same record type as the + # parent record. Fetch the records from Odoo, and return + # the results. + if value_type is Self: + return self._manager.list(field_value) # List of model objects. Fetch the objects from Odoo, # and return the results. if is_subclass(value_type, RecordBase): @@ -340,14 +400,16 @@ def _getattr_model_ref( # and generate the value. record_id: int = field_value[0] record_name: str = field_value[1] - if value_type is int: - return record_id - if value_type is str: - return record_name + if value_type is Self: + return self._manager.get(record_id) if is_subclass(value_type, RecordBase): return self._client._record_manager_mapping[value_type].get( record_id, ) + if value_type is int: + return record_id + if value_type is str: + return record_name raise ValueError( ( "Unsupported field value type for singular model ref: " diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 3a88757..6908f2d 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -37,18 +37,18 @@ from typing_extensions import ( Annotated, + Self, get_args as get_type_args, get_origin as get_type_origin, get_type_hints, ) from ..exceptions import RecordNotFoundError -from .record_base import RecordBase +from .record_base import ModelRef, RecordBase from .util import ( DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_TIME_FORMAT, - ModelRef, get_mapped_field, is_subclass, ) @@ -430,12 +430,19 @@ def _encode_filter_field( type_hint: Any = type_hints[local_field] model_ref = ModelRef.get(type_hint) if model_ref: - value_type = get_type_args(type_hint)[0] + record_class: Type[RecordBase] = ( + self.record_class + if model_ref.record_class is Self + else model_ref.record_class + ) type_hint, remote_field_refs = ( self._client._record_manager_mapping[ - value_type + record_class # type: ignore[index] ]._encode_filter_field( - type_hints=get_type_hints(value_type), + type_hints=get_type_hints( + record_class, + include_extras=True, + ), field=".".join(field_refs[1:]), ) ) diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index 7cc1b95..bc0dff3 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -21,7 +21,6 @@ company as company_module, record_base, record_manager_base, - util, ) @@ -32,13 +31,22 @@ class User(record_base.RecordBase): active_partner: bool """Whether or not the partner this user is associated with is active.""" - company_id: Annotated[int, util.ModelRef("company_id")] + company_id: Annotated[ + int, + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the default company this user is logged in as.""" - company_name: Annotated[str, util.ModelRef("company_id")] + company_name: Annotated[ + str, + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the default company this user is logged in as.""" - company: Annotated[company_module.Company, util.ModelRef("company_id")] + company: Annotated[ + company_module.Company, + record_base.ModelRef("company_id", company_module.Company), + ] """The default company this user is logged in as. This fetches the full record from Odoo once, @@ -48,13 +56,22 @@ class User(record_base.RecordBase): name: str """User name.""" - partner_id: Annotated[int, util.ModelRef("partner_id")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The ID for the partner that this user is associated with.""" - partner_name: Annotated[str, util.ModelRef("partner_id")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The name of the partner that this user is associated with.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The partner that this user is associated with. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/managers/util.py index 73a6e1f..8c4bf02 100644 --- a/openstack_odooclient/managers/util.py +++ b/openstack_odooclient/managers/util.py @@ -15,7 +15,6 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import date, datetime from typing import ( Any, @@ -29,8 +28,6 @@ ) from typing_extensions import ( - Annotated, - Self, get_args as get_type_args, get_origin as get_type_origin, ) @@ -45,59 +42,6 @@ ) -class AnnotationBase: - @classmethod - def get(cls, type_hint: Any) -> Optional[Self]: - """Return the annotation applied to the given type hint, - if the type hint is annotated with this type of annotation. - - If multiple matching annotations are found, the last occurrence - is returned. - - :param type_hint: The type hint to parse - :type type_hint: Any - :return: Applied annotation, or ``None`` if no annotation was found - :rtype: Optional[Self] - """ - if get_type_origin(type_hint) is not Annotated: - return None - matching_annotation: Optional[Self] = None - for annotation in get_type_args(type_hint)[1:]: - if isinstance(annotation, cls): - matching_annotation = annotation - return matching_annotation - - @classmethod - def is_annotated(cls, type_hint: Any) -> bool: - """Checks whether or not the given type hint is annotated - with an annotation of this type. - - :param type_hint: The type hint to parse - :type type_hint: Any - :return: ``True`` if annotated, otherwise ``False`` - :rtype: bool - """ - return bool(cls.get(type_hint)) - - -@dataclass(frozen=True) -class FieldAlias(AnnotationBase): - """An annotation for alias attributes to define the Odoo field name - the attribute is an alias for. - """ - - field: str - - -@dataclass(frozen=True) -class ModelRef(AnnotationBase): - """An annotation for attributes that decode an Odoo model reference, - to define the Odoo field name to be decoded. - """ - - field: str - - def get_mapped_field( field_mapping: Mapping[Optional[str], Mapping[str, str]], odoo_version: str, From 888820c8a5ec8fcc4b506a43257a9fd34cde6cc2 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 14:46:45 +1200 Subject: [PATCH 29/87] Finish defining record class in model refs --- docs/managers/index.md | 20 +-- openstack_odooclient/managers/partner.py | 135 +++++++++++++----- openstack_odooclient/managers/pricelist.py | 25 +++- openstack_odooclient/managers/product.py | 35 +++-- .../managers/product_category.py | 25 ++-- openstack_odooclient/managers/project.py | 74 +++++++--- .../managers/project_contact.py | 29 +++- openstack_odooclient/managers/record_base.py | 2 +- .../managers/record_manager_base.py | 19 ++- .../managers/referral_code.py | 27 ++-- openstack_odooclient/managers/reseller.py | 44 ++++-- .../managers/reseller_tier.py | 20 ++- openstack_odooclient/managers/sale_order.py | 53 +++++-- .../managers/sale_order_line.py | 135 ++++++++++++++---- .../managers/support_subscription.py | 41 ++++-- .../managers/support_subscription_type.py | 37 +++-- openstack_odooclient/managers/tax.py | 29 +++- .../managers/term_discount.py | 42 ++++-- openstack_odooclient/managers/trial.py | 17 ++- openstack_odooclient/managers/uom.py | 14 +- .../managers/volume_discount_range.py | 36 +++-- openstack_odooclient/managers/voucher_code.py | 66 ++++++--- 22 files changed, 670 insertions(+), 255 deletions(-) diff --git a/docs/managers/index.md b/docs/managers/index.md index 63d8b42..27a0acb 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -903,7 +903,7 @@ The time the record was created. create_uid: int ``` -The ID of the partner that created this record. +The ID of the [user](user.md) that created this record. #### `create_name` @@ -911,15 +911,15 @@ The ID of the partner that created this record. create_name: str ``` -The name of the partner that created this record. +The name of the [user](user.md) that created this record. #### `create_user` ```python -create_user: Partner +create_user: User ``` -The partner that created this record. +The [user](user.md) that created this record. This fetches the full record from Odoo once, and caches it for subsequent accesses. @@ -938,7 +938,7 @@ The time the record was last modified. write_uid: int ``` -The ID of the partner that last modified this record. +The ID of the [user](user.md) that last modified this record. #### `write_name` @@ -946,18 +946,18 @@ The ID of the partner that last modified this record. write_name: str ``` -The name of the partner that modified this record. +The name of the [user](user.md) that modified this record. #### `write_user` ```python -write_user: Partner +write_user: User ``` -The partner that last modified this record. +The [user](user.md) that last modified this record. -This fetches a full Partner object from Odoo once, -and caches it for subsequence access. +This fetches the full record from Odoo once, +and caches it for subsequence accesses. #### `as_dict` diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index 9d06f5a..ba990eb 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -17,14 +17,13 @@ from typing import List, Literal, Optional, Union -from typing_extensions import Annotated +from typing_extensions import Annotated, Self from . import ( pricelist, project, record_base, record_manager_base, - util, ) @@ -32,13 +31,22 @@ class Partner(record_base.RecordBase): active: bool """Whether or not this partner is active (enabled).""" - company_id: Annotated[int, util.ModelRef("company_id")] + company_id: Annotated[ + int, + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the company this partner is owned by.""" - company_name: Annotated[str, util.ModelRef("company_id")] + company_name: Annotated[ + str, + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the company this partner is owned by.""" - company: Annotated[company_module.Company, util.ModelRef("company_id")] + company: Annotated[ + company_module.Company, + record_base.ModelRef("company_id", company_module.Company), + ] """The company this partner is owned by. This fetches the full record from Odoo once, @@ -53,7 +61,10 @@ class Partner(record_base.RecordBase): os_customer_group_id: Annotated[ Optional[int], - util.ModelRef("os_customer_group"), + record_base.ModelRef( + "os_customer_group", + customer_group.CustomerGroup, + ), ] """The ID for the customer group this partner is part of, if it is part of one. @@ -61,7 +72,10 @@ class Partner(record_base.RecordBase): os_customer_group_name: Annotated[ Optional[str], - util.ModelRef("os_customer_group"), + record_base.ModelRef( + "os_customer_group", + customer_group.CustomerGroup, + ), ] """The name of the customer group this partner is part of, if it is part of one. @@ -69,7 +83,10 @@ class Partner(record_base.RecordBase): os_customer_group: Annotated[ Optional[customer_group.CustomerGroup], - util.ModelRef("os_customer_group"), + record_base.ModelRef( + "os_customer_group", + customer_group.CustomerGroup, + ), ] """The customer group this partner is part of, if it is part of one. @@ -78,14 +95,17 @@ class Partner(record_base.RecordBase): and caches it for subsequent accesses. """ - os_project_ids: Annotated[List[int], util.ModelRef("os_projects")] + os_project_ids: Annotated[ + List[int], + record_base.ModelRef("os_projects", project.Project), + ] """A list of IDs for the OpenStack projects that belong to this partner. """ os_projects: Annotated[ List[project.Project], - util.ModelRef("os_projects"), + record_base.ModelRef("os_projects", project.Project), ] """The OpenStack projects that belong to this partner. @@ -95,7 +115,10 @@ class Partner(record_base.RecordBase): os_project_contact_ids: Annotated[ List[int], - util.ModelRef("os_project_contacts"), + record_base.ModelRef( + "os_project_contacts", + project_contact.ProjectContact, + ), ] """A list of IDs for the project contacts that are associated with this partner. @@ -103,7 +126,10 @@ class Partner(record_base.RecordBase): os_project_contacts: Annotated[ List[project_contact.ProjectContact], - util.ModelRef("os_project_contacts"), + record_base.ModelRef( + "os_project_contacts", + project_contact.ProjectContact, + ), ] """The project contacts that are associated with this partner. @@ -111,19 +137,25 @@ class Partner(record_base.RecordBase): and caches them for subsequent accesses. """ - os_referral_id: Annotated[Optional[int], util.ModelRef("os_referral")] + os_referral_id: Annotated[ + Optional[int], + record_base.ModelRef("os_referral", referral_code.ReferralCode), + ] """The ID for the referral code the partner used on sign-up, if one was used. """ - os_referral_name: Annotated[Optional[str], util.ModelRef("os_referral")] + os_referral_name: Annotated[ + Optional[str], + record_base.ModelRef("os_referral", referral_code.ReferralCode), + ] """The name of the referral code the partner used on sign-up, if one was used. """ os_referral: Annotated[ Optional[referral_code.ReferralCode], - util.ModelRef("os_referral"), + record_base.ModelRef("os_referral", referral_code.ReferralCode), ] """The referral code the partner used on sign-up, if one was used. @@ -133,13 +165,13 @@ class Partner(record_base.RecordBase): os_referral_code_ids: Annotated[ List[int], - util.ModelRef("os_referral_codes"), + record_base.ModelRef("os_referral_codes", referral_code.ReferralCode), ] """A list of IDs for the referral codes the partner has used.""" os_referral_codes: Annotated[ List[referral_code.ReferralCode], - util.ModelRef("os_referral_codes"), + record_base.ModelRef("os_referral_codes", referral_code.ReferralCode), ] """The referral codes the partner has used. @@ -147,19 +179,25 @@ class Partner(record_base.RecordBase): and caches them for subsequent accesses. """ - os_reseller_id: Annotated[Optional[int], util.ModelRef("os_reseller")] + os_reseller_id: Annotated[ + Optional[int], + record_base.ModelRef("os_reseller", reseller.Reseller), + ] """The ID for the reseller for this partner, if this partner is billed through a reseller. """ - os_reseller_name: Annotated[Optional[str], util.ModelRef("os_reseller")] + os_reseller_name: Annotated[ + Optional[str], + record_base.ModelRef("os_reseller", reseller.Reseller), + ] """The name of the reseller for this partner, if this partner is billed through a reseller. """ os_reseller: Annotated[ Optional[reseller.Reseller], - util.ModelRef("os_reseller"), + record_base.ModelRef("os_reseller", reseller.Reseller), ] """The reseller for this partner, if this partner is billed through a reseller. @@ -168,17 +206,26 @@ class Partner(record_base.RecordBase): and caches it for subsequent accesses. """ - os_trial_id: Annotated[Optional[int], util.ModelRef("os_trial")] + os_trial_id: Annotated[ + Optional[int], + record_base.ModelRef("os_trial", trial.Trial), + ] """The ID for the sign-up trial for this partner, if signed up under a trial. """ - os_trial_name: Annotated[Optional[str], util.ModelRef("os_trial")] + os_trial_name: Annotated[ + Optional[str], + record_base.ModelRef("os_trial", trial.Trial), + ] """The name of the sign-up trial for this partner, if signed up under a trial. """ - os_trial: Annotated[Optional[trial.Trial], util.ModelRef("os_trial")] + os_trial: Annotated[ + Optional[trial.Trial], + record_base.ModelRef("os_trial", trial.Trial), + ] """The sign-up trial for this partner, if signed up under a trial. @@ -186,17 +233,23 @@ class Partner(record_base.RecordBase): and caches it for subsequent accesses. """ - parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + parent_id: Annotated[ + Optional[int], + record_base.ModelRef("parent_id", Self), + ] """The ID for the parent partner of this partner, if it has a parent. """ - parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + parent_name: Annotated[ + Optional[str], + record_base.ModelRef("parent_id", Self), + ] """The name of the parent partner of this partner, if it has a parent. """ - parent: Annotated[Optional[Partner], util.ModelRef("parent_id")] + parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] """The parent partner of this partner, if it has a parent. @@ -206,7 +259,10 @@ class Partner(record_base.RecordBase): property_product_pricelist_id: Annotated[ Optional[int], - util.ModelRef("property_product_pricelist"), + record_base.ModelRef( + "property_product_pricelist", + pricelist.Pricelist, + ), ] """The ID for the pricelist this partner uses, if explicitly set. @@ -217,7 +273,10 @@ class Partner(record_base.RecordBase): property_product_pricelist_name: Annotated[ Optional[str], - util.ModelRef("property_product_pricelist"), + record_base.ModelRef( + "property_product_pricelist", + pricelist.Pricelist, + ), ] """The name of the pricelist this partner uses, if explicitly set. @@ -228,7 +287,10 @@ class Partner(record_base.RecordBase): property_product_pricelist: Annotated[ Optional[pricelist.Pricelist], - util.ModelRef("property_product_pricelist"), + record_base.ModelRef( + "property_product_pricelist", + pricelist.Pricelist, + ), ] """The pricelist this partner uses, if explicitly set. @@ -243,17 +305,26 @@ class Partner(record_base.RecordBase): stripe_customer_id: Union[str, Literal[False]] """The Stripe customer ID for this partner, if one has been assigned.""" - user_id: Annotated[Optional[int], util.ModelRef("user_id")] + user_id: Annotated[ + Optional[int], + record_base.ModelRef("user_id", user_module.User), + ] """The ID of the internal user associated with this partner, if one is assigned. """ - user_name: Annotated[Optional[str], util.ModelRef("user_id")] + user_name: Annotated[ + Optional[str], + record_base.ModelRef("user_id", user_module.User), + ] """The name of the internal user associated with this partner, if one is assigned. """ - user: Annotated[Optional[user_module.User], util.ModelRef("user_id")] + user: Annotated[ + Optional[user_module.User], + record_base.ModelRef("user_id", user_module.User), + ] """The internal user associated with this partner, if one is assigned. diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 16509dd..1fb0418 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -23,7 +23,6 @@ product as product_module, record_base, record_manager_name_base, - util, ) @@ -31,15 +30,21 @@ class Pricelist(record_base.RecordBase): active: bool """Whether or not the pricelist is active.""" - company_id: Annotated[Optional[int], util.ModelRef("company_id")] + company_id: Annotated[ + Optional[int], + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the company for this pricelist, if set.""" - company_name: Annotated[Optional[str], util.ModelRef("company_id")] + company_name: Annotated[ + Optional[str], + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the company for this pricelist, if set.""" company: Annotated[ Optional[company_module.Company], - util.ModelRef("company_id"), + record_base.ModelRef("company_id", company_module.Company), ] """The company for this pricelist, if set. @@ -47,15 +52,21 @@ class Pricelist(record_base.RecordBase): and caches it for subsequent accesses. """ - currency_id: Annotated[int, util.ModelRef("currency_id")] + currency_id: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The ID for the currency used in this pricelist.""" - currency_name: Annotated[str, util.ModelRef("currency_id")] + currency_name: Annotated[ + str, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The name of the currency used in this pricelist.""" currency: Annotated[ currency_module.Currency, - util.ModelRef("currency_id"), + record_base.ModelRef("currency_id", currency_module.Currency), ] """The currency used in this pricelist. diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index d8aaefb..ee21383 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -28,19 +28,25 @@ from typing_extensions import Annotated -from . import record_base, record_manager_unique_field_base, util +from . import record_base, record_manager_unique_field_base class Product(record_base.RecordBase): - categ_id: Annotated[int, util.ModelRef("categ_id")] + categ_id: Annotated[ + int, + record_base.ModelRef("categ_id", product_category.ProductCategory), + ] """The ID for the category this product is under.""" - categ_name: Annotated[str, util.ModelRef("categ_id")] + categ_name: Annotated[ + str, + record_base.ModelRef("categ_id", product_category.ProductCategory), + ] """The name of the category this product is under.""" categ: Annotated[ product_category.ProductCategory, - util.ModelRef("categ_id"), + record_base.ModelRef("categ_id", product_category.ProductCategory), ] """The category this product is under. @@ -48,15 +54,21 @@ class Product(record_base.RecordBase): and caches it for subsequent accesses. """ - company_id: Annotated[Optional[int], util.ModelRef("company_id")] + company_id: Annotated[ + Optional[int], + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the company that owns this product, if set.""" - company_name: Annotated[Optional[str], util.ModelRef("company_id")] + company_name: Annotated[ + Optional[str], + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the company that owns this product, if set.""" company: Annotated[ Optional[company_module.Company], - util.ModelRef("company_id"), + record_base.ModelRef("company_id", company_module.Company), ] """The company that owns this product, if set. @@ -86,13 +98,16 @@ class Product(record_base.RecordBase): name: str """The name of the product.""" - uom_id: Annotated[int, util.ModelRef("uom_id")] + uom_id: Annotated[int, record_base.ModelRef("uom_id", uom_module.Uom)] """The ID for the Unit of Measure for this product.""" - uom_name: Annotated[str, util.ModelRef("uom_id")] + uom_name: Annotated[str, record_base.ModelRef("uom_id", uom_module.Uom)] """The name of the Unit of Measure for this product.""" - uom: Annotated[uom_module.Uom, util.ModelRef("uom_id")] + uom: Annotated[ + uom_module.Uom, + record_base.ModelRef("uom_id", uom_module.Uom), + ] """The Unit of Measure for this product. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index 3c3dffb..c318b18 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -17,22 +17,19 @@ from typing import List, Literal, Optional, Union -from typing_extensions import Annotated +from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class ProductCategory(record_base.RecordBase): - child_id: Annotated[List[int], util.ModelRef("child_id")] + child_id: Annotated[List[int], record_base.ModelRef("child_id", Self)] """A list of IDs for the child categories.""" - child_ids: Annotated[List[int], util.FieldAlias("child_id")] + child_ids: Annotated[List[int], record_base.FieldAlias("child_id")] """An alias for ``child_id``.""" - children: Annotated[ - List[ProductCategory], - util.ModelRef("child_id"), - ] + children: Annotated[List[Self], record_base.ModelRef("child_id", Self)] """The list of child categories. This fetches the full records from Odoo once, @@ -45,17 +42,23 @@ class ProductCategory(record_base.RecordBase): name: str """Name of the product category.""" - parent_id: Annotated[Optional[int], util.ModelRef("parent_id")] + parent_id: Annotated[ + Optional[int], + record_base.ModelRef("parent_id", Self), + ] """The ID for the parent product category, if this category is the child of another category. """ - parent_name: Annotated[Optional[str], util.ModelRef("parent_id")] + parent_name: Annotated[ + Optional[str], + record_base.ModelRef("parent_id", Self), + ] """The name of the parent product category, if this category is the child of another category. """ - parent: Annotated[Optional[ProductCategory], util.ModelRef("parent_id")] + parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] """The parent product category, if this category is the child of another category. diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index 652618c..0c6ad47 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -26,9 +26,9 @@ overload, ) -from typing_extensions import Annotated +from typing_extensions import Annotated, Self -from . import record_base, record_manager_unique_field_base, util +from . import record_base, record_manager_unique_field_base class Project(record_base.RecordBase): @@ -61,30 +61,42 @@ class Project(record_base.RecordBase): set on this Project. """ - owner_id: Annotated[int, util.ModelRef("owner")] + owner_id: Annotated[ + int, + record_base.ModelRef("owner", partner_module.Partner), + ] """The ID for the partner that owns this project.""" - owner_name: Annotated[str, util.ModelRef("owner")] + owner_name: Annotated[ + str, + record_base.ModelRef("owner", partner_module.Partner), + ] """The name of the partner that owns this project.""" - owner: Annotated[partner_module.Partner, util.ModelRef("owner")] + owner: Annotated[ + partner_module.Partner, + record_base.ModelRef("owner", partner_module.Partner), + ] """The partner that owns this project. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - parent_id: Annotated[Optional[int], util.ModelRef("parent")] + parent_id: Annotated[Optional[int], record_base.ModelRef("parent", Self)] """The ID for the parent project, if this project is the child of another project. """ - parent_name: Annotated[Optional[str], util.ModelRef("parent")] + parent_name: Annotated[ + Optional[str], + record_base.ModelRef("parent", Self), + ] """The name of the parent project, if this project is the child of another project. """ - parent: Annotated[Optional[Project], util.ModelRef("parent")] + parent: Annotated[Optional[Self], record_base.ModelRef("parent", Self)] """The parent project, if this project is the child of another project. @@ -106,13 +118,19 @@ class Project(record_base.RecordBase): project_contact_ids: Annotated[ List[int], - util.ModelRef("project_contacts"), + record_base.ModelRef( + "project_contacts", + project_contact.ProjectContact, + ), ] """A list of IDs for the contacts for this project.""" project_contacts: Annotated[ List[project_contact.ProjectContact], - util.ModelRef("project_contacts"), + record_base.ModelRef( + "project_contacts", + project_contact.ProjectContact, + ), ] """The contacts for this project. @@ -120,12 +138,15 @@ class Project(record_base.RecordBase): and caches them for subsequent accesses. """ - project_credit_ids: Annotated[List[int], util.ModelRef("project_credits")] + project_credit_ids: Annotated[ + List[int], + record_base.ModelRef("project_credits", credit.Credit), + ] """A list of IDs for the credits that apply to this project.""" project_credits: Annotated[ List[credit.Credit], - util.ModelRef("project_credits"), + record_base.ModelRef("project_credits", credit.Credit), ] """The credits that apply to this project. @@ -133,12 +154,15 @@ class Project(record_base.RecordBase): and caches them for subsequent accesses. """ - project_grant_ids: Annotated[List[int], util.ModelRef("project_grants")] + project_grant_ids: Annotated[ + List[int], + record_base.ModelRef("project_grants", grant.Grant), + ] """A list of IDs for the grants that apply to this project.""" project_grants: Annotated[ List[grant.Grant], - util.ModelRef("project_grants"), + record_base.ModelRef("project_grants", grant.Grant), ] """The grants that apply to this project. @@ -156,7 +180,10 @@ class Project(record_base.RecordBase): support_subscription_id: Annotated[ Optional[int], - util.ModelRef("support_subscription"), + record_base.ModelRef( + "support_subscription", + support_subscription_module.SupportSubscription, + ), ] """The ID for the support subscription for this project, if the project has one. @@ -164,7 +191,10 @@ class Project(record_base.RecordBase): support_subscription_name: Annotated[ Optional[str], - util.ModelRef("support_subscription"), + record_base.ModelRef( + "support_subscription", + support_subscription_module.SupportSubscription, + ), ] """The name of the support subscription for this project, if the project has one. @@ -172,7 +202,10 @@ class Project(record_base.RecordBase): support_subscription: Annotated[ Optional[support_subscription_module.SupportSubscription], - util.ModelRef("support_subscription"), + record_base.ModelRef( + "support_subscription", + support_subscription_module.SupportSubscription, + ), ] """The support subscription for this project, if the project has one. @@ -181,12 +214,15 @@ class Project(record_base.RecordBase): and caches it for subsequent accesses. """ - term_discount_ids: Annotated[List[int], util.ModelRef("term_discounts")] + term_discount_ids: Annotated[ + List[int], + record_base.ModelRef("term_discounts", term_discount.TermDiscount), + ] """A list of IDs for the term discounts that apply to this project.""" term_discounts: Annotated[ List[term_discount.TermDiscount], - util.ModelRef("term_discounts"), + record_base.ModelRef("term_discounts", term_discount.TermDiscount), ] """The term discounts that apply to this project. diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index 7acf936..dcfaecc 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class ProjectContact(record_base.RecordBase): @@ -35,28 +35,43 @@ class ProjectContact(record_base.RecordBase): inherit: bool """Whether or not this contact should be inherited by child projects.""" - partner_id: Annotated[int, util.ModelRef("partner")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner", partner_module.Partner), + ] """The ID for the partner linked to this project contact.""" - partner_name: Annotated[str, util.ModelRef("partner")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner", partner_module.Partner), + ] """The name of the partner linked to this project contact.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner", partner_module.Partner), + ] """The partner linked to this project contact. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - project_id: Annotated[Optional[int], util.ModelRef("project")] + project_id: Annotated[ + Optional[int], + record_base.ModelRef("project", project_module.Project), + ] """The ID for the project this contact is linked to, if set.""" - project_name: Annotated[Optional[str], util.ModelRef("project")] + project_name: Annotated[ + Optional[str], + record_base.ModelRef("project", project_module.Project), + ] """The name of the project this contact is linked to, if set.""" project: Annotated[ Optional[project_module.Project], - util.ModelRef("project"), + record_base.ModelRef("project", project_module.Project), ] """The project this contact is linked to, if set. diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/managers/record_base.py index fb39846..a8d60c7 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/managers/record_base.py @@ -137,7 +137,7 @@ class RecordBase: """The user that last modified this record. This fetches the full record from Odoo once, - and caches it for subsequence access. + and caches it for subsequence accesses. """ _field_mapping: Dict[Optional[str], Dict[str, str]] = {} diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 6908f2d..0c08d42 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -582,9 +582,13 @@ def _encode_create_field( elif isinstance(v, RecordBase): remote_values.append((4, v.id)) elif isinstance(v, dict): - manager = self._client._record_manager_mapping[ - value_type - ] + manager = ( + self + if value_type is Self + else self._client._record_manager_mapping[ + value_type + ] + ) remote_values.append( (0, 0, manager._encode_create_fields(v)), ) @@ -613,15 +617,18 @@ def _encode_create_field( # parent record so they can both be created. # TODO(callumdickinson): Check that this works. if isinstance(value, dict): + manager = ( + self + if value_type is Self + else self._client._record_manager_mapping[value_type] + ) return ( model_ref_field, [ ( 0, 0, - self._client._record_manager_mapping[ - attr_type - ]._encode_create_fields(value), + manager._encode_create_fields(value), ), ], ) diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index e5ff44a..77213d3 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_code_base, util +from . import record_base, record_manager_code_base class ReferralCode(record_base.RecordBase): @@ -40,12 +40,18 @@ class ReferralCode(record_base.RecordBase): name: str """Automatically generated name for the referral code.""" - referral_ids: Annotated[List[int], util.ModelRef("referrals")] + referral_ids: Annotated[ + List[int], + record_base.ModelRef("referrals", partner.Partner), + ] """A list of IDs for the partners that signed up using this referral code. """ - referrals: Annotated[List[partner.Partner], util.ModelRef("referrals")] + referrals: Annotated[ + List[partner.Partner], + record_base.ModelRef("referrals", partner.Partner), + ] """The partners that signed up using this referral code. This fetches the full records from Odoo once, @@ -60,19 +66,19 @@ class ReferralCode(record_base.RecordBase): referral_credit_type_id: Annotated[ int, - util.ModelRef("referral_credit_type"), + record_base.ModelRef("referral_credit_type", credit_type.CreditType), ] """The ID of the credit type to use for the referral credit.""" referral_credit_type_name: Annotated[ str, - util.ModelRef("referral_credit_type"), + record_base.ModelRef("referral_credit_type", credit_type.CreditType), ] """The name of the credit type to use for the referral credit.""" referral_credit_type: Annotated[ credit_type.CreditType, - util.ModelRef("referral_credit_type"), + record_base.ModelRef("referral_credit_type", credit_type.CreditType), ] """The credit type to use for the referral credit. @@ -86,18 +92,21 @@ class ReferralCode(record_base.RecordBase): reward_credit_duration: int """Duration of the reward credit, in days.""" - reward_credit_type_id: Annotated[int, util.ModelRef("reward_credit_type")] + reward_credit_type_id: Annotated[ + int, + record_base.ModelRef("reward_credit_type", credit_type.CreditType), + ] """The ID of the credit type to use for the reward credit.""" reward_credit_type_name: Annotated[ str, - util.ModelRef("reward_credit_type"), + record_base.ModelRef("reward_credit_type", credit_type.CreditType), ] """The name of the credit type to use for the reward credit.""" reward_credit_type: Annotated[ credit_type.CreditType, - util.ModelRef("reward_credit_type"), + record_base.ModelRef("reward_credit_type", credit_type.CreditType), ] """The credit type to use for the reward credit. diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 6098bf1..3b4a411 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class Reseller(record_base.RecordBase): @@ -29,15 +29,21 @@ class Reseller(record_base.RecordBase): alternative_support_url: Optional[str] """The URL to the cloud support centre for the reseller, if available.""" - demo_project_id: Annotated[Optional[int], util.ModelRef("demo_project")] + demo_project_id: Annotated[ + Optional[int], + record_base.ModelRef("demo_project", project.Project), + ] """The ID for the optional demo project belonging to the reseller.""" - demo_project_name: Annotated[Optional[str], util.ModelRef("demo_project")] + demo_project_name: Annotated[ + Optional[str], + record_base.ModelRef("demo_project", project.Project), + ] """The name of the optional demo project belonging to the reseller.""" demo_project: Annotated[ Optional[project.Project], - util.ModelRef("demo_project"), + record_base.ModelRef("demo_project", project.Project), ] """An optional demo project belonging to the reseller. @@ -57,26 +63,44 @@ class Reseller(record_base.RecordBase): This is set to the reseller partner's name. """ - partner_id: Annotated[int, util.ModelRef("partner")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner", partner_module.Partner), + ] """The ID for the reseller partner.""" - partner_name: Annotated[str, util.ModelRef("partner")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner", partner_module.Partner), + ] """The name of the reseller partner.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner", partner_module.Partner), + ] """The reseller partner. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - tier_id: Annotated[int, util.ModelRef("tier")] + tier_id: Annotated[ + int, + record_base.ModelRef("tier", reseller_tier.ResellerTier), + ] """The ID for the tier this reseller is under.""" - tier_name: Annotated[str, util.ModelRef("tier")] + tier_name: Annotated[ + str, + record_base.ModelRef("tier", reseller_tier.ResellerTier), + ] """The name of the tier this reseller is under.""" - tier: Annotated[reseller_tier.ResellerTier, util.ModelRef("tier")] + tier: Annotated[ + reseller_tier.ResellerTier, + record_base.ModelRef("tier", reseller_tier.ResellerTier), + ] """The tier this reseller is under. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 8b805cb..905d823 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -17,22 +17,28 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class ResellerTier(record_base.RecordBase): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" - discount_product_id: Annotated[int, util.ModelRef("discount_product")] + discount_product_id: Annotated[ + int, + record_base.ModelRef("discount_product", product.Product), + ] """The ID of the discount product for the reseller tier.""" - discount_product_name: Annotated[str, util.ModelRef("discount_product")] + discount_product_name: Annotated[ + str, + record_base.ModelRef("discount_product", product.Product), + ] """The name of the discount product for the reseller tier.""" discount_product: Annotated[ product.Product, - util.ModelRef("discount_product"), + record_base.ModelRef("discount_product", product.Product), ] """The discount product for the reseller tier. @@ -45,7 +51,7 @@ class ResellerTier(record_base.RecordBase): free_monthly_credit_product_id: Annotated[ int, - util.ModelRef("free_monthly_credit_product"), + record_base.ModelRef("free_monthly_credit_product", product.Product), ] """The ID of the product to use when adding the free monthly credit to demo project invoices. @@ -53,7 +59,7 @@ class ResellerTier(record_base.RecordBase): free_monthly_credit_product_name: Annotated[ str, - util.ModelRef("free_monthly_credit_product"), + record_base.ModelRef("free_monthly_credit_product", product.Product), ] """The name of the product to use when adding the free monthly credit to demo project invoices. @@ -61,7 +67,7 @@ class ResellerTier(record_base.RecordBase): free_monthly_credit_product: Annotated[ product.Product, - util.ModelRef("free_monthly_credit_product"), + record_base.ModelRef("free_monthly_credit_product", product.Product), ] """The product to use when adding the free monthly credit to demo project invoices. diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index ff2f6e5..2910a07 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -20,7 +20,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class SaleOrder(record_base.RecordBase): @@ -36,13 +36,22 @@ class SaleOrder(record_base.RecordBase): client_order_ref: Union[str, Literal[False]] """The customer reference for this sale order, if defined.""" - currency_id: Annotated[int, util.ModelRef("currency_id")] + currency_id: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The ID for the currency used in this sale order.""" - currency_name: Annotated[str, util.ModelRef("currency_id")] + currency_name: Annotated[ + str, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The name of the currency used in this sale order.""" - currency: Annotated[currency_module.Currency, util.ModelRef("currency_id")] + currency: Annotated[ + currency_module.Currency, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The currency used in this sale order. This fetches the full record from Odoo once, @@ -75,12 +84,15 @@ class SaleOrder(record_base.RecordBase): Generally used for terms and conditions. """ - order_line_ids: Annotated[List[int], util.ModelRef("order_line")] + order_line_ids: Annotated[ + List[int], + record_base.ModelRef("order_line", sale_order_line.SaleOrderLine), + ] """A list of IDs for the lines added to the sale order.""" order_line: Annotated[ List[sale_order_line.SaleOrderLine], - util.ModelRef("order_line"), + record_base.ModelRef("order_line", sale_order_line.SaleOrderLine), ] """The lines added to the sale order. @@ -90,7 +102,7 @@ class SaleOrder(record_base.RecordBase): order_lines: Annotated[ List[sale_order_line.SaleOrderLine], - util.FieldAlias("order_line"), + record_base.FieldAlias("order_line", sale_order_line.SaleOrderLine), ] """An alias for ``order_line``.""" @@ -104,19 +116,25 @@ class SaleOrder(record_base.RecordBase): from the sale order. """ - os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + os_project_id: Annotated[ + Optional[int], + record_base.ModelRef("os_project", project.Project), + ] """The ID for the the OpenStack project this sale order was was generated for. """ - os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + os_project_name: Annotated[ + Optional[str], + record_base.ModelRef("os_project", project.Project), + ] """The name of the the OpenStack project this sale order was was generated for. """ os_project: Annotated[ Optional[project.Project], - util.ModelRef("os_project"), + record_base.ModelRef("os_project", project.Project), ] """The OpenStack project this sale order was was generated for. @@ -125,13 +143,22 @@ class SaleOrder(record_base.RecordBase): and caches it for subsequent accesses. """ - partner_id: Annotated[int, util.ModelRef("partner_id")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The ID for the recipient partner for the sale order.""" - partner_name: Annotated[str, util.ModelRef("partner_id")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The name of the recipient partner for the sale order.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner_id")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner_id", partner_module.Partner), + ] """The recipient partner for the sale order. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index 1ad60e7..359ded1 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -19,21 +19,30 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class SaleOrderLine(record_base.RecordBase): - company_id: Annotated[int, util.ModelRef("company_id")] + company_id: Annotated[ + int, + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the company this sale order line was generated for. """ - company_name: Annotated[str, util.ModelRef("company_id")] + company_name: Annotated[ + str, + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the company this sale order line was generated for. """ - company: Annotated[company_module.Company, util.ModelRef("company_id")] + company: Annotated[ + company_module.Company, + record_base.ModelRef("company_id", company_module.Company), + ] """The company this sale order line was generated for. @@ -41,15 +50,21 @@ class SaleOrderLine(record_base.RecordBase): and caches it for subsequent accesses. """ - currency_id: Annotated[int, util.ModelRef("currency_id")] + currency_id: Annotated[ + int, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The ID for the currency used in this sale order line.""" - currency_name: Annotated[str, util.ModelRef("currency_id")] + currency_name: Annotated[ + str, + record_base.ModelRef("currency_id", currency_module.Currency), + ] """The name of the currency used in this sale order line.""" currency: Annotated[ currency_module.Currency, - util.ModelRef("currency_id"), + record_base.ModelRef("currency_id", currency_module.Currency), ] """The currency used in this sale order line. @@ -63,14 +78,23 @@ class SaleOrderLine(record_base.RecordBase): display_name: str """Display name for the sale order line in the sale order.""" - invoice_line_ids: Annotated[List[int], util.ModelRef("invoice_lines")] + invoice_line_ids: Annotated[ + List[int], + record_base.ModelRef( + "invoice_lines", + account_move_line.AccountMoveLine, + ), + ] """A list of IDs for the account move (invoice) lines created from this sale order line. """ invoice_lines: Annotated[ List[account_move_line.AccountMoveLine], - util.ModelRef("invoice_lines"), + record_base.ModelRef( + "invoice_lines", + account_move_line.AccountMoveLine, + ), ] """The account move (invoice) lines created from this sale order line. @@ -104,28 +128,43 @@ class SaleOrderLine(record_base.RecordBase): the resource's name. """ - order_id: Annotated[int, util.ModelRef("order_id")] + order_id: Annotated[ + int, + record_base.ModelRef("order_id", sale_order.SaleOrder), + ] """The ID for the sale order this line is linked to.""" - order_name: Annotated[str, util.ModelRef("order_id")] + order_name: Annotated[ + str, + record_base.ModelRef("order_id", sale_order.SaleOrder), + ] """The name of the sale order this line is linked to.""" - order: Annotated[sale_order.SaleOrder, util.ModelRef("order_id")] + order: Annotated[ + sale_order.SaleOrder, + record_base.ModelRef("order_id", sale_order.SaleOrder), + ] """The sale order this line is linked to. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - order_partner_id: Annotated[int, util.ModelRef("order_partner_id")] + order_partner_id: Annotated[ + int, + record_base.ModelRef("order_partner_id", partner.Partner), + ] """The ID for the recipient partner for the sale order.""" - order_partner_name: Annotated[str, util.ModelRef("order_partner_id")] + order_partner_name: Annotated[ + str, + record_base.ModelRef("order_partner_id", partner.Partner), + ] """The name of the recipient partner for the sale order.""" order_partner: Annotated[ partner.Partner, - util.ModelRef("order_partner_id"), + record_base.ModelRef("order_partner_id", partner.Partner), ] """The recipient partner for the sale order. @@ -133,19 +172,25 @@ class SaleOrderLine(record_base.RecordBase): and caches it for subsequent accesses. """ - os_project_id: Annotated[Optional[int], util.ModelRef("os_project")] + os_project_id: Annotated[ + Optional[int], + record_base.ModelRef("os_project", project.Project), + ] """The ID for the the OpenStack project this sale order line was was generated for. """ - os_project_name: Annotated[Optional[str], util.ModelRef("os_project")] + os_project_name: Annotated[ + Optional[str], + record_base.ModelRef("os_project", project.Project), + ] """The name of the the OpenStack project this sale order line was was generated for. """ os_project: Annotated[ Optional[project.Project], - util.ModelRef("os_project"), + record_base.ModelRef("os_project", project.Project), ] """The OpenStack project this sale order line was was generated for. @@ -196,30 +241,48 @@ class SaleOrderLine(record_base.RecordBase): price_unit: float """Base unit price, excluding tax, before any discounts.""" - product_id: Annotated[int, util.ModelRef("product_id")] + product_id: Annotated[ + int, + record_base.ModelRef("product_id", product_module.Product), + ] """The ID of the product charged on this sale order line.""" - product_name: Annotated[str, util.ModelRef("product_id")] + product_name: Annotated[ + str, + record_base.ModelRef("product_id", product_module.Product), + ] """The name of the product charged on this sale order line.""" - product: Annotated[product_module.Product, util.ModelRef("product_id")] + product: Annotated[ + product_module.Product, + record_base.ModelRef("product_id", product_module.Product), + ] """The product charged on this sale order line. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - product_uom_id: Annotated[int, util.ModelRef("product_uom")] + product_uom_id: Annotated[ + int, + record_base.ModelRef("product_uom", uom.Uom), + ] """The ID for the Unit of Measure for the product being charged in this sale order line. """ - product_uom_name: Annotated[str, util.ModelRef("product_uom")] + product_uom_name: Annotated[ + str, + record_base.ModelRef("product_uom", uom.Uom), + ] """The name of the Unit of Measure for the product being charged in this sale order line. """ - product_uom: Annotated[uom.Uom, util.ModelRef("product_uom")] + product_uom: Annotated[ + uom.Uom, + record_base.ModelRef("product_uom", uom.Uom), + ] """The Unit of Measure for the product being charged in this sale order line. @@ -244,17 +307,26 @@ class SaleOrderLine(record_base.RecordBase): qty_to_invoice: float """The product quantity that still needs to be invoiced.""" - salesman_id: Annotated[int, util.ModelRef("salesman_id")] + salesman_id: Annotated[ + int, + record_base.ModelRef("salesman_id", partner.Partner), + ] """The ID for the salesperson partner assigned to this sale order line. """ - salesman_name: Annotated[str, util.ModelRef("salesman_id")] + salesman_name: Annotated[ + str, + record_base.ModelRef("salesman_id", partner.Partner), + ] """The name of the salesperson partner assigned to this sale order line. """ - salesman: Annotated[partner.Partner, util.ModelRef("salesman_id")] + salesman: Annotated[ + partner.Partner, + record_base.ModelRef("salesman_id", partner.Partner), + ] """The salesperson partner assigned to this sale order line. @@ -273,13 +345,16 @@ class SaleOrderLine(record_base.RecordBase): * ``cancel`` - Cancelled sale order, can be deleted """ - tax_id: Annotated[int, util.ModelRef("tax_id")] + tax_id: Annotated[int, record_base.ModelRef("tax_id", tax_module.Tax)] """The ID for the tax used on this sale order line.""" - tax_name: Annotated[str, util.ModelRef("tax_id")] + tax_name: Annotated[str, record_base.ModelRef("tax_id", tax_module.Tax)] """The name of the tax used on this sale order line.""" - tax: Annotated[tax_module.Tax, util.ModelRef("tax_id")] + tax: Annotated[ + tax_module.Tax, + record_base.ModelRef("tax_id", tax_module.Tax), + ] """The tax used on this sale order line. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index 19cb2e3..23bdd5d 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -20,7 +20,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class SupportSubscription(record_base.RecordBase): @@ -36,7 +36,10 @@ class SupportSubscription(record_base.RecordBase): end_date: date """The end date of the credit.""" - partner_id: Annotated[Optional[int], util.ModelRef("partner")] + partner_id: Annotated[ + Optional[int], + record_base.ModelRef("partner", partner_module.Partner), + ] """The ID for the partner linked to this support subscription, if it is linked to a partner. @@ -44,7 +47,10 @@ class SupportSubscription(record_base.RecordBase): cover all projects the partner owns. """ - partner_name: Annotated[Optional[str], util.ModelRef("partner")] + partner_name: Annotated[ + Optional[str], + record_base.ModelRef("partner", partner_module.Partner), + ] """The name of thepartner linked to this support subscription, if it is linked to a partner. @@ -54,7 +60,7 @@ class SupportSubscription(record_base.RecordBase): partner: Annotated[ Optional[partner_module.Partner], - util.ModelRef("partner"), + record_base.ModelRef("partner", partner_module.Partner), ] """The partner linked to this support subscription, if it is linked to a partner. @@ -66,19 +72,25 @@ class SupportSubscription(record_base.RecordBase): and caches it for subsequent accesses. """ - project_id: Annotated[Optional[int], util.ModelRef("project")] + project_id: Annotated[ + Optional[int], + record_base.ModelRef("project", project_module.Project), + ] """The ID of the project this support subscription is for, if it is linked to a specific project. """ - project_name: Annotated[Optional[str], util.ModelRef("project")] + project_name: Annotated[ + Optional[str], + record_base.ModelRef("project", project_module.Project), + ] """The name of the project this support subscription is for, if it is linked to a specific project. """ project: Annotated[ Optional[project_module.Project], - util.ModelRef("project"), + record_base.ModelRef("project", project_module.Project), ] """The project this support subscription is for, if it is linked to a specific project. @@ -92,19 +104,28 @@ class SupportSubscription(record_base.RecordBase): support_subscription_type_id: Annotated[ int, - util.ModelRef("support_subscription_type"), + record_base.ModelRef( + "support_subscription_type", + support_subscription_type_module.SupportSubscriptionType, + ), ] """The ID of the type of the support subscription.""" support_subscription_type_name: Annotated[ str, - util.ModelRef("support_subscription_type"), + record_base.ModelRef( + "support_subscription_type", + support_subscription_type_module.SupportSubscriptionType, + ), ] """The name of the type of the support subscription.""" support_subscription_type: Annotated[ support_subscription_type_module.SupportSubscriptionType, - util.ModelRef("support_subscription_type"), + record_base.ModelRef( + "support_subscription_type", + support_subscription_type_module.SupportSubscriptionType, + ), ] """The type of the support subscription. diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index 406bafe..35fb221 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class SupportSubscriptionType(record_base.RecordBase): @@ -29,17 +29,26 @@ class SupportSubscriptionType(record_base.RecordBase): name: str """The name of the support subscription type.""" - product_id: Annotated[int, util.ModelRef("product")] + product_id: Annotated[ + int, + record_base.ModelRef("product", product_module.Product), + ] """The ID for the product to use to invoice the support subscription. """ - product_name: Annotated[str, util.ModelRef("product")] + product_name: Annotated[ + str, + record_base.ModelRef("product", product_module.Product), + ] """The name of the product to use to invoice the support subscription. """ - product: Annotated[product_module.Product, util.ModelRef("product")] + product: Annotated[ + product_module.Product, + record_base.ModelRef("product", product_module.Product), + ] """The product to use to invoice the support subscription. @@ -50,12 +59,21 @@ class SupportSubscriptionType(record_base.RecordBase): usage_percent: float """Percentage of usage compared to price (0-100).""" - support_subscription_ids: Annotated[List[int], util.ModelRef("product")] + support_subscription_ids: Annotated[ + List[int], + record_base.ModelRef( + "support_subscription", + support_subscription_type.SupportSubscription, + ), + ] """A list of IDs for the support subscriptions of this type.""" support_subscription: Annotated[ List[support_subscription_type.SupportSubscription], - util.ModelRef("product"), + record_base.ModelRef( + "support_subscription", + support_subscription_type.SupportSubscription, + ), ] """The list of support subscriptions of this type. @@ -65,7 +83,10 @@ class SupportSubscriptionType(record_base.RecordBase): support_subscriptions: Annotated[ List[support_subscription_type.SupportSubscription], - util.FieldAlias("support_subscription"), + record_base.ModelRef( + "support_subscription", + support_subscription_type.SupportSubscription, + ), ] """An alias for ``support_subscription``.""" @@ -78,7 +99,7 @@ class SupportSubscriptionTypeManager( # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa :E402 +from . import ( # noqa: E402 product as product_module, support_subscription as support_subscription_type, ) diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index f64d14b..5a3015d 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class Tax(record_base.RecordBase): @@ -44,13 +44,22 @@ class Tax(record_base.RecordBase): to the same analytic account as the invoice line (if any). """ - company_id: Annotated[int, util.ModelRef("company_id")] + company_id: Annotated[ + int, + record_base.ModelRef("company_id", company_module.Company), + ] """The ID for the company this tax is owned by.""" - company_name: Annotated[str, util.ModelRef("company_id")] + company_name: Annotated[ + str, + record_base.ModelRef("company_id", company_module.Company), + ] """The name of the company this tax is owned by.""" - company: Annotated[company_module.Company, util.ModelRef("company_id")] + company: Annotated[ + company_module.Company, + record_base.ModelRef("company_id", company_module.Company), + ] """The company this tax is owned by. This fetches the full record from Odoo once, @@ -83,15 +92,21 @@ class Tax(record_base.RecordBase): * ``on_payment`` - Due as soon as payment of the invoice is received """ - tax_group_id: Annotated[int, util.ModelRef("tax_group_id")] + tax_group_id: Annotated[ + int, + record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), + ] """The ID for the tax group this tax is categorised under.""" - tax_group_name: Annotated[str, util.ModelRef("tax_group_id")] + tax_group_name: Annotated[ + str, + record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), + ] """The name of the tax group this tax is categorised under.""" tax_group: Annotated[ tax_group_module.TaxGroup, - util.ModelRef("tax_group_id"), + record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), ] """The tax group this tax is categorised under. diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index 1c023d6..903b9ee 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -18,9 +18,9 @@ from datetime import date from typing import Optional -from typing_extensions import Annotated +from typing_extensions import Annotated, Self -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class TermDiscount(record_base.RecordBase): @@ -36,20 +36,32 @@ class TermDiscount(record_base.RecordBase): min_commit: float """The minimum commitment for this term discount to apply.""" - partner_id: Annotated[int, util.ModelRef("partner")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner", partner_module.Partner), + ] """The ID for the partner that receives this term discount.""" - partner_name: Annotated[str, util.ModelRef("partner")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner", partner_module.Partner), + ] """The name of the partner that receives this term discount.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner", partner_module.Partner), + ] """The partner that receives this term discount. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - project_id: Annotated[Optional[int], util.ModelRef("project")] + project_id: Annotated[ + Optional[int], + record_base.ModelRef("project", project_module.Project), + ] """The ID for the project this term discount applies to, if it is a project-specific term discount. @@ -57,7 +69,10 @@ class TermDiscount(record_base.RecordBase): the partner owns. """ - project_name: Annotated[Optional[str], util.ModelRef("project")] + project_name: Annotated[ + Optional[str], + record_base.ModelRef("project", project_module.Project), + ] """The name of the project this term discount applies to, if it is a project-specific term discount. @@ -67,7 +82,7 @@ class TermDiscount(record_base.RecordBase): project: Annotated[ Optional[project_module.Project], - util.ModelRef("project"), + record_base.ModelRef("project", project_module.Project), ] """The project this term discount applies to, if it is a project-specific term discount. @@ -82,22 +97,25 @@ class TermDiscount(record_base.RecordBase): start_date: date """The date from which this term discount starts.""" - superseded_by_id: Annotated[Optional[int], util.ModelRef("superseded_by")] + superseded_by_id: Annotated[ + Optional[int], + record_base.ModelRef("superseded_by", Self), + ] """The ID for the term discount that supersedes this one, if superseded. """ superseded_by_name: Annotated[ Optional[str], - util.ModelRef("superseded_by"), + record_base.ModelRef("superseded_by", Self), ] """The name of the term discount that supersedes this one, if superseded. """ superseded_by: Annotated[ - Optional[TermDiscount], - util.ModelRef("superseded_by"), + Optional[Self], + record_base.ModelRef("superseded_by", Self), ] """The term discount that supersedes this one, if superseded. diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index ffbe254..a2dc47c 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -20,7 +20,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class Trial(record_base.RecordBase): @@ -38,13 +38,22 @@ class Trial(record_base.RecordBase): end_date: date """The end date of this trial.""" - partner_id: Annotated[int, util.ModelRef("partner")] + partner_id: Annotated[ + int, + record_base.ModelRef("partner", partner_module.Partner), + ] """The ID for the target partner for this trial.""" - partner_name: Annotated[str, util.ModelRef("partner")] + partner_name: Annotated[ + str, + record_base.ModelRef("partner", partner_module.Partner), + ] """The name of the target partner for this trial.""" - partner: Annotated[partner_module.Partner, util.ModelRef("partner")] + partner: Annotated[ + partner_module.Partner, + record_base.ModelRef("partner", partner_module.Partner), + ] """The target partner for this trial. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index 1b56269..6ab9641 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -19,22 +19,28 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class Uom(record_base.RecordBase): active: bool """Whether or not this Unit of Measure is active (enabled).""" - category_id: Annotated[int, util.ModelRef("category_id")] + category_id: Annotated[ + int, + record_base.ModelRef("category_id", uom_category.UomCategory), + ] """The ID for the category this Unit of Measure is classified as.""" - category_name: Annotated[str, util.ModelRef("category_id")] + category_name: Annotated[ + str, + record_base.ModelRef("category_id", uom_category.UomCategory), + ] """The name of the category this Unit of Measure is classified as.""" category: Annotated[ uom_category.UomCategory, - util.ModelRef("category_id"), + record_base.ModelRef("category_id", uom_category.UomCategory), ] """The category this Unit of Measure is classified as. diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 2298784..166564a 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -19,13 +19,16 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base, util +from . import record_base, record_manager_base class VolumeDiscountRange(record_base.RecordBase): customer_group_id: Annotated[ Optional[int], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The ID for the customer group this volume discount range applies to, if a specific customer group is set. @@ -33,7 +36,10 @@ class VolumeDiscountRange(record_base.RecordBase): customer_group_name: Annotated[ Optional[str], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The name of the customer group this volume discount range applies to, if a specific customer group is set. @@ -41,7 +47,10 @@ class VolumeDiscountRange(record_base.RecordBase): customer_group: Annotated[ Optional[customer_group_module.CustomerGroup], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The customer group this volume discount range applies to, if a specific customer group is set. @@ -81,7 +90,7 @@ def get_for_charge( self, charge: float, customer_group: Optional[ - Union[customer_group_module.CustomerGroup, int], + Union[int, customer_group_module.CustomerGroup], ] = None, ) -> Optional[VolumeDiscountRange]: """Return the volume discount range to apply to a given charge. @@ -98,25 +107,12 @@ def get_for_charge( :param charge: The charge for to find the applicable discount range :type charge: float :param customer_group: Get discount for a specific customer group - :type customer_group: Union[Model, int, Literal[False]], optional + :type customer_group: Optional[Union[int, CustomerGroup]], optional :return: Highest percentage applicable discount range (if found) :rtype: Optional[VolumeDiscountRange] """ ranges = self.search( - [ - ( - "customer_group", - "=", - ( - customer_group.id - if isinstance( - customer_group, - customer_group_module.CustomerGroup, - ) - else (customer_group or False) - ), - ), - ], + [("customer_group", "=", customer_group or False)], ) found_ranges: List[VolumeDiscountRange] = [] for vol_range in ranges: diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 02ce983..b124785 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -20,7 +20,7 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base, util +from . import record_base, record_manager_name_base class VoucherCode(record_base.RecordBase): @@ -35,19 +35,25 @@ class VoucherCode(record_base.RecordBase): created by the voucher code. """ - credit_type_id: Annotated[Optional[int], util.ModelRef("credit_type")] + credit_type_id: Annotated[ + Optional[int], + record_base.ModelRef("credit_type", credit_type_module.CreditType), + ] """The ID of the credit type to use, if a credit is to be created by this voucher code. """ - credit_type_name: Annotated[Optional[str], util.ModelRef("credit_type")] + credit_type_name: Annotated[ + Optional[str], + record_base.ModelRef("credit_type", credit_type_module.CreditType), + ] """The name of the credit type to use, if a credit is to be created by this voucher code. """ credit_type: Annotated[ Optional[credit_type_module.CreditType], - util.ModelRef("credit_type"), + record_base.ModelRef("credit_type", credit_type_module.CreditType), ] """The credit type to use, if a credit is to be created by this voucher code. @@ -63,7 +69,10 @@ class VoucherCode(record_base.RecordBase): customer_group_id: Annotated[ Optional[int], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The ID of the customer group this voucher code is available to. @@ -72,7 +81,10 @@ class VoucherCode(record_base.RecordBase): customer_group_name: Annotated[ Optional[str], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The name of the customer group this voucher code is available to. @@ -81,7 +93,10 @@ class VoucherCode(record_base.RecordBase): customer_group: Annotated[ Optional[customer_group_module.CustomerGroup], - util.ModelRef("customer_group"), + record_base.ModelRef( + "customer_group", + customer_group_module.CustomerGroup, + ), ] """The customer group this voucher code is available to. @@ -99,19 +114,25 @@ class VoucherCode(record_base.RecordBase): created by the voucher code. """ - grant_type_id: Annotated[Optional[int], util.ModelRef("grant_type")] + grant_type_id: Annotated[ + Optional[int], + record_base.ModelRef("grant_type", grant_type_module.GrantType), + ] """The ID of the grant type to use, if a grant is to be created by this voucher code. """ - grant_type_name: Annotated[Optional[str], util.ModelRef("grant_type")] + grant_type_name: Annotated[ + Optional[str], + record_base.ModelRef("grant_type", grant_type_module.GrantType), + ] """The name of the grant type to use, if a grant is to be created by this voucher code. """ grant_type: Annotated[ Optional[grant_type_module.GrantType], - util.ModelRef("grant_type"), + record_base.ModelRef("grant_type", grant_type_module.GrantType), ] """The grant type to use, if a grant is to be created by this voucher code. @@ -145,35 +166,44 @@ class VoucherCode(record_base.RecordBase): If unset, use the default quota size. """ - sales_person_id: Annotated[Optional[int], util.ModelRef("sales_person")] - """The ID for the salesperson responsible for this + sales_person_id: Annotated[ + Optional[int], + record_base.ModelRef("sales_person", partner.Partner), + ] + """The ID for the salesperson partner responsible for this voucher code, if assigned. """ - sales_person_name: Annotated[Optional[str], util.ModelRef("sales_person")] - """The name of the salesperson responsible for this + sales_person_name: Annotated[ + Optional[str], + record_base.ModelRef("sales_person", partner.Partner), + ] + """The name of the salesperson partner responsible for this voucher code, if assigned. """ sales_person: Annotated[ Optional[partner.Partner], - util.ModelRef("sales_person"), + record_base.ModelRef("sales_person"), ] - """The salesperson responsible for this + """The salesperson partner responsible for this voucher code, if assigned. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - tag_ids: Annotated[List[int], util.ModelRef("tags")] + tag_ids: Annotated[ + List[int], + record_base.ModelRef("tags", partner_category.PartnerCategory), + ] """A list of IDs for the tags (partner categories) to assign to partners for new accounts that signed up using this voucher code. """ tags: Annotated[ List[partner_category.PartnerCategory], - util.ModelRef("tags"), + record_base.ModelRef("tags", partner_category.PartnerCategory), ] """The list of tags (partner categories) to assign to partners for new accounts that signed up using this voucher code. From 1059ec2465d9b9b74a7f6d78092ad2b021810416 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 14:55:19 +1200 Subject: [PATCH 30/87] Fix unset record classes --- openstack_odooclient/managers/grant_type.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 4558038..bdfaefd 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -101,17 +101,26 @@ class GrantType(record_base.RecordBase): part of an invoice grouping if it is on the group root project. """ - product_id: Annotated[int, record_base.ModelRef("product")] + product_id: Annotated[ + int, + record_base.ModelRef("product", product_module.Product), + ] """The ID of the product to use when applying the grant to invoices. """ - product_name: Annotated[str, record_base.ModelRef("product")] + product_name: Annotated[ + str, + record_base.ModelRef("product", product_module.Product), + ] """The name of the product to use when applying the grant to invoices. """ - product: Annotated[product_module.Product, record_base.ModelRef("product")] + product: Annotated[ + product_module.Product, + record_base.ModelRef("product", product_module.Product), + ] """The product to use when applying the grant to invoices. This fetches the full record from Odoo once, From c4583d3827152c44091158495062ff56c478641b Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 15:01:06 +1200 Subject: [PATCH 31/87] Fix field alias definitionb --- openstack_odooclient/managers/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 2910a07..513a0ff 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -102,7 +102,7 @@ class SaleOrder(record_base.RecordBase): order_lines: Annotated[ List[sale_order_line.SaleOrderLine], - record_base.FieldAlias("order_line", sale_order_line.SaleOrderLine), + record_base.FieldAlias("order_line"), ] """An alias for ``order_line``.""" From 1321ab7ced665b21e180691ca8cdc97b3710b256 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 15:03:49 +1200 Subject: [PATCH 32/87] Fix more unset record classes --- openstack_odooclient/managers/grant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index b256210..9c1064c 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -76,7 +76,7 @@ class Grant(record_base.RecordBase): voucher_code: Annotated[ Optional[voucher_code_module.VoucherCode], - record_base.ModelRef("voucher_code"), + record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), ] """The voucher code used when applying for the grant, if one was supplied. From 457fe99b3888dddeeb8361bfb30cf20173a0503f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 15:19:28 +1200 Subject: [PATCH 33/87] Handle multi-value search domains --- .../managers/record_manager_base.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/managers/record_manager_base.py index 0c08d42..637b4ed 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/managers/record_manager_base.py @@ -399,7 +399,30 @@ def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: field=f[0], ) operator = f[1] - value = self._encode_value(type_hint=field_type, value=f[2]) + # NOTE(callumdickinson): ORM API search domains. + # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains + if operator in ("in", "not in"): + value = [ + self._encode_value(type_hint=field_type, value=v) + for v in f[2] + ] + elif operator in ("child_of", "parent_of"): + value = ( + [ + self._encode_value(type_hint=field_type, value=v) + for v in f[2] + ] + if isinstance(f[2], (list, set, tuple)) + else self._encode_value( + type_hint=field_type, + value=f[2], + ) + ) + else: + value = self._encode_value( + type_hint=field_type, + value=f[2], + ) _filter = (field_name, operator, value) else: _filter = f From e2409be46413cfb189a2a72d0013db6a40981fc2 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 17 Jun 2024 18:36:56 +1200 Subject: [PATCH 34/87] Start cleaning up imports --- openstack_odooclient/__init__.py | 14 +- openstack_odooclient/base/__init__.py | 0 .../record_base.py => base/record.py} | 24 +-- .../record_manager.py} | 4 +- .../record_manager_coded.py} | 2 +- .../record_manager_named.py} | 2 +- .../record_manager_with_unique_field.py} | 2 +- openstack_odooclient/client.py | 5 +- openstack_odooclient/managers/account_move.py | 60 ++----- .../managers/account_move_line.py | 84 +++------ openstack_odooclient/managers/company.py | 48 ++--- openstack_odooclient/managers/credit.py | 50 ++---- .../managers/credit_transaction.py | 26 +-- openstack_odooclient/managers/credit_type.py | 61 ++----- openstack_odooclient/managers/currency.py | 9 +- .../managers/customer_group.py | 36 ++-- openstack_odooclient/managers/grant.py | 36 ++-- openstack_odooclient/managers/grant_type.py | 58 +++--- openstack_odooclient/managers/partner.py | 159 +++++----------- .../managers/partner_category.py | 39 ++-- openstack_odooclient/managers/pricelist.py | 64 ++----- openstack_odooclient/managers/product.py | 93 ++++------ .../managers/product_category.py | 27 ++- openstack_odooclient/managers/project.py | 103 ++++------- .../managers/project_contact.py | 45 ++--- .../managers/referral_code.py | 38 ++-- openstack_odooclient/managers/reseller.py | 49 ++--- .../managers/reseller_tier.py | 27 ++- openstack_odooclient/managers/sale_order.py | 75 +++----- .../managers/sale_order_line.py | 170 +++++------------- .../managers/support_subscription.py | 64 ++----- .../managers/support_subscription_type.py | 47 ++--- openstack_odooclient/{managers => }/util.py | 0 33 files changed, 478 insertions(+), 1043 deletions(-) create mode 100644 openstack_odooclient/base/__init__.py rename openstack_odooclient/{managers/record_base.py => base/record.py} (95%) rename openstack_odooclient/{managers/record_manager_base.py => base/record_manager.py} (99%) rename openstack_odooclient/{managers/record_manager_code_base.py => base/record_manager_coded.py} (99%) rename openstack_odooclient/{managers/record_manager_name_base.py => base/record_manager_named.py} (99%) rename openstack_odooclient/{managers/record_manager_unique_field_base.py => base/record_manager_with_unique_field.py} (99%) rename openstack_odooclient/{managers => }/util.py (100%) diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index 70c81f1..0e97b87 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -15,6 +15,13 @@ from __future__ import annotations +from .base.record import FieldAlias, ModelRef, RecordBase +from .base.record_manager import RecordManagerBase +from .base.record_manager_coded import CodedRecordManagerBase +from .base.record_manager_named import NamedRecordManagerBase +from .base.record_manager_with_unique_field import ( + RecordManagerWithUniqueFieldBase, +) from .client import Client from .exceptions import ( ClientError, @@ -38,13 +45,6 @@ from .managers.product_category import ProductCategory from .managers.project import Project from .managers.project_contact import ProjectContact -from .managers.record_base import FieldAlias, ModelRef, RecordBase -from .managers.record_manager_base import RecordManagerBase -from .managers.record_manager_code_base import CodedRecordManagerBase -from .managers.record_manager_name_base import NamedRecordManagerBase -from .managers.record_manager_unique_field_base import ( - RecordManagerWithUniqueFieldBase, -) from .managers.referral_code import ReferralCode from .managers.reseller import Reseller from .managers.reseller_tier import ResellerTier diff --git a/openstack_odooclient/base/__init__.py b/openstack_odooclient/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/managers/record_base.py b/openstack_odooclient/base/record.py similarity index 95% rename from openstack_odooclient/managers/record_base.py rename to openstack_odooclient/base/record.py index a8d60c7..a61ffa5 100644 --- a/openstack_odooclient/managers/record_base.py +++ b/openstack_odooclient/base/record.py @@ -40,14 +40,14 @@ get_type_hints, ) -from .util import decode_value, is_subclass +from ..util import decode_value, is_subclass if TYPE_CHECKING: from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from .. import client - from . import record_manager_base + from ..client import Client + from .record_manager import RecordManagerBase class AnnotationBase: @@ -111,13 +111,13 @@ class RecordBase: create_date: datetime """The time the record was created.""" - create_uid: Annotated[int, ModelRef("create_uid", user.User)] + create_uid: Annotated[int, ModelRef("create_uid", User)] """The ID of the user that created this record.""" - create_name: Annotated[str, ModelRef("create_uid", user.User)] + create_name: Annotated[str, ModelRef("create_uid", User)] """The name of the user that created this record.""" - create_user: Annotated[user.User, ModelRef("create_uid", user.User)] + create_user: Annotated[User, ModelRef("create_uid", User)] """The user that created this record. This fetches the full record from Odoo once, @@ -127,13 +127,13 @@ class RecordBase: write_date: datetime """The time the record was last modified.""" - write_uid: Annotated[int, ModelRef("write_uid", user.User)] + write_uid: Annotated[int, ModelRef("write_uid", User)] """The ID for the user that last modified this record.""" - write_name: Annotated[str, ModelRef("write_uid", user.User)] + write_name: Annotated[str, ModelRef("write_uid", User)] """The name of the user that last modified this record.""" - write_user: Annotated[user.User, ModelRef("write_uid", user.User)] + write_user: Annotated[User, ModelRef("write_uid", User)] """The user that last modified this record. This fetches the full record from Odoo once, @@ -155,8 +155,8 @@ class RecordBase: def __init__( self, - client: client.Client, - manager: record_manager_base.RecordManagerBase, + client: Client, + manager: RecordManagerBase, record: Dict[str, Any], fields: Optional[Sequence[str]], ) -> None: @@ -430,4 +430,4 @@ def __repr__(self) -> str: # NOTE(callumdickinson): Import here to avoid circular imports. -from . import user # noqa: E402 +from ..managers.user import User # noqa: E402 diff --git a/openstack_odooclient/managers/record_manager_base.py b/openstack_odooclient/base/record_manager.py similarity index 99% rename from openstack_odooclient/managers/record_manager_base.py rename to openstack_odooclient/base/record_manager.py index 637b4ed..b1b2873 100644 --- a/openstack_odooclient/managers/record_manager_base.py +++ b/openstack_odooclient/base/record_manager.py @@ -44,14 +44,14 @@ ) from ..exceptions import RecordNotFoundError -from .record_base import ModelRef, RecordBase -from .util import ( +from ..util import ( DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_TIME_FORMAT, get_mapped_field, is_subclass, ) +from .record import ModelRef, RecordBase if TYPE_CHECKING: from odoorpc import ODOO # type: ignore[import] diff --git a/openstack_odooclient/managers/record_manager_code_base.py b/openstack_odooclient/base/record_manager_coded.py similarity index 99% rename from openstack_odooclient/managers/record_manager_code_base.py rename to openstack_odooclient/base/record_manager_coded.py index d530bdc..b1f5ee8 100644 --- a/openstack_odooclient/managers/record_manager_code_base.py +++ b/openstack_odooclient/base/record_manager_coded.py @@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, overload -from .record_manager_unique_field_base import ( +from .record_manager_with_unique_field import ( Record, RecordManagerWithUniqueFieldBase, ) diff --git a/openstack_odooclient/managers/record_manager_name_base.py b/openstack_odooclient/base/record_manager_named.py similarity index 99% rename from openstack_odooclient/managers/record_manager_name_base.py rename to openstack_odooclient/base/record_manager_named.py index c2a4aa3..2e0a4f1 100644 --- a/openstack_odooclient/managers/record_manager_name_base.py +++ b/openstack_odooclient/base/record_manager_named.py @@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, overload -from .record_manager_unique_field_base import ( +from .record_manager_with_unique_field import ( Record, RecordManagerWithUniqueFieldBase, ) diff --git a/openstack_odooclient/managers/record_manager_unique_field_base.py b/openstack_odooclient/base/record_manager_with_unique_field.py similarity index 99% rename from openstack_odooclient/managers/record_manager_unique_field_base.py rename to openstack_odooclient/base/record_manager_with_unique_field.py index eb70e5c..0df93c0 100644 --- a/openstack_odooclient/managers/record_manager_unique_field_base.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar, overload from ..exceptions import MultipleRecordsFoundError, RecordNotFoundError -from .record_manager_base import Record, RecordManagerBase +from .record_manager import Record, RecordManagerBase if TYPE_CHECKING: from typing import ( diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 24882f6..75acb00 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -24,6 +24,7 @@ from odoorpc import ODOO # type: ignore[import] from packaging.version import Version +from .base import record from .managers import ( account_move, account_move_line, @@ -67,7 +68,7 @@ from odoorpc.env import Environment # type: ignore[import] from odoorpc.report import Report # type: ignore[import] - from .managers import record_base, record_manager_base + from .managers import record_manager_base class Client: @@ -197,7 +198,7 @@ def __init__( # and used when converting model references on record objects into # # new record objects. self._record_manager_mapping: Dict[ - Type[record_base.RecordBase], + Type[record.RecordBase], record_manager_base.RecordManagerBase, ] = {} # Create record managers. diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index a175d79..9c752ae 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -20,37 +20,26 @@ from typing_extensions import Annotated -from . import ( - currency as currency_module, - project, - record_base, - record_manager_name_base, -) +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase +from .currency import Currency +from .project import Project -class AccountMove(record_base.RecordBase): +class AccountMove(RecordBase): amount_total: float """Total (taxed) amount charged on the account move (invoice).""" amount_untaxed: float """Total (untaxed) amount charged on the account move (invoice).""" - currency_id: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this account move (invoice).""" - currency_name: Annotated[ - str, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_name: Annotated[str, ModelRef("currency_id", Currency)] """The name of the currency used in this account move (invoice).""" - currency: Annotated[ - currency_module.Currency, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency: Annotated[Currency, ModelRef("currency_id", Currency)] """The currency used in this account move (invoice). This fetches the full record from Odoo once, @@ -62,21 +51,15 @@ class AccountMove(record_base.RecordBase): invoice_line_ids: Annotated[ List[int], - record_base.ModelRef( - "invoice_line_ids", - account_move_line.AccountMoveLine, - ), + ModelRef("invoice_line_ids", AccountMoveLine), ] """The list of the IDs for the account move (invoice) lines that comprise this account move (invoice). """ invoice_lines: Annotated[ - List[account_move_line.AccountMoveLine], - record_base.ModelRef( - "invoice_line_ids", - account_move_line.AccountMoveLine, - ), + List[AccountMoveLine], + ModelRef("invoice_line_ids", AccountMoveLine), ] """A list of account move (invoice) lines that comprise this account move (invoice). @@ -113,26 +96,17 @@ class AccountMove(record_base.RecordBase): name: Union[str, Literal[False]] """Name assigned to the account move (invoice), if posted.""" - os_project_id: Annotated[ - Optional[int], - record_base.ModelRef("os_project", project.Project), - ] + os_project_id: Annotated[Optional[int], ModelRef("os_project", Project)] """The ID of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. """ - os_project_name: Annotated[ - Optional[str], - record_base.ModelRef("os_project", project.Project), - ] + os_project_name: Annotated[Optional[str], ModelRef("os_project", Project)] """The name of the OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. """ - os_project: Annotated[ - Optional[project.Project], - record_base.ModelRef("os_project", project.Project), - ] + os_project: Annotated[Optional[Project], ModelRef("os_project", Project)] """The OpenStack project this account move (invoice) was generated for, if this is an invoice for OpenStack project usage. @@ -200,12 +174,10 @@ def send_openstack_invoice_email( ) -class AccountMoveManager( - record_manager_name_base.NamedRecordManagerBase[AccountMove], -): +class AccountMoveManager(NamedRecordManagerBase[AccountMove]): env_name = "account.move" record_class = AccountMove # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import account_move_line # noqa: E402 +from .account_move_line import AccountMoveLine # noqa: E402 diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index 8276bc5..320d62a 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -19,36 +19,25 @@ from typing_extensions import Annotated -from . import ( - currency as currency_module, - product as product_module, - project, - record_base, - record_manager_base, -) - - -class AccountMoveLine(record_base.RecordBase): - currency_id: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase +from .currency import Currency +from .product import Product +from .project import Project + + +class AccountMoveLine(RecordBase): + currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this account move (invoice) line. """ - currency_name: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_name: Annotated[int, ModelRef("currency_id", Currency)] """The name of the currency used in this account move (invoice) line. """ - currency: Annotated[ - currency_module.Currency, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency: Annotated[Currency, ModelRef("currency_id", Currency)] """The currency used in this account move (invoice) line. @@ -59,22 +48,13 @@ class AccountMoveLine(record_base.RecordBase): line_tax_amount: float """Amount charged in tax on the account move (invoice) line.""" - move_id: Annotated[ - int, - record_base.ModelRef("move_id", account_move.AccountMove), - ] + move_id: Annotated[int, ModelRef("move_id", AccountMove)] """The ID for the account move (invoice) this line is part of.""" - move_name: Annotated[ - str, - record_base.ModelRef("move_id", account_move.AccountMove), - ] + move_name: Annotated[str, ModelRef("move_id", AccountMove)] """The name of the account move (invoice) this line is part of.""" - move: Annotated[ - account_move.AccountMove, - record_base.ModelRef("move_id", account_move.AccountMove), - ] + move: Annotated[AccountMove, ModelRef("move_id", AccountMove)] """The account move (invoice) this line is part of. This fetches the full record from Odoo once, @@ -84,26 +64,17 @@ class AccountMoveLine(record_base.RecordBase): name: str """Name of the product charged on the account move (invoice) line.""" - os_project_id: Annotated[ - Optional[int], - record_base.ModelRef("os_project", project.Project), - ] + os_project_id: Annotated[Optional[int], ModelRef("os_project", Project)] """The ID for the OpenStack project this account move (invoice) line was generated for. """ - os_project_name: Annotated[ - Optional[str], - record_base.ModelRef("os_project", project.Project), - ] + os_project_name: Annotated[Optional[str], ModelRef("os_project", Project)] """The name of the OpenStack project this account move (invoice) line was generated for. """ - os_project: Annotated[ - Optional[project.Project], - record_base.ModelRef("os_project", project.Project), - ] + os_project: Annotated[Optional[Project], ModelRef("os_project", Project)] """The OpenStack project this account move (invoice) line was generated for. @@ -142,26 +113,17 @@ class AccountMoveLine(record_base.RecordBase): price_unit: float """Unit price for the product used on the account move (invoice) line.""" - product_id: Annotated[ - int, - record_base.ModelRef("product_id", product_module.Product), - ] + product_id: Annotated[int, ModelRef("product_id", Product)] """The ID for the product charged on the account move (invoice) line. """ - product_name: Annotated[ - str, - record_base.ModelRef("product_id", product_module.Product), - ] + product_name: Annotated[str, ModelRef("product_id", Product)] """The name of the product charged on the account move (invoice) line. """ - product: Annotated[ - product_module.Product, - record_base.ModelRef("product_id", product_module.Product), - ] + product: Annotated[Product, ModelRef("product_id", Product)] """The product charged on the account move (invoice) line. @@ -173,12 +135,10 @@ class AccountMoveLine(record_base.RecordBase): """Quantity of product charged on the account move (invoice) line.""" -class AccountMoveLineManager( - record_manager_base.RecordManagerBase[AccountMoveLine], -): +class AccountMoveLineManager(RecordManagerBase[AccountMoveLine]): env_name = "account.move.line" record_class = AccountMoveLine # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import account_move # noqa: E402 +from .account_move import AccountMove # noqa: E402 diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py index 28c6b85..a48c981 100644 --- a/openstack_odooclient/managers/company.py +++ b/openstack_odooclient/managers/company.py @@ -19,23 +19,18 @@ from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class Company(record_base.RecordBase): +class Company(RecordBase): active: bool """Whether or not this company is active (enabled).""" - child_ids: Annotated[ - List[int], - record_base.ModelRef("child_ids", Self), - ] + child_ids: Annotated[List[int], ModelRef("child_ids", Self)] """A list of IDs for the child companies.""" - children: Annotated[ - List[Self], - record_base.ModelRef("child_ids", Self), - ] + children: Annotated[List[Self], ModelRef("child_ids", Self)] """The list of child companies. This fetches the full records from Odoo once, @@ -45,23 +40,17 @@ class Company(record_base.RecordBase): name: str """Company name, set from the partner name.""" - parent_id: Annotated[ - Optional[int], - record_base.ModelRef("parent_id", Self), - ] + parent_id: Annotated[Optional[int], ModelRef("parent_id", Self)] """The ID for the parent company, if this company is the child of another company. """ - parent_name: Annotated[ - Optional[str], - record_base.ModelRef("parent_id", Self), - ] + parent_name: Annotated[Optional[str], ModelRef("parent_id", Self)] """The name of the parent company, if this company is the child of another company. """ - parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] """The parent company, if this company is the child of another company. @@ -72,22 +61,13 @@ class Company(record_base.RecordBase): parent_path: Union[str, Literal[False]] """The path of the parent company, if there is a parent.""" - partner_id: Annotated[ - int, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner_id", Partner)] """The ID for the partner for the company.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner_id", Partner)] """The name of the partner for the company.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner_id", Partner)] """The partner for the company. This fetches the full record from Odoo once, @@ -95,12 +75,10 @@ class Company(record_base.RecordBase): """ -class CompanyManager( - record_manager_name_base.NamedRecordManagerBase[Company], -): +class CompanyManager(NamedRecordManagerBase[Company]): env_name = "res.company" record_class = Company # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import partner as partner_module # noqa: E402 +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index df2d044..795f8b1 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -20,26 +20,18 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class Credit(record_base.RecordBase): - credit_type_id: Annotated[ - int, - record_base.ModelRef("credit_type", credit_type_module.CreditType), - ] +class Credit(RecordBase): + credit_type_id: Annotated[int, ModelRef("credit_type", CreditType)] """The ID of the type of this credit.""" - credit_type_name: Annotated[ - str, - record_base.ModelRef("credit_type", credit_type_module.CreditType), - ] + credit_type_name: Annotated[str, ModelRef("credit_type", CreditType)] """The name of the type of this credit.""" - credit_type: Annotated[ - credit_type_module.CreditType, - record_base.ModelRef("credit_type", credit_type_module.CreditType), - ] + credit_type: Annotated[CreditType, ModelRef("credit_type", CreditType)] """The type of this credit. This fetches the full record from Odoo once, @@ -63,21 +55,15 @@ class Credit(record_base.RecordBase): transaction_ids: Annotated[ List[int], - record_base.ModelRef( - "transactions", - credit_transaction.CreditTransaction, - ), + ModelRef("transactions", CreditTransaction), ] """A list of IDs for the transactions that have been made using this credit. """ transactions: Annotated[ - List[credit_transaction.CreditTransaction], - record_base.ModelRef( - "transactions", - credit_transaction.CreditTransaction, - ), + List[CreditTransaction], + ModelRef("transactions", CreditTransaction), ] """The transactions that have been made using this credit. @@ -87,7 +73,7 @@ class Credit(record_base.RecordBase): voucher_code_id: Annotated[ Optional[int], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ModelRef("voucher_code", VoucherCode), ] """The ID of the voucher code used when applying for the credit, if one was supplied. @@ -95,15 +81,15 @@ class Credit(record_base.RecordBase): voucher_code_name: Annotated[ Optional[str], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ModelRef("voucher_code", VoucherCode), ] """The name of the voucher code used when applying for the credit, if one was supplied. """ voucher_code: Annotated[ - Optional[voucher_code_module.VoucherCode], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + Optional[VoucherCode], + ModelRef("voucher_code", VoucherCode), ] """The voucher code used when applying for the credit, if one was supplied. @@ -113,14 +99,12 @@ class Credit(record_base.RecordBase): """ -class CreditManager(record_manager_base.RecordManagerBase[Credit]): +class CreditManager(RecordManagerBase[Credit]): env_name = "openstack.credit" record_class = Credit # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - credit_transaction, - credit_type as credit_type_module, - voucher_code as voucher_code_module, -) +from .credit_transaction import CreditTransaction # noqa: E402 +from .credit_type import CreditType # noqa: E402 +from .voucher_code import VoucherCode # noqa: E402 diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index a142228..76d1324 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -17,26 +17,18 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class CreditTransaction(record_base.RecordBase): - credit_id: Annotated[ - int, - record_base.ModelRef("credit", credit_module.Credit), - ] +class CreditTransaction(RecordBase): + credit_id: Annotated[int, ModelRef("credit", Credit)] """The ID of the credit this transaction was made against.""" - credit_name: Annotated[ - str, - record_base.ModelRef("credit", credit_module.Credit), - ] + credit_name: Annotated[str, ModelRef("credit", Credit)] """The name of the credit this transaction was made against.""" - credit: Annotated[ - credit_module.Credit, - record_base.ModelRef("credit", credit_module.Credit), - ] + credit: Annotated[Credit, ModelRef("credit", Credit)] """The credit this transaction was made against. This fetches the full record from Odoo once, @@ -50,12 +42,10 @@ class CreditTransaction(record_base.RecordBase): """The value of the credit transaction.""" -class CreditTransactionManager( - record_manager_base.RecordManagerBase[CreditTransaction], -): +class CreditTransactionManager(RecordManagerBase[CreditTransaction]): env_name = "openstack.credit.transaction" record_class = CreditTransaction # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import credit as credit_module # noqa: E402 +from .credit import Credit # noqa: E402 diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index 3803aef..e3f1466 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -19,25 +19,17 @@ from typing_extensions import Annotated -from . import ( - product as product_module, - product_category, - record_base, - record_manager_name_base, -) +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase +from .product import Product +from .product_category import ProductCategory -class CreditType(record_base.RecordBase): - credit_ids: Annotated[ - List[int], - record_base.ModelRef("credits", credit.Credit), - ] +class CreditType(RecordBase): + credit_ids: Annotated[List[int], ModelRef("credits", Credit)] """A list of IDs for the credits which are of this credit type.""" - credits: Annotated[ - List[credit.Credit], - record_base.ModelRef("credits", credit.Credit), - ] + credits: Annotated[List[Credit], ModelRef("credits", Credit)] """A list of credits which are of this credit type. This fetches the full records from Odoo once, @@ -49,7 +41,7 @@ class CreditType(record_base.RecordBase): only_for_product_ids: Annotated[ List[int], - record_base.ModelRef("only_for_products", product_module.Product), + ModelRef("only_for_products", Product), ] """A list of IDs for the products this credit applies to. @@ -58,8 +50,8 @@ class CreditType(record_base.RecordBase): """ only_for_products: Annotated[ - List[product_module.Product], - record_base.ModelRef("only_for_products", product_module.Product), + List[Product], + ModelRef("only_for_products", Product), ] """A list of products which this credit applies to. @@ -72,10 +64,7 @@ class CreditType(record_base.RecordBase): only_for_product_category_ids: Annotated[ List[int], - record_base.ModelRef( - "only_for_product_categories", - product_category.ProductCategory, - ), + ModelRef("only_for_product_categories", ProductCategory), ] """A list of IDs for the product categories this credit applies to. @@ -85,11 +74,8 @@ class CreditType(record_base.RecordBase): """ only_for_product_categories: Annotated[ - List[product_category.ProductCategory], - record_base.ModelRef( - "only_for_product_categories", - product_category.ProductCategory, - ), + List[ProductCategory], + ModelRef("only_for_product_categories", ProductCategory), ] """A list of product categories which this credit applies to. @@ -101,26 +87,17 @@ class CreditType(record_base.RecordBase): and caches them for subsequent accesses. """ - product_id: Annotated[ - int, - record_base.ModelRef("product", product_module.Product), - ] + product_id: Annotated[int, ModelRef("product", Product)] """The ID of the product to use when applying the credit to invoices. """ - product_name: Annotated[ - str, - record_base.ModelRef("product", product_module.Product), - ] + product_name: Annotated[str, ModelRef("product", Product)] """The name of the product to use when applying the credit to invoices. """ - product: Annotated[ - product_module.Product, - record_base.ModelRef("product", product_module.Product), - ] + product: Annotated[Product, ModelRef("product", Product)] """The product to use when applying the credit to invoices. This fetches the full record from Odoo once, @@ -131,12 +108,10 @@ class CreditType(record_base.RecordBase): """Whether or not the credit is refundable.""" -class CreditTypeManager( - record_manager_name_base.NamedRecordManagerBase[CreditType], -): +class CreditTypeManager(NamedRecordManagerBase[CreditType]): env_name = "openstack.credit.type" record_class = CreditType # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import credit # noqa: E402 +from .credit import Credit # noqa: E402 diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py index 24c09f2..12435b2 100644 --- a/openstack_odooclient/managers/currency.py +++ b/openstack_odooclient/managers/currency.py @@ -18,10 +18,11 @@ from datetime import date as datetime_date from typing import Literal, Union -from . import record_base, record_manager_name_base +from ..base.record import RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class Currency(record_base.RecordBase): +class Currency(RecordBase): active: bool """Whether or not this currency is active (enabled).""" @@ -63,8 +64,6 @@ class Currency(record_base.RecordBase): """The currency sign to be used when printing amounts.""" -class CurrencyManager( - record_manager_name_base.NamedRecordManagerBase[Currency], -): +class CurrencyManager(NamedRecordManagerBase[Currency]): env_name = "res.currency" record_class = Currency diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index 810de23..255ea15 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -19,50 +19,39 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class CustomerGroup(record_base.RecordBase): +class CustomerGroup(RecordBase): name: str """The name of the customer group.""" - partner_ids: Annotated[ - List[int], - record_base.ModelRef("partners", partner.Partner), - ] + partner_ids: Annotated[List[int], ModelRef("partners", Partner)] """A list of IDs for the partners that are part of this customer group. """ - partners: Annotated[ - List[partner.Partner], - record_base.ModelRef("partners", partner.Partner), - ] + partners: Annotated[List[Partner], ModelRef("partners", Partner)] """The partners that are part of this customer group. This fetches the full records from Odoo once, and caches them for subsequent accesses. """ - pricelist_id: Annotated[ - Optional[int], - record_base.ModelRef("pricelist", pricelist_module.Pricelist), - ] + pricelist_id: Annotated[Optional[int], ModelRef("pricelist", Pricelist)] """The ID for the pricelist this customer group uses, if not the default one. """ - pricelist_name: Annotated[ - Optional[str], - record_base.ModelRef("pricelist", pricelist_module.Pricelist), - ] + pricelist_name: Annotated[Optional[str], ModelRef("pricelist", Pricelist)] """The name of the pricelist this customer group uses, if not the default one. """ pricelist: Annotated[ - Optional[pricelist_module.Pricelist], - record_base.ModelRef("pricelist", pricelist_module.Pricelist), + Optional[Pricelist], + ModelRef("pricelist", Pricelist), ] """The pricelist this customer group uses, if not the default one. @@ -71,12 +60,11 @@ class CustomerGroup(record_base.RecordBase): """ -class CustomerGroupManager( - record_manager_name_base.NamedRecordManagerBase[CustomerGroup], -): +class CustomerGroupManager(NamedRecordManagerBase[CustomerGroup]): env_name = "openstack.customer_group" record_class = CustomerGroup # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import partner, pricelist as pricelist_module # noqa: E402 +from .partner import Partner # noqa: E402 +from .pricelist import Pricelist # noqa: E402 diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index 9c1064c..5df03c1 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -20,29 +20,21 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class Grant(record_base.RecordBase): +class Grant(RecordBase): expiry_date: date """The date the grant expires.""" - grant_type_id: Annotated[ - int, - record_base.ModelRef("grant_type", grant_type_module.GrantType), - ] + grant_type_id: Annotated[int, ModelRef("grant_type", GrantType)] """The ID of the type of this grant.""" - grant_type_name: Annotated[ - str, - record_base.ModelRef("grant_type", grant_type_module.GrantType), - ] + grant_type_name: Annotated[str, ModelRef("grant_type", GrantType)] """The name of the type of this grant.""" - grant_type: Annotated[ - grant_type_module.GrantType, - record_base.ModelRef("grant_type", grant_type_module.GrantType), - ] + grant_type: Annotated[GrantType, ModelRef("grant_type", GrantType)] """The type of this grant. This fetches the full record from Odoo once, @@ -60,7 +52,7 @@ class Grant(record_base.RecordBase): voucher_code_id: Annotated[ Optional[int], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ModelRef("voucher_code", VoucherCode), ] """The ID of the voucher code used when applying for the grant, if one was supplied. @@ -68,15 +60,15 @@ class Grant(record_base.RecordBase): voucher_code_name: Annotated[ Optional[str], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + ModelRef("voucher_code", VoucherCode), ] """The name of the voucher code used when applying for the grant, if one was supplied. """ voucher_code: Annotated[ - Optional[voucher_code_module.VoucherCode], - record_base.ModelRef("voucher_code", voucher_code_module.VoucherCode), + Optional[VoucherCode], + ModelRef("voucher_code", VoucherCode), ] """The voucher code used when applying for the grant, if one was supplied. @@ -86,13 +78,11 @@ class Grant(record_base.RecordBase): """ -class GrantManager(record_manager_base.RecordManagerBase[Grant]): +class GrantManager(RecordManagerBase[Grant]): env_name = "openstack.grant" record_class = Grant # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - grant_type as grant_type_module, - voucher_code as voucher_code_module, -) +from .grant_type import GrantType # noqa: E402 +from .voucher_code import VoucherCode # noqa: E402 diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index bdfaefd..80e2040 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -19,20 +19,15 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class GrantType(record_base.RecordBase): - grant_ids: Annotated[ - List[int], - record_base.ModelRef("grants", grant.Grant), - ] +class GrantType(RecordBase): + grant_ids: Annotated[List[int], ModelRef("grants", Grant)] """A list of IDs for the grants which are of this grant type.""" - grants: Annotated[ - List[grant.Grant], - record_base.ModelRef("grants", grant.Grant), - ] + grants: Annotated[List[Grant], ModelRef("grants", Grant)] """A list of grants which are of this grant type. This fetches the full records from Odoo once, @@ -44,7 +39,7 @@ class GrantType(record_base.RecordBase): only_for_product_ids: Annotated[ List[int], - record_base.ModelRef("only_for_products", product_module.Product), + ModelRef("only_for_products", Product), ] """A list of IDs for the products this grant applies to. @@ -53,8 +48,8 @@ class GrantType(record_base.RecordBase): """ only_for_products: Annotated[ - List[product_module.Product], - record_base.ModelRef("only_for_products", product_module.Product), + List[Product], + ModelRef("only_for_products", Product), ] """A list of products which this grant applies to. @@ -67,9 +62,9 @@ class GrantType(record_base.RecordBase): only_for_product_category_ids: Annotated[ List[int], - record_base.ModelRef( + ModelRef( "only_for_product_categories", - product_category.ProductCategory, + ProductCategory, ), ] """A list of IDs for the product categories this grant applies to. @@ -80,10 +75,10 @@ class GrantType(record_base.RecordBase): """ only_for_product_categories: Annotated[ - List[product_category.ProductCategory], - record_base.ModelRef( + List[ProductCategory], + ModelRef( "only_for_product_categories", - product_category.ProductCategory, + ProductCategory, ), ] """A list of product categories which this grant applies to. @@ -101,26 +96,17 @@ class GrantType(record_base.RecordBase): part of an invoice grouping if it is on the group root project. """ - product_id: Annotated[ - int, - record_base.ModelRef("product", product_module.Product), - ] + product_id: Annotated[int, ModelRef("product", Product)] """The ID of the product to use when applying the grant to invoices. """ - product_name: Annotated[ - str, - record_base.ModelRef("product", product_module.Product), - ] + product_name: Annotated[str, ModelRef("product", Product)] """The name of the product to use when applying the grant to invoices. """ - product: Annotated[ - product_module.Product, - record_base.ModelRef("product", product_module.Product), - ] + product: Annotated[Product, ModelRef("product", Product)] """The product to use when applying the grant to invoices. This fetches the full record from Odoo once, @@ -128,16 +114,12 @@ class GrantType(record_base.RecordBase): """ -class GrantTypeManager( - record_manager_name_base.NamedRecordManagerBase[GrantType], -): +class GrantTypeManager(NamedRecordManagerBase[GrantType]): env_name = "openstack.grant.type" record_class = GrantType # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - grant, - product as product_module, - product_category, -) +from .grant import Grant # noqa: E402 +from .product import Product # noqa: E402 +from .product_category import ProductCategory # noqa: E402 diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index ba990eb..9efd225 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -19,34 +19,22 @@ from typing_extensions import Annotated, Self -from . import ( - pricelist, - project, - record_base, - record_manager_base, -) +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase +from .pricelist import Pricelist -class Partner(record_base.RecordBase): +class Partner(RecordBase): active: bool """Whether or not this partner is active (enabled).""" - company_id: Annotated[ - int, - record_base.ModelRef("company_id", company_module.Company), - ] + company_id: Annotated[int, ModelRef("company_id", Company)] """The ID for the company this partner is owned by.""" - company_name: Annotated[ - str, - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[str, ModelRef("company_id", Company)] """The name of the company this partner is owned by.""" - company: Annotated[ - company_module.Company, - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Company, ModelRef("company_id", Company)] """The company this partner is owned by. This fetches the full record from Odoo once, @@ -61,10 +49,7 @@ class Partner(record_base.RecordBase): os_customer_group_id: Annotated[ Optional[int], - record_base.ModelRef( - "os_customer_group", - customer_group.CustomerGroup, - ), + ModelRef("os_customer_group", CustomerGroup), ] """The ID for the customer group this partner is part of, if it is part of one. @@ -72,21 +57,15 @@ class Partner(record_base.RecordBase): os_customer_group_name: Annotated[ Optional[str], - record_base.ModelRef( - "os_customer_group", - customer_group.CustomerGroup, - ), + ModelRef("os_customer_group", CustomerGroup), ] """The name of the customer group this partner is part of, if it is part of one. """ os_customer_group: Annotated[ - Optional[customer_group.CustomerGroup], - record_base.ModelRef( - "os_customer_group", - customer_group.CustomerGroup, - ), + Optional[CustomerGroup], + ModelRef("os_customer_group", CustomerGroup), ] """The customer group this partner is part of, if it is part of one. @@ -95,18 +74,12 @@ class Partner(record_base.RecordBase): and caches it for subsequent accesses. """ - os_project_ids: Annotated[ - List[int], - record_base.ModelRef("os_projects", project.Project), - ] + os_project_ids: Annotated[List[int], ModelRef("os_projects", Project)] """A list of IDs for the OpenStack projects that belong to this partner. """ - os_projects: Annotated[ - List[project.Project], - record_base.ModelRef("os_projects", project.Project), - ] + os_projects: Annotated[List[Project], ModelRef("os_projects", Project)] """The OpenStack projects that belong to this partner. This fetches the full records from Odoo once, @@ -115,21 +88,15 @@ class Partner(record_base.RecordBase): os_project_contact_ids: Annotated[ List[int], - record_base.ModelRef( - "os_project_contacts", - project_contact.ProjectContact, - ), + ModelRef("os_project_contacts", ProjectContact), ] """A list of IDs for the project contacts that are associated with this partner. """ os_project_contacts: Annotated[ - List[project_contact.ProjectContact], - record_base.ModelRef( - "os_project_contacts", - project_contact.ProjectContact, - ), + List[ProjectContact], + ModelRef("os_project_contacts", ProjectContact), ] """The project contacts that are associated with this partner. @@ -139,7 +106,7 @@ class Partner(record_base.RecordBase): os_referral_id: Annotated[ Optional[int], - record_base.ModelRef("os_referral", referral_code.ReferralCode), + ModelRef("os_referral", ReferralCode), ] """The ID for the referral code the partner used on sign-up, if one was used. @@ -147,15 +114,15 @@ class Partner(record_base.RecordBase): os_referral_name: Annotated[ Optional[str], - record_base.ModelRef("os_referral", referral_code.ReferralCode), + ModelRef("os_referral", ReferralCode), ] """The name of the referral code the partner used on sign-up, if one was used. """ os_referral: Annotated[ - Optional[referral_code.ReferralCode], - record_base.ModelRef("os_referral", referral_code.ReferralCode), + Optional[ReferralCode], + ModelRef("os_referral", ReferralCode), ] """The referral code the partner used on sign-up, if one was used. @@ -165,13 +132,13 @@ class Partner(record_base.RecordBase): os_referral_code_ids: Annotated[ List[int], - record_base.ModelRef("os_referral_codes", referral_code.ReferralCode), + ModelRef("os_referral_codes", ReferralCode), ] """A list of IDs for the referral codes the partner has used.""" os_referral_codes: Annotated[ - List[referral_code.ReferralCode], - record_base.ModelRef("os_referral_codes", referral_code.ReferralCode), + List[ReferralCode], + ModelRef("os_referral_codes", ReferralCode), ] """The referral codes the partner has used. @@ -181,7 +148,7 @@ class Partner(record_base.RecordBase): os_reseller_id: Annotated[ Optional[int], - record_base.ModelRef("os_reseller", reseller.Reseller), + ModelRef("os_reseller", Reseller), ] """The ID for the reseller for this partner, if this partner is billed through a reseller. @@ -189,15 +156,15 @@ class Partner(record_base.RecordBase): os_reseller_name: Annotated[ Optional[str], - record_base.ModelRef("os_reseller", reseller.Reseller), + ModelRef("os_reseller", Reseller), ] """The name of the reseller for this partner, if this partner is billed through a reseller. """ os_reseller: Annotated[ - Optional[reseller.Reseller], - record_base.ModelRef("os_reseller", reseller.Reseller), + Optional[Reseller], + ModelRef("os_reseller", Reseller), ] """The reseller for this partner, if this partner is billed through a reseller. @@ -206,26 +173,17 @@ class Partner(record_base.RecordBase): and caches it for subsequent accesses. """ - os_trial_id: Annotated[ - Optional[int], - record_base.ModelRef("os_trial", trial.Trial), - ] + os_trial_id: Annotated[Optional[int], ModelRef("os_trial", Trial)] """The ID for the sign-up trial for this partner, if signed up under a trial. """ - os_trial_name: Annotated[ - Optional[str], - record_base.ModelRef("os_trial", trial.Trial), - ] + os_trial_name: Annotated[Optional[str], ModelRef("os_trial", Trial)] """The name of the sign-up trial for this partner, if signed up under a trial. """ - os_trial: Annotated[ - Optional[trial.Trial], - record_base.ModelRef("os_trial", trial.Trial), - ] + os_trial: Annotated[Optional[Trial], ModelRef("os_trial", Trial)] """The sign-up trial for this partner, if signed up under a trial. @@ -235,7 +193,7 @@ class Partner(record_base.RecordBase): parent_id: Annotated[ Optional[int], - record_base.ModelRef("parent_id", Self), + ModelRef("parent_id", Self), ] """The ID for the parent partner of this partner, if it has a parent. @@ -243,13 +201,13 @@ class Partner(record_base.RecordBase): parent_name: Annotated[ Optional[str], - record_base.ModelRef("parent_id", Self), + ModelRef("parent_id", Self), ] """The name of the parent partner of this partner, if it has a parent. """ - parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] """The parent partner of this partner, if it has a parent. @@ -259,10 +217,7 @@ class Partner(record_base.RecordBase): property_product_pricelist_id: Annotated[ Optional[int], - record_base.ModelRef( - "property_product_pricelist", - pricelist.Pricelist, - ), + ModelRef("property_product_pricelist", Pricelist), ] """The ID for the pricelist this partner uses, if explicitly set. @@ -273,10 +228,7 @@ class Partner(record_base.RecordBase): property_product_pricelist_name: Annotated[ Optional[str], - record_base.ModelRef( - "property_product_pricelist", - pricelist.Pricelist, - ), + ModelRef("property_product_pricelist", Pricelist), ] """The name of the pricelist this partner uses, if explicitly set. @@ -286,11 +238,8 @@ class Partner(record_base.RecordBase): """ property_product_pricelist: Annotated[ - Optional[pricelist.Pricelist], - record_base.ModelRef( - "property_product_pricelist", - pricelist.Pricelist, - ), + Optional[Pricelist], + ModelRef("property_product_pricelist", Pricelist), ] """The pricelist this partner uses, if explicitly set. @@ -305,26 +254,17 @@ class Partner(record_base.RecordBase): stripe_customer_id: Union[str, Literal[False]] """The Stripe customer ID for this partner, if one has been assigned.""" - user_id: Annotated[ - Optional[int], - record_base.ModelRef("user_id", user_module.User), - ] + user_id: Annotated[Optional[int], ModelRef("user_id", User)] """The ID of the internal user associated with this partner, if one is assigned. """ - user_name: Annotated[ - Optional[str], - record_base.ModelRef("user_id", user_module.User), - ] + user_name: Annotated[Optional[str], ModelRef("user_id", User)] """The name of the internal user associated with this partner, if one is assigned. """ - user: Annotated[ - Optional[user_module.User], - record_base.ModelRef("user_id", user_module.User), - ] + user: Annotated[Optional[User], ModelRef("user_id", User)] """The internal user associated with this partner, if one is assigned. @@ -333,18 +273,17 @@ class Partner(record_base.RecordBase): """ -class PartnerManager(record_manager_base.RecordManagerBase[Partner]): +class PartnerManager(RecordManagerBase[Partner]): env_name = "res.partner" record_class = Partner # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - company as company_module, - customer_group, - project_contact, - referral_code, - reseller, - trial, - user as user_module, -) +from .company import Company # noqa: E402 +from .customer_group import CustomerGroup # noqa: E402 +from .project import Project # noqa: E402 +from .project_contact import ProjectContact # noqa: E402 +from .referral_code import ReferralCode # noqa: E402 +from .reseller import Reseller # noqa: E402 +from .trial import Trial # noqa: E402 +from .user import User # noqa: E402 diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index a18064b..a03b24d 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -19,17 +19,18 @@ from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class PartnerCategory(record_base.RecordBase): +class PartnerCategory(RecordBase): active: bool """Whether or not the partner category is active (enabled).""" - child_ids: Annotated[List[int], record_base.ModelRef("child_id", Self)] + child_ids: Annotated[List[int], ModelRef("child_id", Self)] """A list of IDs for the child categories.""" - children: Annotated[List[Self], record_base.ModelRef("child_id", Self)] + children: Annotated[List[Self], ModelRef("child_id", Self)] """The list of child categories. This fetches the full records from Odoo once, @@ -39,29 +40,23 @@ class PartnerCategory(record_base.RecordBase): color: int """Colour index for the partner category.""" - colour: Annotated[int, record_base.FieldAlias("color")] + colour: Annotated[int, FieldAlias("color")] """Alias for ``color``.""" name: str """The name of the partner category.""" - parent_id: Annotated[ - Optional[int], - record_base.ModelRef("parent_id", Self), - ] + parent_id: Annotated[Optional[int], ModelRef("parent_id", Self)] """The ID for the parent partner category, if this category is the child of another category. """ - parent_name: Annotated[ - Optional[str], - record_base.ModelRef("parent_id", Self), - ] + parent_name: Annotated[Optional[str], ModelRef("parent_id", Self)] """The name of the parent partner category, if this category is the child of another category. """ - parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] """The parent partner category, if this category is the child of another category. @@ -72,16 +67,10 @@ class PartnerCategory(record_base.RecordBase): parent_path: Union[str, Literal[False]] """The path of the parent partner category, if there is a parent.""" - partner_ids: Annotated[ - List[int], - record_base.ModelRef("partner_id", partner.Partner), - ] + partner_ids: Annotated[List[int], ModelRef("partner_id", Partner)] """A list of IDs for the partners in this category.""" - partners: Annotated[ - List[partner.Partner], - record_base.ModelRef("partner_id", partner.Partner), - ] + partners: Annotated[List[Partner], ModelRef("partner_id", Partner)] """The list of partners in this category. This fetches the full records from Odoo once, @@ -89,12 +78,10 @@ class PartnerCategory(record_base.RecordBase): """ -class PartnerCategoryManager( - record_manager_name_base.NamedRecordManagerBase[PartnerCategory], -): +class PartnerCategoryManager(NamedRecordManagerBase[PartnerCategory]): env_name = "res.partner.category" record_class = PartnerCategory # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import partner # noqa: E402 +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 1fb0418..154c999 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -19,55 +19,35 @@ from typing_extensions import Annotated -from . import ( - product as product_module, - record_base, - record_manager_name_base, -) +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase +from .product import Product -class Pricelist(record_base.RecordBase): +class Pricelist(RecordBase): active: bool """Whether or not the pricelist is active.""" - company_id: Annotated[ - Optional[int], - record_base.ModelRef("company_id", company_module.Company), - ] + company_id: Annotated[Optional[int], ModelRef("company_id", Company)] """The ID for the company for this pricelist, if set.""" - company_name: Annotated[ - Optional[str], - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[Optional[str], ModelRef("company_id", Company)] """The name of the company for this pricelist, if set.""" - company: Annotated[ - Optional[company_module.Company], - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Optional[Company], ModelRef("company_id", Company)] """The company for this pricelist, if set. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - currency_id: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this pricelist.""" - currency_name: Annotated[ - str, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_name: Annotated[str, ModelRef("currency_id", Currency)] """The name of the currency used in this pricelist.""" - currency: Annotated[ - currency_module.Currency, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency: Annotated[Currency, ModelRef("currency_id", Currency)] """The currency used in this pricelist. This fetches the full record from Odoo once, @@ -86,11 +66,7 @@ class Pricelist(record_base.RecordBase): name: str """The name of this pricelist.""" - def get_price( - self, - product: Union[int, product_module.Product], - qty: float, - ) -> float: + def get_price(self, product: Union[int, Product], qty: float) -> float: """Get the price to charge for a given product and quantity. :param product: Product to get the price for (ID or object) @@ -107,16 +83,14 @@ def get_price( ) -class PricelistManager( - record_manager_name_base.NamedRecordManagerBase[Pricelist], -): +class PricelistManager(NamedRecordManagerBase[Pricelist]): env_name = "product.pricelist" record_class = Pricelist def get_price( self, pricelist: Union[int, Pricelist], - product: Union[int, product_module.Product], + product: Union[int, Product], qty: float, ) -> float: """Get the price to charge for a given pricelist, product @@ -136,18 +110,12 @@ def get_price( ) price = self._env.price_get( pricelist_id, - ( - product.id - if isinstance(product, product_module.Product) - else product - ), + (product.id if isinstance(product, Product) else product), max(qty, 0), )[str(pricelist_id)] return price if qty >= 0 else -price # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - company as company_module, - currency as currency_module, -) +from .company import Company # noqa: E402 +from .currency import Currency # noqa: E402 diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index ee21383..75292d0 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -28,48 +28,33 @@ from typing_extensions import Annotated -from . import record_base, record_manager_unique_field_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_with_unique_field import ( + RecordManagerWithUniqueFieldBase, +) -class Product(record_base.RecordBase): - categ_id: Annotated[ - int, - record_base.ModelRef("categ_id", product_category.ProductCategory), - ] +class Product(RecordBase): + categ_id: Annotated[int, ModelRef("categ_id", ProductCategory)] """The ID for the category this product is under.""" - categ_name: Annotated[ - str, - record_base.ModelRef("categ_id", product_category.ProductCategory), - ] + categ_name: Annotated[str, ModelRef("categ_id", ProductCategory)] """The name of the category this product is under.""" - categ: Annotated[ - product_category.ProductCategory, - record_base.ModelRef("categ_id", product_category.ProductCategory), - ] + categ: Annotated[ProductCategory, ModelRef("categ_id", ProductCategory)] """The category this product is under. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - company_id: Annotated[ - Optional[int], - record_base.ModelRef("company_id", company_module.Company), - ] + company_id: Annotated[Optional[int], ModelRef("company_id", Company)] """The ID for the company that owns this product, if set.""" - company_name: Annotated[ - Optional[str], - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[Optional[str], ModelRef("company_id", Company)] """The name of the company that owns this product, if set.""" - company: Annotated[ - Optional[company_module.Company], - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Optional[Company], ModelRef("company_id", Company)] """The company that owns this product, if set. This fetches the full record from Odoo once, @@ -98,16 +83,13 @@ class Product(record_base.RecordBase): name: str """The name of the product.""" - uom_id: Annotated[int, record_base.ModelRef("uom_id", uom_module.Uom)] + uom_id: Annotated[int, ModelRef("uom_id", Uom)] """The ID for the Unit of Measure for this product.""" - uom_name: Annotated[str, record_base.ModelRef("uom_id", uom_module.Uom)] + uom_name: Annotated[str, ModelRef("uom_id", Uom)] """The name of the Unit of Measure for this product.""" - uom: Annotated[ - uom_module.Uom, - record_base.ModelRef("uom_id", uom_module.Uom), - ] + uom: Annotated[Uom, ModelRef("uom_id", Uom)] """The Unit of Measure for this product. This fetches the full record from Odoo once, @@ -115,19 +97,14 @@ class Product(record_base.RecordBase): """ -class ProductManager( - record_manager_unique_field_base.RecordManagerWithUniqueFieldBase[ - Product, - str, - ], -): +class ProductManager(RecordManagerWithUniqueFieldBase[Product, str]): env_name = "product.product" record_class = Product @overload def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -138,7 +115,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -149,7 +126,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -160,7 +137,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -171,7 +148,7 @@ def get_sellable_company_products( @overload def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], *, fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., @@ -181,7 +158,7 @@ def get_sellable_company_products( def get_sellable_company_products( self, - company: Union[int, company_module.Company], + company: Union[int, Company], fields: Optional[Iterable[str]] = None, order: Optional[str] = None, as_id: bool = False, @@ -217,7 +194,7 @@ def get_sellable_company_products( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -229,7 +206,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -241,7 +218,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -253,7 +230,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -265,7 +242,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -277,7 +254,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -289,7 +266,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -301,7 +278,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -313,7 +290,7 @@ def get_sellable_company_product_by_name( @overload def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, *, fields: Optional[Iterable[str]] = ..., @@ -324,7 +301,7 @@ def get_sellable_company_product_by_name( def get_sellable_company_product_by_name( self, - company: Union[int, company_module.Company], + company: Union[int, Company], name: str, fields: Optional[Iterable[str]] = None, as_id: bool = False, @@ -382,8 +359,6 @@ def get_sellable_company_product_by_name( # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - company as company_module, - product_category, - uom as uom_module, -) +from .company import Company # noqa: E402 +from .product_category import ProductCategory # noqa: E402 +from .uom import Uom # noqa: E402 diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index c318b18..a5848be 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -19,17 +19,18 @@ from typing_extensions import Annotated, Self -from . import record_base, record_manager_name_base +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class ProductCategory(record_base.RecordBase): - child_id: Annotated[List[int], record_base.ModelRef("child_id", Self)] +class ProductCategory(RecordBase): + child_id: Annotated[List[int], ModelRef("child_id", Self)] """A list of IDs for the child categories.""" - child_ids: Annotated[List[int], record_base.FieldAlias("child_id")] + child_ids: Annotated[List[int], FieldAlias("child_id")] """An alias for ``child_id``.""" - children: Annotated[List[Self], record_base.ModelRef("child_id", Self)] + children: Annotated[List[Self], ModelRef("child_id", Self)] """The list of child categories. This fetches the full records from Odoo once, @@ -42,23 +43,17 @@ class ProductCategory(record_base.RecordBase): name: str """Name of the product category.""" - parent_id: Annotated[ - Optional[int], - record_base.ModelRef("parent_id", Self), - ] + parent_id: Annotated[Optional[int], ModelRef("parent_id", Self)] """The ID for the parent product category, if this category is the child of another category. """ - parent_name: Annotated[ - Optional[str], - record_base.ModelRef("parent_id", Self), - ] + parent_name: Annotated[Optional[str], ModelRef("parent_id", Self)] """The name of the parent product category, if this category is the child of another category. """ - parent: Annotated[Optional[Self], record_base.ModelRef("parent_id", Self)] + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] """The parent product category, if this category is the child of another category. @@ -73,8 +68,6 @@ class ProductCategory(record_base.RecordBase): """The number of products under this category.""" -class ProductCategoryManager( - record_manager_name_base.NamedRecordManagerBase[ProductCategory], -): +class ProductCategoryManager(NamedRecordManagerBase[ProductCategory]): env_name = "product.category" record_class = ProductCategory diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index 0c6ad47..a011c90 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -28,10 +28,13 @@ from typing_extensions import Annotated, Self -from . import record_base, record_manager_unique_field_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_with_unique_field import ( + RecordManagerWithUniqueFieldBase, +) -class Project(record_base.RecordBase): +class Project(RecordBase): billing_type: Literal["customer", "internal"] """Billing type for this project. @@ -61,42 +64,30 @@ class Project(record_base.RecordBase): set on this Project. """ - owner_id: Annotated[ - int, - record_base.ModelRef("owner", partner_module.Partner), - ] + owner_id: Annotated[int, ModelRef("owner", Partner)] """The ID for the partner that owns this project.""" - owner_name: Annotated[ - str, - record_base.ModelRef("owner", partner_module.Partner), - ] + owner_name: Annotated[str, ModelRef("owner", Partner)] """The name of the partner that owns this project.""" - owner: Annotated[ - partner_module.Partner, - record_base.ModelRef("owner", partner_module.Partner), - ] + owner: Annotated[Partner, ModelRef("owner", Partner)] """The partner that owns this project. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - parent_id: Annotated[Optional[int], record_base.ModelRef("parent", Self)] + parent_id: Annotated[Optional[int], ModelRef("parent", Self)] """The ID for the parent project, if this project is the child of another project. """ - parent_name: Annotated[ - Optional[str], - record_base.ModelRef("parent", Self), - ] + parent_name: Annotated[Optional[str], ModelRef("parent", Self)] """The name of the parent project, if this project is the child of another project. """ - parent: Annotated[Optional[Self], record_base.ModelRef("parent", Self)] + parent: Annotated[Optional[Self], ModelRef("parent", Self)] """The parent project, if this project is the child of another project. @@ -118,19 +109,13 @@ class Project(record_base.RecordBase): project_contact_ids: Annotated[ List[int], - record_base.ModelRef( - "project_contacts", - project_contact.ProjectContact, - ), + ModelRef("project_contacts", ProjectContact), ] """A list of IDs for the contacts for this project.""" project_contacts: Annotated[ - List[project_contact.ProjectContact], - record_base.ModelRef( - "project_contacts", - project_contact.ProjectContact, - ), + List[ProjectContact], + ModelRef("project_contacts", ProjectContact), ] """The contacts for this project. @@ -140,13 +125,13 @@ class Project(record_base.RecordBase): project_credit_ids: Annotated[ List[int], - record_base.ModelRef("project_credits", credit.Credit), + ModelRef("project_credits", Credit), ] """A list of IDs for the credits that apply to this project.""" project_credits: Annotated[ - List[credit.Credit], - record_base.ModelRef("project_credits", credit.Credit), + List[Credit], + ModelRef("project_credits", Credit), ] """The credits that apply to this project. @@ -154,16 +139,10 @@ class Project(record_base.RecordBase): and caches them for subsequent accesses. """ - project_grant_ids: Annotated[ - List[int], - record_base.ModelRef("project_grants", grant.Grant), - ] + project_grant_ids: Annotated[List[int], ModelRef("project_grants", Grant)] """A list of IDs for the grants that apply to this project.""" - project_grants: Annotated[ - List[grant.Grant], - record_base.ModelRef("project_grants", grant.Grant), - ] + project_grants: Annotated[List[Grant], ModelRef("project_grants", Grant)] """The grants that apply to this project. This fetches the full records from Odoo once, @@ -180,10 +159,7 @@ class Project(record_base.RecordBase): support_subscription_id: Annotated[ Optional[int], - record_base.ModelRef( - "support_subscription", - support_subscription_module.SupportSubscription, - ), + ModelRef("support_subscription", SupportSubscription), ] """The ID for the support subscription for this project, if the project has one. @@ -191,21 +167,15 @@ class Project(record_base.RecordBase): support_subscription_name: Annotated[ Optional[str], - record_base.ModelRef( - "support_subscription", - support_subscription_module.SupportSubscription, - ), + ModelRef("support_subscription", SupportSubscription), ] """The name of the support subscription for this project, if the project has one. """ support_subscription: Annotated[ - Optional[support_subscription_module.SupportSubscription], - record_base.ModelRef( - "support_subscription", - support_subscription_module.SupportSubscription, - ), + Optional[SupportSubscription], + ModelRef("support_subscription", SupportSubscription), ] """The support subscription for this project, if the project has one. @@ -216,13 +186,13 @@ class Project(record_base.RecordBase): term_discount_ids: Annotated[ List[int], - record_base.ModelRef("term_discounts", term_discount.TermDiscount), + ModelRef("term_discounts", TermDiscount), ] """A list of IDs for the term discounts that apply to this project.""" term_discounts: Annotated[ - List[term_discount.TermDiscount], - record_base.ModelRef("term_discounts", term_discount.TermDiscount), + List[TermDiscount], + ModelRef("term_discounts", TermDiscount), ] """The term discounts that apply to this project. @@ -231,12 +201,7 @@ class Project(record_base.RecordBase): """ -class ProjectManager( - record_manager_unique_field_base.RecordManagerWithUniqueFieldBase[ - Project, - str, - ], -): +class ProjectManager(RecordManagerWithUniqueFieldBase[Project, str]): env_name = "openstack.project" record_class = Project @@ -378,11 +343,9 @@ def get_by_os_id( # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - credit, - grant, - partner as partner_module, - project_contact, - support_subscription as support_subscription_module, - term_discount, -) +from .credit import Credit # noqa: E402 +from .grant import Grant # noqa: E402 +from .partner import Partner # noqa: E402 +from .project_contact import ProjectContact # noqa: E402 +from .support_subscription import SupportSubscription # noqa: E402 +from .term_discount import TermDiscount # noqa: E402 diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index dcfaecc..d381f9e 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -19,10 +19,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class ProjectContact(record_base.RecordBase): +class ProjectContact(RecordBase): contact_type: Literal[ "primary", "billing", @@ -35,44 +36,26 @@ class ProjectContact(record_base.RecordBase): inherit: bool """Whether or not this contact should be inherited by child projects.""" - partner_id: Annotated[ - int, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner", Partner)] """The ID for the partner linked to this project contact.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner", Partner)] """The name of the partner linked to this project contact.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner", Partner)] """The partner linked to this project contact. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - project_id: Annotated[ - Optional[int], - record_base.ModelRef("project", project_module.Project), - ] + project_id: Annotated[Optional[int], ModelRef("project", Project)] """The ID for the project this contact is linked to, if set.""" - project_name: Annotated[ - Optional[str], - record_base.ModelRef("project", project_module.Project), - ] + project_name: Annotated[Optional[str], ModelRef("project", Project)] """The name of the project this contact is linked to, if set.""" - project: Annotated[ - Optional[project_module.Project], - record_base.ModelRef("project", project_module.Project), - ] + project: Annotated[Optional[Project], ModelRef("project", Project)] """The project this contact is linked to, if set. This fetches the full record from Odoo once, @@ -80,15 +63,11 @@ class ProjectContact(record_base.RecordBase): """ -class ProjectContactManager( - record_manager_base.RecordManagerBase[ProjectContact], -): +class ProjectContactManager(RecordManagerBase[ProjectContact]): env_name = "openstack.project_contact" record_class = ProjectContact # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import ( # noqa: E402 - partner as partner_module, - project as project_module, -) +from .partner import Partner # noqa: E402 +from .project import Project # noqa: E402 diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index 77213d3..407d8bb 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -19,10 +19,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_code_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_coded import CodedRecordManagerBase -class ReferralCode(record_base.RecordBase): +class ReferralCode(RecordBase): allowed_uses: int """The number of allowed uses of this referral code. @@ -40,18 +41,12 @@ class ReferralCode(record_base.RecordBase): name: str """Automatically generated name for the referral code.""" - referral_ids: Annotated[ - List[int], - record_base.ModelRef("referrals", partner.Partner), - ] + referral_ids: Annotated[List[int], ModelRef("referrals", Partner)] """A list of IDs for the partners that signed up using this referral code. """ - referrals: Annotated[ - List[partner.Partner], - record_base.ModelRef("referrals", partner.Partner), - ] + referrals: Annotated[List[Partner], ModelRef("referrals", Partner)] """The partners that signed up using this referral code. This fetches the full records from Odoo once, @@ -66,19 +61,19 @@ class ReferralCode(record_base.RecordBase): referral_credit_type_id: Annotated[ int, - record_base.ModelRef("referral_credit_type", credit_type.CreditType), + ModelRef("referral_credit_type", CreditType), ] """The ID of the credit type to use for the referral credit.""" referral_credit_type_name: Annotated[ str, - record_base.ModelRef("referral_credit_type", credit_type.CreditType), + ModelRef("referral_credit_type", CreditType), ] """The name of the credit type to use for the referral credit.""" referral_credit_type: Annotated[ - credit_type.CreditType, - record_base.ModelRef("referral_credit_type", credit_type.CreditType), + CreditType, + ModelRef("referral_credit_type", CreditType), ] """The credit type to use for the referral credit. @@ -94,19 +89,19 @@ class ReferralCode(record_base.RecordBase): reward_credit_type_id: Annotated[ int, - record_base.ModelRef("reward_credit_type", credit_type.CreditType), + ModelRef("reward_credit_type", CreditType), ] """The ID of the credit type to use for the reward credit.""" reward_credit_type_name: Annotated[ str, - record_base.ModelRef("reward_credit_type", credit_type.CreditType), + ModelRef("reward_credit_type", CreditType), ] """The name of the credit type to use for the reward credit.""" reward_credit_type: Annotated[ - credit_type.CreditType, - record_base.ModelRef("reward_credit_type", credit_type.CreditType), + CreditType, + ModelRef("reward_credit_type", CreditType), ] """The credit type to use for the reward credit. @@ -115,12 +110,11 @@ class ReferralCode(record_base.RecordBase): """ -class ReferralCodeManager( - record_manager_code_base.CodedRecordManagerBase[ReferralCode], -): +class ReferralCodeManager(CodedRecordManagerBase[ReferralCode]): env_name = "openstack.referral_code" record_class = ReferralCode # NOTE(callumdickinson): Import here to avoid circular imports. -from . import credit_type, partner # noqa: E402 +from .credit_type import CreditType # noqa: E402 +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 3b4a411..30f48c7 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -19,10 +19,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class Reseller(record_base.RecordBase): +class Reseller(RecordBase): alternative_billing_url: Optional[str] """The URL to the cloud billing page for the reseller, if available.""" @@ -31,19 +32,19 @@ class Reseller(record_base.RecordBase): demo_project_id: Annotated[ Optional[int], - record_base.ModelRef("demo_project", project.Project), + ModelRef("demo_project", Project), ] """The ID for the optional demo project belonging to the reseller.""" demo_project_name: Annotated[ Optional[str], - record_base.ModelRef("demo_project", project.Project), + ModelRef("demo_project", Project), ] """The name of the optional demo project belonging to the reseller.""" demo_project: Annotated[ - Optional[project.Project], - record_base.ModelRef("demo_project", project.Project), + Optional[Project], + ModelRef("demo_project", Project), ] """An optional demo project belonging to the reseller. @@ -63,44 +64,26 @@ class Reseller(record_base.RecordBase): This is set to the reseller partner's name. """ - partner_id: Annotated[ - int, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner", Partner)] """The ID for the reseller partner.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner", Partner)] """The name of the reseller partner.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner", Partner)] """The reseller partner. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - tier_id: Annotated[ - int, - record_base.ModelRef("tier", reseller_tier.ResellerTier), - ] + tier_id: Annotated[int, ModelRef("tier", ResellerTier)] """The ID for the tier this reseller is under.""" - tier_name: Annotated[ - str, - record_base.ModelRef("tier", reseller_tier.ResellerTier), - ] + tier_name: Annotated[str, ModelRef("tier", ResellerTier)] """The name of the tier this reseller is under.""" - tier: Annotated[ - reseller_tier.ResellerTier, - record_base.ModelRef("tier", reseller_tier.ResellerTier), - ] + tier: Annotated[ResellerTier, ModelRef("tier", ResellerTier)] """The tier this reseller is under. This fetches the full record from Odoo once, @@ -108,10 +91,12 @@ class Reseller(record_base.RecordBase): """ -class ResellerManager(record_manager_base.RecordManagerBase[Reseller]): +class ResellerManager(RecordManagerBase[Reseller]): env_name = "openstack.reseller" record_class = Reseller # NOTE(callumdickinson): Import here to avoid circular imports. -from . import partner as partner_module, project, reseller_tier # noqa: E402 +from .partner import Partner # noqa: E402 +from .project import Project # noqa: E402 +from .reseller_tier import ResellerTier # noqa: E402 diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 905d823..15abd4e 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -17,28 +17,29 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class ResellerTier(record_base.RecordBase): +class ResellerTier(RecordBase): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" discount_product_id: Annotated[ int, - record_base.ModelRef("discount_product", product.Product), + ModelRef("discount_product", Product), ] """The ID of the discount product for the reseller tier.""" discount_product_name: Annotated[ str, - record_base.ModelRef("discount_product", product.Product), + ModelRef("discount_product", Product), ] """The name of the discount product for the reseller tier.""" discount_product: Annotated[ - product.Product, - record_base.ModelRef("discount_product", product.Product), + Product, + ModelRef("discount_product", Product), ] """The discount product for the reseller tier. @@ -51,7 +52,7 @@ class ResellerTier(record_base.RecordBase): free_monthly_credit_product_id: Annotated[ int, - record_base.ModelRef("free_monthly_credit_product", product.Product), + ModelRef("free_monthly_credit_product", Product), ] """The ID of the product to use when adding the free monthly credit to demo project invoices. @@ -59,15 +60,15 @@ class ResellerTier(record_base.RecordBase): free_monthly_credit_product_name: Annotated[ str, - record_base.ModelRef("free_monthly_credit_product", product.Product), + ModelRef("free_monthly_credit_product", Product), ] """The name of the product to use when adding the free monthly credit to demo project invoices. """ free_monthly_credit_product: Annotated[ - product.Product, - record_base.ModelRef("free_monthly_credit_product", product.Product), + Product, + ModelRef("free_monthly_credit_product", Product), ] """The product to use when adding the free monthly credit to demo project invoices. @@ -88,12 +89,10 @@ class ResellerTier(record_base.RecordBase): """The minimum required usage amount for the reseller tier.""" -class ResellerTierManager( - record_manager_name_base.NamedRecordManagerBase[ResellerTier], -): +class ResellerTierManager(NamedRecordManagerBase[ResellerTier]): env_name = "openstack.reseller.tier" record_class = ResellerTier # NOTE(callumdickinson): Import here to avoid circular imports. -from . import product # noqa: E402 +from .product import Product # noqa: E402 diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 513a0ff..1dda3c0 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -20,10 +20,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class SaleOrder(record_base.RecordBase): +class SaleOrder(RecordBase): amount_untaxed: float """The untaxed total cost of the sale order.""" @@ -36,22 +37,13 @@ class SaleOrder(record_base.RecordBase): client_order_ref: Union[str, Literal[False]] """The customer reference for this sale order, if defined.""" - currency_id: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this sale order.""" - currency_name: Annotated[ - str, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_name: Annotated[str, ModelRef("currency_id", Currency)] """The name of the currency used in this sale order.""" - currency: Annotated[ - currency_module.Currency, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency: Annotated[Currency, ModelRef("currency_id", Currency)] """The currency used in this sale order. This fetches the full record from Odoo once, @@ -86,13 +78,13 @@ class SaleOrder(record_base.RecordBase): order_line_ids: Annotated[ List[int], - record_base.ModelRef("order_line", sale_order_line.SaleOrderLine), + ModelRef("order_line", SaleOrderLine), ] """A list of IDs for the lines added to the sale order.""" order_line: Annotated[ - List[sale_order_line.SaleOrderLine], - record_base.ModelRef("order_line", sale_order_line.SaleOrderLine), + List[SaleOrderLine], + ModelRef("order_line", SaleOrderLine), ] """The lines added to the sale order. @@ -100,10 +92,7 @@ class SaleOrder(record_base.RecordBase): and caches them for subsequent accesses. """ - order_lines: Annotated[ - List[sale_order_line.SaleOrderLine], - record_base.FieldAlias("order_line"), - ] + order_lines: Annotated[List[SaleOrderLine], FieldAlias("order_line")] """An alias for ``order_line``.""" os_invoice_date: date @@ -116,26 +105,17 @@ class SaleOrder(record_base.RecordBase): from the sale order. """ - os_project_id: Annotated[ - Optional[int], - record_base.ModelRef("os_project", project.Project), - ] + os_project_id: Annotated[Optional[int], ModelRef("os_project", Project)] """The ID for the the OpenStack project this sale order was was generated for. """ - os_project_name: Annotated[ - Optional[str], - record_base.ModelRef("os_project", project.Project), - ] + os_project_name: Annotated[Optional[str], ModelRef("os_project", Project)] """The name of the the OpenStack project this sale order was was generated for. """ - os_project: Annotated[ - Optional[project.Project], - record_base.ModelRef("os_project", project.Project), - ] + os_project: Annotated[Optional[Project], ModelRef("os_project", Project)] """The OpenStack project this sale order was was generated for. @@ -143,22 +123,13 @@ class SaleOrder(record_base.RecordBase): and caches it for subsequent accesses. """ - partner_id: Annotated[ - int, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner_id", Partner)] """The ID for the recipient partner for the sale order.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner_id", Partner)] """The name of the recipient partner for the sale order.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner_id", Partner)] """The recipient partner for the sale order. This fetches the full record from Odoo once, @@ -185,9 +156,7 @@ def create_invoices(self) -> None: self._client.sale_orders.create_invoices(self) -class SaleOrderManager( - record_manager_name_base.NamedRecordManagerBase[SaleOrder], -): +class SaleOrderManager(NamedRecordManagerBase[SaleOrder]): env_name = "sale.order" record_class = SaleOrder @@ -221,9 +190,7 @@ def create_invoices(self, sale_order: Union[int, SaleOrder]) -> None: # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - currency as currency_module, - partner as partner_module, - project, - sale_order_line, -) +from .currency import Currency # noqa: E402 +from .partner import Partner # noqa: E402 +from .project import Project # noqa: E402 +from .sale_order_line import SaleOrderLine # noqa: E402 diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index 359ded1..edd6c92 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -19,30 +19,22 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class SaleOrderLine(record_base.RecordBase): - company_id: Annotated[ - int, - record_base.ModelRef("company_id", company_module.Company), - ] +class SaleOrderLine(RecordBase): + company_id: Annotated[int, ModelRef("company_id", Company)] """The ID for the company this sale order line was generated for. """ - company_name: Annotated[ - str, - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[str, ModelRef("company_id", Company)] """The name of the company this sale order line was generated for. """ - company: Annotated[ - company_module.Company, - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Company, ModelRef("company_id", Company)] """The company this sale order line was generated for. @@ -50,22 +42,13 @@ class SaleOrderLine(record_base.RecordBase): and caches it for subsequent accesses. """ - currency_id: Annotated[ - int, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this sale order line.""" - currency_name: Annotated[ - str, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency_name: Annotated[str, ModelRef("currency_id", Currency)] """The name of the currency used in this sale order line.""" - currency: Annotated[ - currency_module.Currency, - record_base.ModelRef("currency_id", currency_module.Currency), - ] + currency: Annotated[Currency, ModelRef("currency_id", Currency)] """The currency used in this sale order line. This fetches the full record from Odoo once, @@ -80,21 +63,15 @@ class SaleOrderLine(record_base.RecordBase): invoice_line_ids: Annotated[ List[int], - record_base.ModelRef( - "invoice_lines", - account_move_line.AccountMoveLine, - ), + ModelRef("invoice_lines", AccountMoveLine), ] """A list of IDs for the account move (invoice) lines created from this sale order line. """ invoice_lines: Annotated[ - List[account_move_line.AccountMoveLine], - record_base.ModelRef( - "invoice_lines", - account_move_line.AccountMoveLine, - ), + List[AccountMoveLine], + ModelRef("invoice_lines", AccountMoveLine), ] """The account move (invoice) lines created from this sale order line. @@ -128,70 +105,43 @@ class SaleOrderLine(record_base.RecordBase): the resource's name. """ - order_id: Annotated[ - int, - record_base.ModelRef("order_id", sale_order.SaleOrder), - ] + order_id: Annotated[int, ModelRef("order_id", SaleOrder)] """The ID for the sale order this line is linked to.""" - order_name: Annotated[ - str, - record_base.ModelRef("order_id", sale_order.SaleOrder), - ] + order_name: Annotated[str, ModelRef("order_id", SaleOrder)] """The name of the sale order this line is linked to.""" - order: Annotated[ - sale_order.SaleOrder, - record_base.ModelRef("order_id", sale_order.SaleOrder), - ] + order: Annotated[SaleOrder, ModelRef("order_id", SaleOrder)] """The sale order this line is linked to. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - order_partner_id: Annotated[ - int, - record_base.ModelRef("order_partner_id", partner.Partner), - ] + order_partner_id: Annotated[int, ModelRef("order_partner_id", Partner)] """The ID for the recipient partner for the sale order.""" - order_partner_name: Annotated[ - str, - record_base.ModelRef("order_partner_id", partner.Partner), - ] + order_partner_name: Annotated[str, ModelRef("order_partner_id", Partner)] """The name of the recipient partner for the sale order.""" - order_partner: Annotated[ - partner.Partner, - record_base.ModelRef("order_partner_id", partner.Partner), - ] + order_partner: Annotated[Partner, ModelRef("order_partner_id", Partner)] """The recipient partner for the sale order. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - os_project_id: Annotated[ - Optional[int], - record_base.ModelRef("os_project", project.Project), - ] + os_project_id: Annotated[Optional[int], ModelRef("os_project", Project)] """The ID for the the OpenStack project this sale order line was was generated for. """ - os_project_name: Annotated[ - Optional[str], - record_base.ModelRef("os_project", project.Project), - ] + os_project_name: Annotated[Optional[str], ModelRef("os_project", Project)] """The name of the the OpenStack project this sale order line was was generated for. """ - os_project: Annotated[ - Optional[project.Project], - record_base.ModelRef("os_project", project.Project), - ] + os_project: Annotated[Optional[Project], ModelRef("os_project", Project)] """The OpenStack project this sale order line was was generated for. @@ -241,48 +191,30 @@ class SaleOrderLine(record_base.RecordBase): price_unit: float """Base unit price, excluding tax, before any discounts.""" - product_id: Annotated[ - int, - record_base.ModelRef("product_id", product_module.Product), - ] + product_id: Annotated[int, ModelRef("product_id", Product)] """The ID of the product charged on this sale order line.""" - product_name: Annotated[ - str, - record_base.ModelRef("product_id", product_module.Product), - ] + product_name: Annotated[str, ModelRef("product_id", Product)] """The name of the product charged on this sale order line.""" - product: Annotated[ - product_module.Product, - record_base.ModelRef("product_id", product_module.Product), - ] + product: Annotated[Product, ModelRef("product_id", Product)] """The product charged on this sale order line. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - product_uom_id: Annotated[ - int, - record_base.ModelRef("product_uom", uom.Uom), - ] + product_uom_id: Annotated[int, ModelRef("product_uom", Uom)] """The ID for the Unit of Measure for the product being charged in this sale order line. """ - product_uom_name: Annotated[ - str, - record_base.ModelRef("product_uom", uom.Uom), - ] + product_uom_name: Annotated[str, ModelRef("product_uom", Uom)] """The name of the Unit of Measure for the product being charged in this sale order line. """ - product_uom: Annotated[ - uom.Uom, - record_base.ModelRef("product_uom", uom.Uom), - ] + product_uom: Annotated[Uom, ModelRef("product_uom", Uom)] """The Unit of Measure for the product being charged in this sale order line. @@ -307,26 +239,17 @@ class SaleOrderLine(record_base.RecordBase): qty_to_invoice: float """The product quantity that still needs to be invoiced.""" - salesman_id: Annotated[ - int, - record_base.ModelRef("salesman_id", partner.Partner), - ] + salesman_id: Annotated[int, ModelRef("salesman_id", Partner)] """The ID for the salesperson partner assigned to this sale order line. """ - salesman_name: Annotated[ - str, - record_base.ModelRef("salesman_id", partner.Partner), - ] + salesman_name: Annotated[str, ModelRef("salesman_id", Partner)] """The name of the salesperson partner assigned to this sale order line. """ - salesman: Annotated[ - partner.Partner, - record_base.ModelRef("salesman_id", partner.Partner), - ] + salesman: Annotated[Partner, ModelRef("salesman_id", Partner)] """The salesperson partner assigned to this sale order line. @@ -345,16 +268,13 @@ class SaleOrderLine(record_base.RecordBase): * ``cancel`` - Cancelled sale order, can be deleted """ - tax_id: Annotated[int, record_base.ModelRef("tax_id", tax_module.Tax)] + tax_id: Annotated[int, ModelRef("tax_id", Tax)] """The ID for the tax used on this sale order line.""" - tax_name: Annotated[str, record_base.ModelRef("tax_id", tax_module.Tax)] + tax_name: Annotated[str, ModelRef("tax_id", Tax)] """The name of the tax used on this sale order line.""" - tax: Annotated[ - tax_module.Tax, - record_base.ModelRef("tax_id", tax_module.Tax), - ] + tax: Annotated[Tax, ModelRef("tax_id", Tax)] """The tax used on this sale order line. This fetches the full record from Odoo once, @@ -372,22 +292,18 @@ class SaleOrderLine(record_base.RecordBase): """ -class SaleOrderLineManager( - record_manager_base.RecordManagerBase[SaleOrderLine], -): +class SaleOrderLineManager(RecordManagerBase[SaleOrderLine]): env_name = "sale.order.line" record_class = SaleOrderLine # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - account_move_line, - company as company_module, - currency as currency_module, - partner, - product as product_module, - project, - sale_order, - tax as tax_module, - uom, -) +from .account_move_line import AccountMoveLine # noqa: E402 +from .company import Company # noqa: E402 +from .currency import Currency # noqa: E402 +from .partner import Partner # noqa: E402 +from .product import Product # noqa: E402 +from .project import Project # noqa: E402 +from .sale_order import SaleOrder # noqa: E402 +from .tax import Tax # noqa: E402 +from .uom import Uom # noqa: E402 diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index 23bdd5d..c3fd5a5 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -20,10 +20,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class SupportSubscription(record_base.RecordBase): +class SupportSubscription(RecordBase): billing_type: Literal["paid", "complimentary"] """The method of billing for the support subscription. @@ -36,10 +37,7 @@ class SupportSubscription(record_base.RecordBase): end_date: date """The end date of the credit.""" - partner_id: Annotated[ - Optional[int], - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_id: Annotated[Optional[int], ModelRef("partner", Partner)] """The ID for the partner linked to this support subscription, if it is linked to a partner. @@ -47,10 +45,7 @@ class SupportSubscription(record_base.RecordBase): cover all projects the partner owns. """ - partner_name: Annotated[ - Optional[str], - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_name: Annotated[Optional[str], ModelRef("partner", Partner)] """The name of thepartner linked to this support subscription, if it is linked to a partner. @@ -58,10 +53,7 @@ class SupportSubscription(record_base.RecordBase): cover all projects the partner owns. """ - partner: Annotated[ - Optional[partner_module.Partner], - record_base.ModelRef("partner", partner_module.Partner), - ] + partner: Annotated[Optional[Partner], ModelRef("partner", Partner)] """The partner linked to this support subscription, if it is linked to a partner. @@ -72,26 +64,17 @@ class SupportSubscription(record_base.RecordBase): and caches it for subsequent accesses. """ - project_id: Annotated[ - Optional[int], - record_base.ModelRef("project", project_module.Project), - ] + project_id: Annotated[Optional[int], ModelRef("project", Project)] """The ID of the project this support subscription is for, if it is linked to a specific project. """ - project_name: Annotated[ - Optional[str], - record_base.ModelRef("project", project_module.Project), - ] + project_name: Annotated[Optional[str], ModelRef("project", Project)] """The name of the project this support subscription is for, if it is linked to a specific project. """ - project: Annotated[ - Optional[project_module.Project], - record_base.ModelRef("project", project_module.Project), - ] + project: Annotated[Optional[Project], ModelRef("project", Project)] """The project this support subscription is for, if it is linked to a specific project. @@ -104,28 +87,19 @@ class SupportSubscription(record_base.RecordBase): support_subscription_type_id: Annotated[ int, - record_base.ModelRef( - "support_subscription_type", - support_subscription_type_module.SupportSubscriptionType, - ), + ModelRef("support_subscription_type", SupportSubscriptionType), ] """The ID of the type of the support subscription.""" support_subscription_type_name: Annotated[ str, - record_base.ModelRef( - "support_subscription_type", - support_subscription_type_module.SupportSubscriptionType, - ), + ModelRef("support_subscription_type", SupportSubscriptionType), ] """The name of the type of the support subscription.""" support_subscription_type: Annotated[ - support_subscription_type_module.SupportSubscriptionType, - record_base.ModelRef( - "support_subscription_type", - support_subscription_type_module.SupportSubscriptionType, - ), + SupportSubscriptionType, + ModelRef("support_subscription_type", SupportSubscriptionType), ] """The type of the support subscription. @@ -134,16 +108,12 @@ class SupportSubscription(record_base.RecordBase): """ -class SupportSubscriptionManager( - record_manager_base.RecordManagerBase[SupportSubscription], -): +class SupportSubscriptionManager(RecordManagerBase[SupportSubscription]): env_name = "openstack.support_subscription" record_class = SupportSubscription # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - partner as partner_module, - project as project_module, - support_subscription_type as support_subscription_type_module, -) +from .partner import Partner # noqa: E402 +from .project import Project # noqa: E402 +from .support_subscription_type import SupportSubscriptionType # noqa: E402 diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index 35fb221..4a4a986 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -19,36 +19,28 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class SupportSubscriptionType(record_base.RecordBase): +class SupportSubscriptionType(RecordBase): billing_type: Literal["paid", "complimentary"] """The type of support subscription.""" name: str """The name of the support subscription type.""" - product_id: Annotated[ - int, - record_base.ModelRef("product", product_module.Product), - ] + product_id: Annotated[int, ModelRef("product", Product)] """The ID for the product to use to invoice the support subscription. """ - product_name: Annotated[ - str, - record_base.ModelRef("product", product_module.Product), - ] + product_name: Annotated[str, ModelRef("product", Product)] """The name of the product to use to invoice the support subscription. """ - product: Annotated[ - product_module.Product, - record_base.ModelRef("product", product_module.Product), - ] + product: Annotated[Product, ModelRef("product", Product)] """The product to use to invoice the support subscription. @@ -61,19 +53,13 @@ class SupportSubscriptionType(record_base.RecordBase): support_subscription_ids: Annotated[ List[int], - record_base.ModelRef( - "support_subscription", - support_subscription_type.SupportSubscription, - ), + ModelRef("support_subscription", SupportSubscription), ] """A list of IDs for the support subscriptions of this type.""" support_subscription: Annotated[ - List[support_subscription_type.SupportSubscription], - record_base.ModelRef( - "support_subscription", - support_subscription_type.SupportSubscription, - ), + List[SupportSubscription], + ModelRef("support_subscription", SupportSubscription), ] """The list of support subscriptions of this type. @@ -82,24 +68,19 @@ class SupportSubscriptionType(record_base.RecordBase): """ support_subscriptions: Annotated[ - List[support_subscription_type.SupportSubscription], - record_base.ModelRef( - "support_subscription", - support_subscription_type.SupportSubscription, - ), + List[SupportSubscription], + ModelRef("support_subscription", SupportSubscription), ] """An alias for ``support_subscription``.""" class SupportSubscriptionTypeManager( - record_manager_name_base.NamedRecordManagerBase[SupportSubscriptionType], + NamedRecordManagerBase[SupportSubscriptionType], ): env_name = "openstack.support_subscription.type" record_class = SupportSubscriptionType # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - product as product_module, - support_subscription as support_subscription_type, -) +from .product import Product # noqa: E402 +from .support_subscription import SupportSubscription # noqa: E402 diff --git a/openstack_odooclient/managers/util.py b/openstack_odooclient/util.py similarity index 100% rename from openstack_odooclient/managers/util.py rename to openstack_odooclient/util.py From f6cee637515bf869c33ac0b3fec731164931317a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 10:03:35 +1200 Subject: [PATCH 35/87] Clean up imports, switch client to use type hints --- openstack_odooclient/__init__.py | 136 ++++-- openstack_odooclient/base/client.py | 221 ++++++++++ openstack_odooclient/client.py | 404 ++++++------------ openstack_odooclient/managers/tax.py | 43 +- openstack_odooclient/managers/tax_group.py | 9 +- .../managers/term_discount.py | 51 +-- openstack_odooclient/managers/trial.py | 24 +- openstack_odooclient/managers/uom.py | 24 +- openstack_odooclient/managers/uom_category.py | 7 +- openstack_odooclient/managers/user.py | 44 +- .../managers/volume_discount_range.py | 32 +- openstack_odooclient/managers/voucher_code.py | 75 ++-- 12 files changed, 549 insertions(+), 521 deletions(-) create mode 100644 openstack_odooclient/base/client.py diff --git a/openstack_odooclient/__init__.py b/openstack_odooclient/__init__.py index 0e97b87..d981426 100644 --- a/openstack_odooclient/__init__.py +++ b/openstack_odooclient/__init__.py @@ -15,6 +15,7 @@ from __future__ import annotations +from .base.client import ClientBase from .base.record import FieldAlias, ModelRef, RecordBase from .base.record_manager import RecordManagerBase from .base.record_manager_coded import CodedRecordManagerBase @@ -28,83 +29,132 @@ MultipleRecordsFoundError, RecordNotFoundError, ) -from .managers.account_move import AccountMove -from .managers.account_move_line import AccountMoveLine -from .managers.company import Company -from .managers.credit import Credit -from .managers.credit_transaction import CreditTransaction -from .managers.credit_type import CreditType -from .managers.currency import Currency -from .managers.customer_group import CustomerGroup -from .managers.grant import Grant -from .managers.grant_type import GrantType -from .managers.partner import Partner -from .managers.partner_category import PartnerCategory -from .managers.pricelist import Pricelist -from .managers.product import Product -from .managers.product_category import ProductCategory -from .managers.project import Project -from .managers.project_contact import ProjectContact -from .managers.referral_code import ReferralCode -from .managers.reseller import Reseller -from .managers.reseller_tier import ResellerTier -from .managers.sale_order import SaleOrder -from .managers.sale_order_line import SaleOrderLine -from .managers.support_subscription import SupportSubscription -from .managers.support_subscription_type import SupportSubscriptionType -from .managers.tax import Tax -from .managers.tax_group import TaxGroup -from .managers.term_discount import TermDiscount -from .managers.trial import Trial -from .managers.uom import Uom -from .managers.uom_category import UomCategory -from .managers.user import User -from .managers.volume_discount_range import VolumeDiscountRange -from .managers.voucher_code import VoucherCode +from .managers.account_move import AccountMove, AccountMoveManager +from .managers.account_move_line import ( + AccountMoveLine, + AccountMoveLineManager, +) +from .managers.company import Company, CompanyManager +from .managers.credit import Credit, CreditManager +from .managers.credit_transaction import ( + CreditTransaction, + CreditTransactionManager, +) +from .managers.credit_type import CreditType, CreditTypeManager +from .managers.currency import Currency, CurrencyManager +from .managers.customer_group import CustomerGroup, CustomerGroupManager +from .managers.grant import Grant, GrantManager +from .managers.grant_type import GrantType, GrantTypeManager +from .managers.partner import Partner, PartnerManager +from .managers.partner_category import PartnerCategory, PartnerCategoryManager +from .managers.pricelist import Pricelist, PricelistManager +from .managers.product import Product, ProductManager +from .managers.product_category import ProductCategory, ProductCategoryManager +from .managers.project import Project, ProjectManager +from .managers.project_contact import ProjectContact, ProjectContactManager +from .managers.referral_code import ReferralCode, ReferralCodeManager +from .managers.reseller import Reseller, ResellerManager +from .managers.reseller_tier import ResellerTier, ResellerTierManager +from .managers.sale_order import SaleOrder, SaleOrderManager +from .managers.sale_order_line import SaleOrderLine, SaleOrderLineManager +from .managers.support_subscription import ( + SupportSubscription, + SupportSubscriptionManager, +) +from .managers.support_subscription_type import ( + SupportSubscriptionType, + SupportSubscriptionTypeManager, +) +from .managers.tax import Tax, TaxManager +from .managers.tax_group import TaxGroup, TaxGroupManager +from .managers.term_discount import TermDiscount, TermDiscountManager +from .managers.trial import Trial, TrialManager +from .managers.uom import Uom, UomManager +from .managers.uom_category import UomCategory, UomCategoryManager +from .managers.user import User, UserManager +from .managers.volume_discount_range import ( + VolumeDiscountRange, + VolumeDiscountRangeManager, +) +from .managers.voucher_code import VoucherCode, VoucherCodeManager __all__ = [ + "ClientBase", + "RecordBase", + "FieldAlias", + "ModelRef", + "RecordManagerBase", + "CodedRecordManagerBase", + "NamedRecordManagerBase", + "RecordManagerWithUniqueFieldBase", "Client", - "ClientError", - "MultipleRecordsFoundError", - "RecordNotFoundError", "AccountMove", + "AccountMoveManager", "AccountMoveLine", + "AccountMoveLineManager", "Company", + "CompanyManager", "Credit", + "CreditManager", "CreditTransaction", + "CreditTransactionManager", "CreditType", + "CreditTypeManager", "Currency", + "CurrencyManager", "CustomerGroup", + "CustomerGroupManager", "Grant", + "GrantManager", "GrantType", + "GrantTypeManager", "Partner", + "PartnerManager", "PartnerCategory", + "PartnerCategoryManager", "Pricelist", + "PricelistManager", "Product", + "ProductManager", "ProductCategory", + "ProductCategoryManager", "Project", + "ProjectManager", "ProjectContact", - "RecordBase", - "RecordManagerBase", - "CodedRecordManagerBase", - "NamedRecordManagerBase", - "RecordManagerWithUniqueFieldBase", + "ProjectContactManager", "ReferralCode", + "ReferralCodeManager", "Reseller", + "ResellerManager", "ResellerTier", + "ResellerTierManager", "SaleOrder", + "SaleOrderManager", "SaleOrderLine", + "SaleOrderLineManager", "SupportSubscription", + "SupportSubscriptionManager", "SupportSubscriptionType", + "SupportSubscriptionTypeManager", "Tax", + "TaxManager", "TaxGroup", + "TaxGroupManager", "TermDiscount", + "TermDiscountManager", "Trial", + "TrialManager", "Uom", + "UomManager", "UomCategory", + "UomCategoryManager", "User", - "FieldAlias", - "ModelRef", + "UserManager", "VolumeDiscountRange", + "VolumeDiscountRangeManager", "VoucherCode", + "VoucherCodeManager", + "ClientError", + "MultipleRecordsFoundError", + "RecordNotFoundError", ] diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py new file mode 100644 index 0000000..4d465bf --- /dev/null +++ b/openstack_odooclient/base/client.py @@ -0,0 +1,221 @@ +# Copyright (C) 2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import ssl +import urllib.request + +from pathlib import Path +from typing import TYPE_CHECKING, overload + +from odoorpc import ODOO # type: ignore[import] +from packaging.version import Version +from typing_extensions import get_origin as get_type_origin, get_type_hints + +from ..util import is_subclass +from .record import RecordBase +from .record_manager import RecordManagerBase + +if TYPE_CHECKING: + from typing import Dict, Literal, Optional, Type, Union + + from odoorpc.db import DB # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + from odoorpc.report import Report # type: ignore[import] + + +class ClientBase: + """A client base class for managing the OpenStack Odoo ERP. + + No managers are included on this base class. + This class should be inherited, and manager classes defined + on the subclass using type hints, as shown below. + + The manager class will be instantiated and added to the + client object when the client is created. + + >>> from openstack_odooclient import ClientBase, UserManager + >>> class Client(ClientBase): + ... users: UserManager + >>> Client(...).users + + + Connect to an Odoo server by either passing the required + connection and authentication information, + or passing in a pre-existing OdooRPC ``ODOO`` object. + + When connecting to an Odoo server using SSL, set ``protocol`` + to ``jsonrpc+ssl``. SSL certificate verification can be disabled + by setting ``verify`` to ``False``. If a custom CA certificate + is required to verify the Odoo server's host certificate, + this can be configured by passing the certificate path to ``verify``. + + All parameters must be specified as keyword arguments. + + :param hostname: Server hostname, required if ``odoo`` is not set + :type hostname: Optional[str], optional + :param database: Database name, required if ``odoo`` is not set + :type database: Optional[str], optional + :param username: Username, required if ``odoo`` is not set + :type username: Optional[str], optional + :param password: Password (or API key), required if ``odoo`` is not set + :type password: Optional[str], optional + :param protocol: Communication protocol, defaults to ``jsonrpc`` + :type protocol: str, optional + :param port: Access port, defaults to ``8069`` + :type port: int, optional + :param verify: Configure SSL cert verification, defaults to ``True`` + :type verify: Union[bool, Path, str] + :param version: Server version, defaults to ``None`` (auto-detect) + :type version: Optional[str], optional + """ + + @overload + def __init__( + self, + *, + hostname: Optional[str] = ..., + database: Optional[str] = ..., + username: Optional[str] = ..., + password: Optional[str] = ..., + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: ODOO, + ) -> None: ... + + @overload + def __init__( + self, + *, + hostname: str, + database: str, + username: str, + password: str, + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: Literal[None] = ..., + ) -> None: ... + + @overload + def __init__( + self, + *, + hostname: Optional[str] = ..., + database: Optional[str] = ..., + username: Optional[str] = ..., + password: Optional[str] = ..., + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = ..., + version: Optional[str] = ..., + odoo: Optional[ODOO] = ..., + ) -> None: ... + + def __init__( + self, + *, + hostname: Optional[str] = None, + database: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + protocol: str = "jsonrpc", + port: int = 8069, + verify: Union[bool, Path, str] = True, + version: Optional[str] = None, + odoo: Optional[ODOO] = None, + ) -> None: + # If an OdooRPC object is provided, use that directly. + # Otherwise, make a new one with the provided settings. + if odoo: + self._odoo = odoo + else: + opener = None + if protocol.endswith("+ssl"): + ssl_verify = verify is not False + ssl_cafile = ( + str(verify) if isinstance(verify, (Path, str)) else None + ) + if not ssl_verify or ssl_cafile: + ssl_context = ssl.create_default_context(cafile=ssl_cafile) + if not ssl_verify: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + opener = urllib.request.build_opener( + urllib.request.HTTPSHandler(context=ssl_context), + urllib.request.HTTPCookieProcessor(), + ) + self._odoo = ODOO( + protocol=protocol, + host=hostname, + port=port, + version=version, + opener=opener, + ) + self._odoo.login(database, username, password) + # Create an internal mapping between record classes and their managers. + # This is populated by the manager classes themselves when created, + # and used when converting model references on record objects into + # new record objects. + self._record_manager_mapping: Dict[ + Type[RecordBase], + RecordManagerBase, + ] = {} + # Create record managers defined in the type hints. + for attr_name, type_hint in get_type_hints(type(self)).items(): + attr_type = get_type_origin(type_hint) + if is_subclass(attr_type, RecordManagerBase): + setattr(self, attr_name, attr_type(self)) + + @property + def db(self) -> DB: + """The database management service.""" + return self._odoo.db + + @property + def report(self) -> Report: + """The report management service.""" + return self._odoo.report + + @property + def env(self) -> Environment: + """The OdooRPC environment wrapper object. + + This allows interacting with models that do not have managers + within this Odoo client. + Usage is the same as on a native ``odoorpc.ODOO`` object. + """ + return self._odoo.env + + @property + def user_id(self) -> int: + """The ID for the currently logged in user.""" + return self._odoo.env.uid + + @property + def version(self) -> Version: + """The version of the server, + as a comparable ``packaging.version.Version`` object. + """ + return Version(self._odoo.version) + + @property + def version_str(self) -> str: + """The version of the server, as a string.""" + return self._odoo.version diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 75acb00..8fbb64e 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -15,64 +15,44 @@ from __future__ import annotations -import ssl -import urllib.request - -from pathlib import Path -from typing import TYPE_CHECKING, overload - -from odoorpc import ODOO # type: ignore[import] -from packaging.version import Version - -from .base import record -from .managers import ( - account_move, - account_move_line, - company, - credit, - credit_transaction, - credit_type, - currency, - customer_group, - grant, - grant_type, - partner, - partner_category, - pricelist, - product, - product_category, - project, - project_contact, - referral_code, - reseller, - reseller_tier, - sale_order, - sale_order_line, - support_subscription, - support_subscription_type, - tax, - tax_group, - term_discount, - trial, - uom, - uom_category, - user, - volume_discount_range, - voucher_code, -) - -if TYPE_CHECKING: - from typing import Dict, Literal, Optional, Type, Union - - from odoorpc.db import DB # type: ignore[import] - from odoorpc.env import Environment # type: ignore[import] - from odoorpc.report import Report # type: ignore[import] - - from .managers import record_manager_base - - -class Client: - """A client class for managing the OpenStack Odoo ERP. +from .base.client import ClientBase +from .managers.account_move import AccountMoveManager +from .managers.account_move_line import AccountMoveLineManager +from .managers.company import CompanyManager +from .managers.credit import CreditManager +from .managers.credit_transaction import CreditTransactionManager +from .managers.credit_type import CreditTypeManager +from .managers.currency import CurrencyManager +from .managers.customer_group import CustomerGroupManager +from .managers.grant import GrantManager +from .managers.grant_type import GrantTypeManager +from .managers.partner import PartnerManager +from .managers.partner_category import PartnerCategoryManager +from .managers.pricelist import PricelistManager +from .managers.product import ProductManager +from .managers.product_category import ProductCategoryManager +from .managers.project import ProjectManager +from .managers.project_contact import ProjectContactManager +from .managers.referral_code import ReferralCodeManager +from .managers.reseller import ResellerManager +from .managers.reseller_tier import ResellerTierManager +from .managers.sale_order import SaleOrderManager +from .managers.sale_order_line import SaleOrderLineManager +from .managers.support_subscription import SupportSubscriptionManager +from .managers.support_subscription_type import SupportSubscriptionTypeManager +from .managers.tax import TaxManager +from .managers.tax_group import TaxGroupManager +from .managers.term_discount import TermDiscountManager +from .managers.trial import TrialManager +from .managers.uom import UomManager +from .managers.uom_category import UomCategoryManager +from .managers.user import User, UserManager +from .managers.volume_discount_range import VolumeDiscountRangeManager +from .managers.voucher_code import VoucherCodeManager + + +class Client(ClientBase): + """The client for managing the OpenStack Odoo ERP. Connect to an Odoo server by either passing the required connection and authentication information, @@ -104,223 +84,109 @@ class Client: :type version: Optional[str], optional """ - # TODO(callumdickinson): Use type hints to define managers, - # to allow for easy expansion of the Odoo client class. - - @overload - def __init__( - self, - *, - hostname: Optional[str] = ..., - database: Optional[str] = ..., - username: Optional[str] = ..., - password: Optional[str] = ..., - protocol: str = "jsonrpc", - port: int = 8069, - verify: Union[bool, Path, str] = ..., - version: Optional[str] = ..., - odoo: ODOO, - ) -> None: ... - - @overload - def __init__( - self, - *, - hostname: str, - database: str, - username: str, - password: str, - protocol: str = "jsonrpc", - port: int = 8069, - verify: Union[bool, Path, str] = ..., - version: Optional[str] = ..., - odoo: Literal[None] = ..., - ) -> None: ... - - @overload - def __init__( - self, - *, - hostname: Optional[str] = ..., - database: Optional[str] = ..., - username: Optional[str] = ..., - password: Optional[str] = ..., - protocol: str = "jsonrpc", - port: int = 8069, - verify: Union[bool, Path, str] = ..., - version: Optional[str] = ..., - odoo: Optional[ODOO] = ..., - ) -> None: ... - - def __init__( - self, - *, - hostname: Optional[str] = None, - database: Optional[str] = None, - username: Optional[str] = None, - password: Optional[str] = None, - protocol: str = "jsonrpc", - port: int = 8069, - verify: Union[bool, Path, str] = True, - version: Optional[str] = None, - odoo: Optional[ODOO] = None, - ) -> None: - # If an OdooRPC object is provided, use that directly. - # Otherwise, make a new one with the provided settings. - if odoo: - self._odoo = odoo - else: - opener = None - if protocol.endswith("+ssl"): - ssl_verify = verify is not False - ssl_cafile = ( - str(verify) if isinstance(verify, (Path, str)) else None - ) - if not ssl_verify or ssl_cafile: - ssl_context = ssl.create_default_context(cafile=ssl_cafile) - if not ssl_verify: - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - opener = urllib.request.build_opener( - urllib.request.HTTPSHandler(context=ssl_context), - urllib.request.HTTPCookieProcessor(), - ) - self._odoo = ODOO( - protocol=protocol, - host=hostname, - port=port, - version=version, - opener=opener, - ) - self._odoo.login(database, username, password) - # Create an internal mapping between record classes and their managers. - # This is populated by the manager classes themselves when created, - # and used when converting model references on record objects into - # # new record objects. - self._record_manager_mapping: Dict[ - Type[record.RecordBase], - record_manager_base.RecordManagerBase, - ] = {} - # Create record managers. - self.account_moves = account_move.AccountMoveManager(self) - """Account Move (Invoice) manager.""" - self.account_move_lines = account_move_line.AccountMoveLineManager( - self, - ) - """Company manager.""" - self.companies = company.CompanyManager(self) - """Account Move (Invoice) Line manager.""" - self.credits = credit.CreditManager(self) - """Credit manager.""" - self.credit_transactions = credit_transaction.CreditTransactionManager( - self - ) - """Credit Transaction manager.""" - self.credit_types = credit_type.CreditTypeManager(self) - """Credit Type manager.""" - self.currencies = currency.CurrencyManager(self) - """Currency manager.""" - self.customer_groups = customer_group.CustomerGroupManager(self) - """Customer Group manager.""" - self.grants = grant.GrantManager(self) - """Grant manager.""" - self.grant_types = grant_type.GrantTypeManager(self) - """Grant Type manager.""" - self.partners = partner.PartnerManager(self) - """Partner manager.""" - self.partner_categories = partner_category.PartnerCategoryManager( - self, - ) - """Partner Category manager.""" - self.pricelists = pricelist.PricelistManager(self) - """Pricelist manager.""" - self.products = product.ProductManager(self) - """Product manager.""" - self.product_categories = product_category.ProductCategoryManager( - self, - ) - """Product Category manager.""" - self.projects = project.ProjectManager(self) - """OpenStack Project manager.""" - self.project_contacts = project_contact.ProjectContactManager(self) - """Project Contact manager.""" - self.referral_codes = referral_code.ReferralCodeManager(self) - """Referral Code manager.""" - self.resellers = reseller.ResellerManager(self) - """Reseller manager.""" - self.reseller_tiers = reseller_tier.ResellerTierManager(self) - """Reseller Tier manager.""" - self.sale_orders = sale_order.SaleOrderManager(self) - """Sale Order manager.""" - self.sale_order_lines = sale_order_line.SaleOrderLineManager(self) - """Sale Order Line manager.""" - self.support_subscriptions = ( - support_subscription.SupportSubscriptionManager(self) - ) - """Support Subscription manager.""" - self.support_subscription_types = ( - support_subscription_type.SupportSubscriptionTypeManager(self) - ) - self.taxes = tax.TaxManager(self) - """Tax manager.""" - self.tax_groups = tax_group.TaxGroupManager(self) - """Tax Group manager.""" - """Support Subscription Type manager.""" - self.term_discounts = term_discount.TermDiscountManager(self) - """Term Discount manager.""" - self.trials = trial.TrialManager(self) - """Trial manager.""" - self.uoms = uom.UomManager(self) - """Unit of Measure (UoM) manager.""" - self.uom_categories = uom_category.UomCategoryManager(self) - """Unit of Measure (UoM) Category manager.""" - self.users = user.UserManager(self) - """User manager.""" - self.volume_discount_ranges = ( - volume_discount_range.VolumeDiscountRangeManager(self) - ) - """Volume Discount Range manager.""" - self.voucher_codes = voucher_code.VoucherCodeManager(self) - """Voucher Code manager.""" + account_moves: AccountMoveManager + """Account move (invoice) manager.""" - @property - def db(self) -> DB: - """The database management service.""" - return self._odoo.db + account_move_lines: AccountMoveLineManager + """Account move (invoice) line manager.""" - @property - def report(self) -> Report: - """The report management service.""" - return self._odoo.report + companies: CompanyManager + """Company manager.""" - @property - def env(self) -> Environment: - """The OdooRPC environment wrapper object. + credits: CreditManager + """OpenStack credit manager.""" - This allows interacting with models that do not have managers - within this Odoo client. - Usage is the same as on a native ``odoorpc.ODOO`` object. - """ - return self._odoo.env + credit_transactions: CreditTransactionManager + """OpenStack credit transaction manager.""" - @property - def user_id(self) -> int: - """The ID for the currently logged in user.""" - return self._odoo.env.uid + credit_types: CreditTypeManager + """OpenStack credit type manager.""" - @property - def user(self) -> user.User: - """The currently logged in user.""" - return self.users.get(self.user_id) + currencies: CurrencyManager + """Currency manager.""" - @property - def version(self) -> Version: - """The version of the server, - as a comparable ``packaging.version.Version`` object. - """ - return Version(self._odoo.version) + customer_groups: CustomerGroupManager + """OpenStack customer group manager.""" + + grants: GrantManager + """OpenStack grant manager.""" + + grant_types: GrantTypeManager + """OpenStack grant type manager.""" + + partners: PartnerManager + """Partner manager.""" + + partner_categories: PartnerCategoryManager + """Partner category manager.""" + + pricelists: PricelistManager + """Pricelist manager.""" + + products: ProductManager + """Product manager.""" + + product_categories: ProductCategoryManager + """Product category manager.""" + + projects: ProjectManager + """OpenStack project manager.""" + + project_contacts: ProjectContactManager + """OpenStack project contact manager.""" + + referral_codes: ReferralCodeManager + """OpenStack referral code manager.""" + + resellers: ResellerManager + """OpenStack reseller manager.""" + + reseller_tiers: ResellerTierManager + """OpenStack reseller tier manager.""" + + sale_orders: SaleOrderManager + """Sale order manager.""" + + sale_order_lines: SaleOrderLineManager + """Sale order line manager.""" + + support_subscriptions: SupportSubscriptionManager + """OpenStack support subscription manager.""" + + support_subscription_types: SupportSubscriptionTypeManager + """OpenStack support subscription type manager.""" + + taxes: TaxManager + """Tax manager.""" + + tax_groups: TaxGroupManager + """Tax Group manager.""" + + term_discounts: TermDiscountManager + """OpenStack term discount manager.""" + + trials: TrialManager + """OpenStack trial manager.""" + + uoms: UomManager + """Unit of Measure (UoM) manager.""" + + uom_categories: UomCategoryManager + """Unit of Measure (UoM) category manager.""" + + users: UserManager + """User manager.""" + + volume_discount_ranges: VolumeDiscountRangeManager + """OpenStack volume discount range manager.""" + + voucher_codes: VoucherCodeManager + """Voucher code manager.""" @property - def version_str(self) -> str: - """The version of the server, as a string.""" - return self._odoo.version + def user(self) -> User: + """The currently logged in user. + + This fetches the full record from Odoo. + """ + return self.users.get(self.user_id) diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 5a3015d..302353f 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -19,10 +19,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class Tax(record_base.RecordBase): +class Tax(RecordBase): active: bool """Whether or not this tax is active (enabled).""" @@ -44,22 +45,13 @@ class Tax(record_base.RecordBase): to the same analytic account as the invoice line (if any). """ - company_id: Annotated[ - int, - record_base.ModelRef("company_id", company_module.Company), - ] + company_id: Annotated[int, ModelRef("company_id", Company)] """The ID for the company this tax is owned by.""" - company_name: Annotated[ - str, - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[str, ModelRef("company_id", Company)] """The name of the company this tax is owned by.""" - company: Annotated[ - company_module.Company, - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Company, ModelRef("company_id", Company)] """The company this tax is owned by. This fetches the full record from Odoo once, @@ -92,22 +84,13 @@ class Tax(record_base.RecordBase): * ``on_payment`` - Due as soon as payment of the invoice is received """ - tax_group_id: Annotated[ - int, - record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), - ] + tax_group_id: Annotated[int, ModelRef("tax_group_id", TaxGroup)] """The ID for the tax group this tax is categorised under.""" - tax_group_name: Annotated[ - str, - record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), - ] + tax_group_name: Annotated[str, ModelRef("tax_group_id", TaxGroup)] """The name of the tax group this tax is categorised under.""" - tax_group: Annotated[ - tax_group_module.TaxGroup, - record_base.ModelRef("tax_group_id", tax_group_module.TaxGroup), - ] + tax_group: Annotated[TaxGroup, ModelRef("tax_group_id", TaxGroup)] """The tax group this tax is categorised under. This fetches the full record from Odoo once, @@ -115,13 +98,11 @@ class Tax(record_base.RecordBase): """ -class TaxManager(record_manager_name_base.NamedRecordManagerBase[Tax]): +class TaxManager(NamedRecordManagerBase[Tax]): env_name = "account.tax" record_class = Tax # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - company as company_module, - tax_group as tax_group_module, -) +from .company import Company # noqa: E402 +from .tax_group import TaxGroup # noqa: E402 diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py index a529c43..9c893b9 100644 --- a/openstack_odooclient/managers/tax_group.py +++ b/openstack_odooclient/managers/tax_group.py @@ -15,16 +15,15 @@ from __future__ import annotations -from . import record_base, record_manager_name_base +from ..base.record import RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class TaxGroup(record_base.RecordBase): +class TaxGroup(RecordBase): name: str """Tax group name.""" -class TaxGroupManager( - record_manager_name_base.NamedRecordManagerBase[TaxGroup], -): +class TaxGroupManager(NamedRecordManagerBase[TaxGroup]): env_name = "account.tax.group" record_class = TaxGroup diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index 903b9ee..b78b893 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -20,10 +20,11 @@ from typing_extensions import Annotated, Self -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class TermDiscount(record_base.RecordBase): +class TermDiscount(RecordBase): discount_percent: float """The maximum discount percentage for this term discount (0-100).""" @@ -36,32 +37,20 @@ class TermDiscount(record_base.RecordBase): min_commit: float """The minimum commitment for this term discount to apply.""" - partner_id: Annotated[ - int, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner", Partner)] """The ID for the partner that receives this term discount.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner", Partner)] """The name of the partner that receives this term discount.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner", Partner)] """The partner that receives this term discount. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - project_id: Annotated[ - Optional[int], - record_base.ModelRef("project", project_module.Project), - ] + project_id: Annotated[Optional[int], ModelRef("project", Project)] """The ID for the project this term discount applies to, if it is a project-specific term discount. @@ -69,10 +58,7 @@ class TermDiscount(record_base.RecordBase): the partner owns. """ - project_name: Annotated[ - Optional[str], - record_base.ModelRef("project", project_module.Project), - ] + project_name: Annotated[Optional[str], ModelRef("project", Project)] """The name of the project this term discount applies to, if it is a project-specific term discount. @@ -80,10 +66,7 @@ class TermDiscount(record_base.RecordBase): the partner owns. """ - project: Annotated[ - Optional[project_module.Project], - record_base.ModelRef("project", project_module.Project), - ] + project: Annotated[Optional[Project], ModelRef("project", Project)] """The project this term discount applies to, if it is a project-specific term discount. @@ -99,7 +82,7 @@ class TermDiscount(record_base.RecordBase): superseded_by_id: Annotated[ Optional[int], - record_base.ModelRef("superseded_by", Self), + ModelRef("superseded_by", Self), ] """The ID for the term discount that supersedes this one, if superseded. @@ -107,7 +90,7 @@ class TermDiscount(record_base.RecordBase): superseded_by_name: Annotated[ Optional[str], - record_base.ModelRef("superseded_by", Self), + ModelRef("superseded_by", Self), ] """The name of the term discount that supersedes this one, if superseded. @@ -115,7 +98,7 @@ class TermDiscount(record_base.RecordBase): superseded_by: Annotated[ Optional[Self], - record_base.ModelRef("superseded_by", Self), + ModelRef("superseded_by", Self), ] """The term discount that supersedes this one, if superseded. @@ -125,15 +108,11 @@ class TermDiscount(record_base.RecordBase): """ -class TermDiscountManager( - record_manager_base.RecordManagerBase[TermDiscount], -): +class TermDiscountManager(RecordManagerBase[TermDiscount]): env_name = "openstack.term_discount" record_class = TermDiscount # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa :E402 - partner as partner_module, - project as project_module, -) +from .partner import Partner # noqa: E402 +from .project import Project # noqa: E402 diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index a2dc47c..280dcee 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -20,10 +20,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class Trial(record_base.RecordBase): +class Trial(RecordBase): account_suspended_date: Union[date, Literal[False]] """The date the account was suspended, following the end of the trial.""" @@ -38,22 +39,13 @@ class Trial(record_base.RecordBase): end_date: date """The end date of this trial.""" - partner_id: Annotated[ - int, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner", Partner)] """The ID for the target partner for this trial.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner", Partner)] """The name of the target partner for this trial.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner", Partner)] """The target partner for this trial. This fetches the full record from Odoo once, @@ -64,10 +56,10 @@ class Trial(record_base.RecordBase): """The start date of this trial.""" -class TrialManager(record_manager_base.RecordManagerBase[Trial]): +class TrialManager(RecordManagerBase[Trial]): env_name = "openstack.trial" record_class = Trial # NOTE(callumdickinson): Import here to avoid circular imports. -from . import partner as partner_module # noqa: E402 +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index 6ab9641..201f8ca 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -19,29 +19,21 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class Uom(record_base.RecordBase): +class Uom(RecordBase): active: bool """Whether or not this Unit of Measure is active (enabled).""" - category_id: Annotated[ - int, - record_base.ModelRef("category_id", uom_category.UomCategory), - ] + category_id: Annotated[int, ModelRef("category_id", UomCategory)] """The ID for the category this Unit of Measure is classified as.""" - category_name: Annotated[ - str, - record_base.ModelRef("category_id", uom_category.UomCategory), - ] + category_name: Annotated[str, ModelRef("category_id", UomCategory)] """The name of the category this Unit of Measure is classified as.""" - category: Annotated[ - uom_category.UomCategory, - record_base.ModelRef("category_id", uom_category.UomCategory), - ] + category: Annotated[UomCategory, ModelRef("category_id", UomCategory)] """The category this Unit of Measure is classified as. This fetches the full record from Odoo once, @@ -94,10 +86,10 @@ class Uom(record_base.RecordBase): """ -class UomManager(record_manager_base.RecordManagerBase[Uom]): +class UomManager(RecordManagerBase[Uom]): env_name = "uom.uom" record_class = Uom # NOTE(callumdickinson): Import here to avoid circular imports. -from . import uom_category # noqa: E402 +from .uom_category import UomCategory # noqa: E402 diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py index 7827f43..810fb75 100644 --- a/openstack_odooclient/managers/uom_category.py +++ b/openstack_odooclient/managers/uom_category.py @@ -17,10 +17,11 @@ from typing import Literal -from . import record_base, record_manager_base +from ..base.record import RecordBase +from ..base.record_manager import RecordManagerBase -class UomCategory(record_base.RecordBase): +class UomCategory(RecordBase): measure_type: Literal[ "unit", "weight", @@ -45,6 +46,6 @@ class UomCategory(record_base.RecordBase): """Unit of Measure (UoM) category name.""" -class UomCategoryManager(record_manager_base.RecordManagerBase[UomCategory]): +class UomCategoryManager(RecordManagerBase[UomCategory]): env_name = "uom.category" record_class = UomCategory diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index bc0dff3..dbe5c61 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -17,36 +17,25 @@ from typing_extensions import Annotated -from . import ( - company as company_module, - record_base, - record_manager_base, -) +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase +from .company import Company -class User(record_base.RecordBase): +class User(RecordBase): active: bool """Whether or not this user is active.""" active_partner: bool """Whether or not the partner this user is associated with is active.""" - company_id: Annotated[ - int, - record_base.ModelRef("company_id", company_module.Company), - ] + company_id: Annotated[int, ModelRef("company_id", Company)] """The ID for the default company this user is logged in as.""" - company_name: Annotated[ - str, - record_base.ModelRef("company_id", company_module.Company), - ] + company_name: Annotated[str, ModelRef("company_id", Company)] """The name of the default company this user is logged in as.""" - company: Annotated[ - company_module.Company, - record_base.ModelRef("company_id", company_module.Company), - ] + company: Annotated[Company, ModelRef("company_id", Company)] """The default company this user is logged in as. This fetches the full record from Odoo once, @@ -56,22 +45,13 @@ class User(record_base.RecordBase): name: str """User name.""" - partner_id: Annotated[ - int, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_id: Annotated[int, ModelRef("partner_id", Partner)] """The ID for the partner that this user is associated with.""" - partner_name: Annotated[ - str, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner_name: Annotated[str, ModelRef("partner_id", Partner)] """The name of the partner that this user is associated with.""" - partner: Annotated[ - partner_module.Partner, - record_base.ModelRef("partner_id", partner_module.Partner), - ] + partner: Annotated[Partner, ModelRef("partner_id", Partner)] """The partner that this user is associated with. This fetches the full record from Odoo once, @@ -79,10 +59,10 @@ class User(record_base.RecordBase): """ -class UserManager(record_manager_base.RecordManagerBase[User]): +class UserManager(RecordManagerBase[User]): env_name = "res.users" record_class = User # NOTE(callumdickinson): Import here to make sure circular imports work. -from . import partner as partner_module # noqa: E402 +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 166564a..002cd9e 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -19,16 +19,14 @@ from typing_extensions import Annotated -from . import record_base, record_manager_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase -class VolumeDiscountRange(record_base.RecordBase): +class VolumeDiscountRange(RecordBase): customer_group_id: Annotated[ Optional[int], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + ModelRef("customer_group", CustomerGroup), ] """The ID for the customer group this volume discount range applies to, if a specific customer group is set. @@ -36,21 +34,15 @@ class VolumeDiscountRange(record_base.RecordBase): customer_group_name: Annotated[ Optional[str], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + ModelRef("customer_group", CustomerGroup), ] """The name of the customer group this volume discount range applies to, if a specific customer group is set. """ customer_group: Annotated[ - Optional[customer_group_module.CustomerGroup], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + Optional[CustomerGroup], + ModelRef("customer_group", CustomerGroup), ] """The customer group this volume discount range applies to, if a specific customer group is set. @@ -80,18 +72,14 @@ class VolumeDiscountRange(record_base.RecordBase): """Use the ``max`` field, if defined.""" -class VolumeDiscountRangeManager( - record_manager_base.RecordManagerBase[VolumeDiscountRange], -): +class VolumeDiscountRangeManager(RecordManagerBase[VolumeDiscountRange]): env_name = "openstack.volume_discount_range" record_class = VolumeDiscountRange def get_for_charge( self, charge: float, - customer_group: Optional[ - Union[int, customer_group_module.CustomerGroup], - ] = None, + customer_group: Optional[Union[int, CustomerGroup]] = None, ) -> Optional[VolumeDiscountRange]: """Return the volume discount range to apply to a given charge. @@ -127,4 +115,4 @@ def get_for_charge( # NOTE(callumdickinson): Import here to avoid circular imports. -from . import customer_group as customer_group_module # noqa: E402 +from .customer_group import CustomerGroup # noqa: E402 diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index b124785..93fed84 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -20,10 +20,11 @@ from typing_extensions import Annotated -from . import record_base, record_manager_name_base +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase -class VoucherCode(record_base.RecordBase): +class VoucherCode(RecordBase): claimed: bool """Whether or not this voucher code has been claimed.""" @@ -37,7 +38,7 @@ class VoucherCode(record_base.RecordBase): credit_type_id: Annotated[ Optional[int], - record_base.ModelRef("credit_type", credit_type_module.CreditType), + ModelRef("credit_type", CreditType), ] """The ID of the credit type to use, if a credit is to be created by this voucher code. @@ -45,15 +46,15 @@ class VoucherCode(record_base.RecordBase): credit_type_name: Annotated[ Optional[str], - record_base.ModelRef("credit_type", credit_type_module.CreditType), + ModelRef("credit_type", CreditType), ] """The name of the credit type to use, if a credit is to be created by this voucher code. """ credit_type: Annotated[ - Optional[credit_type_module.CreditType], - record_base.ModelRef("credit_type", credit_type_module.CreditType), + Optional[CreditType], + ModelRef("credit_type", CreditType), ] """The credit type to use, if a credit is to be created by this voucher code. @@ -69,10 +70,7 @@ class VoucherCode(record_base.RecordBase): customer_group_id: Annotated[ Optional[int], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + ModelRef("customer_group", CustomerGroup), ] """The ID of the customer group this voucher code is available to. @@ -81,10 +79,7 @@ class VoucherCode(record_base.RecordBase): customer_group_name: Annotated[ Optional[str], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + ModelRef("customer_group", CustomerGroup), ] """The name of the customer group this voucher code is available to. @@ -92,11 +87,8 @@ class VoucherCode(record_base.RecordBase): """ customer_group: Annotated[ - Optional[customer_group_module.CustomerGroup], - record_base.ModelRef( - "customer_group", - customer_group_module.CustomerGroup, - ), + Optional[CustomerGroup], + ModelRef("customer_group", CustomerGroup), ] """The customer group this voucher code is available to. @@ -114,25 +106,22 @@ class VoucherCode(record_base.RecordBase): created by the voucher code. """ - grant_type_id: Annotated[ - Optional[int], - record_base.ModelRef("grant_type", grant_type_module.GrantType), - ] + grant_type_id: Annotated[Optional[int], ModelRef("grant_type", GrantType)] """The ID of the grant type to use, if a grant is to be created by this voucher code. """ grant_type_name: Annotated[ Optional[str], - record_base.ModelRef("grant_type", grant_type_module.GrantType), + ModelRef("grant_type", GrantType), ] """The name of the grant type to use, if a grant is to be created by this voucher code. """ grant_type: Annotated[ - Optional[grant_type_module.GrantType], - record_base.ModelRef("grant_type", grant_type_module.GrantType), + Optional[GrantType], + ModelRef("grant_type", GrantType), ] """The grant type to use, if a grant is to be created by this voucher code. @@ -168,7 +157,7 @@ class VoucherCode(record_base.RecordBase): sales_person_id: Annotated[ Optional[int], - record_base.ModelRef("sales_person", partner.Partner), + ModelRef("sales_person", Partner), ] """The ID for the salesperson partner responsible for this voucher code, if assigned. @@ -176,15 +165,15 @@ class VoucherCode(record_base.RecordBase): sales_person_name: Annotated[ Optional[str], - record_base.ModelRef("sales_person", partner.Partner), + ModelRef("sales_person", Partner), ] """The name of the salesperson partner responsible for this voucher code, if assigned. """ sales_person: Annotated[ - Optional[partner.Partner], - record_base.ModelRef("sales_person"), + Optional[Partner], + ModelRef("sales_person", Partner), ] """The salesperson partner responsible for this voucher code, if assigned. @@ -193,18 +182,12 @@ class VoucherCode(record_base.RecordBase): and caches it for subsequent accesses. """ - tag_ids: Annotated[ - List[int], - record_base.ModelRef("tags", partner_category.PartnerCategory), - ] + tag_ids: Annotated[List[int], ModelRef("tags", PartnerCategory)] """A list of IDs for the tags (partner categories) to assign to partners for new accounts that signed up using this voucher code. """ - tags: Annotated[ - List[partner_category.PartnerCategory], - record_base.ModelRef("tags", partner_category.PartnerCategory), - ] + tags: Annotated[List[PartnerCategory], ModelRef("tags", PartnerCategory)] """The list of tags (partner categories) to assign to partners for new accounts that signed up using this voucher code. @@ -213,18 +196,14 @@ class VoucherCode(record_base.RecordBase): """ -class VoucherCodeManager( - record_manager_name_base.NamedRecordManagerBase[VoucherCode], -): +class VoucherCodeManager(NamedRecordManagerBase[VoucherCode]): env_name = "openstack.voucher_code" record_class = VoucherCode # NOTE(callumdickinson): Import here to avoid circular imports. -from . import ( # noqa: E402 - credit_type as credit_type_module, - customer_group as customer_group_module, - grant_type as grant_type_module, - partner, - partner_category, -) +from .credit_type import CreditType # noqa: E402 +from .customer_group import CustomerGroup # noqa: E402 +from .grant_type import GrantType # noqa: E402 +from .partner import Partner # noqa: E402 +from .partner_category import PartnerCategory # noqa: E402 From 11e9b930a6c8e014e1dd5c0a845e84693eb6e850 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 11:21:21 +1200 Subject: [PATCH 36/87] Fix circular imports --- openstack_odooclient/base/client.py | 10 +++++----- openstack_odooclient/client.py | 2 +- openstack_odooclient/managers/account_move.py | 4 ++-- openstack_odooclient/managers/account_move_line.py | 6 +++--- openstack_odooclient/managers/credit_type.py | 4 ++-- openstack_odooclient/managers/partner.py | 2 +- openstack_odooclient/managers/pricelist.py | 2 +- openstack_odooclient/managers/user.py | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py index 4d465bf..f678e38 100644 --- a/openstack_odooclient/base/client.py +++ b/openstack_odooclient/base/client.py @@ -78,7 +78,7 @@ class ClientBase: :param port: Access port, defaults to ``8069`` :type port: int, optional :param verify: Configure SSL cert verification, defaults to ``True`` - :type verify: Union[bool, Path, str] + :type verify: Union[bool, str, Path] :param version: Server version, defaults to ``None`` (auto-detect) :type version: Optional[str], optional """ @@ -93,7 +93,7 @@ def __init__( password: Optional[str] = ..., protocol: str = "jsonrpc", port: int = 8069, - verify: Union[bool, Path, str] = ..., + verify: Union[bool, str, Path] = ..., version: Optional[str] = ..., odoo: ODOO, ) -> None: ... @@ -108,7 +108,7 @@ def __init__( password: str, protocol: str = "jsonrpc", port: int = 8069, - verify: Union[bool, Path, str] = ..., + verify: Union[bool, str, Path] = ..., version: Optional[str] = ..., odoo: Literal[None] = ..., ) -> None: ... @@ -123,7 +123,7 @@ def __init__( password: Optional[str] = ..., protocol: str = "jsonrpc", port: int = 8069, - verify: Union[bool, Path, str] = ..., + verify: Union[bool, str, Path] = ..., version: Optional[str] = ..., odoo: Optional[ODOO] = ..., ) -> None: ... @@ -137,7 +137,7 @@ def __init__( password: Optional[str] = None, protocol: str = "jsonrpc", port: int = 8069, - verify: Union[bool, Path, str] = True, + verify: Union[bool, str, Path] = True, version: Optional[str] = None, odoo: Optional[ODOO] = None, ) -> None: diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 8fbb64e..14ddb79 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -79,7 +79,7 @@ class Client(ClientBase): :param port: Access port, defaults to ``8069`` :type port: int, optional :param verify: Configure SSL cert verification, defaults to ``True`` - :type verify: Union[bool, Path, str] + :type verify: Union[bool, str, Path] :param version: Server version, defaults to ``None`` (auto-detect) :type version: Optional[str], optional """ diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index 9c752ae..4e12a5e 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -22,8 +22,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager_named import NamedRecordManagerBase -from .currency import Currency -from .project import Project class AccountMove(RecordBase): @@ -181,3 +179,5 @@ class AccountMoveManager(NamedRecordManagerBase[AccountMove]): # NOTE(callumdickinson): Import here to make sure circular imports work. from .account_move_line import AccountMoveLine # noqa: E402 +from .currency import Currency # noqa: E402 +from .project import Project # noqa: E402 diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index 320d62a..4b71d1a 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -21,9 +21,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager import RecordManagerBase -from .currency import Currency -from .product import Product -from .project import Project class AccountMoveLine(RecordBase): @@ -142,3 +139,6 @@ class AccountMoveLineManager(RecordManagerBase[AccountMoveLine]): # NOTE(callumdickinson): Import here to make sure circular imports work. from .account_move import AccountMove # noqa: E402 +from .currency import Currency # noqa: E402 +from .product import Product # noqa: E402 +from .project import Project # noqa: E402 diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index e3f1466..ada5ec2 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -21,8 +21,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager_named import NamedRecordManagerBase -from .product import Product -from .product_category import ProductCategory class CreditType(RecordBase): @@ -115,3 +113,5 @@ class CreditTypeManager(NamedRecordManagerBase[CreditType]): # NOTE(callumdickinson): Import here to make sure circular imports work. from .credit import Credit # noqa: E402 +from .product import Product # noqa: E402 +from .product_category import ProductCategory # noqa: E402 diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index 9efd225..ad49226 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -21,7 +21,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager import RecordManagerBase -from .pricelist import Pricelist class Partner(RecordBase): @@ -281,6 +280,7 @@ class PartnerManager(RecordManagerBase[Partner]): # NOTE(callumdickinson): Import here to make sure circular imports work. from .company import Company # noqa: E402 from .customer_group import CustomerGroup # noqa: E402 +from .pricelist import Pricelist # noqa: E402 from .project import Project # noqa: E402 from .project_contact import ProjectContact # noqa: E402 from .referral_code import ReferralCode # noqa: E402 diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 154c999..471cd98 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -21,7 +21,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager_named import NamedRecordManagerBase -from .product import Product class Pricelist(RecordBase): @@ -119,3 +118,4 @@ def get_price( # NOTE(callumdickinson): Import here to make sure circular imports work. from .company import Company # noqa: E402 from .currency import Currency # noqa: E402 +from .product import Product # noqa: E402 diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index dbe5c61..e1f83aa 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -19,7 +19,6 @@ from ..base.record import ModelRef, RecordBase from ..base.record_manager import RecordManagerBase -from .company import Company class User(RecordBase): @@ -65,4 +64,5 @@ class UserManager(RecordManagerBase[User]): # NOTE(callumdickinson): Import here to make sure circular imports work. +from .company import Company # noqa: E402 from .partner import Partner # noqa: E402 From bceb74716b73911b2f1b5536d5b4fd637a825f19 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 11:40:44 +1200 Subject: [PATCH 37/87] Set type hint if get_type_origin returns None --- openstack_odooclient/base/client.py | 5 ++--- openstack_odooclient/base/record_manager.py | 2 +- openstack_odooclient/util.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py index f678e38..869019e 100644 --- a/openstack_odooclient/base/client.py +++ b/openstack_odooclient/base/client.py @@ -23,7 +23,7 @@ from odoorpc import ODOO # type: ignore[import] from packaging.version import Version -from typing_extensions import get_origin as get_type_origin, get_type_hints +from typing_extensions import get_type_hints from ..util import is_subclass from .record import RecordBase @@ -178,8 +178,7 @@ def __init__( RecordManagerBase, ] = {} # Create record managers defined in the type hints. - for attr_name, type_hint in get_type_hints(type(self)).items(): - attr_type = get_type_origin(type_hint) + for attr_name, attr_type in get_type_hints(type(self)).items(): if is_subclass(attr_type, RecordManagerBase): setattr(self, attr_name, attr_type(self)) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index b1b2873..592638c 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -733,7 +733,7 @@ def _encode_field(self, field: str) -> str: return self._get_remote_field(self._resolve_alias(field)) def _encode_value(self, type_hint: Any, value: Any) -> Any: - type_origin = get_type_origin(type_hint) + type_origin = get_type_origin(type_hint) or type_hint value_types = ( get_type_args(type_hint) if type_origin is Union else [type_origin] ) diff --git a/openstack_odooclient/util.py b/openstack_odooclient/util.py index 8c4bf02..43ebaea 100644 --- a/openstack_odooclient/util.py +++ b/openstack_odooclient/util.py @@ -110,7 +110,7 @@ def decode_value(type_hint: Type[T], value: Any) -> T: :rtype: T """ - value_type = get_type_origin(type_hint) + value_type = get_type_origin(type_hint) or type_hint # The basic data types that need special handling. if value_type is date: From 63953c1dad6c0a2a6c697c0b7b4a6bb35a233d7a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 11:55:12 +1200 Subject: [PATCH 38/87] Move decode_value into the RecordBase class --- openstack_odooclient/base/record.py | 68 +++++++++++++++++++++- openstack_odooclient/util.py | 87 +---------------------------- 2 files changed, 68 insertions(+), 87 deletions(-) diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index a61ffa5..d4a69de 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -18,7 +18,7 @@ import copy from dataclasses import dataclass -from datetime import datetime +from datetime import date, datetime, time from typing import ( TYPE_CHECKING, Any, @@ -40,7 +40,7 @@ get_type_hints, ) -from ..util import decode_value, is_subclass +from ..util import is_subclass if TYPE_CHECKING: from odoorpc import ODOO # type: ignore[import] @@ -335,7 +335,10 @@ def __getattr__(self, name: str) -> Any: ) # Base case: Decode the value according to the field's type hint, # cache the value, and return it. - self._values[name] = decode_value(type_hint, self._get_field(name)) + self._values[name] = self._decode_value( + type_hint, + self._get_field(name), + ) return self._values[name] def _getattr_model_ref( @@ -417,6 +420,65 @@ def _getattr_model_ref( ), ) + @classmethod + def _decode_value(cls, type_hint: Any, value: Any) -> Any: + value_type = get_type_origin(type_hint) or type_hint + # The basic data types that need special handling. + if value_type is date: + return date.fromisoformat(value) + if value_type is datetime: + return datetime.fromisoformat(value) + if value_type is time: + return time.fromisoformat(value) + # When a list is expected, decode each value individually + # and return the result as a new list with the same order. + if value_type is list: + return [ + cls._decode_value(get_type_args(type_hint)[0], v) + for v in value + ] + # When a dict is expected, decode the key and the value of each + # item separately, and combine the result into a new dict. + if value_type is dict: + k_type, v_type = get_type_args(type_hint) + return { + cls._decode_value(k_type, k): cls._decode_value(v_type, v) + for k, v in value.items() + } + # Basic case for handling specific union structures. + # Not suitable for handling complicated union structures. + # TODO(callumdickinson): Find a way to handle complicated + # union structures more smartly. + if value_type is Union: + attr_union_types = get_type_args(type_hint) + if len(attr_union_types) == 2: # noqa: PLR2004 + # Optional[T] + if type(None) in attr_union_types and value is not None: + return cls._decode_value( + next( + ( + t + for t in attr_union_types + if t is not type(None) + ), + ), + value, + ) + # Union[T, Literal[False]] + if Literal[False] in attr_union_types and value is not False: + return cls._decode_value( + next( + ( + t + for t in attr_union_types + if t is not Literal[False] + ), + ), + value, + ) + # Base case: Return the passed value unmodified. + return value + def __str__(self) -> str: return ( f"{type(self).__name__}(" diff --git a/openstack_odooclient/util.py b/openstack_odooclient/util.py index 43ebaea..5e8a654 100644 --- a/openstack_odooclient/util.py +++ b/openstack_odooclient/util.py @@ -15,24 +15,10 @@ from __future__ import annotations -from datetime import date, datetime -from typing import ( - Any, - Literal, - Mapping, - Optional, - Tuple, - Type, - TypeVar, - Union, -) - -from typing_extensions import ( - get_args as get_type_args, - get_origin as get_type_origin, -) +from typing import TYPE_CHECKING -T = TypeVar("T") +if TYPE_CHECKING: + from typing import Any, Mapping, Optional, Tuple, Type, Union # Same values as defined in odoo.tools.misc. DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" @@ -96,70 +82,3 @@ def is_subclass( return issubclass(type_obj, classes) except TypeError: return False - - -def decode_value(type_hint: Type[T], value: Any) -> T: - """Decode a raw Odoo JSON field value to its local representation, - based on the given type hint from the record class. - - :param type_hint: The type hint to use to decode the value - :type type_hint: Type[T] - :param value: The value to decode - :type value: Any - :return: The decoded value - :rtype: T - """ - - value_type = get_type_origin(type_hint) or type_hint - - # The basic data types that need special handling. - if value_type is date: - return date.fromisoformat(value) # type: ignore[return-value] - - if value_type is datetime: - return datetime.fromisoformat(value) # type: ignore[return-value] - - # When a list is expected, decode each value individually - # and return the result as a new list with the same order. - if value_type is list: - return [ # type: ignore[return-value] - decode_value(get_type_args(type_hint)[0], v) for v in value - ] - - # When a dict is expected, decode the key and the value of each - # item separately, and combine the result into a new dict. - if value_type is dict: - key_type, value_type = get_type_args(type_hint) - return { # type: ignore[return-value] - decode_value(key_type, k): decode_value(value_type, v) - for k, v in value.items() - } - - # Basic case for handling specific union structures. - # Not suitable for handling complicated union structures. - # TODO(callumdickinson): Find a way to handle complicated - # union structures more smartly. - if value_type is Union: - attr_union_types = get_type_args(type_hint) - if len(attr_union_types) == 2: # noqa: PLR2004 - # Optional[T] - if type(None) in attr_union_types and value is not None: - return decode_value( - next(t for t in attr_union_types if t is not type(None)), - value, - ) - # Union[T, Literal[False]] - if Literal[False] in attr_union_types and value is not False: - return decode_value( - next( - ( - t - for t in attr_union_types - if t is not Literal[False] - ), - ), - value, - ) - - # Base case: Return the passed value unmodified. - return value From 41909e6560e996d974e5687d9146a6c2959da726 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 12:11:01 +1200 Subject: [PATCH 39/87] Add docs for support subscriptions --- docs/managers/support-subscription.md | 176 ++++++++++++++++++ .../managers/support_subscription.py | 2 +- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/docs/managers/support-subscription.md b/docs/managers/support-subscription.md index e69de29..be218ef 100644 --- a/docs/managers/support-subscription.md +++ b/docs/managers/support-subscription.md @@ -0,0 +1,176 @@ +# OpenStack Support Subscriptions + +This page documents how to use the manager and record objects +for support subscriptions. + +## Details + +| Name | Value | +|-----------------|----------------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.support_subscription` | +| Manager | `support_subscriptions` | +| Record Type | `SupportSubscription` | + +## Manager + +The support subscription manager is available as the `support_subscription` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.support_subscriptions.get(1234) +SupportSubscription(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The support subscription manager returns `SupportSubscription` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import SupportSubscription +``` + +The record class currently implements the following fields and methods. + +### `billing_type` + +```python +billing_type: Literal["paid", "complimentary"] +``` + +The method of billing for the support subscription. + +Values: + +* ``paid`` - Charge the subscription independently +* ``complimentary`` - Bundled with a contract that includes the charge + + +### `end_date` + +```python +end_date: date +``` + +The end date of the credit. + +### `partner_id` + +```python +partner_id: int | None +``` + +The ID for the [partner](partner.md) linked to this support subscription, +if it is linked to a partner. + +Support subscriptions linked to a partner +cover all projects the partner owns. + + +### `partner_name` + +```python +partner_name: str | None +``` + +The name of the [partner](partner.md) linked to this support subscription, +if it is linked to a partner. + +Support subscriptions linked to a partner +cover all projects the partner owns. + + +### `partner` + +```python +partner: Partner | None +``` + +The [partner](partner.md) linked to this support subscription, +if it is linked to a partner. + +Support subscriptions linked to a partner +cover all projects the partner owns. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `project_id` + +```python +project_id: int | None +``` + +The ID of the [project](project.md) this support subscription is for, +if it is linked to a specific project. + + +### `project_name` + +```python +project_name: str | None +``` + +The name of the [project](project.md) this support subscription is for, +if it is linked to a specific project. + +### `project` + +```python +project: Project | None +``` + +The [project](project.md) this support subscription is for, +if it is linked to a specific project. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + + +### `start_date` + +```python +start_date: date +``` + +The start date of the credit. + +### `support_subscription_type_id` + +```python +support_subscription_type_id: int +``` + +The ID of the [type](support-subscription-type.md) of the support subscription. + +### `support_subscription_type_name` + +```python +support_subscription_type_name: str +``` + +The name of the [type](support-subscription-type.md) of the support subscription. + +### `support_subscription_type` + +```python +support_subscription_type: SupportSubscriptionType +``` + +The [type](support-subscription-type.md) of the support subscription. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index c3fd5a5..3c4506c 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -46,7 +46,7 @@ class SupportSubscription(RecordBase): """ partner_name: Annotated[Optional[str], ModelRef("partner", Partner)] - """The name of thepartner linked to this support subscription, + """The name of the partner linked to this support subscription, if it is linked to a partner. Support subscriptions linked to a partner From 8b679d8efd30ebb572ee4234b3579d7007a5ecaf Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 13:03:49 +1200 Subject: [PATCH 40/87] Add docs for support subscription types and taxes --- docs/managers/support-subscription-type.md | 132 ++++++++++++++ docs/managers/support-subscription.md | 6 +- docs/managers/tax.md | 195 +++++++++++++++++++++ openstack_odooclient/managers/tax.py | 5 +- 4 files changed, 331 insertions(+), 7 deletions(-) diff --git a/docs/managers/support-subscription-type.md b/docs/managers/support-subscription-type.md index e69de29..ff88bd6 100644 --- a/docs/managers/support-subscription-type.md +++ b/docs/managers/support-subscription-type.md @@ -0,0 +1,132 @@ +# OpenStack Support Subscription Types + +This page documents how to use the manager and record objects +for support subscription types. + +## Details + +| Name | Value | +|-----------------|---------------------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.support_subscription.type` | +| Manager | `support_subscription_types` | +| Record Type | `SupportSubscriptionType` | + +## Manager + +The support subscription type manager is available as the `support_subscription_types` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.support_subscription_types.get(1234) +SupportSubscriptionType(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The support subscription type manager returns `SupportSubscriptionType` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import SupportSubscriptionType +``` + +The record class currently implements the following fields and methods. + +### `billing_type` + +```python +billing_type: Literal["paid", "complimentary"] +``` + +The type of support subscription. + +Values: + +* ``paid`` - Charge the subscription independently +* ``complimentary`` - Bundled with a contract that includes the charge + +### `name` + +```python +name: str +``` +The name of the support subscription type. + +### `product_id` + +```python +product_id: int +``` + +The ID for the [product](product.md) to use to invoice +the support subscription. + +### `product_name` + +```python +product_name: str +``` + +The name of the [product](product.md) to use to invoice +the support subscription. + + +### `product` + +```python +product: Product +``` + +The [product](product.md) to use to invoice +the support subscription. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `usage_percent` + +```python +usage_percent: float +``` + +Percentage of usage compared to price (0-100). + +### `support_subscription_ids` + +```python +support_subscription_ids: list[int] +``` + +A list of IDs for the [support subscriptions](support-subscription.md) of this type. + +### `support_subscription` + +```python +support_subscription: list[SupportSubscription] +``` + +The list of [support subscriptions](support-subscription.md) of this type. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. + +### `support_subscriptions` + +```python +support_subscriptions: list[SupportSubscription] +``` + +An alias for [``support_subscription``](#support_subscription). diff --git a/docs/managers/support-subscription.md b/docs/managers/support-subscription.md index be218ef..d520224 100644 --- a/docs/managers/support-subscription.md +++ b/docs/managers/support-subscription.md @@ -14,7 +14,7 @@ for support subscriptions. ## Manager -The support subscription manager is available as the `support_subscription` +The support subscription manager is available as the `support_subscriptions` attribute on the Odoo client object. ```python @@ -79,7 +79,6 @@ if it is linked to a partner. Support subscriptions linked to a partner cover all projects the partner owns. - ### `partner_name` ```python @@ -92,7 +91,6 @@ if it is linked to a partner. Support subscriptions linked to a partner cover all projects the partner owns. - ### `partner` ```python @@ -117,7 +115,6 @@ project_id: int | None The ID of the [project](project.md) this support subscription is for, if it is linked to a specific project. - ### `project_name` ```python @@ -139,7 +136,6 @@ if it is linked to a specific project. This fetches the full record from Odoo once, and caches it for subsequent accesses. - ### `start_date` ```python diff --git a/docs/managers/tax.md b/docs/managers/tax.md index e69de29..430d411 100644 --- a/docs/managers/tax.md +++ b/docs/managers/tax.md @@ -0,0 +1,195 @@ +# Taxes + +This page documents how to use the manager and record objects +for taxes. + +## Details + +| Name | Value | +|-----------------|---------------| +| Odoo Modules | Accounting | +| Odoo Model Name | `account.tax` | +| Manager | `taxes` | +| Record Type | `Tax` | + +## Manager + +The tax manager is available as the `taxes` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.taxes.get(1234) +Tax(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The tax manager returns `Tax` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Tax +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this tax is active (enabled). + +### `amount` + +```python +amount: float +``` + +The amount of tax to apply. + +### `amount_type` + +```python +amount_type: Literal["group", "fixed", "percent", "division"] +``` + +The method that should be used to tax invoices. + +Values: + +* ``group`` - Group of Taxes +* ``fixed`` - Fixed +* ``percent`` - Percentage of Price +* ``division`` - Percentage of Price Tax Included + +### `analytic` + +```python +analytic: bool +``` + +When set to ``True``, the amount computed by this tax will be assigned +to the same analytic account as the invoice line (if any). + + +### `company_id` + +```python +company_id: int +``` + +The ID for the [company](company.md) this tax is owned by. + +### `company_name` + +```python +company_name: str +``` + +The name of the [company](company.md) this tax is owned by. + +### `company` + +```python +company: Company +``` + +The [company](company.md) this tax is owned by. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `country_code` + +```python +country_code: str +``` + +The country code for this tax. + +### `description` + +```python +description: str +``` + +The label for this tax on invoices. + +### `include_base_amount` + +```python +include_base_amount: bool +``` + +When set to ``True``, taxes included after this one will be calculated +based on the price with this tax included. + +### `name` + +```python +name: str +``` + +Name of the tax. + +### `price_include` + +```python +price_include: bool +``` + +Whether or not prices included in invoices should include this tax. + +### `tax_eligibility` + +```python +tax_eligibility: Literal["on_invoice", "on_payment"] +``` + +When the tax is due for the invoice. + +Values: + +* ``on_invoice`` - Due as soon as the invoice is validated +* ``on_payment`` - Due as soon as payment of the invoice is received + +### `tax_group_id` + +```python +tax_group_id: int +``` + +The ID for the [tax group](tax-group.md) this tax is categorised under. + +### `tax_group_name` + +```python +tax_group_name: str +``` + +The name of the [tax group](tax-group.md) this tax is categorised under. + +### `tax_group` + +```python +tax_group: TaxGroup +``` + +The [tax group](tax-group.md) this tax is categorised under. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 302353f..2e050f6 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -31,7 +31,8 @@ class Tax(RecordBase): """The amount of tax to apply.""" amount_type: Literal["group", "fixed", "percent", "division"] - """ + """The method that should be used to tax invoices. + Values: * ``group`` - Group of Taxes @@ -70,7 +71,7 @@ class Tax(RecordBase): """ name: str - """Tax name.""" + """Name of the tax.""" price_include: bool """Whether or not prices included in invoices should include this tax.""" From adccfb3da3f713d3c5431119e76a7abc40403eb9 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 18 Jun 2024 18:53:00 +1200 Subject: [PATCH 41/87] Add more docs for more managers --- docs/managers/tax-group.md | 54 ++++++ docs/managers/term-discount.md | 184 +++++++++++++++++++++ docs/managers/trial.md | 114 +++++++++++++ docs/managers/uom.md | 147 ++++++++++++++++ openstack_odooclient/managers/tax_group.py | 2 +- 5 files changed, 500 insertions(+), 1 deletion(-) diff --git a/docs/managers/tax-group.md b/docs/managers/tax-group.md index e69de29..8603680 100644 --- a/docs/managers/tax-group.md +++ b/docs/managers/tax-group.md @@ -0,0 +1,54 @@ +# Tax Groups + +This page documents how to use the manager and record objects +for tax groups. + +## Details + +| Name | Value | +|-----------------|---------------------| +| Odoo Modules | Accounting | +| Odoo Model Name | `account.tax.group` | +| Manager | `tax_groups` | +| Record Type | `TaxGroup` | + +## Manager + +The tax group manager is available as the `tax_groups` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.tax_groups.get(1234) +TaxGroup(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The tax group manager returns `TaxGroup` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import TaxGroup +``` + +The record class currently implements the following fields and methods. + +### `name` + +```python +name: str +``` + +Name of the tax group. diff --git a/docs/managers/term-discount.md b/docs/managers/term-discount.md index e69de29..ab415d4 100644 --- a/docs/managers/term-discount.md +++ b/docs/managers/term-discount.md @@ -0,0 +1,184 @@ +# OpenStack Term Discounts + +This page documents how to use the manager and record objects +for term discounts. + +## Details + +| Name | Value | +|-----------------|---------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.term_discount` | +| Manager | `term_discounts` | +| Record Type | `TermDiscount` | + +## Manager + +The term discount manager is available as the `term_discounts` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.term_discounts.get(1234) +TermDiscount(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The term discount manager returns `TermDiscount` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import TermDiscount +``` + +The record class currently implements the following fields and methods. + +### `discount_percent` + +```python +discount_percent: float +``` + +The maximum discount percentage for this term discount (0-100). + +### `early_termination_date` + +```python +early_termination_date: date | None +``` + +An optional early termination date for the term discount. + +### `end_date` + +```python +end_date: date +``` + +The date that the term discount expires on. + +### `min_commit` + +```python +min_commit: float +``` + +The minimum commitment for this term discount to apply. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the [partner](partner.md) that receives this term discount. + +### `partner_name` + +```python +partner_name: str +``` + +The name of the [partner](partner.md) that receives this term discount. + +### `partner` + +```python +partner: Partner +``` + +The [partner](partner.md) that receives this term discount. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `project_id` + +```python +project_id: int | None +``` + +The ID for the [project](project.md) this term discount applies to, +if it is a project-specific term discount. + +If not set, the term discount applies to all projects +the partner owns. + +### `project_name` + +```python +project_name: str | None +``` + +The name of the [project](project.md) this term discount applies to, +if it is a project-specific term discount. + +If not set, the term discount applies to all projects +the partner owns. + +### `project` + +```python +project: Project | None +``` + +The [project](project.md) this term discount applies to, +if it is a project-specific term discount. + +If not set, the term discount applies to all projects +the partner owns. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `start_date` + +```python +start_date: date +``` + +The date from which this term discount starts. + +### `superseded_by_id` + +```python +superseded_by_id: int | None +``` + +The ID for the term discount that supersedes this one, +if superseded. + + +### `superseded_by_name` + +```python +superseded_by_name: str | None +``` + +The name of the term discount that supersedes this one, +if superseded. + + +### `superseded_by` + +```python +superseded_by: TermDiscount | None +``` + +The term discount that supersedes this one, +if superseded. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/trial.md b/docs/managers/trial.md index e69de29..df89550 100644 --- a/docs/managers/trial.md +++ b/docs/managers/trial.md @@ -0,0 +1,114 @@ +# OpenStack Trials + +This page documents how to use the manager and record objects +for trials. + +## Details + +| Name | Value | +|-----------------|-----------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.trial` | +| Manager | `trials` | +| Record Type | `Trial` | + +## Manager + +The trial manager is available as the `trials` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.trials.get(1234) +Trial(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The trial manager returns `Trial` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Trial +``` + +The record class currently implements the following fields and methods. + +### `account_suspended_date` + +```python +account_suspended_date: date | Literal[False] +``` + +The date the account was suspended, following the end of the trial. + +### `account_terminated_date` + +```python +account_terminated_date: date | Literal[False] +``` + +The date the account was terminated, following the end of the trial. + +### `account_upgraded_date` + +```python +account_upgraded_date: date | Literal[False] +``` + +The date the account was upgraded to a full account, +following the end of the trial. + +### `end_date` + +```python +end_date: date +``` + +The end date of this trial. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the target [partner](partner.md) for this trial. + +### `partner_name` + +```python +partner_name: str +``` + +The name of the target [partner](partner.md) for this trial. + +### `partner` + +```python +partner: Partner +``` + +The target [partner](partner.md) for this trial. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `start_date` + +```python +start_date: date +``` + +The start date of this trial. diff --git a/docs/managers/uom.md b/docs/managers/uom.md index e69de29..8d7bd74 100644 --- a/docs/managers/uom.md +++ b/docs/managers/uom.md @@ -0,0 +1,147 @@ +# Units of Measure (UoM) + +This page documents how to use the manager and record objects +for Units of Measure (UoM). + +## Details + +| Name | Value | +|-----------------|------------------------| +| Odoo Modules | Units of Measure (UoM) | +| Odoo Model Name | `uom.uom` | +| Manager | `uoms` | +| Record Type | `Uom` | + +## Manager + +The Unit of Measure (UoM) manager is available as the `uoms` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.uoms.get(1234) +Uom(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The Unit of Measure (UoM) manager returns `Uom` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import Uom +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this Unit of Measure is active (enabled). + +### `category_id` + +```python +category_id: int +``` + +The ID for the [category](uom-category.md) this Unit of Measure is classified as. + +### `category_name` + +```python +category_name: str +``` + +The name of the [category](uom-category.md) this Unit of Measure is classified as. + +### `category` + +```python +category: UomCategory +``` + +The [category](uom-category.md) this Unit of Measure is classified as. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `factor` + +```python +factor: float +``` + +How much bigger or smaller this unit is compared to the reference +Unit of Measure (UoM) for the classified category. + +### `factor_inv` + +```python +factor_inv: float +``` + +How many times this Unit of Measure is bigger than the reference +Unit of Measure (UoM) for the classified category. + +### `measure_type` + +```python +measure_type: Literal[ + "unit", + "weight", + "working_time", + "length", + "volume", +] +``` + +The type of category this Unit of Measure (UoM) is classified as. + +This field no longer exists from Odoo 14 onwards. + +Values: + +* ``unit`` - Default Units +* ``weight`` - Default Weight +* ``working_time`` - Default Working Time +* ``length`` - Default Length +* ``volume`` - Default Volume + +### `name` + +```python +name: str +``` + +Unit of Measure (UoM) name. + +### `uom_type` + +```python +uom_type: Literal["bigger", "reference", "smaller"] +``` + +The type of the Unit of Measure (UoM). + +This determines its relationship with other UoMs in the same category. + +Values: + +* ``bigger`` - Bigger than the reference Unit of Measure +* ``reference`` - Reference Unit of Measure for the selected category +* ``smaller`` - Smaller than the reference Unit of Measure diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py index 9c893b9..afe369f 100644 --- a/openstack_odooclient/managers/tax_group.py +++ b/openstack_odooclient/managers/tax_group.py @@ -21,7 +21,7 @@ class TaxGroup(RecordBase): name: str - """Tax group name.""" + """Name of the tax group.""" class TaxGroupManager(NamedRecordManagerBase[TaxGroup]): From d2a6610a17584df7b9c4c8909879698f8e84df2c Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 19 Jun 2024 13:02:15 +1200 Subject: [PATCH 42/87] Finish record/manager docs --- .gitignore | 3 + README.md | 46 +- docs/index.md | 135 ++++ docs/managers/credit.md | 6 +- docs/managers/custom.md | 753 +++++++++++++++++- docs/managers/grant.md | 6 +- docs/managers/index.md | 256 ++++-- docs/managers/pricelist.md | 12 +- docs/managers/product.md | 18 +- docs/managers/sale-order.md | 8 +- docs/managers/uom-category.md | 78 ++ docs/managers/uom.md | 12 +- docs/managers/user.md | 124 +++ docs/managers/volume-discount-range.md | 192 +++++ docs/managers/voucher-code.md | 276 +++++++ mkdocs.yml | 58 ++ openstack_odooclient/base/record.py | 8 +- openstack_odooclient/managers/uom_category.py | 2 +- openstack_odooclient/managers/user.py | 2 +- .../managers/volume_discount_range.py | 13 +- openstack_odooclient/managers/voucher_code.py | 32 +- pdm.lock | 683 +++++++++++++++- pyproject.toml | 3 + 23 files changed, 2577 insertions(+), 149 deletions(-) create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index 82f9275..6ce4859 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# VS Code +.vscode diff --git a/README.md b/README.md index 899926b..e00dceb 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ changes between Odoo versions. ## Installation -To install the library package, simply install `openstack-odooclient` using `pip`. +The Odoo Client library supports Python 3.8 and later. -```python +To install the library package, simply install the `openstack-odooclient` package using `pip`. + +```bash python -m pip install openstack-odooclient ``` @@ -100,41 +102,8 @@ For example, performing a simple search query would look something like this: [1234] ``` -### Available Managers - -* `account_moves` - Account Moves (Invoices) (Odoo Model: `account.move`) -* `account_move_lines` - Account Move (Invoice) Lines (Odoo Model: `account.move.line`) -* `companies` - Companies (Odoo Model: `res.company`) -* `credits` - OpenStack Credits (Odoo Model: `openstack.credit`) -* `credit_transactions` - OpenStack Credit Transactions (Odoo Model: `openstack.credit.transaction`) -* `credit_types` - OpenStack Credit Types (Odoo Model: `openstack.credit.type`) -* `currencies` - Currencies (Odoo Model: `res.currency`) -* `customer_groups` - OpenStack Customer Groups (Odoo Model: `openstack.customer_group`) -* `grants` - OpenStack Grants (Odoo Model: `openstack.grant`) -* `grant_types` - OpenStack Grant Types (Odoo Model: `openstack.grant.type`) -* `partners` - Partners (Odoo Model: `res.partner`) -* `partner_categories` - Partner Categories (Odoo Model: `res.partner.category`) -* `pricelists` - Pricelists (Odoo Model: `product.pricelist`) -* `products` - Products (Odoo Model: `product.product`) -* `product_categories` - Product Categories (Odoo Model: `product.category`) -* `projects` - OpenStack Projects (Odoo Model: `openstack.project`) -* `project_contacts` - OpenStack Project Contacts (Odoo Model: `openstack.project_contact`) -* `referral_codes` - OpenStack Referral Codes (Odoo Model: `openstack.referral_code`) -* `resellers` - OpenStack Resellers (Odoo Model: `openstack.reseller`) -* `reseller_tiers` - OpenStack Reseller Tiers (Odoo Model: `openstack.reseller.tier`) -* `sale_orders` - Sale Orders (Odoo Model: `sale.order`) -* `sale_order_lines` - Sale Order Lines (Odoo Model: `sale.order.line`) -* `support_subscriptions` - OpenStack Support Subscriptions (Odoo Model: `openstack.support_subscription`) -* `support_subscription_types` - OpenStack Support Subscription Types (Odoo Model: `openstack.support_subscription.type`) -* `taxes` - Taxes (Odoo Model: `account.tax`) -* `tax_groups` - Tax Groups (Odoo Model: `account.tax.group`) -* `term_discounts` - OpenStack Term Discounts (Odoo Model: `openstack.term_discount`) -* `trials` - OpenStack Trials (Odoo Model: `openstack.trial`) -* `uoms` - Units of Measure (UoM) (Odoo Model: `uom.uom`) -* `uom_category` - Unit of Measure (UoM) Categories (Odoo Model: `uom.category`) -* `users` - Users (Odoo Model: `res.user`) -* `volume_discount_ranges` - OpenStack Volume Discount Ranges (Odoo Model: `openstack.volume_discount_range`) -* `voucher_codes` - OpenStack Voucher Codes (Odoo Model: `openstack.voucher_code`) +For more information on the available managers and their functions, +check the [Managers](docs/managers/index.md) page in the documentation. ## Records @@ -163,3 +132,6 @@ User(record={'id': 1234, ...}, fields=None) >>> user.id 1234 ``` + +For more information on the available managers and their functions, +check the [Records](docs/managers/index.md#records) section in the documentation. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..29ab427 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,135 @@ +# OpenStack Odoo Client Library + +This is an Odoo client library for Python with support for the +[OpenStack Integration add-on](https://github.com/catalyst-cloud/odoo-openstack-integration), +intended to be used by OpenStack projects such as +[Distil](https://github.com/catalyst-cloud/distil). + +This library provides a higher level interface than [OdooRPC](https://pythonhosted.org/OdooRPC) +(which is used internally), and is intended to make it possible to develop applications against +a common API, without having to take into account considerations such as backward-incompatible +changes between Odoo versions. + +## Installation + +The Odoo Client library supports Python 3.8 and later. + +To install the library package, simply install the `openstack-odooclient` package using `pip`. + +```bash +python -m pip install openstack-odooclient +``` + +## Connecting to Odoo + +To connect to an Odoo server, create an `openstack_odooclient.Client` object and +pass the connection details to it. + +```python +openstack_odooclient.Client( + *, + hostname: str, + database: str, + username: str, + password: str, + protocol: str = "jsonrpc", + port: int = 8069, + verify: bool | Path | str = True, + version: str | None = None, +) -> Client +``` + +This is the recommended way of creating the Odoo client object, +as it provides some extra parameters for convenience. + +```python +from openstack_odooclient import Client as OdooClient + +odoo_client = OdooClient( + hostname="localhost", + database="odoodb", + user="test-user", + password="", + protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. + port=8069, + # verify=True, # Enable/disable SSL verification, or pass the path to a CA certificate. + # version="14.0", # Optionally specify the server version. Default is to auto-detect. +) +``` + +If you have a pre-existing `odoorpc.ODOO` connection object, that can instead +be passed directly into `openstack_odooclient.Client`. + +```python +openstack_odooclient.Client(*, odoo: odoorpc.ODOO) -> Client +``` + +This allows for sharing a single OdooRPC connection object with other code. + +```python +from odoorpc import ODOO +from openstack_odooclient import Client as OdooClient + +odoo = ODOO( + host="localhost", + port=8069, + protocol="jsonrpc", # HTTP, or "jsonrpc+ssl" for HTTPS. + # version="14.0", # Optionally specify the server version. Default is to auto-detect. +) +odoo.login("odoodb", "test-user", "") + +odoo_client = OdooClient(odoo=odoo) +``` + +## Managers + +The Odoo Client object exposes a number of record managers, which contain methods +used to query specific record types, or create one or more new records of that type. + +For example, performing a simple search query would look something like this: + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search([("id", "=", odoo_client.user_id)], as_id=True) +[1234] +``` + +For more information on the available managers and their functions, see [Managers](managers/index.md). + +## Records + +Record manager methods return record objects for the corresponding model +in Odoo. + +Record fields can be accessed as attributes on these record objects. +The record classes are fully type hinted, allowing IDEs and validation +tools such as Mypy to verify that your application is using the fields +correctly. + +```python +>>> from openstack_odooclient import Client as OdooClient, User +>>> user: User | None = None +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.id +1234 +``` + +For more information on record objects, see [Records](managers/index.md#records). diff --git a/docs/managers/credit.md b/docs/managers/credit.md index 6bcd512..e1bd141 100644 --- a/docs/managers/credit.md +++ b/docs/managers/credit.md @@ -51,7 +51,7 @@ The record class currently implements the following fields and methods. credit_type_id: int ``` -The ID of the [type of this credit](#credit-type.md). +The ID of the [type of this credit](credit-type.md). ### `credit_type_name` @@ -59,7 +59,7 @@ The ID of the [type of this credit](#credit-type.md). credit_type_name: str ``` -The name of the [type of this credit](#credit-type.md). +The name of the [type of this credit](credit-type.md). ### `credit_type` @@ -67,7 +67,7 @@ The name of the [type of this credit](#credit-type.md). credit_type: CreditType ``` -The [type of this credit](#credit-type.md). +The [type of this credit](credit-type.md). This fetches the full record from Odoo once, and caches it for subsequent accesses. diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 89b6c7f..1074ad9 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -1,3 +1,754 @@ # Custom Managers and Record Types -TODO(callumdickinson): Write this page. +The OpenStack Odoo Client library supports defining new record types and +adding managers for them to the Odoo client object, allowing for adding +support for custom Odoo add-ons. + +!!! note + + When defining custom record types and managers, it is **highly recommended** + to enable postponed evaluation of annotations by adding the following future import + to the top of your Python source files: + + ```python + from __future__ import annotations + ``` + + If this is not enabled, circular imports are not supported, and you may encounter + issues with undefined type hint objects. + +## Records + +Odoo records are represented by **record classes** in the Odoo Client library. + +Record classes are implementations (subclasses) of the `RecordBase` class, +which the record manager uses to create immutable objects for the record model. + +Record fields are defined as type hints on the record class. +These type hints are parsed by the Odoo Client library, +and field values from the API are automatically coerced to the +correct types when referenced by applications. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" +``` + +### Field Types + +The following basic field types from Odoo are supported. + +#### `bool` + +Corresponds to the `Boolean` field type in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: bool + """Description of the field.""" +``` + +#### `int` + +Corresponds to the `Integer` field type in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: int + """Description of the field.""" +``` + +#### `str` + +Corresponds to the `Char` field type in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" +``` + +#### `float` + +Corresponds to the `Float` field type in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: int + """Description of the field.""" +``` + +#### `date` + +Corresponds to the `Date` field type in Odoo. + +```python +from __future__ import annotations + +from datetime import date + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: date + """Description of the field.""" +``` + +#### `datetime` + +Corresponds to the `DateTime` field type in Odoo. + +```python +from __future__ import annotations + +from datetime import datetime + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: date + """Description of the field.""" +``` + +#### `time` + +Corresponds to the `Time` field type in Odoo. + +```python +from __future__ import annotations + +from datetime import time + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: time + """Description of the field.""" +``` + +### Optional Fields + +Any supported field type can be made optional. Two types of optional fields +are supported, depending on what Odoo returns when a field is not set. + +#### `False` + +The most common case is that `False` is returned, in which case +the type hint should be defined as shown below. + +```python +from __future__ import annotations + +from typing import Literal, Union + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: Union[str, Literal[False]] + """Description of the field.""" +``` + +#### `None` + +If the default value is `None` when a field is not set, +the type hint should be defined as shown below instead. + +```python +from __future__ import annotations + +from typing import Optional + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: Optional[str] + """Description of the field.""" +``` + +### Field Aliases + +Aliases to other fields can be defined by adding the `FieldAlias` +annotation to the field type hint. Aliases can be made for all supported +field types, including [model refs](#model-refs). + +Field aliases are resolved internally to fetch the value for the target field. +They can also be used instead of the target field name when +[defining record search filters](index.md#search) and [creating new records](index.md#create). + +While not required, it is **highly** recommended that the target field +also have a type hint defined for it on the model class. +The type of the alias **must** match the type of the target field. + +```python +from __future__ import annotations + +from openstack_odooclient import FieldAlias, RecordBase +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + + field_alias: Annotated[str, FieldAlias("custom_field")] + """Alias for ``custom_field``.""" +``` + +### Model Refs + +Records in Odoo can reference other records to establish relationships +between them, allowing for easier querying and management of multiple record types. + +The Odoo Client library supports a higher level interface for managing these +relationships using the `ModelRef` type hint annotation. + +The `ModelRef` annotation takes two arguments: the name of the model ref +field in Odoo, and the record class that implements the model in the Odoo Client library. + +There are two types of model refs that can be expressed on record classes: +**singular records**, and **record lists**. + +#### Singular Record (One2one/Many2one) + +Singular record model refs correspond to the `One2one` and `Many2one` relationship types in Odoo. +With these relationship types, the model class references a single record. + +Suppose that we want to add a model ref for a `user_id` field to our record class, +which references the `res.users` model +(which is implemented as the [`User`](user.md#record) record class). + +There are three ways to reference fields of this type on the record class. +All of these should all be defined on your record class. + +The first is to expose the record's ID directly as an integer. + +```python +from __future__ import annotations + +from openstack_odooclient import ModelRef, RecordBase, User +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + user_id: Annotated[int, ModelRef("user_id", User)] + """ID for the user that owns this record.""" +``` + +The second is to expose the target record's display name as a string. + +```python +from __future__ import annotations + +from openstack_odooclient import ModelRef, RecordBase, User +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + user_name: Annotated[str, ModelRef("user_id", User)] + """Name of the user that owns this record.""" +``` + +The third and final one is to define the target record itself as a record object. + +```python +from __future__ import annotations + +from openstack_odooclient import ModelRef, RecordBase, User +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + user: Annotated[User, ModelRef("user_id", User)] + """The user that owns this record. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ +``` + +The target record ID and display name are always available on the containing record object, +with no query required to retrieve their values. + +For performance reasons, the target record as an object is not fetched automatically. +Instead, it is fetched from Odoo on demand when referenced for the first time (lazy loading). +The resulting record object is then cached to ensure the same object is returned every time. + +The final result should be the following fields defined on your record class. + +```python +from __future__ import annotations + +from openstack_odooclient import ModelRef, RecordBase, User +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + user_id: Annotated[int, ModelRef("user_id", User)] + """ID for the user that owns this record.""" + + user_name: Annotated[str, ModelRef("user_id", User)] + """Name of the user that owns this record.""" + + user: Annotated[User, ModelRef("user_id", User)] + """The user that owns this record. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ +``` + +Similar to field aliases, any of these model ref fields can be used +instead of the actual model ref field name when +[defining record search filters](index.md#search) and [creating new records](index.md#create). +The record ID or the record object can be passed directly to those methods +(the record display name is not guaranteed to be unique, and thus, not accepted). + +Record references can be made optional by encasing the type hint with `Optional`. +If the record reference is not set, `None` will be returned. + +```python +from __future__ import annotations + +from typing import Optional + +from openstack_odooclient import ModelRef, RecordBase, User +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + user_id: Annotated[Optional[int], ModelRef("user_id", User)] + """ID for the user that owns this record.""" + + user_name: Annotated[Optional[str], ModelRef("user_id", User)] + """Name of the user that owns this record.""" + + user: Annotated[Optional[User], ModelRef("user_id", User)] + """The user that owns this record. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ +``` + +Recursive model refs are also supported using the `Self` type hint. +In the below example, `Self` resolves to `CustomRecord`. + +```python +from __future__ import annotations + +from typing import Optional + +from openstack_odooclient import ModelRef, RecordBase +from typing_extensions import Annotated, Self + +class CustomRecord(RecordBase): + record_id: Annotated[Optional[int], ModelRef("user_id", Self)] + """ID for the record related to this one, if set..""" + + record_name: Annotated[Optional[str], ModelRef("user_id", Self)] + """Name of the record related to this one, if set.""" + + record: Annotated[Optional[Self], ModelRef("user_id", Self)] + """The record related to this one, if set. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ +``` + +#### Record Lists (One2many/Many2many) + +Record list model refs correspond to the `One2many` and `Many2Many` relationship types in Odoo. +With these relationship types, the model class references multiple records in a list structure. + +Suppose that we want to add a model ref for a `product_id` list field to our record class, +which references the `product.product` model +(which is implemented as the [`Product`](product.md#record) record class). + +There are two ways to reference fields of this type on the record class. +All of these should all be defined on your record class. + +The first is to expose the record's ID directly as an integer. + +```python +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase, Product +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + product_ids: Annotated[List[int], ModelRef("product_id", Product)] + """The list of IDs for the products to use.""" +``` + +The second and final one is to define the field as a list of record objects. + +```python +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase, Product +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + products: Annotated[List[Product], ModelRef("product_id", Product)] + """The list of products to use. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ +``` + +The target record IDs are always available on the containing record object, +with no query required to retrieve their values. + +For performance reasons, the target record objects are not fetched automatically. +Instead, they fetched from Odoo on demand when referenced for the first time (lazy loading). +The resulting record objects are then cached to ensure the same objects are returned every time. + +The final result should be the following fields defined on your record class. + +```python +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase, Product +from typing_extensions import Annotated + +class CustomRecord(RecordBase): + product_ids: Annotated[List[int], ModelRef("product_id", Product)] + """The list of IDs for the products to use.""" + + products: Annotated[List[Product], ModelRef("product_id", Product)] + """The list of products to use. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ +``` + +Similar to field aliases, any of these model ref fields can be used +instead of the actual model ref field name when +[defining record search filters](index.md#search) and [creating new records](index.md#create). +The passed values (or lists of values) may consist of either record IDs, record objects, +or any combination of the two. + +Recursive list model refs are also supported using the `Self` type hint. +In the below example, `Self` resolves to `CustomRecord`. + +```python +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase +from typing_extensions import Annotated, Self + +class CustomRecord(RecordBase): + child_ids: Annotated[List[int], ModelRef("child_id", Self)] + """The list of IDs for the child records.""" + + children: Annotated[List[Self], ModelRef("child_id", Self)] + """The list of child records. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ +``` + +#### Circular Model Refs + +Due to the way type hints are dereferenced by Python, two record classes +that reference each other are difficult (but not impossible) to support. + +Generally two things must be done for circular model refs to work: + +* Postponed evaluation of annotations is enabled using `from __future__ import annotations` +* Imports of the referenced model classes must be defined *after* the model classes + are defined in each source file + +Below is an example of two record classes that correctly reference each other. + +```python title="parent.py" +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase +from typing_extensions import Annotated + +class Parent(RecordBase): + child_ids: Annotated[List[int], ModelRef("child_id", Child)] + """The list of IDs for the children records.""" + + children: Annotated[List[Parent], ModelRef("child_id", Child)] + """The list of children records. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + +from .child import Child # noqa: E402 +``` + +```python title="child.py" +from __future__ import annotations + +from typing import Optional + +from openstack_odooclient import ModelRef, RecordBase +from typing_extensions import Annotated + +class Child(RecordBase): + parent_id: Annotated[Optional[int], ModelRef("parent_id", Parent)] + """ID for the parent record, if it has one.""" + + parent_name: Annotated[Optional[str], ModelRef("parent_id", Parent)] + """Name of the parent record, if it has one.""" + + parent: Annotated[Optional[Parent], ModelRef("parent_id", Parent)] + """The parent record, if it has one. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + +from .parent import Parent # noqa: E402 +``` + +### Record Methods + +Methods can be defined on record types to provide additional functionality. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + + def custom_field_is_defined(self) -> bool: + return bool(self.custom_field) +``` + +In addition to all of the Odoo fields defined on the record class, +the following internal attributes are also available for use in object methods: + +* `_client` - The Odoo Client object the record was created from +* `_manager` - The manager object the record was created from +* `_records` - The raw record fields from OdooRPC (as a dictionary) +* `_fields` - The fields that were selected during the query (or `None` for all fields) +* `_odoo` - The OdooRPC connection object +* `_env` - The OdooRPC environment object for the model + +!!! note + + Record objects are intended to be immutable. Custom methods should not change + the internal state, or the fields, of the record object. + +## Managers + +**Manager classes** are used to provide query methods and other functionality +neccessary for managing record objects in the Odoo Client library. + +Once you have defined your record class, a manager class must be created +for implementing the query methods for the record class. + +### Creating a Manager Class + +Manager classes are subclasses of the generic `ManagerBase` class, +specifying the record class the generic type argument, +and defining the following class attributes: + +* `env_name` - The name of the Odoo environment (database model) for the record class +* `record_class` - The record class object + +Below is a simple example of a custom record type and its manager class. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import ManagerBase, RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(ManagerBase[CustomRecord]): + env_name = "custom.record" + record_class = CustomRecord +``` + +### Using a Manager Class + +There are two ways of using manager classes. The first is to simply +instantiate a manager object, passing in the [`Client`](../index.md#connecting-to-odoo) +object as the sole argument. + +This will allow manager methods to be used, exactly the same as +the built-in record managers. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import Client, ManagerBase, RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(ManagerBase[CustomRecord]): + env_name = "custom.record" + record_class = CustomRecord + +odoo_client = Client(...) +custom_records = CustomRecordManager(odoo_client) +``` + +The disadvantage of using this method is that the client and manager objects +are effectively separate, and must be managed as two separate variables. + +To create a single object from which you can manage **all** of your custom +types and managers, subclass the `Client` class, and add a type hint for your +custom manager class. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import ManagerBase, RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(ManagerBase[CustomRecord]): + env_name = "custom.record" + record_class = CustomRecord + +class CustomClient(Client): + custom_records: CustomRecordManager +``` + +This adds the record manager to the client class, allowing you to +reference it on client objects created from it. + +```python +>>> odoo_client = CustomClient(...) +>>> odoo_client.custom_records.get(1234) +CustomRecord(record={'id' 1234, 'custom_record': 'Hello, world!'}, fields=None) +``` + +### Manager Methods + +Methods can be defined on manager classes to provide additional functionality. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import ManagerBase, RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(ManagerBase[CustomRecord]): + env_name = "custom.record" + record_class = CustomRecord + + def search_by_custom_field(self, custom_field: str) -> List[Record]: + return self.search([("custom_field", "ilike", custom_field)]) + + def perform_action(self, custom_record: Union[int, CustomRecord]) -> None: + self._env.perform_action( + ( + custom_record.id + if isinstance(custom_record, CustomRecord) + else custion_record + ), + ) +``` + +The following internal attributes are also available for use in methods: + +* `env_name` - The name of the Odoo environment (database model) for the record class +* `record_class` - The record class object +* `default_fields` - The default list of fields to fetch on queries (or `None` to fetch all) +* `_client` - The Odoo Client object the record was created from +* `_odoo` - The OdooRPC connection object +* `_env` - The OdooRPC environment object for the model + +## Extending Existing Record Types + +The Odoo Client library provides *limited* support for extending the built-in record types. + +It is possible to subclass built-in record types, and create custom record managers +that manage this record class. + +```python +from __future__ import annotations + +from openstack_odooclient import Client, RecordManager, User, UserManager + +class CustomUser(User): + custom_field: str + """Description of the field.""" + +class CustomUserManager(RecordManager[CustomUser]): + env_name = UserManager.env_name + record_class = CustomUser + +class CustomClient(Client): + custom_users: CustomUserManager +``` + +Due to the Odoo Client library using type hints to determine what record classes to use, +and the type hints being physically defined in code to allow type analysis tools such as Mypy +and Pyright to properly evaluate the source, *existing* references on *existing* record classes +cannot be automatically updated to use the custom versions. + +However, it is possible to **cast** a record object of the base type into the custom type +using the record class's ``from_record_obj`` class method. + +```python +>>> odoo_client = CustomClient(...) +>>> user = odoo_client.users.get(1234) +>>> user +User(record={'id': 1234, 'custom_field': 'Hello, world!', ...}, fields=None) +>>> custom_user = CustomUser.from_record_obj(user) +>>> custom_user +CustomUser(record={'id': 1234, 'custom_field': 'Hello, world!', ...}, fields=None) +>>> custom_user.custom_field +'Hello, world!' +``` + +This should cover the majority of use cases where custom add-ons add new functionality +to existing models. diff --git a/docs/managers/grant.md b/docs/managers/grant.md index f68a0fd..817a627 100644 --- a/docs/managers/grant.md +++ b/docs/managers/grant.md @@ -59,7 +59,7 @@ The date the grant expires. grant_type_id: int ``` -The ID of the [type of this grant](#grant-type.md). +The ID of the [type of this grant](grant-type.md). ### `grant_type_name` @@ -67,7 +67,7 @@ The ID of the [type of this grant](#grant-type.md). grant_type_name: str ``` -The name of the [type of this grant](#grant-type.md). +The name of the [type of this grant](grant-type.md). ### `grant_type` @@ -75,7 +75,7 @@ The name of the [type of this grant](#grant-type.md). grant_type: GrantType ``` -The [type of this grant](#grant-type.md). +The [type of this grant](grant-type.md). This fetches the full record from Odoo once, and caches it for subsequent accesses. diff --git a/docs/managers/index.md b/docs/managers/index.md index 27a0acb..a3dbde8 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -51,7 +51,7 @@ For example, performing a simple search query would look something like this: * [OpenStack Trials](trial.md) * [Units of Measure (UoM)](uom.md) * [Unit of Measure (UoM) Categories](uom-category.md) -* [Users](users.md) +* [Users](user.md) * [OpenStack Volume Discount Ranges](volume-discount-range.md) * [OpenStack Voucher Codes](voucher-code.md) @@ -152,8 +152,8 @@ returns an empty list. | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|------------| -| `ids` | `int \| Iterable[int]` | Record ID, or list of record IDs | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `ids` | `int | Iterable[int]` | Record ID, or list of record IDs | (required) | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | | `as_dict` | `bool` | Return records as dictionaries | `False` | #### Returns @@ -257,7 +257,7 @@ a ``dict`` object, instead of a record object. | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| | `id` | `int` | Record ID | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | | `as_dict` | `bool` | Return record as a dictionary | `False` | | `optional` | `bool` | Return `None` if not found | `False` | @@ -445,9 +445,9 @@ a list of `dict` objects, instead of record objects. | Name | Type | Description | Default | |-----------|-------------------------|---------------------------------------------------|---------| -| `filters` | `Sequence[Any] \| None` | Filters to query by (or `None` for no filters) | `None` | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `order` | `str \| None` | Field to order results by, if ordering results | `None` | +| `filters` | `Sequence[Any] | None` | Filters to query by (or `None` for no filters) | `None` | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `order` | `str | None` | Field to order results by, if ordering results | `None` | | `as_id` | `bool` | Return the record IDs only | `False` | | `as_dict` | `bool` | Return records as dictionaries | `False` | @@ -617,11 +617,11 @@ All specified records will be deleted in a single request. | Name | Type | Description | Default | |------------|--------------------------------------------|------------------------------------------------------------------------------|------------| -| `*records` | `Record \| int \| Iterable[Record \| int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | +| `*records` | `Record | int | Iterable[Record | int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | ## Named Record Managers -Some record types have a name field that is generally expected to be unique. +Some record types have a `name` field that is generally expected to be unique. The managers for these record types have additional methods for querying records by name. * [Account Moves (Invoices)](account-move.md) @@ -792,13 +792,13 @@ None #### Parameters -| Name | Type | Description | Default | -|------------|-------------------------|---------------------------------------------------|------------| -| `name` | `str` | The record name | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select (or `None` to select all fields) | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | -| `optional` | `bool` | Return `None` if not found | `False` | +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| +| `name` | `str` | The record name | (required) | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | #### Raises @@ -816,19 +816,80 @@ None | `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | | `None` | If a record with the given name does not exist (when `optional` is `True`) | -## Records +## Coded Record Managers -Record manager methods return record objects for the corresponding model -in Odoo. +Some record types have a `code` field that is guaranteed to be unique. +The managers for these record types have additional methods for querying records by code. -Record fields can be accessed as attributes on these record objects. -The record classes are fully type hinted, allowing IDEs and validation -tools such as Mypy to verify that your application is using the fields -correctly. +* [OpenStack Referral Codes](referral-code.md) +* [OpenStack Voucher Codes](voucher-code.md) + +### `get_by_code` ```python ->>> from openstack_odooclient import Client as OdooClient, User ->>> user: User | None = None +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> Record +``` + +```python +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> Record | None +``` + +```python +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> int +``` + +```python +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> int | None +``` + +```python +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get_by_code( + code: str, + fields: Iterable[str] | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Query a unique record by code. + +```python +>>> from openstack_odooclient import Client as OdooClient >>> odoo_client = OdooClient( ... hostname="localhost", ... port=8069, @@ -837,44 +898,134 @@ correctly. ... user="test-user", ... password="", ... ) ->>> user = odoo_client.users.get(1234) ->>> user -User(record={'id': 1234, ...}, fields=None) ->>> user.id -1234 +>>> odoo_client.voucher_codes.get_by_code("OSCODE123") +VoucherCode(record={'id': 1234, 'code': 'OSCODE123', ...}, fields=None) ``` -### Custom Attributes +A number of parameters are available to configure the return type, +and what happens when a result is not found. -Most of the model fields commonly used by applications have been defined -in the record classes, but if your installation of Odoo has add-ons -installed that define custom fields that the Odoo client library -does not know about, these can still be used (just without type hinting). +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. -Access these fields as object attributes, the same way as you would any -other field. +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher_codes.get_by_code("OSCODE123", fields={"code", "multi_use"}) +VoucherCode(record={'id': 1234, 'code': 'OSCODE123', 'multi_use': True, ...}, fields=['code', 'multi_use']) +``` + +Use the `as_id` parameter to return the ID of the record, +instead of the record object. ```python ->>> user.custom_field_name -'custom-field-value' +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher_codes.get_by_code("OSCODE123", as_id=True) +1234 +``` + +Use the `as_dict` parameter to return the record as +a `dict` object, instead of a record object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher_codes.get_by_code("OSCODE123", as_dict=True) +{'id': 1234, ...} ``` -If the custom field is a reference to another model record, -it will be available on the record object as 2-member list. -The first value is the record ID, and the second value -is the display name of the record. +When `optional` is `True`, `None` is returned if a record +with the given code does not exist, instead of raising an error. ```python ->>> user.custom_model_ref -[5678, 'custom-record-name'] +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher+codes.get_by_code("non-existent", optional=True) +None ``` -If the custom field is a list of model records, -the record IDs will be made available as type `list[int]`. +#### Parameters + +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| +| `code` | `str` | The record code | (required) | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +#### Raises + +| Type | Description | +|-----------------------------|-------------------------------------------------------------------------| +| `RecordNotFoundError` | If no record with the given code was found (when `optional` is `False`) | +| `MultipleRecordsFoundError` | If multiple records were found with the same code | + +#### Returns + +| Type | Description | +|------------------|----------------------------------------------------------------------------| +| `Record` | Record object (default) | +| `int` | Record ID (when `as_id` is `True`) | +| `dict[str, Any]` | Record dictionary (when `as_dict` is `True`) | +| `None` | If a record with the given code does not exist (when `optional` is `True`) | + +## Records + +Record manager methods return record objects for the corresponding model +in Odoo. + +Record fields can be accessed as attributes on these record objects. +The record classes are fully type hinted, allowing IDEs and validation +tools such as Mypy to verify that your application is using the fields +correctly. ```python ->>> user.custom_model_refs -[5678, 9012, ...] +>>> from openstack_odooclient import Client as OdooClient, User +>>> user: User | None = None +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> user +User(record={'id': 1234, ...}, fields=None) +>>> user.id +1234 ``` ### Attributes and Methods @@ -1046,3 +1197,10 @@ User(record={'id': 1234, 'name': 'Old Name', ...}, fields=None) ... openstack_odooclient.exceptions.RecordNotFoundError: User record not found with ID: 1234 ``` + +## Custom Managers and Record Types + +The OpenStack Odoo Client library supports defining new record types and adding +managers for them to the Odoo client object, allowing for adding support for custom Odoo add-ons. + +For more information, see [Custom Managers and Record Types](custom.md). diff --git a/docs/managers/pricelist.md b/docs/managers/pricelist.md index f7496b1..2e6c13e 100644 --- a/docs/managers/pricelist.md +++ b/docs/managers/pricelist.md @@ -68,11 +68,11 @@ and quantity. #### Parameters -| Name | Type | Description | Default | -|-------------|--------------------|---------------------------------------------|------------| -| `pricelist` | `int \| Pricelist` | Pricelist to reference (ID or object) | (required) | -| `product` | `int \| Product` | Product to get the price for (ID or object) | (required) | -| `qty` | `float` | Quantity to charge for | (required) | +| Name | Type | Description | Default | +|-------------|-------------------|---------------------------------------------|------------| +| `pricelist` | `int | Pricelist` | Pricelist to reference (ID or object) | (required) | +| `product` | `int | Product` | Product to get the price for (ID or object) | (required) | +| `qty` | `float` | Quantity to charge for | (required) | #### Returns @@ -208,7 +208,7 @@ Get the price to charge for a given product and quantity. | Name | Type | Description | Default | |-------------|--------------------|---------------------------------------------|------------| -| `product` | `int \| Product` | Product to get the price for (ID or object) | (required) | +| `product` | `int | Product` | Product to get the price for (ID or object) | (required) | | `qty` | `float` | Quantity to charge for | (required) | #### Returns diff --git a/docs/managers/product.md b/docs/managers/product.md index 17e2443..5428513 100644 --- a/docs/managers/product.md +++ b/docs/managers/product.md @@ -87,13 +87,13 @@ Fetch a list of active and saleable products for the given company. #### Parameters -| Name | Type | Description | Default | -|-----------|-------------------------|---------------------------------------------------|------------| -| `company` | `int \| Company` | The company to search for products (ID or object) | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select, defaults to `None` (select all) | `None` | -| `order` | `str \| None` | Order results by a specific field | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | +| Name | Type | Description | Default | +|-----------|------------------------|---------------------------------------------------|------------| +| `company` | `int | Company` | The company to search for products (ID or object) | (required) | +| `fields` | `Iterable[str] | None` | Fields to select, defaults to `None` (select all) | `None` | +| `order` | `str | None` | Order results by a specific field | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | #### Returns @@ -215,9 +215,9 @@ with the given name does not exist, instead of raising an error. | Name | Type | Description | Default | |------------|-------------------------|---------------------------------------------------|------------| -| `company` | `int \| Company` | The company to search for products (ID or object) | (required) | +| `company` | `int | Company` | The company to search for products (ID or object) | (required) | | `name` | `str` | The product name | (required) | -| `fields` | `Iterable[str] \| None` | Fields to select, defaults to `None` (select all) | `None` | +| `fields` | `Iterable[str] | None` | Fields to select, defaults to `None` (select all) | `None` | | `as_id` | `bool` | Return a record ID | `False` | | `as_dict` | `bool` | Return the record as a dictionary | `False` | | `optional` | `bool` | Return `None` if not found | `False` | diff --git a/docs/managers/sale-order.md b/docs/managers/sale-order.md index 78e40c1..7c8a4e0 100644 --- a/docs/managers/sale-order.md +++ b/docs/managers/sale-order.md @@ -62,9 +62,9 @@ Confirm the given sale order. #### Parameters -| Name | Type | Description | Default | -|--------------|--------------------|---------------------------|------------| -| `sale_order` | `int \| SaleOrder` | The sale order to confirm | (required) | +| Name | Type | Description | Default | +|--------------|-------------------|---------------------------|------------| +| `sale_order` | `int | SaleOrder` | The sale order to confirm | (required) | ### `create_invoices` @@ -95,7 +95,7 @@ Create invoices from the given sale order. | Name | Type | Description | Default | |--------------|--------------------|----------------------------------------|------------| -| `sale_order` | `int \| SaleOrder` | The sale order to create invoices from | (required) | +| `sale_order` | `int | SaleOrder` | The sale order to create invoices from | (required) | ## Record diff --git a/docs/managers/uom-category.md b/docs/managers/uom-category.md index e69de29..70c1292 100644 --- a/docs/managers/uom-category.md +++ b/docs/managers/uom-category.md @@ -0,0 +1,78 @@ +# Unit of Measure (UoM) Categories + +This page documents how to use the manager and record objects +for Unit of Measure (UoM) categories. + +## Details + +| Name | Value | +|-----------------|------------------------| +| Odoo Modules | Units of Measure (UoM) | +| Odoo Model Name | `uom.uom.category` | +| Manager | `uom_categories` | +| Record Type | `UomCategory` | + +## Manager + +The Unit of Measure (UoM) category manager is available as the `uom_categories` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.uom_categories.get(1234) +UomCategory(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The Unit of Measure (UoM) category manager returns `UomCategory` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import UomCategory +``` + +The record class currently implements the following fields and methods. + +### `measure_type` + +```python +measure_type: Literal[ + "unit", + "weight", + "working_time", + "length", + "volume", +] +``` + +The type of Unit of Measure (UoM) category. + +This field no longer exists from Odoo 14 onwards. + +Values: + +* ``unit`` - Default Units +* ``weight`` - Default Weight +* ``working_time`` - Default Working Time +* ``length`` - Default Length +* ``volume`` - Default Volume + +### `name` + +```python +name: str +``` + +The name of the Unit of Measure (UoM) category. diff --git a/docs/managers/uom.md b/docs/managers/uom.md index 8d7bd74..22dc377 100644 --- a/docs/managers/uom.md +++ b/docs/managers/uom.md @@ -5,12 +5,12 @@ for Units of Measure (UoM). ## Details -| Name | Value | -|-----------------|------------------------| -| Odoo Modules | Units of Measure (UoM) | -| Odoo Model Name | `uom.uom` | -| Manager | `uoms` | -| Record Type | `Uom` | +| Name | Value | +|-----------------|---------------------------------| +| Odoo Modules | Units of Measure (UoM), Product | +| Odoo Model Name | `uom.uom` | +| Manager | `uoms` | +| Record Type | `Uom` | ## Manager diff --git a/docs/managers/user.md b/docs/managers/user.md index e69de29..8f032c7 100644 --- a/docs/managers/user.md +++ b/docs/managers/user.md @@ -0,0 +1,124 @@ +# Users + +This page documents how to use the manager and record objects +for users. + +## Details + +| Name | Value | +|-----------------|------------------| +| Odoo Modules | Base, Accounting | +| Odoo Model Name | `res.users` | +| Manager | `users` | +| Record Type | `User` | + +## Manager + +The user manager is available as the `users` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.get(1234) +User(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The user manager returns `User` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import User +``` + +The record class currently implements the following fields and methods. + +### `active` + +```python +active: bool +``` + +Whether or not this user is active (enabled). + +### `active_partner` + +```python +active_partner: bool +``` + +Whether or not the [partner](partner.md) this user is associated with is active. + +### `company_id` + +```python +company_id: int +``` + +The ID for the default [company](company.md) this user is logged in as. + +### `company_name` + +```python +company_name: str +``` + +The name of the default [company](company.md) this user is logged in as. + +### `company` + +```python +company: Company +``` + +The default [company](company.md) this user is logged in as. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `name` + +```python +name: str +``` + +User name. + +### `partner_id` + +```python +partner_id: int +``` + +The ID for the [partner](partner.md) that this user is associated with. + +### `partner_name` + +```python +partner_name: str +``` + +The name of the [partner](partner.md) that this user is associated with. + +### `partner` + +```python +partner: Partner +``` + +The [partner](partner.md) that this user is associated with. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. diff --git a/docs/managers/volume-discount-range.md b/docs/managers/volume-discount-range.md index e69de29..1dd2b79 100644 --- a/docs/managers/volume-discount-range.md +++ b/docs/managers/volume-discount-range.md @@ -0,0 +1,192 @@ +# OpenStack Volume Discount Ranges + +This page documents how to use the manager and record objects +for volume discount ranges. + +## Details + +| Name | Value | +|-----------------|-----------------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.volume_discount_range` | +| Manager | `volume_discount_ranges` | +| Record Type | `VolumeDiscountRange` | + +## Manager + +The volume discount range manager is available as the `volume_discount_ranges` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.volume_discount_ranges.get(1234) +VolumeDiscountRange(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +The following manager methods are also available, in addition to the standard methods. + +### `get_for_charge` + +```python +get_for_charge( + charge: float, + customer_group: int | CustomerGroup | None = None, +) -> VolumeDiscountRange | None +``` + +Return the volume discount range to apply to a given charge. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.volume_discount_ranges.get_for_charge(1000) +VolumeDiscountRange(record={'id': 1234, 'min': 500, ...}, fields=None) +``` + +If ``customer_group`` is supplied, volume discount ranges for +a specific customer group are returned. When set to ``None`` +(the default), volume discount ranges for all customers are returned. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.volume_discount_ranges.get_for_charge(1000, customer_group=5678) +VolumeDiscountRange(record={'id': 9012, 'customer_group': [5678, 'Customer Group'], 'min': 500, ...}, fields=None) +``` + +If multiple volume discount ranges can be applied, the range with +the highest discount percentage is selected. +If no applicable volume discount ranges were found, +``None`` is returned. + +#### Parameters + +| Name | Type | Description | Default | +|------------------|--------------------------------|-----------------------------------------------------------|------------| +| `charge` | `float` | The charge to find the applicable discount range for | (required) | +| `customer_group` | `int | CustomerGroup | None` | Get discount for a specific customer group (ID or object) | `None` | + +#### Returns + +| Type | Description | +|-----------------------|----------------------------------------------------------------| +| `VolumeDiscountRange` | Highest percentage applicable volume discount range (if found) | +| `None` | If no applicable volume discount range was found | + +## Record + +The volume discount range manager returns `VolumeDiscountRange` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import VolumeDiscountRange +``` + +The record class currently implements the following fields and methods. + +### `customer_group_id` + +```python +customer_group_id: int | None +``` + +The ID for the [customer group](customer-group.md) this volume discount range +applies to, if a specific customer group is set. + +If no customer group is set, this volume discount range +applies to all customers. + +### `customer_group_name` + +```python +customer_group_name: str | None +``` + +The name of the [customer group](customer-group.md) this volume discount range +applies to, if a specific customer group is set. + +If no customer group is set, this volume discount range +applies to all customers. + +### `customer_group` + +```python +customer_group: CustomerGroup | None +``` + +The [customer group](customer-group.md) this volume discount range +applies to, if a specific customer group is set. + +If no customer group is set, this volume discount range +applies to all customers. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `discount_percent` + +```python +discount_percent: float +``` + +Discount percentage of this volume discount range (0-100). + +### `name` + +```python +name: str +``` + +The automatically generated name (description) of +this volume discount range. + +### `max` + +```python +max: float | None +``` + +Optional maximum charge for this volume discount range. + +Intended to be used when creating tiered volume discounts for customers. + +### `min` + +```python +min: float +``` + +Minimum charge for this volume discount range. + +### `use_max` + +```python +use_max: bool +``` + +Use the [``max``](#max) field, if defined. diff --git a/docs/managers/voucher-code.md b/docs/managers/voucher-code.md index e69de29..74828a6 100644 --- a/docs/managers/voucher-code.md +++ b/docs/managers/voucher-code.md @@ -0,0 +1,276 @@ +# OpenStack Voucher Codes + +This page documents how to use the manager and record objects +for voucher codes. + +## Details + +| Name | Value | +|-----------------|--------------------------| +| Odoo Modules | OpenStack Integration | +| Odoo Model Name | `openstack.voucher_code` | +| Manager | `voucher_codes` | +| Record Type | `VoucherCode` | + +## Manager + +The voucher code manager is available as the `voucher_codes` +attribute on the Odoo client object. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher_codes.get(1234) +VoucherCode(record={'id': 1234, ...}, fields=None) +``` + +For more information on how to use managers, refer to [Managers](index.md). + +## Record + +The voucher code manager returns `VoucherCode` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import VoucherCode +``` + +The record class currently implements the following fields and methods. + +### `claimed` + +```python +claimed: bool +``` + +Whether or not this voucher code has been claimed. + +### `code` + +```python +code: str +``` + +The code string for this voucher code. + +### `credit_amount` + +```python +credit_amount: float | Literal[False] +``` + +The initial credit balance for the voucher code, if a credit is to be +created by the voucher code. + +### `credit_type_id` + +```python +credit_type_id: int | None +``` + +The ID of the [credit type](credit-type.md) to use, if a credit is to be +created by this voucher code. + +### `credit_type_name` + +```python +credit_type_name: str | None +``` + +The name of the [credit type](credit-type.md) to use, if a credit is to be +created by this voucher code. + +### `credit_type` + +```python +credit_type: CreditType | None +``` + +The [credit type](credit-type.md) to use, if a credit is to be +created by this voucher code. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `credit_duration` + +```python +credit_duration: int | Literal[False] +``` + +The duration of the [credit](credit.md), in days, if a credit is to be +created by the voucher code. + +### `customer_group_id` + +```python +customer_group_id: int | None +``` + +The ID of the [customer group](customer-group.md) to add the customer to, if set. + +### `customer_group_name` + +```python +customer_group_name: str | None +``` + +The name of the [customer group](customer-group.md) to add the customer to, if set. + +### `customer_group` + +```python +customer_group: CustomerGroup | None +``` + +The [customer group](customer-group.md) to add the customer to, if set. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `expiry_date` + +```python +expiry_date: date | Literal[False] +``` + +The date the voucher code expires. + +### `grant_duration` + +```python +grant_duration: int | Literal[False] +``` + +The duration of the [grant](grant.md), in days, if a grant is to be +created by the voucher code. + +### `grant_type_id` + +```python +grant_type_id: int | None +``` + +The ID of the [grant type](grant-type.md) to use, if a grant is to be +created by this voucher code. + +### `grant_type_name` + +```python +grant_type_name: str | None +``` + +The name of the [grant type](grant-type.md) to use, if a grant is to be +created by this voucher code. + +### `grant_type` + +```python +grant_type: GrantType | None +``` + +The [grant type](grant-type.md) to use, if a grant is to be +created by this voucher code. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `grant_value` + +```python +grant_value: float | Literal[False] +``` + +The value of the [grant](grant.md), if a grant is to be +created by the voucher code. + +### `multi_use` + +```python +multi_use: bool +``` + +Whether or not this is a multi-use voucher code. + +A multi-use voucher code can be used an unlimited number of times +until it expires. + +### `name` + +```python +name: str +``` + +The unique name of this voucher code. + +This uses the code specified in the record as-is. + +### `quota_size` + +```python +quota_size: str | Literal[False] +``` + +The default quota size for new projects signed up +using this voucher code. + +If unset, use the default quota size. + +### `sales_person_id` + +```python +sales_person_id: int | None +``` + +The ID for the salesperson [partner](partner.md) responsible for this +voucher code, if assigned. + +### `sales_person_name` + +```python +sales_person_name: str | None +``` + +The name of the salesperson [partner](partner.md) responsible for this +voucher code, if assigned. + +### `sales_person` + +```python +sales_person: Partner | None +``` + +The salesperson [partner](partner.md) responsible for this +voucher code, if assigned. + +This fetches the full record from Odoo once, +and caches it for subsequent accesses. + +### `tag_ids` + +```python +tag_ids: list[int] +``` + +A list of IDs for the tags ([partner categories](partner-category.md)) to assign +to partners for new accounts that signed up using this voucher code. + +### `tags` + +```python +tags: list[PartnerCategory] +``` + +The list of tags ([partner categories](partner-category.md)) to assign +to partners for new accounts that signed up using this voucher code. + +This fetches the full records from Odoo once, +and caches them for subsequent accesses. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..04f0132 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,58 @@ +--- + +site_name: OpenStack Odoo Client for Python +repo_url: https://github.com/catalyst-cloud/python-openstack-odooclient + +markdown_extensions: + - admonition + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - toc: + permalink: true + title: Table of Contents + +plugins: + - offline + - search + +theme: + name: material + icon: + repo: fontawesome/brands/github + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + primary: purple + toggle: + icon: material/brightness-auto + name: Switch to light mode + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: purple + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: purple + toggle: + icon: material/brightness-4 + name: Switch to system preference + features: + - navigation.footer + - navigation.indexes + - navigation.path + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.highlight + - search.share + - search.suggest + - toc.follow diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index d4a69de..45ecd61 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -166,14 +166,14 @@ def __init__( self._fields = fields self._values: Dict[str, Any] = {} - @property - def _env(self) -> Environment: - return self._manager._env - @property def _odoo(self) -> ODOO: return self._client._odoo + @property + def _env(self) -> Environment: + return self._manager._env + @classmethod def from_record_obj(cls, record_obj: RecordBase) -> Self: """Create a record object of this class's type diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py index 810fb75..677f475 100644 --- a/openstack_odooclient/managers/uom_category.py +++ b/openstack_odooclient/managers/uom_category.py @@ -43,7 +43,7 @@ class UomCategory(RecordBase): """ name: str - """Unit of Measure (UoM) category name.""" + """The name of the Unit of Measure (UoM) category.""" class UomCategoryManager(RecordManagerBase[UomCategory]): diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index e1f83aa..94d98c0 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -23,7 +23,7 @@ class User(RecordBase): active: bool - """Whether or not this user is active.""" + """Whether or not this user is active (enabled).""" active_partner: bool """Whether or not the partner this user is associated with is active.""" diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 002cd9e..8b81756 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -30,6 +30,9 @@ class VolumeDiscountRange(RecordBase): ] """The ID for the customer group this volume discount range applies to, if a specific customer group is set. + + If no customer group is set, this volume discount range + applies to all customers. """ customer_group_name: Annotated[ @@ -38,6 +41,9 @@ class VolumeDiscountRange(RecordBase): ] """The name of the customer group this volume discount range applies to, if a specific customer group is set. + + If no customer group is set, this volume discount range + applies to all customers. """ customer_group: Annotated[ @@ -47,6 +53,9 @@ class VolumeDiscountRange(RecordBase): """The customer group this volume discount range applies to, if a specific customer group is set. + If no customer group is set, this volume discount range + applies to all customers. + This fetches the full record from Odoo once, and caches it for subsequent accesses. """ @@ -84,7 +93,7 @@ def get_for_charge( """Return the volume discount range to apply to a given charge. If ``customer_group`` is supplied, volume discount ranges for - a specific customer group are returned. When set to ``False`` + a specific customer group are returned. When set to ``None`` (the default), volume discount ranges for all customers are returned. If multiple volume discount ranges can be applied, the range with @@ -92,7 +101,7 @@ def get_for_charge( If no applicable volume discount ranges were found, ``None`` is returned. - :param charge: The charge for to find the applicable discount range + :param charge: The charge to find the applicable discount range for :type charge: float :param customer_group: Get discount for a specific customer group :type customer_group: Optional[Union[int, CustomerGroup]], optional diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 93fed84..09c6e4b 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -31,7 +31,7 @@ class VoucherCode(RecordBase): code: str """The code string for this voucher code.""" - credit_amount: float + credit_amount: Union[float, Literal[False]] """The initial credit balance for the voucher code, if a credit is to be created by the voucher code. """ @@ -63,7 +63,7 @@ class VoucherCode(RecordBase): and caches it for subsequent accesses. """ - credit_duration: int + credit_duration: Union[int, Literal[False]] """The duration of the credit, in days, if a credit is to be created by the voucher code. """ @@ -72,37 +72,29 @@ class VoucherCode(RecordBase): Optional[int], ModelRef("customer_group", CustomerGroup), ] - """The ID of the customer group this voucher code is available to. - - If not set, the voucher code is available to all customers. - """ + """The ID of the customer group to add the customer to, if set.""" customer_group_name: Annotated[ Optional[str], ModelRef("customer_group", CustomerGroup), ] - """The name of the customer group this voucher code is available to. - - If not set, the voucher code is available to all customers. - """ + """The name of the customer group to add the customer to, if set.""" customer_group: Annotated[ Optional[CustomerGroup], ModelRef("customer_group", CustomerGroup), ] - """The customer group this voucher code is available to. - - If not set, the voucher code is available to all customers. + """The customer group to add the customer to, if set. This fetches the full record from Odoo once, and caches it for subsequent accesses. """ - expiry_date: date + expiry_date: Union[date, Literal[False]] """The date the voucher code expires.""" - grant_amount: float - """The value of the grant, if a grant is to be + grant_duration: Union[int, Literal[False]] + """The duration of the grant, in days, if a grant is to be created by the voucher code. """ @@ -130,8 +122,8 @@ class VoucherCode(RecordBase): and caches it for subsequent accesses. """ - grant_duration: int - """The duration of the grant, in days, if a grant is to be + grant_value: Union[float, Literal[False]] + """The value of the grant, if a grant is to be created by the voucher code. """ @@ -148,8 +140,8 @@ class VoucherCode(RecordBase): This uses the code specified in the record as-is. """ - quota: Union[str, Literal[False]] - """The quota size to set for new projects signed up + quota_size: Union[str, Literal[False]] + """The default quota size for new projects signed up using this voucher code. If unset, use the default quota size. diff --git a/pdm.lock b/pdm.lock index 4d49148..a192044 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,363 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "lint"] +groups = ["default", "docs", "lint"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:d0e03148a1ea1fbab3399c5ab4351cda37039d37482272dc2a555621c735fc1b" +content_hash = "sha256:c6999cf16bc1f92a511a81874c7408a907193281fe84622ed53d6a2b5f222c12" + +[[package]] +name = "babel" +version = "2.15.0" +requires_python = ">=3.8" +summary = "Internationalization utilities" +groups = ["docs"] +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[[package]] +name = "certifi" +version = "2024.6.2" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["docs"] +files = [ + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["docs"] +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["docs"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["docs"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +summary = "Copy your docs directly to the gh-pages branch." +groups = ["docs"] +dependencies = [ + "python-dateutil>=2.8.1", +] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["docs"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.1.0" +requires_python = ">=3.8" +summary = "Read metadata from Python packages" +groups = ["docs"] +marker = "python_version < \"3.10\"" +dependencies = [ + "zipp>=0.5", +] +files = [ + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["docs"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "markdown" +version = "3.6" +requires_python = ">=3.8" +summary = "Python implementation of John Gruber's Markdown." +groups = ["docs"] +dependencies = [ + "importlib-metadata>=4.4; python_version < \"3.10\"", +] +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["docs"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +requires_python = ">=3.6" +summary = "A deep merge function for 🐍." +groups = ["docs"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.0" +requires_python = ">=3.8" +summary = "Project documentation with Markdown." +groups = ["docs"] +dependencies = [ + "click>=7.0", + "colorama>=0.4; platform_system == \"Windows\"", + "ghp-import>=1.0", + "importlib-metadata>=4.4; python_version < \"3.10\"", + "jinja2>=2.11.1", + "markdown>=3.3.6", + "markupsafe>=2.0.1", + "mergedeep>=1.3.4", + "mkdocs-get-deps>=0.2.0", + "packaging>=20.5", + "pathspec>=0.11.1", + "pyyaml-env-tag>=0.1", + "pyyaml>=5.1", + "watchdog>=2.0", +] +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +requires_python = ">=3.8" +summary = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +groups = ["docs"] +dependencies = [ + "importlib-metadata>=4.3; python_version < \"3.10\"", + "mergedeep>=1.3.4", + "platformdirs>=2.2.0", + "pyyaml>=5.1", +] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.27" +requires_python = ">=3.8" +summary = "Documentation that simply works" +groups = ["docs"] +dependencies = [ + "babel~=2.10", + "colorama~=0.4", + "jinja2~=3.0", + "markdown~=3.2", + "mkdocs-material-extensions~=1.3", + "mkdocs~=1.6", + "paginate~=0.5", + "pygments~=2.16", + "pymdown-extensions~=10.2", + "regex>=2022.4", + "requests~=2.26", +] +files = [ + {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"}, + {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"}, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +requires_python = ">=3.8" +summary = "Extension pack for Python Markdown and MkDocs Material." +groups = ["docs"] +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] [[package]] name = "mypy" @@ -74,12 +427,261 @@ name = "packaging" version = "24.1" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["default"] +groups = ["default", "docs"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +summary = "Divides large result sets into pages for easier browsing" +groups = ["docs"] +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["docs"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["docs"] +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["docs"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +requires_python = ">=3.8" +summary = "Extension pack for Python Markdown." +groups = ["docs"] +dependencies = [ + "markdown>=3.6", + "pyyaml", +] +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["docs"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "pytz" +version = "2024.1" +summary = "World timezone definitions, modern and historical" +groups = ["docs"] +marker = "python_version < \"3.9\"" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +groups = ["docs"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +requires_python = ">=3.6" +summary = "A custom YAML tag for referencing environment variables in YAML files. " +groups = ["docs"] +dependencies = [ + "pyyaml", +] +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[[package]] +name = "regex" +version = "2024.5.15" +requires_python = ">=3.8" +summary = "Alternative regular expression module, to replace re." +groups = ["docs"] +files = [ + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, + {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, + {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, + {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, + {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, + {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, + {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, + {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, + {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, + {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, + {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, + {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["docs"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + [[package]] name = "ruff" version = "0.4.8" @@ -106,6 +708,17 @@ files = [ {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"}, ] +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["docs"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -128,3 +741,67 @@ files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] + +[[package]] +name = "urllib3" +version = "2.2.2" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["docs"] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[[package]] +name = "watchdog" +version = "4.0.1" +requires_python = ">=3.8" +summary = "Filesystem events monitoring" +groups = ["docs"] +files = [ + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, +] + +[[package]] +name = "zipp" +version = "3.19.2" +requires_python = ">=3.8" +summary = "Backport of pathlib-compatible object wrapper for zip files" +groups = ["docs"] +marker = "python_version < \"3.10\"" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] diff --git a/pyproject.toml b/pyproject.toml index c86a533..961af7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ lint = [ "mypy==1.10.0", "ruff==0.4.8", ] +docs = [ + "mkdocs-material>=9.5.27", +] [tool.pdm.scripts] lint = {cmd = "ruff check"} From 6e82c3b7720f00c0e5670ca0f48378cd16b465f8 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 19 Jun 2024 15:00:18 +1200 Subject: [PATCH 43/87] Additions and fixes to docs --- README.md | 4 +- docs/index.md | 6 +- docs/managers/custom.md | 65 ++++++++++++- docs/managers/index.md | 102 ++++++++++++++++++-- mkdocs.yml | 42 ++++++++ openstack_odooclient/base/record_manager.py | 3 + 6 files changed, 205 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e00dceb..4f69e05 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ changes between Odoo versions. ## Installation -The Odoo Client library supports Python 3.8 and later. +The OpenStack Odoo Client library supports Python 3.8 and later. To install the library package, simply install the `openstack-odooclient` package using `pip`. @@ -34,7 +34,7 @@ openstack_odooclient.Client( password: str, protocol: str = "jsonrpc", port: int = 8069, - verify: bool | Path | str = True, + verify: bool | str | Path = True, version: str | None = None, ) -> Client ``` diff --git a/docs/index.md b/docs/index.md index 29ab427..2c2a0d8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# OpenStack Odoo Client Library +# OpenStack Odoo Client Library for Python This is an Odoo client library for Python with support for the [OpenStack Integration add-on](https://github.com/catalyst-cloud/odoo-openstack-integration), @@ -12,7 +12,7 @@ changes between Odoo versions. ## Installation -The Odoo Client library supports Python 3.8 and later. +The OpenStack Odoo Client library supports Python 3.8 and later. To install the library package, simply install the `openstack-odooclient` package using `pip`. @@ -34,7 +34,7 @@ openstack_odooclient.Client( password: str, protocol: str = "jsonrpc", port: int = 8069, - verify: bool | Path | str = True, + verify: bool | str | Path = True, version: str | None = None, ) -> Client ``` diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 1074ad9..5f6cc70 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -535,6 +535,62 @@ class Child(RecordBase): from .parent import Parent # noqa: E402 ``` +### Odoo Version Compatibility + +Major releases of Odoo may change the database models to introduce +new functionality. + +The way models usually change in a backwards-incompatible way is +that fields are renamed so that they are referenced using another name, +without providing an alias for the old one. + +In the OpenStack Odoo Client library, this is handled by defining +the `_field_mapping` attribute on the record class. + +```python +_field_mapping: dict[str | None, dict[str, str]] +``` + +The `_field_mapping` attribute is a nested dictionary structure used +to define local-to-remote field name mappings. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: str + """Description of the field.""" + + custom_field_2: int + """Description of the second field.""" + + custom_field_3: float + """Description of the third field.""" + + _field_mapping = { + # The Odoo version for which to generate the mapping. + "13.0": { + # Key is local field name. Value is the field name in Odoo 13. + "custom_field": "old_custom_field", + } + # Use None to provide a mapping to use for all Odoo versions. + None: { + "custom_field_2": "old_custom_field_2", + }, + # custom_field_3 is not defined here. + # The field name will be used as-is on all Odoo versions. + } +``` + +Mappings can be added for specific Odoo versions, or by using `None`, +mappings that apply to all Odoo versions can be defined. + +When the Odoo Client library interfaces with Odoo, it will automatically find +and use the correct field name to present based on the server version +and the record class's field mapping. + ### Record Methods Methods can be defined on record types to provide additional functionality. @@ -581,8 +637,13 @@ Manager classes are subclasses of the generic `ManagerBase` class, specifying the record class the generic type argument, and defining the following class attributes: -* `env_name` - The name of the Odoo environment (database model) for the record class -* `record_class` - The record class object +* `env_name: str` - The name of the Odoo environment (database model) for the record class +* `record_class: Type[RecordBase]` - The record class to use to create record objects + +The following optional class attributes are also available: + +* `default_fields: Set[str] | None` - A set of fields to select by default in queries + if a field list is not supplied (default is `None` to select all fields) Below is a simple example of a custom record type and its manager class. diff --git a/docs/managers/index.md b/docs/managers/index.md index a3dbde8..023807e 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -443,13 +443,13 @@ a list of `dict` objects, instead of record objects. #### Parameters -| Name | Type | Description | Default | -|-----------|-------------------------|---------------------------------------------------|---------| +| Name | Type | Description | Default | +|-----------|------------------------|---------------------------------------------------|---------| | `filters` | `Sequence[Any] | None` | Filters to query by (or `None` for no filters) | `None` | | `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | | `order` | `str | None` | Field to order results by, if ordering results | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | #### Returns @@ -478,7 +478,86 @@ as input fields. ... user="test-user", ... password="", ... ) ->>> odoo_client.sales_order_lines.create(...) +>>> odoo_client.sales_orders.create(...) +1234 +``` + +This method allows a lot of flexibility in how input fields +should be defined. + +The fields passed to this method should use the same field names +and value types that are defined on the record classes. +The Odoo Client library will convert the values to the formats +that the Odoo API expects. + +For example, when defining references to another record, +you can either pass the record ID, or the record object. +The field name can also either be for the ID or the object. + +Field aliases are also resolved to their target field names. + +```python +>>> from datetime import date +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(5678) +>>> odoo_client.sales_orders.create( +... user=user, # User object +... partner_id=9012, # Partner ID +... os_invoice_date=date(2024, 6, 30), +... os_invoice_due_date=date(2024, 7, 20), +... os_project=3456, # Field name is for the object, value is the ID +... order_lines=[7890], # Field alias used. +... ) +) +1234 +``` + +By **nesting** a record mapping where an ID or object would normally go, +a new record will be created for that mapping, and linked to the outer record. +This nested record mapping is recursively validated and processed in the same way +as the outer record. + +```python +>>> from datetime import date +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(5678) +>>> odoo_client.sales_orders.create( +... user=user, # User object +... partner_id=9012, # Partner ID +... os_invoice_date=date(2024, 6, 30), +... os_invoice_due_date=date(2024, 7, 20), +... os_project=3456, # Field name for object, value is ID +... order_lines=[ # Create the sale order lines. +... { +... "name": "test-instance", +... "product": odoo_client.products.get(7890), # Product object +... "product_uom": 123456, # Field name for object, value is ID +... "product_uom_qty": 1.0, +... "price_unit": 0.05, +... "os_project_id": 3456, # Project ID +... "os_resource_id": "1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a", +... "os_region": "RegionOne", +... "os_resource_type": "Virtual Machine", +... "os_resource_name": "m1.small", +... }, +... ], +... ) 1234 ``` @@ -523,6 +602,9 @@ Create one or more new records in a single request, passing in the mappings containing the record's input fields as positional arguments. +The record mappings should be in the same format as with +the [``create``](#create) method. + ```python >>> from openstack_odooclient import Client as OdooClient >>> odoo_client = OdooClient( @@ -533,7 +615,7 @@ as positional arguments. ... user="test-user", ... password="", ... ) ->>> odoo_client.sales_order_lines.create_multi({...}, {...}) +>>> odoo_client.sales_orders.create_multi({...}, {...}) [1234, 1235] ``` @@ -550,10 +632,10 @@ pass the returned IDs to the [``list``](#list) method. ... user="test-user", ... password="", ... ) ->>> odoo_client.sale_order_lines.list( -... odoo_client.sales_order_lines.create_multi({...}, {...}), +>>> odoo_client.sale_orders.list( +... odoo_client.sales_orders.create_multi({...}, {...}), ... ) -[SaleOrderLine(record={'id': 1234, ...}, fields=None), SaleOrderLine(record={'id': 1235, ...}, fields=None)] +[SaleOrder(record={'id': 1234, ...}, fields=None), SaleOrder(record={'id': 1235, ...}, fields=None)] ``` #### Parameters @@ -970,7 +1052,7 @@ with the given code does not exist, instead of raising an error. ... user="test-user", ... password="", ... ) ->>> odoo_client.voucher+codes.get_by_code("non-existent", optional=True) +>>> odoo_client.voucher_codes.get_by_code("non-existent", optional=True) None ``` diff --git a/mkdocs.yml b/mkdocs.yml index 04f0132..7519907 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,10 @@ --- site_name: OpenStack Odoo Client for Python +site_description: The documentation for the OpenStack Odoo Client library for Python. +site_author: Callum Dickinson repo_url: https://github.com/catalyst-cloud/python-openstack-odooclient +copyright: Copyright © 2024 Catalyst Cloud Limited markdown_extensions: - admonition @@ -20,6 +23,45 @@ plugins: - offline - search +nav: + - Getting Started: index.md + - Managers: + - managers/index.md + - managers/account-move.md + - managers/account-move-line.md + - managers/company.md + - managers/credit.md + - managers/credit-type.md + - managers/credit-transaction.md + - managers/currency.md + - managers/customer-group.md + - managers/grant.md + - managers/grant-type.md + - managers/partner.md + - managers/partner-category.md + - managers/pricelist.md + - managers/product.md + - managers/product-category.md + - managers/project.md + - managers/project-contact.md + - managers/referral-code.md + - managers/reseller.md + - managers/reseller-tier.md + - managers/sale-order.md + - managers/sale-order-line.md + - managers/support-subscription.md + - managers/support-subscription-type.md + - managers/tax.md + - managers/tax-group.md + - managers/term-discount.md + - managers/trial.md + - managers/uom.md + - managers/uom-category.md + - managers/user.md + - managers/volume-discount-range.md + - managers/voucher-code.md + - managers/custom.md + theme: name: material icon: diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 592638c..2f8d866 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -504,6 +504,9 @@ def create_multi(self, *records: Mapping[str, Any]) -> List[int]: passing in the mappings containing the record's input fields as positional arguments. + The record mappings should be in the same format as with + the ``create`` method. + To fetch the newly created record objects, pass the returned IDs to the ``list`` method. From ff140660026918d741321c428552a8283644dde4 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 19 Jun 2024 16:44:39 +1200 Subject: [PATCH 44/87] Finish documentation --- docs/managers/account-move-line.md | 3 + docs/managers/account-move.md | 3 + docs/managers/company.md | 3 + docs/managers/credit-transaction.md | 3 + docs/managers/credit-type.md | 3 + docs/managers/credit.md | 3 + docs/managers/currency.md | 3 + docs/managers/customer-group.md | 3 + docs/managers/grant-type.md | 3 + docs/managers/grant.md | 3 + docs/managers/index.md | 137 ++++++++++++++++---- docs/managers/partner-category.md | 3 + docs/managers/partner.md | 3 + docs/managers/pricelist.md | 3 + docs/managers/product-category.md | 3 + docs/managers/product.md | 3 + docs/managers/project-contact.md | 3 + docs/managers/project.md | 3 + docs/managers/referral-code.md | 3 + docs/managers/reseller-tier.md | 3 + docs/managers/reseller.md | 3 + docs/managers/sale-order-line.md | 15 +++ docs/managers/sale-order.md | 3 + docs/managers/support-subscription-type.md | 3 + docs/managers/support-subscription.md | 3 + docs/managers/tax-group.md | 3 + docs/managers/tax.md | 3 + docs/managers/term-discount.md | 3 + docs/managers/trial.md | 3 + docs/managers/uom-category.md | 3 + docs/managers/uom.md | 3 + docs/managers/user.md | 3 + docs/managers/volume-discount-range.md | 3 + docs/managers/voucher-code.md | 3 + openstack_odooclient/base/record_manager.py | 97 ++++++++++---- 35 files changed, 295 insertions(+), 50 deletions(-) diff --git a/docs/managers/account-move-line.md b/docs/managers/account-move-line.md index c44069f..d6416f8 100644 --- a/docs/managers/account-move-line.md +++ b/docs/managers/account-move-line.md @@ -45,6 +45,9 @@ from openstack_odooclient import AccountMoveLine The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `currency_id` ```python diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md index aa31499..0b515e2 100644 --- a/docs/managers/account-move.md +++ b/docs/managers/account-move.md @@ -45,6 +45,9 @@ from openstack_odooclient import AccountMove The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `amount_total` ```python diff --git a/docs/managers/company.md b/docs/managers/company.md index 8508b6d..4e6b321 100644 --- a/docs/managers/company.md +++ b/docs/managers/company.md @@ -45,6 +45,9 @@ from openstack_odooclient import Company The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + #### `active` ```python diff --git a/docs/managers/credit-transaction.md b/docs/managers/credit-transaction.md index a590050..c93943e 100644 --- a/docs/managers/credit-transaction.md +++ b/docs/managers/credit-transaction.md @@ -45,6 +45,9 @@ from openstack_odooclient import CreditTransaction The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `credit_id` ```python diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md index be68e6a..bb1450c 100644 --- a/docs/managers/credit-type.md +++ b/docs/managers/credit-type.md @@ -45,6 +45,9 @@ from openstack_odooclient import CreditType The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `credit_ids` ```python diff --git a/docs/managers/credit.md b/docs/managers/credit.md index e1bd141..6bff3d8 100644 --- a/docs/managers/credit.md +++ b/docs/managers/credit.md @@ -45,6 +45,9 @@ from openstack_odooclient import Credit The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `credit_type_id` ```python diff --git a/docs/managers/currency.md b/docs/managers/currency.md index 1e6749c..c4c8a51 100644 --- a/docs/managers/currency.md +++ b/docs/managers/currency.md @@ -45,6 +45,9 @@ from openstack_odooclient import Currency The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/customer-group.md b/docs/managers/customer-group.md index 24bfe83..1c88fb1 100644 --- a/docs/managers/customer-group.md +++ b/docs/managers/customer-group.md @@ -45,6 +45,9 @@ from openstack_odooclient import CustomerGroup The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `name` ```python diff --git a/docs/managers/grant-type.md b/docs/managers/grant-type.md index 8c66625..e8e7f23 100644 --- a/docs/managers/grant-type.md +++ b/docs/managers/grant-type.md @@ -45,6 +45,9 @@ from openstack_odooclient import GrantType The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `grant_ids` ```python diff --git a/docs/managers/grant.md b/docs/managers/grant.md index 817a627..ea5d01c 100644 --- a/docs/managers/grant.md +++ b/docs/managers/grant.md @@ -45,6 +45,9 @@ from openstack_odooclient import Grant The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `expiry_date` ```python diff --git a/docs/managers/index.md b/docs/managers/index.md index 023807e..f176763 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -279,7 +279,7 @@ a ``dict`` object, instead of a record object. ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = False, @@ -290,7 +290,7 @@ search( ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = False, @@ -301,7 +301,7 @@ search( ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = True, @@ -312,7 +312,7 @@ search( ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = True, @@ -323,7 +323,7 @@ search( ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = False, @@ -334,7 +334,7 @@ search( ```python search( - filters: Sequence[Any] | None = None, + filters: Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None = None, fields: Iterable[str] | None = None, order: str | None = None, as_id: bool = False, @@ -361,16 +361,97 @@ and return the results. [User(record={'id': 1234, ...}, fields=None)] ``` -Query filters should be defined using the same format as OdooRPC, -but some additional features are supported: +Query filters should be defined using the +[ORM API search domain](https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains) +format, which is a sequence of criteria, where each criterion +is one of the following types of values: -* Odoo client field aliases can be specified as the field name, - in additional to the original field name on the Odoo model - (e.g. `create_user` instead of `create_uid`). -* Record objects can be directly passed as the value - on a filter, where a record ID would normally be expected. -* Sets and tuples are supported when specifying a range of values, - in addition to lists. +* A 3-tuple or 3-element sequence in `(field_name, operator, value)` + format, where: + + * `field_name` (`str`) is the the name of the field to filter by. + * `operator` (`str`) is the comparison operator to use (for more + information on the available operators, check the ORM API + search domain documentation). + * `value` (`Any`) is the value to compare records against. + +* A logical operator which prefixes the following filter criteria + to form a **criteria combination**: + + * `&` is a logical AND. Records only match if **both** of the + following **two** criteria match. + * `|` is a logical OR. Records match if **either** of the + following **two** criteria match. + * `!` is a logical NOT (negation). Records match if the + following **one** criterion does **NOT** match. + +Every criteria combination is implicitly combined using a logical AND +to form the overall filter to use to query records. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search( +... [ +... # Both user AND connected partner are active +... ("active", "=", True), +... ("active_partner", "=", True), +... # Name is either Lorem Ipsum or Alice Bob +... "|", +... ("name", "=", "Lorem Ipsum"), +... ("name", "=", "Alice Bob"), +... ], +... ) +[User(record={'id': 1234, 'name': 'Lorem Ipsum', ...}, fields=None), User(record={'id': 5678, 'name': 'Alice Bob', ...}, fields=None)] +``` + +For the field value, this method accepts the same types as defined +on the record objects. + +In addition to the native Odoo field names, field aliases +and model ref field names can be specified as the field name +in the search filter. Record objects can also be directly +passed as the value on a filter, not just record IDs. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> odoo_client.users.search([("create_user", "=", user)]) +[User(record={'id': 5678, ...}, fields=None), ...] +``` + +When specifying a range of possible values, lists, tuples +and sets are supported. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> user = odoo_client.users.get(1234) +>>> odoo_client.users.search([("create_user", "in", {user})]) +[User(record={'id': 5678, ...}, fields=None), ...] +``` To search *all* records, leave ``filters`` unset (or set it to ``None``). @@ -443,13 +524,13 @@ a list of `dict` objects, instead of record objects. #### Parameters -| Name | Type | Description | Default | -|-----------|------------------------|---------------------------------------------------|---------| -| `filters` | `Sequence[Any] | None` | Filters to query by (or `None` for no filters) | `None` | -| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | -| `order` | `str | None` | Field to order results by, if ordering results | `None` | -| `as_id` | `bool` | Return the record IDs only | `False` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | +| Name | Type | Description | Default | +|-----------|---------------------------------------------------------------|---------------------------------------------------|---------| +| `filters` | `Sequence[Tuple[str, str, Any] | Sequence[Any] | str] | None` | Filters to query by (or `None` for no filters) | `None` | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `order` | `str | None` | Field to order results by, if ordering results | `None` | +| `as_id` | `bool` | Return the record IDs only | `False` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | #### Returns @@ -520,10 +601,11 @@ Field aliases are also resolved to their target field names. 1234 ``` -By **nesting** a record mapping where an ID or object would normally go, -a new record will be created for that mapping, and linked to the outer record. -This nested record mapping is recursively validated and processed in the same way -as the outer record. +By **nesting** a record mapping where an ID or object would +normally go, a new record will be created for that mapping, +and linked to the outer record. +This nested record mapping is recursively validated and +processed in the same way as the outer record. ```python >>> from datetime import date @@ -1114,6 +1196,9 @@ User(record={'id': 1234, ...}, fields=None) The following attributes and methods are available on all record types. +To find the available attributes and methods on specific record classes, +check the manager page for the record type. + #### `id` ```python diff --git a/docs/managers/partner-category.md b/docs/managers/partner-category.md index 40820cc..7780a4a 100644 --- a/docs/managers/partner-category.md +++ b/docs/managers/partner-category.md @@ -45,6 +45,9 @@ from openstack_odooclient import PartnerCategory The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/partner.md b/docs/managers/partner.md index 46ee961..d2f08d2 100644 --- a/docs/managers/partner.md +++ b/docs/managers/partner.md @@ -45,6 +45,9 @@ from openstack_odooclient import Partner The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/pricelist.md b/docs/managers/pricelist.md index 2e6c13e..a02db62 100644 --- a/docs/managers/pricelist.md +++ b/docs/managers/pricelist.md @@ -92,6 +92,9 @@ from openstack_odooclient import Partner The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/product-category.md b/docs/managers/product-category.md index 7ccaa6e..e2476f6 100644 --- a/docs/managers/product-category.md +++ b/docs/managers/product-category.md @@ -45,6 +45,9 @@ from openstack_odooclient import ProductCategory The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `child_id` ```python diff --git a/docs/managers/product.md b/docs/managers/product.md index 5428513..4ecf004 100644 --- a/docs/managers/product.md +++ b/docs/managers/product.md @@ -250,6 +250,9 @@ from openstack_odooclient import Product The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `categ_id` ```python diff --git a/docs/managers/project-contact.md b/docs/managers/project-contact.md index 6df4dc2..9030bc9 100644 --- a/docs/managers/project-contact.md +++ b/docs/managers/project-contact.md @@ -45,6 +45,9 @@ from openstack_odooclient import ProjectContact The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `contact_type` ```python diff --git a/docs/managers/project.md b/docs/managers/project.md index b6308ce..d2695b0 100644 --- a/docs/managers/project.md +++ b/docs/managers/project.md @@ -45,6 +45,9 @@ from openstack_odooclient import Project The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `display_name` ```python diff --git a/docs/managers/referral-code.md b/docs/managers/referral-code.md index 435c56a..9a69b24 100644 --- a/docs/managers/referral-code.md +++ b/docs/managers/referral-code.md @@ -45,6 +45,9 @@ from openstack_odooclient import ReferralCode The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `allowed_uses` ```python diff --git a/docs/managers/reseller-tier.md b/docs/managers/reseller-tier.md index a984e67..0bbaec2 100644 --- a/docs/managers/reseller-tier.md +++ b/docs/managers/reseller-tier.md @@ -45,6 +45,9 @@ from openstack_odooclient import ResellerTier The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `discount_percent` ```python diff --git a/docs/managers/reseller.md b/docs/managers/reseller.md index e1734fb..1ba7c44 100644 --- a/docs/managers/reseller.md +++ b/docs/managers/reseller.md @@ -45,6 +45,9 @@ from openstack_odooclient import Reseller The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `alternative_billing_url` ```python diff --git a/docs/managers/sale-order-line.md b/docs/managers/sale-order-line.md index b5d48b2..57ccb5e 100644 --- a/docs/managers/sale-order-line.md +++ b/docs/managers/sale-order-line.md @@ -33,6 +33,21 @@ SaleOrderLine(record={'id': 1234, ...}, fields=None) For more information on how to use managers, refer to [Managers](index.md). +## Record + +The sale order line manager returns `SaleOrderLine` record objects. + +To import the record class for type hinting purposes: + +```python +from openstack_odooclient import SaleOrderLine +``` + +The record class currently implements the following fields and methods. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `company_id` ```python diff --git a/docs/managers/sale-order.md b/docs/managers/sale-order.md index 7c8a4e0..4e26fca 100644 --- a/docs/managers/sale-order.md +++ b/docs/managers/sale-order.md @@ -109,6 +109,9 @@ from openstack_odooclient import SaleOrder The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `amount_untaxed` ```python diff --git a/docs/managers/support-subscription-type.md b/docs/managers/support-subscription-type.md index ff88bd6..dd86e77 100644 --- a/docs/managers/support-subscription-type.md +++ b/docs/managers/support-subscription-type.md @@ -45,6 +45,9 @@ from openstack_odooclient import SupportSubscriptionType The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `billing_type` ```python diff --git a/docs/managers/support-subscription.md b/docs/managers/support-subscription.md index d520224..027c0a4 100644 --- a/docs/managers/support-subscription.md +++ b/docs/managers/support-subscription.md @@ -45,6 +45,9 @@ from openstack_odooclient import SupportSubscription The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `billing_type` ```python diff --git a/docs/managers/tax-group.md b/docs/managers/tax-group.md index 8603680..f3c2e90 100644 --- a/docs/managers/tax-group.md +++ b/docs/managers/tax-group.md @@ -45,6 +45,9 @@ from openstack_odooclient import TaxGroup The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `name` ```python diff --git a/docs/managers/tax.md b/docs/managers/tax.md index 430d411..3ecf28f 100644 --- a/docs/managers/tax.md +++ b/docs/managers/tax.md @@ -45,6 +45,9 @@ from openstack_odooclient import Tax The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/term-discount.md b/docs/managers/term-discount.md index ab415d4..a58d349 100644 --- a/docs/managers/term-discount.md +++ b/docs/managers/term-discount.md @@ -45,6 +45,9 @@ from openstack_odooclient import TermDiscount The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `discount_percent` ```python diff --git a/docs/managers/trial.md b/docs/managers/trial.md index df89550..41f8be1 100644 --- a/docs/managers/trial.md +++ b/docs/managers/trial.md @@ -45,6 +45,9 @@ from openstack_odooclient import Trial The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `account_suspended_date` ```python diff --git a/docs/managers/uom-category.md b/docs/managers/uom-category.md index 70c1292..4ef3490 100644 --- a/docs/managers/uom-category.md +++ b/docs/managers/uom-category.md @@ -45,6 +45,9 @@ from openstack_odooclient import UomCategory The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `measure_type` ```python diff --git a/docs/managers/uom.md b/docs/managers/uom.md index 22dc377..f2b63da 100644 --- a/docs/managers/uom.md +++ b/docs/managers/uom.md @@ -45,6 +45,9 @@ from openstack_odooclient import Uom The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/user.md b/docs/managers/user.md index 8f032c7..eef6f01 100644 --- a/docs/managers/user.md +++ b/docs/managers/user.md @@ -45,6 +45,9 @@ from openstack_odooclient import User The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `active` ```python diff --git a/docs/managers/volume-discount-range.md b/docs/managers/volume-discount-range.md index 1dd2b79..1ccb853 100644 --- a/docs/managers/volume-discount-range.md +++ b/docs/managers/volume-discount-range.md @@ -109,6 +109,9 @@ from openstack_odooclient import VolumeDiscountRange The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `customer_group_id` ```python diff --git a/docs/managers/voucher-code.md b/docs/managers/voucher-code.md index 74828a6..2709000 100644 --- a/docs/managers/voucher-code.md +++ b/docs/managers/voucher-code.md @@ -45,6 +45,9 @@ from openstack_odooclient import VoucherCode The record class currently implements the following fields and methods. +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + ### `claimed` ```python diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 2f8d866..26cd1f5 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -60,6 +60,7 @@ from .. import client Record = TypeVar("Record", bound=RecordBase) +FilterCriteria = Union[Tuple[str, str, Any], Sequence[Any], str] class RecordManagerBase(Generic[Record]): @@ -280,7 +281,7 @@ def get( @overload def search( self, - filters: Optional[Sequence[Any]] = ..., + filters: Optional[Sequence[FilterCriteria]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: Literal[False] = ..., @@ -290,7 +291,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[Any]] = ..., + filters: Optional[Sequence[FilterCriteria]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -301,7 +302,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[Any]] = ..., + filters: Optional[Sequence[FilterCriteria]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: Literal[False] = ..., @@ -312,7 +313,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[Any]] = ..., + filters: Optional[Sequence[FilterCriteria]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -323,7 +324,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[Any]] = ..., + filters: Optional[Sequence[FilterCriteria]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: bool = ..., @@ -332,7 +333,7 @@ def search( def search( self, - filters: Optional[Sequence[Any]] = None, + filters: Optional[Sequence[FilterCriteria]] = None, fields: Optional[Iterable[str]] = None, order: Optional[str] = None, as_id: bool = False, @@ -342,16 +343,42 @@ def search( filters to constrain the search and other parameters, and return the results. - Query filters should be defined using the same format as OdooRPC, - but some additional features are supported: + Query filters should be defined using the ORM API search domain + format, which is a sequence of criteria, where each criterion + is one of the following types of values: - * Odoo client field aliases can be specified as the field name, - in additional to the original field name on the Odoo model - (e.g. ``create_user`` instead of ``create_uid``). - * Record objects can be directly passed as the value - on a filter, where a record ID would normally be expected. - * Sets and tuples are supported when specifying a range of values, - in addition to lists. + * A 3-tuple or 3-element sequence in ``(field_name, operator, value)`` + format, where: + + * ``field_name`` (``str``) is the the name of the field to filter by. + * ``operator`` (`str`) is the comparison operator to use (for more + information on the available operators, check the ORM API + search domain documentation). + * ``value`` (`Any`) is the value to compare records against. + + * A logical operator which prefixes the following filter criteria + to form a **criteria combination**: + + * ``&`` is a logical AND. Records only match if **both** of the + following **two** criteria match. + * ``|`` is a logical OR. Records match if **either** of the + following **two** criteria match. + * ``!`` is a logical NOT (negation). Records match if the + following **one** criterion does **NOT** match. + + Every criteria combination is implicitly combined using a logical AND + to form the overall filter to use to query records. + + For the field value, this method accepts the same types as defined + on the record objects. + + In addition to the native Odoo field names, field aliases + and model ref field names can be specified as the field name + in the search filter. Record objects can also be directly + passed as the value on a filter, not just record IDs. + + When specifying a range of possible values, lists, tuples + and sets are supported. To search *all* records, leave ``filters`` unset (or set it to ``None``). @@ -367,7 +394,7 @@ def search( a list of ``dict`` objects, instead of record objects. :param filters: Filters to query by, defaults to ``None`` (no filters) - :type filters: Sequence[Any] or None, optional + :type filters: Union[Tuple[str, str, Any], Sequence[Any], str] | None :param fields: Fields to select, defaults to ``None`` (select all) :type fields: Iterable[int] or None, optional :param order: Order results by field name, defaults to ``None`` @@ -389,16 +416,21 @@ def search( return self.list(ids, fields=fields, as_dict=as_dict) return [] # type: ignore[return-value] - def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: - _filters: List[Any] = [] + def _encode_filters( + self, + filters: Sequence[FilterCriteria], + ) -> List[Union[str, Tuple[str, str, Any]]]: + _filters: List[Union[str, Tuple[str, str, Any]]] = [] type_hints = get_type_hints(self.record_class, include_extras=True) for f in filters: - if isinstance(f, tuple): + if isinstance(f, str): + _filters.append(f) + else: field_type, field_name = self._encode_filter_field( type_hints=type_hints, field=f[0], ) - operator = f[1] + operator: str = f[1] # NOTE(callumdickinson): ORM API search domains. # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains if operator in ("in", "not in"): @@ -423,10 +455,7 @@ def _encode_filters(self, filters: Sequence[Any]) -> List[Any]: type_hint=field_type, value=f[2], ) - _filter = (field_name, operator, value) - else: - _filter = f - _filters.append(_filter) + _filters.append((field_name, operator, value)) return _filters def _encode_filter_field( @@ -491,6 +520,26 @@ def create(self, **fields) -> int: """Create a new record, using the specified keyword arguments as input fields. + This method allows a lot of flexibility in how input fields + should be defined. + + The fields passed to this method should use the same field names + and value types that are defined on the record classes. + The Odoo Client library will convert the values to the formats + that the Odoo API expects. + + For example, when defining references to another record, + you can either pass the record ID, or the record object. + The field name can also either be for the ID or the object. + + Field aliases are also resolved to their target field names. + + By nesting a record mapping where an ID or object would + normally go, a new record will be created for that mapping, + and linked to the outer record. + This nested record mapping is recursively validated and + processed in the same way as the outer record. + To fetch the newly created record object, pass the returned ID to the ``get`` method. From 4d7d8cdd91255bd1bfc3da71181f068a3c5d06ff Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 19 Jun 2024 18:15:47 +1200 Subject: [PATCH 45/87] Add and improve more docs, make returning all IDs required by default for the record manager list method --- README.md | 2 +- docs/changelog.md | 5 ++ docs/index.md | 2 +- docs/managers/index.md | 81 +++++++++++++++++-- mkdocs.yml | 1 + openstack_odooclient/base/client.py | 9 ++- openstack_odooclient/base/record.py | 40 ++++++++- openstack_odooclient/base/record_manager.py | 62 +++++++++++--- .../base/record_manager_coded.py | 11 +++ .../base/record_manager_named.py | 11 +++ .../base/record_manager_with_unique_field.py | 25 ++++++ openstack_odooclient/client.py | 2 +- 12 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 docs/changelog.md diff --git a/README.md b/README.md index 4f69e05..c3364cd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ intended to be used by OpenStack projects such as This library provides a higher level interface than [OdooRPC](https://pythonhosted.org/OdooRPC) (which is used internally), and is intended to make it possible to develop applications against -a common API, without having to take into account considerations such as backward-incompatible +a well-defined API, without having to take into account considerations such as backward-incompatible changes between Odoo versions. ## Installation diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..0f14ccd --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ +# Changelog + +## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) (2024-06-19) + +Initial release of the OpenStack Odoo Client library for Python. diff --git a/docs/index.md b/docs/index.md index 2c2a0d8..21dec6d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ intended to be used by OpenStack projects such as This library provides a higher level interface than [OdooRPC](https://pythonhosted.org/OdooRPC) (which is used internally), and is intended to make it possible to develop applications against -a common API, without having to take into account considerations such as backward-incompatible +a well-defined API, without having to take into account considerations such as backward-incompatible changes between Odoo versions. ## Installation diff --git a/docs/managers/index.md b/docs/managers/index.md index f176763..6712862 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -67,6 +67,7 @@ list( ids: int | Iterable[int], fields: Iterable[str] | None = None, as_dict: bool = False, + optional: bool = False, ) -> list[Record] ``` @@ -75,6 +76,7 @@ list( ids: int | Iterable[int], fields: Iterable[str] | None = None, as_dict: bool = True, + optional: bool = False, ) -> list[dict[str, Any]] ``` @@ -96,6 +98,42 @@ Get one or more specific records by ID. [User(record={'id': 1234, ...}, fields=None), User(record={'id': 5678, ...}, fields=None)] ``` +By default, the method checks that all provided IDs +were returned (and will raise an error if any are missing), +at the cost of a small performance hit. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(999999) +... +openstack_odooclient.exceptions.RecordNotFoundError: User records with IDs not found: 999999 +``` + +To instead return the list of records that were found +without raising an error, set `optional` to `True`. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.list(999999, optional=True) +[] +``` + By default all fields available on the record model will be selected, but this can be filtered using the `fields` parameter. @@ -150,11 +188,18 @@ returns an empty list. #### Parameters -| Name | Type | Description | Default | -|-----------|-------------------------|---------------------------------------------------|------------| -| `ids` | `int | Iterable[int]` | Record ID, or list of record IDs | (required) | -| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| +| `ids` | `int | Iterable[int]` | Record ID, or list of record IDs | (required) | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `required` | `bool` | Check if all provided records IDs were found | `False` | + +#### Raises + +| Type | Description | +|-----------------------|---------------------------------------------------------------------------| +| `RecordNotFoundError` | If any of the given record IDs were not found (when `required` is `True`) | #### Returns @@ -453,6 +498,32 @@ and sets are supported. [User(record={'id': 5678, ...}, fields=None), ...] ``` +Search criteria using nested field references can be defined +by using the dot-notation (`.`) to specify what field on what +record reference to check. +Field names and values for nested field references are +validated and encoded just like criteria for standard +field references. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.users.search( +... [ +... # Check that the "name" field inside "create_user" matches +... ("create_user.name", "=", "Lorem Ipsum"), +... ], +... ) +[User(record={'id': 5678, ...}, fields=None), ...] +``` + To search *all* records, leave ``filters`` unset (or set it to ``None``). diff --git a/mkdocs.yml b/mkdocs.yml index 7519907..e36c219 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - managers/volume-discount-range.md - managers/voucher-code.md - managers/custom.md + - changelog.md theme: name: material diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py index 869019e..2400987 100644 --- a/openstack_odooclient/base/client.py +++ b/openstack_odooclient/base/client.py @@ -38,7 +38,7 @@ class ClientBase: - """A client base class for managing the OpenStack Odoo ERP. + """The client base class for managing the OpenStack Odoo ERP. No managers are included on this base class. This class should be inherited, and manager classes defined @@ -182,6 +182,13 @@ def __init__( if is_subclass(attr_type, RecordManagerBase): setattr(self, attr_name, attr_type(self)) + @property + def odoo(self) -> ODOO: + """The OdooRPC connection object currently being used + by this client. + """ + return self._odoo + @property def db(self) -> DB: """The database management service.""" diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index 45ecd61..d6dcf26 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -87,8 +87,18 @@ def is_annotated(cls, type_hint: Any) -> bool: @dataclass(frozen=True) class FieldAlias(AnnotationBase): - """An annotation for alias attributes to define the Odoo field name - the attribute is an alias for. + """An annotation for defining field aliases + (fields that point to other fields). + + Aliases are automatically resolved to the target field + when searching or creating records, or referencing field values + on record objects. + + >>> from typing_extensions import Annotated + >>> from openstack_odooclient import FieldAlias, RecordBase + >>> class CustomRecord(RecordBase): + ... name: str + ... name_alias: Annotated[str, FieldAlias("name")] """ field: str @@ -96,8 +106,23 @@ class FieldAlias(AnnotationBase): @dataclass(frozen=True) class ModelRef(AnnotationBase): - """An annotation for attributes that decode an Odoo model reference, - to define the Odoo field name to be decoded. + """An annotation for defining model refs + (fields that provide an interface to a model reference on a record). + + Model refs are used to express relationships between record types. + The first argument is the name of the relationship field in Odoo, + the second argument is the record class that type is represented by + in the OpenStack Odoo Client library. + + >>> from typing_extensions import Annotated + >>> from openstack_odooclient import ModelRef, RecordBase, User + >>> class CustomRecord(RecordBase): + ... user_id: Annotated[int, ModelRef("user_id", User)] + ... user_name: Annotated[str, ModelRef("user_id", User)] + ... user: Annotated[User, ModelRef("user_id", User)] + + For more information, check the OpenStack Odoo Client + library documentation. """ field: str @@ -105,6 +130,11 @@ class ModelRef(AnnotationBase): class RecordBase: + """The base class for records. + + Subclass this class to implement the record class for custom record types. + """ + id: int """The record's ID in Odoo.""" @@ -168,10 +198,12 @@ def __init__( @property def _odoo(self) -> ODOO: + """The OdooRPC connection object this record was created from.""" return self._client._odoo @property def _env(self) -> Environment: + """The OdooRPC environment object this record was created from.""" return self._manager._env @classmethod diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 26cd1f5..b669373 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -92,35 +92,42 @@ def __init__(self, client: client.Client) -> None: @property def _odoo(self) -> ODOO: + """The OdooRPC connection object this record manager uses.""" return self._client._odoo @property def _env(self) -> Environment: + """The OdooRPC environment object this record manager uses.""" return self._odoo.env[self.env_name] @overload def list( self, ids: Union[int, Iterable[int]], + *, fields: Optional[Iterable[str]] = ..., as_dict: Literal[False] = ..., + optional: bool = ..., ) -> List[Record]: ... @overload def list( self, ids: Union[int, Iterable[int]], - fields: Optional[Iterable[str]] = ..., *, + fields: Optional[Iterable[str]] = ..., as_dict: Literal[True], + optional: bool = ..., ) -> List[Dict[str, Any]]: ... @overload def list( self, ids: Union[int, Iterable[int]], + *, fields: Optional[Iterable[str]] = ..., as_dict: bool = ..., + optional: bool = ..., ) -> Union[List[Record], List[Dict[str, Any]]]: ... def list( @@ -128,6 +135,7 @@ def list( ids: Union[int, Iterable[int]], fields: Optional[Iterable[str]] = None, as_dict: bool = False, + optional: bool = False, ) -> Union[List[Record], List[Dict[str, Any]]]: """Get one or more specific records by ID. @@ -138,6 +146,12 @@ def list( Use the ``as_dict`` parameter to return records as ``dict`` objects, instead of record objects. + By default, the method checks that all provided IDs + were returned (and will raise an error if any are missing), + at the cost of a small performance hit. + To instead return the list of records that were found + without raising an error, set ``optional`` to ``True``. + If ``ids`` is given an empty iterator, this method returns an empty list. @@ -147,6 +161,9 @@ def list( :type fields: Optional[Iterable[str]], optional :param as_dict: Return records as dictionaries, defaults to ``False`` :type as_dict: bool, optional + :param optional: Disable missing record errors, defaults to ``False`` + :type optional: bool, optional + :raises RecordNotFoundError: If IDs are required but some are missing :return: List of records :rtype: list[Record] or list[dict[str, Any]] """ @@ -171,22 +188,40 @@ def list( fields=_fields, ) if as_dict: - return [ + res_dicts = [ { self._get_local_field(field): value for field, value in record_dict.items() } for record_dict in records ] - return [ - self.record_class( - client=self._client, - manager=self, - record=record, - fields=_fields, + else: + res_objs = [ + self.record_class( + client=self._client, + manager=self, + record=record, + fields=_fields, + ) + for record in records + ] + if not optional: + required_ids = {_ids} if isinstance(_ids, int) else set(_ids) + found_ids: Set[int] = ( + set(record["id"] for record in res_dicts) + if as_dict + else set(record.id for record in res_objs) ) - for record in records - ] + missing_ids = required_ids - found_ids + if missing_ids: + raise RecordNotFoundError( + ( + f"{self.record_class.__name__} records " + "with IDs not found: " + f"{', '.join(str(i) for i in sorted(missing_ids))}" + ), + ) + return res_dicts if as_dict else res_objs @overload def get( @@ -380,6 +415,13 @@ def search( When specifying a range of possible values, lists, tuples and sets are supported. + Search criteria using nested field references can be defined + by using the dot-notation (``.``) to specify what field on what + record reference to check. + Field names and values for nested field references are + validated and encoded just like criteria for standard + field references. + To search *all* records, leave ``filters`` unset (or set it to ``None``). diff --git a/openstack_odooclient/base/record_manager_coded.py b/openstack_odooclient/base/record_manager_coded.py index b1f5ee8..1c7f858 100644 --- a/openstack_odooclient/base/record_manager_coded.py +++ b/openstack_odooclient/base/record_manager_coded.py @@ -34,6 +34,17 @@ class CodedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): + """A record manager base class for record types with a code field. + + This code field is reasonably expected to be unique, which allows + for methos for getting records by name to be defined. + + The record class should be type hinted with the field to use as the code, + just like any other field. + Configure the name of the code field on the manager class by defining + the ``code_field`` attribute (set to ``code`` by default). + """ + code_field: str = "code" """The field code to use when querying by code in the ``get_by_code`` method. diff --git a/openstack_odooclient/base/record_manager_named.py b/openstack_odooclient/base/record_manager_named.py index 2e0a4f1..25c4484 100644 --- a/openstack_odooclient/base/record_manager_named.py +++ b/openstack_odooclient/base/record_manager_named.py @@ -34,6 +34,17 @@ class NamedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): + """A record manager base class for record types with a name field. + + This name field is reasonably expected to be unique, which allows + for methods for getting records by name to be defined. + + The record class should be type hinted with the field to use as the name, + just like any other field. + Configure the name of the name field on the manager class by defining + the ``name_field`` attribute (set to ``name`` by default). + """ + name_field: str = "name" """The field name to use when querying by name in the ``get_by_name`` method. diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index 0df93c0..1383689 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -39,6 +39,31 @@ class RecordManagerWithUniqueFieldBase( RecordManagerBase[Record], Generic[Record, T], ): + """A generic record manager base class for defining a record class + with a searchable unique field. + + In addition to the usual generic type arg, a second type arg + should be provided when subclassing ``RecordManagerWithUniqueFieldBase``. + This becomes the expected type of the searchable unique field. + + >>> from openstack_odooclient import ( + ... RecordBase, + ... RecordManagerWithUniqueFieldBase, + ... ) + >>> class CustomRecord(RecordBase): + ... name: str + >>> class CustomRecordManager( + ... RecordManagerWithUniqueFieldBase[Record, str], + ... ): + ... env_name = "custom.record" + ... record_class = CustomRecord + + + Once you have your manager class, you can define methods + that use the provided ``_get_by_unique_field`` method to implement + custom search functionality according to your needs. + """ + @overload def _get_by_unique_field( self, diff --git a/openstack_odooclient/client.py b/openstack_odooclient/client.py index 14ddb79..fd41852 100644 --- a/openstack_odooclient/client.py +++ b/openstack_odooclient/client.py @@ -52,7 +52,7 @@ class Client(ClientBase): - """The client for managing the OpenStack Odoo ERP. + """A client for managing the OpenStack Odoo ERP. Connect to an Odoo server by either passing the required connection and authentication information, From fc1c2a8ac5dea954b832bf5b043935ed539f025f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 19 Jun 2024 18:22:44 +1200 Subject: [PATCH 46/87] Fix list docs section, set optional to True on list method call in get --- docs/managers/index.md | 56 ++++++++++----------- openstack_odooclient/base/record_manager.py | 9 +++- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docs/managers/index.md b/docs/managers/index.md index 6712862..fbacd92 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -98,9 +98,9 @@ Get one or more specific records by ID. [User(record={'id': 1234, ...}, fields=None), User(record={'id': 5678, ...}, fields=None)] ``` -By default, the method checks that all provided IDs -were returned (and will raise an error if any are missing), -at the cost of a small performance hit. +By default all fields available on the record model +will be selected, but this can be filtered using the +`fields` parameter. ```python >>> from openstack_odooclient import Client as OdooClient @@ -112,13 +112,12 @@ at the cost of a small performance hit. ... user="test-user", ... password="", ... ) ->>> odoo_client.users.list(999999) -... -openstack_odooclient.exceptions.RecordNotFoundError: User records with IDs not found: 999999 +>>> odoo_client.users.list(1234, fields={"ids"}) +[User(record={'id': 1234}, fields=['ids'])] ``` -To instead return the list of records that were found -without raising an error, set `optional` to `True`. +Use the `as_dict` parameter to return records as `dict` +objects, instead of record objects. ```python >>> from openstack_odooclient import Client as OdooClient @@ -130,13 +129,13 @@ without raising an error, set `optional` to `True`. ... user="test-user", ... password="", ... ) ->>> odoo_client.users.list(999999, optional=True) -[] +>>> odoo_client.users.list(1234, as_dict=True) +[{'id': 1234, ...}] ``` -By default all fields available on the record model -will be selected, but this can be filtered using the -`fields` parameter. +By default, the method checks that all provided IDs +were found and returned (and will raise an error if any are missing), +at the cost of a small performance hit. ```python >>> from openstack_odooclient import Client as OdooClient @@ -148,12 +147,13 @@ will be selected, but this can be filtered using the ... user="test-user", ... password="", ... ) ->>> odoo_client.users.list(1234, fields={"ids"}) -[User(record={'id': 1234}, fields=['ids'])] +>>> odoo_client.users.list(999999) +... +openstack_odooclient.exceptions.RecordNotFoundError: User records with IDs not found: 999999 ``` -Use the `as_dict` parameter to return records as `dict` -objects, instead of record objects. +To instead return the list of records that were found +without raising an error, set `optional` to `True`. ```python >>> from openstack_odooclient import Client as OdooClient @@ -165,8 +165,8 @@ objects, instead of record objects. ... user="test-user", ... password="", ... ) ->>> odoo_client.users.list(1234, as_dict=True) -[{'id': 1234, ...}] +>>> odoo_client.users.list(999999, optional=True) +[] ``` If `ids` is given an empty iterator, this method @@ -188,18 +188,18 @@ returns an empty list. #### Parameters -| Name | Type | Description | Default | -|------------|------------------------|---------------------------------------------------|------------| -| `ids` | `int | Iterable[int]` | Record ID, or list of record IDs | (required) | -| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | -| `as_dict` | `bool` | Return records as dictionaries | `False` | -| `required` | `bool` | Check if all provided records IDs were found | `False` | +| Name | Type | Description | Default | +|------------|------------------------|-----------------------------------------------------|------------| +| `ids` | `int | Iterable[int]` | Record ID, or list of record IDs | (required) | +| `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | +| `as_dict` | `bool` | Return records as dictionaries | `False` | +| `optional` | `bool` | Do not raise an error if not all records were found | `False` | #### Raises -| Type | Description | -|-----------------------|---------------------------------------------------------------------------| -| `RecordNotFoundError` | If any of the given record IDs were not found (when `required` is `True`) | +| Type | Description | +|-----------------------|----------------------------------------------------------------------------| +| `RecordNotFoundError` | If any of the given record IDs were not found (when `optional` is `False`) | #### Returns diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index b669373..21f9aed 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -147,7 +147,7 @@ def list( objects, instead of record objects. By default, the method checks that all provided IDs - were returned (and will raise an error if any are missing), + were found and returned (and will raise an error if any are missing), at the cost of a small performance hit. To instead return the list of records that were found without raising an error, set ``optional`` to ``True``. @@ -301,7 +301,12 @@ def get( :rtype: Union[Record, List[str, Any]] """ try: - return self.list(id, fields=fields, as_dict=as_dict)[0] + return self.list( + id, + fields=fields, + as_dict=as_dict, + optional=True, + )[0] except IndexError: if optional: return None From 107d12ee21b189c0d971a625ed698cb7eb741732 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 09:34:59 +1200 Subject: [PATCH 47/87] Add "Performance Considerations" page --- docs/performance.md | 177 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 178 insertions(+) create mode 100644 docs/performance.md diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..8daf800 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,177 @@ +# Performance Considerations + +The OpenStack Odoo Client library uses [OdooRPC](https://pythonhosted.org/OdooRPC) +to communicate with Odoo. OdooRPC performs synchronous RPC requests, some of which +can take a long time to execute depending on what work is being done. + +Optimising performance when interfacing with Odoo mostly revolves around minimising +the number of requests, and reducing the amount of data selected to the minimum +necessary. The OpenStack Odoo Client library offers a few ways of doing this. + +## Selecting Fields + +By default, all fields on a record are selected when performing queries. + +When querying a lot of records this can mean a lot more data is being selected, +serialised and deserialised than necessary, causing requests to take longer +than they need to. + +```python +>>> from datetime import datetime +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> before_dt = datetime.now() +>>> odoo_client.products.get_sellable_company_products(1234) +>>> print(f"{(datetime.now() - before_dt).total_seconds():.1f} seconds") +2.1 seconds +``` + +If requests are taking longer than expected, try using the `fields` parameter on the +query function to limit the selected field to only the fields required for the task. + +```python +>>> from datetime import datetime +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> before_dt = datetime.now() +>>> odoo_client.products.get_sellable_company_products(1234, fields={"name", "default_code"}) +>>> print(f"{(datetime.now() - before_dt).total_seconds():.1f} seconds") +0.4 seconds +``` + +## Clustering Queries + +The Odoo Client library provides nested model references on record objects, +which makes it easier to interface with Odoo records in a more Pythonic manner. + +However, using them results in the application making a number of smaller +requests to Odoo to retrieve records, which is not very efficient. +Depending on the use case, other ways of querying records are preferable. + +```python +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> invoice = odoo_client.account_moves.get(1234) +>>> for invoice_line in invoice.invoice_lines: +... print( +... ( +... f"{invoice_line.name}" +... f"- {invoice_line.product.name}" +... f" - {invoice_line.quantity} {invoice_line.product.default_code}" +... f" - {invoice.price_subtotal}" +... ), +... ) +... +test-instance-1 - m1.small - 1.0 hour - 0.012 +test-instance - m1.small - 744.0 hour - 8.928 +``` + +In the above example, [`invoice_line.product`](managers/account-move-line.md#product), +which is a [product](managers/product.md) model ref within an +[account move (invoice) line](managers/account-move-line.md), would be +individually queried for each invoice line in the loop, increasing the runtime +of the task. + +Some record types, such as products as shown above, are commonly referenced in +relationships in a number of other record types. For these record types, +it is usually more efficient to fetch all of them in a single dedicated query, +save the resulting objects, and reference the objects using the record's IDs. + +```python +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> products = { +... p.id: p +... for p in odoo_client.products.get_sellable_company_products( +... company=5678, +... ) +... } +>>> invoice = odoo_client.account_moves.get(1234) +>>> for invoice_line in invoice.invoice_lines: +... product = products[invoice_line.product_id] +... print( +... ( +... f"{invoice_line.name}" +... f"- {product.name}" +... f" - {invoice_line.quantity} {product.default_code}" +... f" - {invoice.price_subtotal}" +... ), +... ) +... +test-instance-1 - m1.small - 1.0 hour - 0.012 +test-instance - m1.small - 744.0 hour - 8.928 +``` + +## Creating Records + +In many cases multiple records need to be created that have a relationship +with each other. + +In the below example an empty sale order is created, with a sale order line +then being created and linked to that sale order. Since a sale order +can have multiple sale order lines, creating sale orders this way can be +inefficient. + +```python +>>> from datetime import date +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> order = odoo_client.sales_orders.create( +... user=5678, +... partner=9012, +... os_invoice_date=date(2024, 6, 30), +... os_invoice_due_date=date(2024, 7, 20), +... os_project=3456, +... order_lines=[], +... ) +>>> odoo_client.sale_order_lines.create( +... name="test-instance", +... product=7890, +... product_uom=123456, +... product_uom_qty=1.0, +... price_unit=0.05, +... os_project=3456, +... os_resource_id="1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a", +... os_region="RegionOne", +... os_resource_type="Virtual Machine", +... os_resource_name="m1.small", +... order=order, +... ) +789012 +``` + +If the contents of the sale order lines are known before the sale order is +created, this can be optimised by nesting the sale order lines within the +[`create`](managers/index.md#create) method call for the sale order itself, +as shown below. + +All of the child records will be created and linked to the parent record, +in a single request. + +```python +>>> from datetime import date +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> odoo_client.sales_orders.create( +... user=5678, +... partner=9012, +... os_invoice_date=date(2024, 6, 30), +... os_invoice_due_date=date(2024, 7, 20), +... os_project=3456, +... order_lines=[ +... { +... "name": "test-instance", +... "product": 7890, +... "product_uom": 123456, +... "product_uom_qty": 1.0, +... "price_unit": 0.05, +... "os_project": 3456, +... "os_resource_id": "1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a", +... "os_region": "RegionOne", +... "os_resource_type": "Virtual Machine", +... "os_resource_name": "m1.small", +... }, +... ], +... ) +1234 +``` diff --git a/mkdocs.yml b/mkdocs.yml index e36c219..97a0f60 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ nav: - managers/volume-discount-range.md - managers/voucher-code.md - managers/custom.md + - performance.md - changelog.md theme: From 17361631b0dfc7a450a7197c599ee7d74d9dcda0 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 09:35:59 +1200 Subject: [PATCH 48/87] function -> method --- docs/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/performance.md b/docs/performance.md index 8daf800..8dc9e43 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -27,7 +27,7 @@ than they need to. ``` If requests are taking longer than expected, try using the `fields` parameter on the -query function to limit the selected field to only the fields required for the task. +query method to limit the selected field to only the fields required for the task. ```python >>> from datetime import datetime From b0f9988fffdf8e3cbb0369c3c6d69d43d6b3af41 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 09:40:05 +1200 Subject: [PATCH 49/87] Add more links --- docs/performance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index 8dc9e43..e9b6b26 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -107,10 +107,10 @@ test-instance - m1.small - 744.0 hour - 8.928 In many cases multiple records need to be created that have a relationship with each other. -In the below example an empty sale order is created, with a sale order line -then being created and linked to that sale order. Since a sale order -can have multiple sale order lines, creating sale orders this way can be -inefficient. +In the below example an empty [sale order](managers/sale-order.md) is created, +with a [sale order line](managers/sale-order-line.md) then being created and +linked to that sale order. Since a sale order can have multiple sale order +lines, creating sale orders this way can be inefficient. ```python >>> from datetime import date From 30d061a7a4251d43fc30c9dee618aa486612f893 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 10:57:59 +1200 Subject: [PATCH 50/87] Resolve model refs in _(decode|encode)_field --- openstack_odooclient/base/record_manager.py | 103 +++++++++++++------- 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 21f9aed..c56bd3d 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -79,6 +79,15 @@ class RecordManagerBase(Generic[Record]): def __init__(self, client: client.Client) -> None: self._client = client + """Odoo Client object.""" + # Assign this record manager object as the manager + # responsible for the configured record class in the client. + self._client._record_manager_mapping[self.record_class] = self + self._record_type_hints = get_type_hints( + self.record_class, + include_extras=True, + ) + """The type hints for the fields defined in the record class.""" self._field_mapping_reverse = { odoo_version: { remote_field: local_field @@ -88,7 +97,32 @@ def __init__(self, client: client.Client) -> None: self.record_class._field_mapping.items() ) } - self._client._record_manager_mapping[self.record_class] = self + """Dynamically generated "reverse" field mapping for the + record class, mapping Odoo version-specific remote field names + to their representations on the record class. + """ + self._model_ref_mapping: Dict[str, str] = {} + """Mapping of the remote field name for a model ref + to the local field name representing the model ref's IDs. + + Examples: + + * (remote) ``product_id`` -> ``product_id`` (local) + * (remote) ``child_id`` -> ``child_ids`` (local) + * (remote) ``os_project`` -> ``os_project_id`` (local) + """ + for local_field, type_hint in self._record_type_hints.items(): + model_ref = ModelRef.get(type_hint) + if model_ref: + field_type = get_type_args(type_hint)[0] + try: + if field_type is int or ( + get_type_origin(field_type) is list + and get_type_args(field_type)[0] is int + ): + self._model_ref_mapping[model_ref.field] = local_field + except IndexError: + pass @property def _odoo(self) -> ODOO: @@ -468,15 +502,11 @@ def _encode_filters( filters: Sequence[FilterCriteria], ) -> List[Union[str, Tuple[str, str, Any]]]: _filters: List[Union[str, Tuple[str, str, Any]]] = [] - type_hints = get_type_hints(self.record_class, include_extras=True) for f in filters: if isinstance(f, str): _filters.append(f) else: - field_type, field_name = self._encode_filter_field( - type_hints=type_hints, - field=f[0], - ) + field_type, field_name = self._encode_filter_field(field=f[0]) operator: str = f[1] # NOTE(callumdickinson): ORM API search domains. # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains @@ -505,11 +535,7 @@ def _encode_filters( _filters.append((field_name, operator, value)) return _filters - def _encode_filter_field( - self, - type_hints: Mapping[str, Any], - field: str, - ) -> Tuple[Any, str]: + def _encode_filter_field(self, field: str) -> Tuple[Any, str]: # The field reference in a filter may be nested. # Split the reference by the delimiter (.), # so we can perform a recursive lookup of the correct field @@ -524,9 +550,9 @@ def _encode_filter_field( if len(field_refs) > 1: local_field = self._decode_field(field_refs[0]) remote_field = self._encode_field(field_refs[0]) - if local_field not in type_hints: + if local_field not in self._record_type_hints: return (Any, f"{remote_field}.{'.'.join(field_refs[1:])}") - type_hint: Any = type_hints[local_field] + type_hint: Any = self._record_type_hints[local_field] model_ref = ModelRef.get(type_hint) if model_ref: record_class: Type[RecordBase] = ( @@ -538,10 +564,6 @@ def _encode_filter_field( self._client._record_manager_mapping[ record_class # type: ignore[index] ]._encode_filter_field( - type_hints=get_type_hints( - record_class, - include_extras=True, - ), field=".".join(field_refs[1:]), ) ) @@ -550,14 +572,15 @@ def _encode_filter_field( # Base base: The field reference is not nested # (references a local field on this manager's record class.) # Fetch the local and remote representations of the given field. - # Field aliases are resolved at this point. + # Field aliases and model ref target fields are resolved + # at this point. local_field = self._decode_field(field) remote_field = self._encode_field(field) # If there is no type hint defined for the given field, # return the Any type to denote that no processing should be done. - if local_field not in type_hints: + if local_field not in self._record_type_hints: return (Any, remote_field) - type_hint = type_hints[local_field] + type_hint = self._record_type_hints[local_field] # If the type hint is annotated, get the original data type. if get_type_origin(type_hint) is Annotated: return (get_type_args(type_hint)[0], remote_field) @@ -622,10 +645,8 @@ def _encode_create_fields( ) -> Dict[str, Any]: create_fields: Dict[str, Any] = {} field_remote_mapping: Dict[str, str] = {} - type_hints = get_type_hints(self.record_class, include_extras=True) for field, value in fields.items(): remote_field, remote_value = self._encode_create_field( - type_hints=type_hints, field=field, value=value, ) @@ -644,26 +665,25 @@ def _encode_create_fields( def _encode_create_field( self, - type_hints: Mapping[str, Any], field: str, value: Any, ) -> Tuple[str, Any]: # Fetch the local and remote representations of the given field. - # Field aliases are resolved at this point. + # Field aliases and model ref target fields are resolved + # at this point. local_field = self._decode_field(field) remote_field = self._encode_field(field) # If there is no type hint for the given field, map the value # to the field unchanged. - if local_field not in type_hints: + if local_field not in self._record_type_hints: return (remote_field, value) # Fetch the type hint for parsing. - type_hint = type_hints[local_field] + type_hint = self._record_type_hints[local_field] # If this field is a model ref, encode the model ref # according to the given value's type, and map the result # to the Odoo model's ref field name. model_ref = ModelRef.get(type_hint) if model_ref: - attr_type: Any = get_type_args(type_hint)[0] # NOTE(callumdickinson): JSON RPC API model link reference. # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#odoo.models.Model.write # * (0, 0, {values}) - Link to a new record that needs to @@ -683,14 +703,14 @@ def _encode_create_field( # * (6, 0, [ids]) - Replace the list of linked IDs # with *ids*. Functions like using (5), then (4, id) # for each ID in the list of IDs. - model_ref_field = self._get_remote_field(model_ref.field) + attr_type: Any = get_type_args(type_hint)[0] # If the field is a list of multiple model refs, # iterate over the given value and decode the elements # appropriately. if get_type_origin(attr_type) is list: value_type = get_type_args(attr_type)[0] if not value: - return (model_ref_field, []) + return (remote_field, []) remote_values: List[ Union[ Tuple[int, int], @@ -722,16 +742,16 @@ def _encode_create_field( f"when creating record: {v}" ), ) - return (model_ref_field, remote_values) + return (remote_field, remote_values) # If the value type is an integer, treat it as a record ID # and assign i to the field. if isinstance(value, int): - return (model_ref_field, value) + return (remote_field, value) # If the value type is a record object, then treat it as if # it already exists on Odoo, and return the record ID to assign # to the field. if isinstance(value, RecordBase): - return (model_ref_field, value.id) + return (remote_field, value.id) # If the value type is a dictionary, then treat it as a nested # record to be created alongside the parent record. # Encode the contents of the dict recursively using the @@ -745,7 +765,7 @@ def _encode_create_field( else self._client._record_manager_mapping[value_type] ) return ( - model_ref_field, + remote_field, [ ( 0, @@ -809,6 +829,14 @@ def delete( self.unlink(*records) def _get_remote_field(self, field: str) -> str: + # If the field is a model ref, use the reference field name + # as the remote field. + if field in self._record_type_hints: + model_ref = ModelRef.get(self._record_type_hints[field]) + if model_ref: + field = model_ref.field + # Map the local field to the correct remote field name + # based on the version of the Odoo server. return get_mapped_field( field_mapping=self.record_class._field_mapping, odoo_version=self._odoo.version, @@ -816,11 +844,18 @@ def _get_remote_field(self, field: str) -> str: ) def _get_local_field(self, field: str) -> str: - return get_mapped_field( + # Map the remote field to the correct local field name + # based on the version of the Odoo server. + local_field = get_mapped_field( field_mapping=self._field_mapping_reverse, odoo_version=self._odoo.version, field=field, ) + # If the field is a model ref, find the local field + # presenting the model ref's record IDs. + if local_field in self._model_ref_mapping: + return self._model_ref_mapping[local_field] + return local_field def _resolve_alias(self, alias: str) -> str: return self.record_class._resolve_alias(alias) From 2ab43e132a45befe7fbcca73a75455b8df25c995 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 11:12:36 +1200 Subject: [PATCH 51/87] Fix mapping record class to manager --- openstack_odooclient/base/record_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index c56bd3d..c33fe5e 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -728,7 +728,7 @@ def _encode_create_field( self if value_type is Self else self._client._record_manager_mapping[ - value_type + model_ref.record_class ] ) remote_values.append( From ee7b4bd27457126bcd86ea12bd9143fafff1252f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 11:36:33 +1200 Subject: [PATCH 52/87] Fix search->list race condition, add docs about querying record IDs --- docs/performance.md | 15 +++++++++++++++ openstack_odooclient/base/record_manager.py | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index e9b6b26..f5d8225 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -39,6 +39,21 @@ query method to limit the selected field to only the fields required for the tas 0.4 seconds ``` +You can also query only the record IDs using the `as_id` parameter on +the query method. This eliminates the step where the record contents are fetched, +improving performance further, but means that you will need to make another query +to fetch the contents of records when required. + +```python +>>> from datetime import datetime +>>> from openstack_odooclient import Client +>>> odoo_client = Client(...) +>>> before_dt = datetime.now() +>>> odoo_client.products.get_sellable_company_products(1234, as_id=True) +>>> print(f"{(datetime.now() - before_dt).total_seconds():.1f} seconds") +0.2 seconds +``` + ## Clustering Queries The Odoo Client library provides nested model references on record objects, diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index c33fe5e..682b68b 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -494,7 +494,15 @@ def search( if as_id: return ids if ids: - return self.list(ids, fields=fields, as_dict=as_dict) + return self.list( + ids, + fields=fields, + as_dict=as_dict, + # A race condition might occur where a record is eleted + # after finding the ID but before querying the contents of it. + # If this happens, silently drop the record ID from the result. + optional=True, + ) return [] # type: ignore[return-value] def _encode_filters( @@ -718,7 +726,6 @@ def _encode_create_field( ], ] = [] for v in value: - # TODO(callumdickinson): Check if this works. if isinstance(v, int): remote_values.append((4, v)) elif isinstance(v, RecordBase): From c1c8f5f481ddd2cba1659b5d1bfcb2356e280ba5 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 12:30:40 +1200 Subject: [PATCH 53/87] Remove creating new records for Many2one relations (not possible) --- docs/managers/custom.md | 4 +- docs/managers/index.md | 11 +++--- docs/performance.md | 3 ++ openstack_odooclient/base/record_manager.py | 41 ++++++--------------- 4 files changed, 23 insertions(+), 36 deletions(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 5f6cc70..8688a5b 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -228,9 +228,9 @@ field in Odoo, and the record class that implements the model in the Odoo Client There are two types of model refs that can be expressed on record classes: **singular records**, and **record lists**. -#### Singular Record (One2one/Many2one) +#### Singular Record (Many2one) -Singular record model refs correspond to the `One2one` and `Many2one` relationship types in Odoo. +Singular record model refs correspond to the `Many2one` relationship type in Odoo. With these relationship types, the model class references a single record. Suppose that we want to add a model ref for a `user_id` field to our record class, diff --git a/docs/managers/index.md b/docs/managers/index.md index fbacd92..c0875c7 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -672,11 +672,12 @@ Field aliases are also resolved to their target field names. 1234 ``` -By **nesting** a record mapping where an ID or object would -normally go, a new record will be created for that mapping, -and linked to the outer record. -This nested record mapping is recursively validated and -processed in the same way as the outer record. +When creating a record with a list of references to another record +(a `One2many` or `Many2many` relation), it is possible to **nest** +record mappings where an ID or object would normally go. +New records will be created for those mappings, and linked +to the parent record. Nested record mappings are recursively validated +and processed in the same way as the parent record. ```python >>> from datetime import date diff --git a/docs/performance.md b/docs/performance.md index f5d8225..85b5554 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -190,3 +190,6 @@ in a single request. ... ) 1234 ``` + +This can be done for any record type with a list of references +to another record type (a `One2many` or `Many2many` relation). diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 682b68b..3a0d56e 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -612,11 +612,12 @@ def create(self, **fields) -> int: Field aliases are also resolved to their target field names. - By nesting a record mapping where an ID or object would - normally go, a new record will be created for that mapping, - and linked to the outer record. - This nested record mapping is recursively validated and - processed in the same way as the outer record. + When creating a record with a list of references to another record + (a ``One2many`` or ``Many2many`` relation), it is possible to nest + record mappings where an ID or object would normally go. + New records will be created for those mappings, and linked + to the parent record. Nested record mappings are recursively validated + and processed in the same way as the parent record. To fetch the newly created record object, pass the returned ID to the ``get`` method. @@ -716,7 +717,6 @@ def _encode_create_field( # iterate over the given value and decode the elements # appropriately. if get_type_origin(attr_type) is list: - value_type = get_type_args(attr_type)[0] if not value: return (remote_field, []) remote_values: List[ @@ -733,7 +733,7 @@ def _encode_create_field( elif isinstance(v, dict): manager = ( self - if value_type is Self + if model_ref.record_class is Self else self._client._record_manager_mapping[ model_ref.record_class ] @@ -759,28 +759,11 @@ def _encode_create_field( # to the field. if isinstance(value, RecordBase): return (remote_field, value.id) - # If the value type is a dictionary, then treat it as a nested - # record to be created alongside the parent record. - # Encode the contents of the dict recursively using the - # record class's manager object, and assign it to the - # parent record so they can both be created. - # TODO(callumdickinson): Check that this works. - if isinstance(value, dict): - manager = ( - self - if value_type is Self - else self._client._record_manager_mapping[value_type] - ) - return ( - remote_field, - [ - ( - 0, - 0, - manager._encode_create_fields(value), - ), - ], - ) + # NOTE(callumdickinson): Nested records cannot be created + # for singular record refs (Many2one relations). + # The target record must be created separately first, + # then linked in this record using either the target record's + # object or ID. raise ValueError( ( f"Unsupported value for model ref field '{field}' " From 9329a172578e1858a0d0413eb1f495fd0de8339a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 12:56:59 +1200 Subject: [PATCH 54/87] Comments --- docs/managers/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/managers/index.md b/docs/managers/index.md index c0875c7..4f8162e 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -666,7 +666,7 @@ Field aliases are also resolved to their target field names. ... os_invoice_date=date(2024, 6, 30), ... os_invoice_due_date=date(2024, 7, 20), ... os_project=3456, # Field name is for the object, value is the ID -... order_lines=[7890], # Field alias used. +... order_lines=[7890], # Field alias used ... ) ) 1234 @@ -697,7 +697,7 @@ and processed in the same way as the parent record. ... os_invoice_date=date(2024, 6, 30), ... os_invoice_due_date=date(2024, 7, 20), ... os_project=3456, # Field name for object, value is ID -... order_lines=[ # Create the sale order lines. +... order_lines=[ # Create the sale order lines ... { ... "name": "test-instance", ... "product": odoo_client.products.get(7890), # Product object From 463d53961b0e5a581439a49930ab98cb6ffb0b6a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 20 Jun 2024 13:06:50 +1200 Subject: [PATCH 55/87] Add shields, URLs to package metadata --- README.md | 2 ++ docs/index.md | 2 ++ pyproject.toml | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/README.md b/README.md index c3364cd..b1125c4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # OpenStack Odoo Client library for Python +[![PyPI](https://img.shields.io/pypi/v/openstack-odooclient)](https://pypi.org/project/openstack-odooclient) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openstack-odooclient) [![GitHub](https://img.shields.io/github/license/catalyst-cloud/python-openstack-odooclient)](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/LICENSE) ![Test Status](https://img.shields.io/github/actions/workflow/status/catalyst-cloud/python-openstack-odooclient/test.yml?label=tests) + This is an Odoo client library for Python with support for the [OpenStack Integration add-on](https://github.com/catalyst-cloud/odoo-openstack-integration), intended to be used by OpenStack projects such as diff --git a/docs/index.md b/docs/index.md index 21dec6d..cbf40f1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,7 @@ # OpenStack Odoo Client Library for Python +[![PyPI](https://img.shields.io/pypi/v/openstack-odooclient)](https://pypi.org/project/openstack-odooclient) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openstack-odooclient) [![GitHub](https://img.shields.io/github/license/catalyst-cloud/python-openstack-odooclient)](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/LICENSE) ![Test Status](https://img.shields.io/github/actions/workflow/status/catalyst-cloud/python-openstack-odooclient/test.yml?label=tests) + This is an Odoo client library for Python with support for the [OpenStack Integration add-on](https://github.com/catalyst-cloud/odoo-openstack-integration), intended to be used by OpenStack projects such as diff --git a/pyproject.toml b/pyproject.toml index 961af7e..16dfa34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,13 @@ dependencies = [ ] dynamic = ["version"] +[project.urls] +Homepage = "https://github.com/catalyst-cloud/python-openstack-odooclient" +Documentation = "https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/index.md" +Repository = "https://github.com/catalyst-cloud/python-openstack-odooclient" +Issues = "https://github.com/catalyst-cloud/python-openstack-odooclient/issues" +Changelog = "https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/changelog.md" + [tool.setuptools_scm] [tool.pdm.dev-dependencies] From 2260123033665d3895b216c775577cf2c458b5af Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 12:40:17 +1200 Subject: [PATCH 56/87] Turn Record._manager into a read-only property, make other Record and RecordManager attributes read-only, fix issues in the docs --- README.md | 2 +- docs/index.md | 2 +- docs/managers/custom.md | 77 ++++++++++----------- docs/managers/index.md | 2 +- openstack_odooclient/base/record.py | 27 +++++--- openstack_odooclient/base/record_manager.py | 5 +- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index b1125c4..c956dc8 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ odoo_client = OdooClient(odoo=odoo) ## Managers -The Odoo Client object exposes a number of record managers, which contain methods +The Odoo client object exposes a number of record managers, which contain methods used to query specific record types, or create one or more new records of that type. For example, performing a simple search query would look something like this: diff --git a/docs/index.md b/docs/index.md index cbf40f1..e06b4c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,7 +85,7 @@ odoo_client = OdooClient(odoo=odoo) ## Managers -The Odoo Client object exposes a number of record managers, which contain methods +The Odoo client object exposes a number of record managers, which contain methods used to query specific record types, or create one or more new records of that type. For example, performing a simple search query would look something like this: diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 8688a5b..b6823e2 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -231,7 +231,7 @@ There are two types of model refs that can be expressed on record classes: #### Singular Record (Many2one) Singular record model refs correspond to the `Many2one` relationship type in Odoo. -With these relationship types, the model class references a single record. +With this relationship type, the model class references a single record. Suppose that we want to add a model ref for a `user_id` field to our record class, which references the `res.users` model @@ -314,8 +314,8 @@ class CustomRecord(RecordBase): ``` Similar to field aliases, any of these model ref fields can be used -instead of the actual model ref field name when -[defining record search filters](index.md#search) and [creating new records](index.md#create). +instead of the target field name when [defining record search filters](index.md#search) +and [creating new records](index.md#create). The record ID or the record object can be passed directly to those methods (the record display name is not guaranteed to be unique, and thus, not accepted). @@ -383,7 +383,7 @@ which references the `product.product` model There are two ways to reference fields of this type on the record class. All of these should all be defined on your record class. -The first is to expose the record's ID directly as an integer. +The first is to expose the record IDs directly as a list of integers. ```python from __future__ import annotations @@ -398,7 +398,7 @@ class CustomRecord(RecordBase): """The list of IDs for the products to use.""" ``` -The second and final one is to define the field as a list of record objects. +The second and final one is to expose the records as a list of record objects. ```python from __future__ import annotations @@ -447,8 +447,8 @@ class CustomRecord(RecordBase): ``` Similar to field aliases, any of these model ref fields can be used -instead of the actual model ref field name when -[defining record search filters](index.md#search) and [creating new records](index.md#create). +instead of the target field name when [defining record search filters](index.md#search) +and [creating new records](index.md#create). The passed values (or lists of values) may consist of either record IDs, record objects, or any combination of the two. @@ -480,7 +480,7 @@ class CustomRecord(RecordBase): Due to the way type hints are dereferenced by Python, two record classes that reference each other are difficult (but not impossible) to support. -Generally two things must be done for circular model refs to work: +Generally the following must be done for circular model refs to work: * Postponed evaluation of annotations is enabled using `from __future__ import annotations` * Imports of the referenced model classes must be defined *after* the model classes @@ -611,12 +611,12 @@ class CustomRecord(RecordBase): In addition to all of the Odoo fields defined on the record class, the following internal attributes are also available for use in object methods: -* `_client` - The Odoo Client object the record was created from -* `_manager` - The manager object the record was created from -* `_records` - The raw record fields from OdooRPC (as a dictionary) -* `_fields` - The fields that were selected during the query (or `None` for all fields) -* `_odoo` - The OdooRPC connection object -* `_env` - The OdooRPC environment object for the model +* `_client` ([`Client`](../index.md#connecting-to-odoo)) - The Odoo client object the record was created from +* `_manager` (`RecordManagerBase`) - The manager object the record was created from +* `_records` (`MappingProxyType[str, Any]`) - The raw record fields from OdooRPC +* `_fields` (`tuple[str, ...] | None`) - The fields that were selected during the query (or `None` for all fields) +* `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object +* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model !!! note @@ -626,23 +626,23 @@ the following internal attributes are also available for use in object methods: ## Managers **Manager classes** are used to provide query methods and other functionality -neccessary for managing record objects in the Odoo Client library. +necessary for managing record objects in the Odoo Client library. Once you have defined your record class, a manager class must be created for implementing the query methods for the record class. ### Creating a Manager Class -Manager classes are subclasses of the generic `ManagerBase` class, +Manager classes are subclasses of the generic `RecordManagerBase` class, specifying the record class the generic type argument, and defining the following class attributes: -* `env_name: str` - The name of the Odoo environment (database model) for the record class -* `record_class: Type[RecordBase]` - The record class to use to create record objects +* `env_name` (`str`) - The name of the Odoo environment (database model) for the record class +* `record_class` (`Type[RecordBase]`) - The record class to use to create record objects The following optional class attributes are also available: -* `default_fields: Set[str] | None` - A set of fields to select by default in queries +* `default_fields` (`set[str] | None`) - A set of fields to select by default in queries if a field list is not supplied (default is `None` to select all fields) Below is a simple example of a custom record type and its manager class. @@ -652,13 +652,13 @@ from __future__ import annotations from typing import List, Union -from openstack_odooclient import ManagerBase, RecordBase +from openstack_odooclient import RecordBase, RecordManagerBase class CustomRecord(RecordBase): custom_field: str """Description of the field.""" -class CustomRecordManager(ManagerBase[CustomRecord]): +class CustomRecordManager(RecordManagerBase[CustomRecord]): env_name = "custom.record" record_class = CustomRecord ``` @@ -666,24 +666,23 @@ class CustomRecordManager(ManagerBase[CustomRecord]): ### Using a Manager Class There are two ways of using manager classes. The first is to simply -instantiate a manager object, passing in the [`Client`](../index.md#connecting-to-odoo) +create a manager object, passing the [`Client`](../index.md#connecting-to-odoo) object as the sole argument. -This will allow manager methods to be used, exactly the same as -the built-in record managers. +This will allow manager methods to be used by calling them on the manager object. ```python from __future__ import annotations from typing import List, Union -from openstack_odooclient import Client, ManagerBase, RecordBase +from openstack_odooclient import Client, RecordBase, RecordManagerBase class CustomRecord(RecordBase): custom_field: str """Description of the field.""" -class CustomRecordManager(ManagerBase[CustomRecord]): +class CustomRecordManager(RecordManagerBase[CustomRecord]): env_name = "custom.record" record_class = CustomRecord @@ -691,8 +690,8 @@ odoo_client = Client(...) custom_records = CustomRecordManager(odoo_client) ``` -The disadvantage of using this method is that the client and manager objects -are effectively separate, and must be managed as two separate variables. +This will work perfectly fine, but the disadvantage of using this method is +that the client and manager objects are managed as two separate variables. To create a single object from which you can manage **all** of your custom types and managers, subclass the `Client` class, and add a type hint for your @@ -703,13 +702,13 @@ from __future__ import annotations from typing import List, Union -from openstack_odooclient import ManagerBase, RecordBase +from openstack_odooclient import RecordBase, RecordManagerBase class CustomRecord(RecordBase): custom_field: str """Description of the field.""" -class CustomRecordManager(ManagerBase[CustomRecord]): +class CustomRecordManager(RecordManagerBase[CustomRecord]): env_name = "custom.record" record_class = CustomRecord @@ -735,13 +734,13 @@ from __future__ import annotations from typing import List, Union -from openstack_odooclient import ManagerBase, RecordBase +from openstack_odooclient import RecordBase, RecordManagerBase class CustomRecord(RecordBase): custom_field: str """Description of the field.""" -class CustomRecordManager(ManagerBase[CustomRecord]): +class CustomRecordManager(RecordManagerBase[CustomRecord]): env_name = "custom.record" record_class = CustomRecord @@ -760,12 +759,12 @@ class CustomRecordManager(ManagerBase[CustomRecord]): The following internal attributes are also available for use in methods: -* `env_name` - The name of the Odoo environment (database model) for the record class -* `record_class` - The record class object -* `default_fields` - The default list of fields to fetch on queries (or `None` to fetch all) -* `_client` - The Odoo Client object the record was created from -* `_odoo` - The OdooRPC connection object -* `_env` - The OdooRPC environment object for the model +* `env_name` (`str`) - The name of the Odoo environment (database model) for the record class +* `record_class` (`Type[T]`) - The record class object +* `default_fields` (`tuple[str, ...] | None`) - The default list of fields to fetch on queries (or `None` to fetch all) +* `_client` ([`Client`](../index.md#connecting-to-odoo)) - The Odoo client object the record manager uses +* `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object +* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model ## Extending Existing Record Types @@ -797,7 +796,7 @@ and Pyright to properly evaluate the source, *existing* references on *existing* cannot be automatically updated to use the custom versions. However, it is possible to **cast** a record object of the base type into the custom type -using the record class's ``from_record_obj`` class method. +using the record class's `from_record_obj` class method. ```python >>> odoo_client = CustomClient(...) diff --git a/docs/managers/index.md b/docs/managers/index.md index 4f8162e..707b057 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -1,6 +1,6 @@ # Managers -The Odoo Client object exposes a number of record managers, which contain methods +The Odoo client object exposes a number of record managers, which contain methods used to query specific record types, or create one or more new records of that type. For example, performing a simple search query would look something like this: diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index d6dcf26..07c0948 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -19,12 +19,14 @@ from dataclasses import dataclass from datetime import date, datetime, time +from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, Dict, List, Literal, + Mapping, Optional, Sequence, Set, @@ -186,15 +188,22 @@ class RecordBase: def __init__( self, client: Client, - manager: RecordManagerBase, - record: Dict[str, Any], + record: Mapping[str, Any], fields: Optional[Sequence[str]], ) -> None: self._client = client - self._manager = manager - self._record = record - self._fields = fields + """The Odoo client that created this record object.""" + self._record = MappingProxyType(record) + """The raw record fields from OdooRPC.""" + self._fields = tuple(fields) if fields else None + """The fields selected in the query that created this record object.""" self._values: Dict[str, Any] = {} + """The cache for the processed record field values.""" + + @property + def _manager(self) -> RecordManagerBase: + """The manager object responsible for this record.""" + return self._client._record_manager_mapping[type(self)] @property def _odoo(self) -> ODOO: @@ -222,7 +231,6 @@ def from_record_obj(cls, record_obj: RecordBase) -> Self: """ return cls( client=record_obj._client, - manager=record_obj._manager, record=record_obj._record, fields=record_obj._fields, ) @@ -245,7 +253,7 @@ def as_dict(self, raw: bool = False) -> Dict[str, Any]: :rtype: Dict[str, Any] """ return ( - copy.deepcopy(self._record) + copy.deepcopy(dict(self._record)) if raw else { self._manager._get_local_field(field): copy.deepcopy(value) @@ -264,7 +272,6 @@ def refresh(self) -> Self: """ return type(self)( client=self._client, - manager=self._manager, record=self._env.read( self.id, fields=self._fields, @@ -514,8 +521,8 @@ def _decode_value(cls, type_hint: Any, value: Any) -> Any: def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"record={self._record}" - f", fields={self._fields}" + f"record={dict(self._record)}" + f", fields={list(self._fields) if self._fields else None}" ")" ) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 3a0d56e..ee20e1a 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -70,7 +70,7 @@ class RecordManagerBase(Generic[Record]): record_class: Type[Record] """The record object type to instatiate using this manager.""" - default_fields: Optional[Set[str]] = None + default_fields: Optional[Tuple[str, ...]] = None """List of fields to fetch by default if a field list is not supplied in queries. @@ -79,7 +79,7 @@ class RecordManagerBase(Generic[Record]): def __init__(self, client: client.Client) -> None: self._client = client - """Odoo Client object.""" + """The Odoo client object the manager uses.""" # Assign this record manager object as the manager # responsible for the configured record class in the client. self._client._record_manager_mapping[self.record_class] = self @@ -233,7 +233,6 @@ def list( res_objs = [ self.record_class( client=self._client, - manager=self, record=record, fields=_fields, ) From 1d4d06e9810771ff4f468ec4906f00daf8be57e0 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 12:43:19 +1200 Subject: [PATCH 57/87] Fix variable ref in example docs --- docs/managers/custom.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index b6823e2..3abc88c 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -752,7 +752,7 @@ class CustomRecordManager(RecordManagerBase[CustomRecord]): ( custom_record.id if isinstance(custom_record, CustomRecord) - else custion_record + else custom_record ), ) ``` From 38e81053d0873ec9a3317dcee1d9ac7f2aab0cd3 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 12:53:18 +1200 Subject: [PATCH 58/87] Add more details for what needs to be set in `record_class` --- docs/managers/custom.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 3abc88c..84a1198 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -638,7 +638,7 @@ specifying the record class the generic type argument, and defining the following class attributes: * `env_name` (`str`) - The name of the Odoo environment (database model) for the record class -* `record_class` (`Type[RecordBase]`) - The record class to use to create record objects +* `record_class` (`Type[T]`) - The record class to use to create record objects (**must** be the same class as the one specified in the generic subclass definition) The following optional class attributes are also available: From 7575106fd2466b4eaba4948569e28daf0580b22e Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 12:54:16 +1200 Subject: [PATCH 59/87] Correct type for default_fields --- docs/managers/custom.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 84a1198..a04be4d 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -642,7 +642,7 @@ and defining the following class attributes: The following optional class attributes are also available: -* `default_fields` (`set[str] | None`) - A set of fields to select by default in queries +* `default_fields` (`tuple[str, ...] | None`) - A set of fields to select by default in queries if a field list is not supplied (default is `None` to select all fields) Below is a simple example of a custom record type and its manager class. From d53010e77292cc79bce677ef2cb081af47414286 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 12:55:47 +1200 Subject: [PATCH 60/87] RecordManager -> RecordManagerBase --- docs/managers/custom.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index a04be4d..0830ff0 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -776,13 +776,13 @@ that manage this record class. ```python from __future__ import annotations -from openstack_odooclient import Client, RecordManager, User, UserManager +from openstack_odooclient import Client, RecordManagerBase, User, UserManager class CustomUser(User): custom_field: str """Description of the field.""" -class CustomUserManager(RecordManager[CustomUser]): +class CustomUserManager(RecordManagerBase[CustomUser]): env_name = UserManager.env_name record_class = CustomUser From 682beb8970295be4cfe77b55dd3b284d4b93d56e Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 13:43:28 +1200 Subject: [PATCH 61/87] README.md: Change relative links to full URLs (for PyPI) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c956dc8..538a91f 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ For example, performing a simple search query would look something like this: ``` For more information on the available managers and their functions, -check the [Managers](docs/managers/index.md) page in the documentation. +check the [Managers](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/managers/index.md) page in the documentation. ## Records @@ -136,4 +136,4 @@ User(record={'id': 1234, ...}, fields=None) ``` For more information on the available managers and their functions, -check the [Records](docs/managers/index.md#records) section in the documentation. +check the [Records](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/managers/index.md#records) section in the documentation. From 6ee424544df3e1f13ede48270bfcce0ebf65d685 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Fri, 21 Jun 2024 13:46:15 +1200 Subject: [PATCH 62/87] Consistent branding --- README.md | 2 +- docs/changelog.md | 2 +- mkdocs.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 538a91f..99f200a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenStack Odoo Client library for Python +# OpenStack Odoo Client Library for Python [![PyPI](https://img.shields.io/pypi/v/openstack-odooclient)](https://pypi.org/project/openstack-odooclient) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/openstack-odooclient) [![GitHub](https://img.shields.io/github/license/catalyst-cloud/python-openstack-odooclient)](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/LICENSE) ![Test Status](https://img.shields.io/github/actions/workflow/status/catalyst-cloud/python-openstack-odooclient/test.yml?label=tests) diff --git a/docs/changelog.md b/docs/changelog.md index 0f14ccd..848f0a1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,4 +2,4 @@ ## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) (2024-06-19) -Initial release of the OpenStack Odoo Client library for Python. +Initial release of the OpenStack Odoo Client Library for Python. diff --git a/mkdocs.yml b/mkdocs.yml index 97a0f60..7dd950c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ --- -site_name: OpenStack Odoo Client for Python +site_name: OpenStack Odoo Client Library for Python site_description: The documentation for the OpenStack Odoo Client library for Python. site_author: Callum Dickinson repo_url: https://github.com/catalyst-cloud/python-openstack-odooclient From 3b8869e6ef08f13812d4458482e021221111f0b3 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 12:21:55 +1200 Subject: [PATCH 63/87] Address review comments --- docs/managers/credit-type.md | 22 +-- docs/managers/currency.md | 2 +- docs/managers/index.md | 19 +-- docs/managers/product.md | 13 +- docs/managers/project.md | 128 ++++++++++++++++++ openstack_odooclient/base/record_manager.py | 10 +- .../base/record_manager_coded.py | 4 +- .../base/record_manager_named.py | 4 +- .../base/record_manager_with_unique_field.py | 4 +- openstack_odooclient/managers/credit_type.py | 20 +-- openstack_odooclient/managers/currency.py | 2 +- openstack_odooclient/managers/product.py | 4 +- openstack_odooclient/managers/project.py | 23 +++- 13 files changed, 202 insertions(+), 53 deletions(-) diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md index bb1450c..b0d4652 100644 --- a/docs/managers/credit-type.md +++ b/docs/managers/credit-type.md @@ -83,8 +83,9 @@ only_for_product_ids: list[int] A list of IDs for the [products](product.md) this credit applies to. -Mutually exclusive with [`only_for_product_category_ids`](#only_for_product_category_ids). -If neither are specified, the credit applies to all products. +Mutually exclusive with +[`only_for_product_category_ids`](#only_for_product_category_ids)/[`only_for_product_categories`](#only_for_product_categories). +If none of these values are specified, the credit applies to all products. ### `only_for_products` @@ -94,8 +95,9 @@ only_for_products: list[Product] A list of [products](product.md) which this credit applies to. -Mutually exclusive with [`only_for_product_categories`](#only_for_product_categories). -If neither are specified, the credit applies to all products. +Mutually exclusive with +[`only_for_product_category_ids`](#only_for_product_category_ids)/[`only_for_product_categories`](#only_for_product_categories). +If none of these values are specified, the credit applies to all products. This fetches the full records from Odoo once, and caches them for subsequent accesses. @@ -108,9 +110,9 @@ only_for_product_category_ids: list[int] A list of IDs for the [product categories](product-category.md) this credit applies to. -Mutually exclusive with [`only_for_product_ids`](#only_for_product_ids). -If neither are specified, the credit applies to all product -categories. +Mutually exclusive with +[`only_for_product_ids`](#only_for_product_ids)/[`only_for_products`](#only_for_products). +If none of these values are specified, the credit applies to all products. ### `only_for_product_categories` @@ -120,9 +122,9 @@ only_for_product_categories: list[ProductCategory] A list of [product categories](product-category.md) which this credit applies to. -Mutually exclusive with [`only_for_products`](#only_for_products). -If neither are specified, the credit applies to all product -categories. +Mutually exclusive with +[`only_for_product_ids`](#only_for_product_ids)/[`only_for_products`](#only_for_products). +If none of these values are specified, the credit applies to all products. This fetches the full records from Odoo once, and caches them for subsequent accesses. diff --git a/docs/managers/currency.md b/docs/managers/currency.md index c4c8a51..cd79548 100644 --- a/docs/managers/currency.md +++ b/docs/managers/currency.md @@ -78,7 +78,7 @@ The sub-unit label for this currency, if set. date: date ``` -The current date to which the currency rate is up to date. +The age of the set currency rate. ### `decimal_places` diff --git a/docs/managers/index.md b/docs/managers/index.md index 707b057..7002da6 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -299,12 +299,12 @@ a ``dict`` object, instead of a record object. #### Parameters -| Name | Type | Description | Default | -|------------|-------------------------|---------------------------------------------------|------------| -| `id` | `int` | Record ID | (required) | +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| +| `id` | `int` | Record ID | (required) | | `fields` | `Iterable[str] | None` | Fields to select (or `None` to select all fields) | `None` | -| `as_dict` | `bool` | Return record as a dictionary | `False` | -| `optional` | `bool` | Return `None` if not found | `False` | +| `as_dict` | `bool` | Return record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | #### Raises @@ -408,7 +408,9 @@ and return the results. Query filters should be defined using the [ORM API search domain](https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains) -format, which is a sequence of criteria, where each criterion +format. + +Filters are a sequence of criteria, where each criterion is one of the following types of values: * A 3-tuple or 3-element sequence in `(field_name, operator, value)` @@ -416,8 +418,9 @@ is one of the following types of values: * `field_name` (`str`) is the the name of the field to filter by. * `operator` (`str`) is the comparison operator to use (for more - information on the available operators, check the ORM API - search domain documentation). + information on the available operators, check the + [ORM API search domain](https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains) + documentation). * `value` (`Any`) is the value to compare records against. * A logical operator which prefixes the following filter criteria diff --git a/docs/managers/product.md b/docs/managers/product.md index 4ecf004..0dc38c4 100644 --- a/docs/managers/product.md +++ b/docs/managers/product.md @@ -176,6 +176,7 @@ get_sellable_company_product_by_name( optional: bool = True, ) -> dict[str, Any] | None ``` + Query a unique product for the given company by name. ```python @@ -213,14 +214,14 @@ with the given name does not exist, instead of raising an error. #### Parameters -| Name | Type | Description | Default | -|------------|-------------------------|---------------------------------------------------|------------| +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| | `company` | `int | Company` | The company to search for products (ID or object) | (required) | -| `name` | `str` | The product name | (required) | +| `name` | `str` | The product name | (required) | | `fields` | `Iterable[str] | None` | Fields to select, defaults to `None` (select all) | `None` | -| `as_id` | `bool` | Return a record ID | `False` | -| `as_dict` | `bool` | Return the record as a dictionary | `False` | -| `optional` | `bool` | Return `None` if not found | `False` | +| `as_id` | `bool` | Return a record ID | `False` | +| `as_dict` | `bool` | Return the record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | #### Raises diff --git a/docs/managers/project.md b/docs/managers/project.md index d2695b0..dcd59af 100644 --- a/docs/managers/project.md +++ b/docs/managers/project.md @@ -33,6 +33,134 @@ Project(record={'id': 1234, ...}, fields=None) For more information on how to use managers, refer to [Managers](index.md). +The following manager methods are also available, in addition to the standard methods. + +### `get_by_os_id` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = False, +) -> Project +``` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = False, + optional: bool = True, +) -> Project | None +``` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = False, +) -> int +``` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = True, + as_dict: bool = False, + optional: bool = True, +) -> int | None +``` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = False, +) -> dict[str, Any] +``` + +```python +get_by_os_id( + os_id: str, + fields: Iterable[str] | None = None, + order: str | None = None, + as_id: bool = False, + as_dict: bool = True, + optional: bool = True, +) -> dict[str, Any] | None +``` + +Query a unique record by OpenStack project ID. + +```python +>>> from openstack_odooclient import Client as OdooClient +>>> odoo_client = OdooClient( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.projects.get_by_os_id("1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a") +Project(record={'id': 1234, 'name': 'test-project', 'os_id': '1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a', ...}, fields=None) +``` + +A number of parameters are available to configure the return type, +and what happens when a result is not found. + +By default all fields available on the record model +will be selected, but this can be filtered using the +``fields`` parameter. + +Use the ``as_id`` parameter to return the ID of the record, +instead of the record object. + +Use the ``as_dict`` parameter to return the record as +a ``dict`` object, instead of a record object. + +When ``optional`` is ``True``, ``None`` is returned if a record +with the given name does not exist, instead of raising an error. + +#### Parameters + +| Name | Type | Description | Default | +|------------|------------------------|---------------------------------------------------|------------| +| `os_id` | `str` | The OpenStack project ID to search for | (required) | +| `fields` | `Iterable[str] | None` | Fields to select, defaults to `None` (select all) | `None` | +| `as_id` | `bool` | Return a record ID | `False` | +| `as_dict` | `bool` | Return the record as a dictionary | `False` | +| `optional` | `bool` | Return `None` if not found | `False` | + +#### Raises + +| Type | Description | +|-----------------------------|-------------------------------------------------------------------| +| `MultipleRecordsFoundError` | Multiple records with the same name were found | +| `RecordNotFoundError` | Record with the given name not found (when `optional` is `False`) | + +#### Returns + +| Type | Description | +|------------------|----------------------------------------------------------------------------| +| `Project` | Project object (default) | +| `int` | Project ID (when `as_id` is `True`) | +| `dict[str, Any]` | Project dictionary (when `as_dict` is `True`) | +| `None` | If a project with the given name was not found (when `optional` is `True`) | + ## Record The project manager returns `Project` record objects. diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index ee20e1a..b02719d 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -417,7 +417,11 @@ def search( and return the results. Query filters should be defined using the ORM API search domain - format, which is a sequence of criteria, where each criterion + format. For more information on the ORM API search domain format: + + https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains + + Filters are a sequence of criteria, where each criterion is one of the following types of values: * A 3-tuple or 3-element sequence in ``(field_name, operator, value)`` @@ -476,7 +480,7 @@ def search( :param filters: Filters to query by, defaults to ``None`` (no filters) :type filters: Union[Tuple[str, str, Any], Sequence[Any], str] | None :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param order: Order results by field name, defaults to ``None`` :type order: str or None, optional :param as_id: Return the record IDs only, defaults to ``False`` @@ -515,8 +519,6 @@ def _encode_filters( else: field_type, field_name = self._encode_filter_field(field=f[0]) operator: str = f[1] - # NOTE(callumdickinson): ORM API search domains. - # https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains if operator in ("in", "not in"): value = [ self._encode_value(type_hint=field_type, value=v) diff --git a/openstack_odooclient/base/record_manager_coded.py b/openstack_odooclient/base/record_manager_coded.py index 1c7f858..607b75f 100644 --- a/openstack_odooclient/base/record_manager_coded.py +++ b/openstack_odooclient/base/record_manager_coded.py @@ -37,7 +37,7 @@ class CodedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): """A record manager base class for record types with a code field. This code field is reasonably expected to be unique, which allows - for methos for getting records by name to be defined. + methods for getting records by code to be defined. The record class should be type hinted with the field to use as the code, just like any other field. @@ -180,7 +180,7 @@ def get_by_code( :param as_id: Return a record ID, defaults to False :type as_id: bool, optional :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param as_dict: Return the record as a dictionary, defaults to False :type as_dict: bool, optional :param optional: Return ``None`` if not found, defaults to False diff --git a/openstack_odooclient/base/record_manager_named.py b/openstack_odooclient/base/record_manager_named.py index 25c4484..ed79532 100644 --- a/openstack_odooclient/base/record_manager_named.py +++ b/openstack_odooclient/base/record_manager_named.py @@ -37,7 +37,7 @@ class NamedRecordManagerBase(RecordManagerWithUniqueFieldBase[Record, str]): """A record manager base class for record types with a name field. This name field is reasonably expected to be unique, which allows - for methods for getting records by name to be defined. + methods for getting records by name to be defined. The record class should be type hinted with the field to use as the name, just like any other field. @@ -180,7 +180,7 @@ def get_by_name( :param as_id: Return a record ID, defaults to False :type as_id: bool, optional :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param as_dict: Return the record as a dictionary, defaults to False :type as_dict: bool, optional :param optional: Return ``None`` if not found, defaults to False diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index 1383689..04d0a09 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -196,14 +196,14 @@ def _get_by_unique_field( A number of parameters are available to configure the return type, and what happens when a result is not found. - :param value: The unique field name to query by + :param name: The unique field name to query by :type name: str :param value: The unique field value :type name: T :param filters: Optional additional filters to apply, defaults to None :type filters: Optional[Iterable[Any]], optional :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param as_id: Return a record ID, defaults to False :type as_id: bool, optional :param as_dict: Return the record as a dictionary, defaults to False diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index ada5ec2..e139d8d 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -43,8 +43,9 @@ class CreditType(RecordBase): ] """A list of IDs for the products this credit applies to. - Mutually exclusive with ``only_for_product_category_ids``. - If neither are specified, the credit applies to all products. + Mutually exclusive with + ``only_for_product_category_ids``/``only_for_product_categories``. + If none of these values are specified, the credit applies to all products. """ only_for_products: Annotated[ @@ -53,8 +54,9 @@ class CreditType(RecordBase): ] """A list of products which this credit applies to. - Mutually exclusive with ``only_for_product_categories``. - If neither are specified, the credit applies to all products. + Mutually exclusive with + ``only_for_product_category_ids``/``only_for_product_categories``. + If none of these values are specified, the credit applies to all products. This fetches the full records from Odoo once, and caches them for subsequent accesses. @@ -66,9 +68,8 @@ class CreditType(RecordBase): ] """A list of IDs for the product categories this credit applies to. - Mutually exclusive with ``only_for_product_ids``. - If neither are specified, the credit applies to all product - categories. + Mutually exclusive with ``only_for_product_ids``/``only_for_products``. + If none of these values are specified, the credit applies to all products. """ only_for_product_categories: Annotated[ @@ -77,9 +78,8 @@ class CreditType(RecordBase): ] """A list of product categories which this credit applies to. - Mutually exclusive with ``only_for_products``. - If neither are specified, the credit applies to all product - categories. + Mutually exclusive with ``only_for_product_ids``/``only_for_products``. + If none of these values are specified, the credit applies to all products. This fetches the full records from Odoo once, and caches them for subsequent accesses. diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py index 12435b2..9dd0ab6 100644 --- a/openstack_odooclient/managers/currency.py +++ b/openstack_odooclient/managers/currency.py @@ -33,7 +33,7 @@ class Currency(RecordBase): """The sub-unit label for this currency, if set.""" date: datetime_date - """The current date to which the currency rate is up to date.""" + """The age of the set currency rate.""" decimal_places: int """Decimal places taken into account for operations on amounts diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index 75292d0..e3e7e15 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -169,7 +169,7 @@ def get_sellable_company_products( :param company: The company to search for products (ID or object) :type company: int | Company :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param order: Order results by a specific field, defaults to None :type order: Optional[str], optional :param as_id: Return the record IDs only, defaults to False @@ -331,7 +331,7 @@ def get_sellable_company_product_by_name( :param name: The product name :type name: str :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param as_id: Return a record ID, defaults to False :type as_id: bool, optional :param as_dict: Return the record as a dictionary, defaults to False diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index a011c90..258237b 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -317,18 +317,31 @@ def get_by_os_id( A number of parameters are available to configure the return type, and what happens when a result is not found. - :param name: The record name - :type name: str + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given name does not exist, instead of raising an error. + + :param os_id: The OpenStack project ID to search for + :type os_id: str :param as_id: Return a record ID, defaults to False :type as_id: bool, optional :param fields: Fields to select, defaults to ``None`` (select all) - :type fields: Iterable[int] or None, optional + :type fields: Iterable[str] or None, optional :param as_dict: Return the record as a dictionary, defaults to False :type as_dict: bool, optional :param optional: Return ``None`` if not found, defaults to False :type optional: bool, optional - :raises MultipleRecordsFoundError: Multiple records with the same name - :raises RecordNotFoundError: Record with the given name not found + :raises MultipleRecordsFoundError: Multiple with matching project IDs + :raises RecordNotFoundError: No record with the given project ID found :return: Query result (or ``None`` if record not found and optional) :rtype: Optional[Union[Project, int, Dict[str, Any]]] """ From 551cec1187fd50495be0d301222a16ba8c761a01 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 12:29:05 +1200 Subject: [PATCH 64/87] Remove `time` type handling (doesn't exist in Odoo), add docs for custom `Selection` field types --- docs/managers/custom.md | 27 ++++++++++++++++++++- openstack_odooclient/base/record.py | 4 +-- openstack_odooclient/base/record_manager.py | 5 +--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 0830ff0..334e3a3 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -73,7 +73,7 @@ class CustomRecord(RecordBase): #### `str` -Corresponds to the `Char` field type in Odoo. +Corresponds to the `Char` or `Text` field types in Odoo. ```python from __future__ import annotations @@ -147,6 +147,31 @@ class CustomRecord(RecordBase): """Description of the field.""" ``` +#### `Literal["value1", ...]` + +Corresponds to the `Selection` field type in Odoo. + +Define all possible values for the field. + +```python +from __future__ import annotations + +from typing import Literal + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase): + custom_field: Literal["value1", "value2", "value3"] + """Description of the field. + + Values: + + * ``value1`` - Value 1 + * ``value2`` - Value 2 + * ``value3`` - Value 3 + """ +``` + ### Optional Fields Any supported field type can be made optional. Two types of optional fields diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index 07c0948..6081a41 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -18,7 +18,7 @@ import copy from dataclasses import dataclass -from datetime import date, datetime, time +from datetime import date, datetime from types import MappingProxyType from typing import ( TYPE_CHECKING, @@ -467,8 +467,6 @@ def _decode_value(cls, type_hint: Any, value: Any) -> Any: return date.fromisoformat(value) if value_type is datetime: return datetime.fromisoformat(value) - if value_type is time: - return time.fromisoformat(value) # When a list is expected, decode each value individually # and return the result as a new list with the same order. if value_type is list: diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index b02719d..bfa860c 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -15,7 +15,7 @@ from __future__ import annotations -from datetime import date, datetime, time +from datetime import date, datetime from typing import ( TYPE_CHECKING, Any, @@ -47,7 +47,6 @@ from ..util import ( DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, - DEFAULT_SERVER_TIME_FORMAT, get_mapped_field, is_subclass, ) @@ -870,8 +869,6 @@ def _encode_value(self, type_hint: Any, value: Any) -> Any: return value.id if value_type is date and isinstance(value, date): return value.strftime(DEFAULT_SERVER_DATE_FORMAT) - if value_type is time and isinstance(value, time): - return value.strftime(DEFAULT_SERVER_TIME_FORMAT) if value_type is datetime and isinstance(value, datetime): return value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if value_type is list and isinstance(value, (list, set, tuple)): From 233590d9aba3b19d906af4eafd1b9020ea84ed27 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 12:30:14 +1200 Subject: [PATCH 65/87] Remove the docs for the `time` type --- docs/managers/custom.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index 334e3a3..f8abf88 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -131,22 +131,6 @@ class CustomRecord(RecordBase): """Description of the field.""" ``` -#### `time` - -Corresponds to the `Time` field type in Odoo. - -```python -from __future__ import annotations - -from datetime import time - -from openstack_odooclient import RecordBase - -class CustomRecord(RecordBase): - custom_field: time - """Description of the field.""" -``` - #### `Literal["value1", ...]` Corresponds to the `Selection` field type in Odoo. From 3f5a72ebad61a17a9f47b4a6f7058de204191114 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 12:32:34 +1200 Subject: [PATCH 66/87] Fix docstrings --- .../base/record_manager_with_unique_field.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index 04d0a09..2093b34 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -196,10 +196,10 @@ def _get_by_unique_field( A number of parameters are available to configure the return type, and what happens when a result is not found. - :param name: The unique field name to query by - :type name: str + :param field: The unique field name to query by + :type field: str :param value: The unique field value - :type name: T + :type value: T :param filters: Optional additional filters to apply, defaults to None :type filters: Optional[Iterable[Any]], optional :param fields: Fields to select, defaults to ``None`` (select all) From 4929675245f92ac166477ff6e2fc5b00c2151d25 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 12:33:00 +1200 Subject: [PATCH 67/87] Add missing detail --- .../base/record_manager_with_unique_field.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index 2093b34..e95b563 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -196,6 +196,19 @@ def _get_by_unique_field( A number of parameters are available to configure the return type, and what happens when a result is not found. + By default all fields available on the record model + will be selected, but this can be filtered using the + ``fields`` parameter. + + Use the ``as_id`` parameter to return the ID of the record, + instead of the record object. + + Use the ``as_dict`` parameter to return the record as + a ``dict`` object, instead of a record object. + + When ``optional`` is ``True``, ``None`` is returned if a record + with the given name does not exist, instead of raising an error. + :param field: The unique field name to query by :type field: str :param value: The unique field value From e0bf02a210e1b5eac2cd70bd4fc494bc90d54f9a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 16:36:21 +1200 Subject: [PATCH 68/87] Add automatic changelog generating using `towncrier`, upgrade PDM --- .github/workflows/release.yml | 4 +-- .github/workflows/test.yml | 4 +-- .pre-commit-config.yaml | 2 +- changelog.d/1.added.md | 1 + docs/changelog.md | 9 +++++-- pdm.lock | 49 ++++++++++++++++++++++++++++++++--- pyproject.toml | 40 ++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 changelog.d/1.added.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24cd544..df87e54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: uses: pdm-project/setup-pdm@v4 with: python-version: "3.12" - version: "2.15.4" + version: "2.16.1" - name: Build source dist and wheels run: pdm build --verbose - name: Upload source dist and wheels to artifacts @@ -48,7 +48,7 @@ jobs: uses: pdm-project/setup-pdm@v4 with: python-version: "3.12" - version: "2.15.4" + version: "2.16.1" - name: Publish source dist and wheels to PyPI run: pdm publish --no-build --verbose diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c84ae3e..a0b8caa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: uses: pdm-project/setup-pdm@v4 with: python-version: "3.12" - version: "2.15.4" + version: "2.16.1" - name: Build source dist and wheels run: pdm build --verbose - name: Upload source dist and wheels to artifacts @@ -69,7 +69,7 @@ jobs: # uses: pdm-project/setup-pdm@v4 # with: # python-version: ${{ matrix.python_version }} - # version: "2.15.4" + # version: "2.16.1" # - name: Create virtual environment # run: pdm install # - name: Run tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4b7c30..d7e308d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,6 @@ repos: - packaging - typing-extensions>=4.0.0 - repo: https://github.com/pdm-project/pdm - rev: "2.15.4" + rev: "2.16.1" hooks: - id: pdm-lock-check diff --git a/changelog.d/1.added.md b/changelog.d/1.added.md new file mode 100644 index 0000000..396763b --- /dev/null +++ b/changelog.d/1.added.md @@ -0,0 +1 @@ +Create the OpenStack Odoo Client Library for Python diff --git a/docs/changelog.md b/docs/changelog.md index 848f0a1..fc0541d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # Changelog -## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) (2024-06-19) + -Initial release of the OpenStack Odoo Client Library for Python. +## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) (2024-06-27) + + +### Added + +- Create the OpenStack Odoo Client Library for Python ([#1](https://github.com/catalyst-cloud/python-openstack-odooclient/pull/1)) diff --git a/pdm.lock b/pdm.lock index a192044..3673c0b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,8 +4,8 @@ [metadata] groups = ["default", "docs", "lint"] strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.1" -content_hash = "sha256:c6999cf16bc1f92a511a81874c7408a907193281fe84622ed53d6a2b5f222c12" +lock_version = "4.4.2" +content_hash = "sha256:78da01b66903f73ca96d65052d90b18b960bcbf3e86f2b7e8ede779a4d77048b" [[package]] name = "babel" @@ -182,6 +182,31 @@ files = [ {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] +[[package]] +name = "importlib-resources" +version = "6.4.0" +requires_python = ">=3.8" +summary = "Read resources from Python packages" +groups = ["docs"] +marker = "python_version < \"3.10\"" +dependencies = [ + "zipp>=3.1.0; python_version < \"3.10\"", +] +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[[package]] +name = "incremental" +version = "22.10.0" +summary = "\"A small library that versions your Python projects.\"" +groups = ["docs"] +files = [ + {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, + {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, +] + [[package]] name = "jinja2" version = "3.1.4" @@ -724,13 +749,31 @@ name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" -groups = ["lint"] +groups = ["docs", "lint"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "towncrier" +version = "23.11.0" +requires_python = ">=3.8" +summary = "Building newsfiles for your project." +groups = ["docs"] +dependencies = [ + "click", + "importlib-resources>=5; python_version < \"3.10\"", + "incremental", + "jinja2", + "tomli; python_version < \"3.11\"", +] +files = [ + {file = "towncrier-23.11.0-py3-none-any.whl", hash = "sha256:2e519ca619426d189e3c98c99558fe8be50c9ced13ea1fc20a4a353a95d2ded7"}, + {file = "towncrier-23.11.0.tar.gz", hash = "sha256:13937c247e3f8ae20ac44d895cf5f96a60ad46cfdcc1671759530d7837d9ee5d"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" diff --git a/pyproject.toml b/pyproject.toml index 16dfa34..656d4da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,11 +55,13 @@ lint = [ ] docs = [ "mkdocs-material>=9.5.27", + "towncrier>=23.11.0", ] [tool.pdm.scripts] lint = {cmd = "ruff check"} format = {cmd = "ruff format"} +update-changelog = {cmd = "scriv collect --keep"} [tool.ruff] fix = true @@ -128,3 +130,41 @@ required-imports = [ [tool.mypy] python_version = "3.8" pretty = true + +[tool.towncrier] +directory = "changelog.d" +filename = "docs/changelog.md" +start_string = "\n" +underlines = ["", "", ""] +title_format = "## [{version}](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/{version}) ({project_date})" +issue_format = "[#{issue}](https://github.com/catalyst-cloud/python-openstack-odooclient/pull/{issue})" + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true From 2cfe9f02fc2244d341927ce1e2a8f387b0e13288 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 17:34:48 +1200 Subject: [PATCH 69/87] Add mike for docs versioning, make sure changelog loosely conforms to Keep a Changelog standards --- changelog.d/1.added.md | 1 - docs/changelog.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 changelog.d/1.added.md diff --git a/changelog.d/1.added.md b/changelog.d/1.added.md deleted file mode 100644 index 396763b..0000000 --- a/changelog.d/1.added.md +++ /dev/null @@ -1 +0,0 @@ -Create the OpenStack Odoo Client Library for Python diff --git a/docs/changelog.md b/docs/changelog.md index fc0541d..cb5bba0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ -## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) (2024-06-27) +## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) - 2024-06-27 ### Added From f1c0d6e346a86da8030de3cbc4cec3df5395c31a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 17:52:19 +1200 Subject: [PATCH 70/87] Change release workflow to tag workflow, add publishing to GitHub Pages --- .github/workflows/main.yml | 26 ++++++++++++ .github/workflows/{release.yml => tag.yml} | 43 ++++++++++++++------ mkdocs.yml | 5 +++ pdm.lock | 46 ++++++++++++++++++++-- pyproject.toml | 3 +- 5 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/main.yml rename .github/workflows/{release.yml => tag.yml} (62%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..eec634f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +--- + +name: main + +on: + push: + branches: + - main + +jobs: + publish-github-pages-develop: + runs-on: ubuntu-22.04 + steps: + - name: Clone full tree, and checkout branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Python and PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: "3.12" + version: "2.16.1" + - name: Create virtual environment + run: pdm install + - name: Publish the latest version of the docs to GitHub Pages + run: pdm run mike deploy --push develop diff --git a/.github/workflows/release.yml b/.github/workflows/tag.yml similarity index 62% rename from .github/workflows/release.yml rename to .github/workflows/tag.yml index df87e54..2ab7733 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/tag.yml @@ -1,9 +1,9 @@ -name: release +name: tag on: - release: - types: - - published + push: + tags: + - "*.*.*" jobs: build: @@ -65,11 +65,32 @@ jobs: with: name: dist path: dist/ - - name: Publish source dist and wheels to GitHub Release - uses: xresloader/upload-to-github-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Extract changelog + id: extract-changelog + uses: sean0x42/markdown-extract@v2 with: - file: dist/* - release_id: ${{ github.event.release.id }} - overwrite: true + file: docs/changelog.md + pattern: ${{ github.ref_name }} + - name: Publish the GitHub Release + uses: softprops/action-gh-release@v2 + with: + body: ${{ steps.extract-changelog.outputs.markdown }} + files: dist/* + fail_on_unmatched_files: true + + publish-github-pages-latest: + runs-on: ubuntu-22.04 + steps: + - name: Clone full tree, and checkout branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Python and PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: "3.12" + version: "2.16.1" + - name: Create virtual environment + run: pdm install + - name: Publish the latest version of the docs to GitHub Pages + run: mike deploy --push --update-aliases ${{ github.ref_name }} latest diff --git a/mkdocs.yml b/mkdocs.yml index 7dd950c..f2ecb21 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ site_name: OpenStack Odoo Client Library for Python site_description: The documentation for the OpenStack Odoo Client library for Python. site_author: Callum Dickinson +site_url: https://catalyst-cloud.github.io/python-openstack-odooclient repo_url: https://github.com/catalyst-cloud/python-openstack-odooclient copyright: Copyright © 2024 Catalyst Cloud Limited @@ -23,6 +24,10 @@ plugins: - offline - search +extra: + version: + provider: mike + nav: - Getting Started: index.md - Managers: diff --git a/pdm.lock b/pdm.lock index 3673c0b..90ac863 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "docs", "lint"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" -content_hash = "sha256:78da01b66903f73ca96d65052d90b18b960bcbf3e86f2b7e8ede779a4d77048b" +content_hash = "sha256:95c41758d35d5104a0d319482ed751666ca4dc8c619221953114a9d12b06cb98" [[package]] name = "babel" @@ -173,7 +173,6 @@ version = "7.1.0" requires_python = ">=3.8" summary = "Read metadata from Python packages" groups = ["docs"] -marker = "python_version < \"3.10\"" dependencies = [ "zipp>=0.5", ] @@ -188,7 +187,6 @@ version = "6.4.0" requires_python = ">=3.8" summary = "Read resources from Python packages" groups = ["docs"] -marker = "python_version < \"3.10\"" dependencies = [ "zipp>=3.1.0; python_version < \"3.10\"", ] @@ -306,6 +304,26 @@ files = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] +[[package]] +name = "mike" +version = "2.1.2" +summary = "Manage multiple versions of your MkDocs-powered documentation" +groups = ["docs"] +dependencies = [ + "importlib-metadata", + "importlib-resources", + "jinja2>=2.7", + "mkdocs>=1.0", + "pyparsing>=3.0", + "pyyaml-env-tag", + "pyyaml>=5.1", + "verspec", +] +files = [ + {file = "mike-2.1.2-py3-none-any.whl", hash = "sha256:d61d9b423ab412d634ca2bd520136d5114e3cc73f4bbd1aa6a0c6625c04918c0"}, + {file = "mike-2.1.2.tar.gz", hash = "sha256:d59cc8054c50f9c8a046cfd47f9b700cf9ff1b2b19f420bd8812ca6f94fa8bd3"}, +] + [[package]] name = "mkdocs" version = "1.6.0" @@ -515,6 +533,17 @@ files = [ {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, ] +[[package]] +name = "pyparsing" +version = "3.1.2" +requires_python = ">=3.6.8" +summary = "pyparsing module - Classes and methods to define and execute parsing grammars" +groups = ["docs"] +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -796,6 +825,16 @@ files = [ {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] +[[package]] +name = "verspec" +version = "0.1.0" +summary = "Flexible version handling" +groups = ["docs"] +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + [[package]] name = "watchdog" version = "4.0.1" @@ -843,7 +882,6 @@ version = "3.19.2" requires_python = ">=3.8" summary = "Backport of pathlib-compatible object wrapper for zip files" groups = ["docs"] -marker = "python_version < \"3.10\"" files = [ {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, diff --git a/pyproject.toml b/pyproject.toml index 656d4da..358ddbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ lint = [ docs = [ "mkdocs-material>=9.5.27", "towncrier>=23.11.0", + "mike>=2.1.2", ] [tool.pdm.scripts] @@ -136,7 +137,7 @@ directory = "changelog.d" filename = "docs/changelog.md" start_string = "\n" underlines = ["", "", ""] -title_format = "## [{version}](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/{version}) ({project_date})" +title_format = "## [{version}](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/{version}) - {project_date}" issue_format = "[#{issue}](https://github.com/catalyst-cloud/python-openstack-odooclient/pull/{issue})" [[tool.towncrier.type]] From 04a970d0feba7e0457d7e583d91fa459cbda18fe Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 17:54:59 +1200 Subject: [PATCH 71/87] Run mike in pdm --- .github/workflows/tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 2ab7733..ab9a1e9 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -93,4 +93,4 @@ jobs: - name: Create virtual environment run: pdm install - name: Publish the latest version of the docs to GitHub Pages - run: mike deploy --push --update-aliases ${{ github.ref_name }} latest + run: pdm run mike deploy --push --update-aliases ${{ github.ref_name }} latest From 61d86e8206081722494de024d19f8201d7379970 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 18:06:17 +1200 Subject: [PATCH 72/87] Simplify workflow job names --- .github/workflows/main.yml | 2 +- .github/workflows/tag.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eec634f..7c1abe6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,5 +22,5 @@ jobs: version: "2.16.1" - name: Create virtual environment run: pdm install - - name: Publish the latest version of the docs to GitHub Pages + - name: Publish the docs to GitHub Pages run: pdm run mike deploy --push develop diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index ab9a1e9..9b08a1f 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -92,5 +92,5 @@ jobs: version: "2.16.1" - name: Create virtual environment run: pdm install - - name: Publish the latest version of the docs to GitHub Pages + - name: Publish the docs to GitHub Pages run: pdm run mike deploy --push --update-aliases ${{ github.ref_name }} latest From 8703abb8af0cba5976a7037f9e65085648cf412f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 18:10:24 +1200 Subject: [PATCH 73/87] Simplify Literal heading --- docs/managers/custom.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/custom.md b/docs/managers/custom.md index f8abf88..c12cb62 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -131,7 +131,7 @@ class CustomRecord(RecordBase): """Description of the field.""" ``` -#### `Literal["value1", ...]` +#### `Literal` Corresponds to the `Selection` field type in Odoo. From c4a7d44532c40512bf71e4f32136aad64eae35cc Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Thu, 27 Jun 2024 18:18:10 +1200 Subject: [PATCH 74/87] Grammar fix --- docs/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/performance.md b/docs/performance.md index 85b5554..2b6251f 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -27,7 +27,7 @@ than they need to. ``` If requests are taking longer than expected, try using the `fields` parameter on the -query method to limit the selected field to only the fields required for the task. +query method to limit the selected fields to only the fields required for the task. ```python >>> from datetime import datetime From 862b09853d0de67939b5711ea754e60c8d328849 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 09:53:35 +1200 Subject: [PATCH 75/87] Fix filtering by record ID fields using the record object value, add the invoice_date_due field to account moves, update package metadata --- docs/managers/account-move.md | 10 ++++++- openstack_odooclient/base/record_manager.py | 27 +++++++------------ openstack_odooclient/managers/account_move.py | 5 +++- pyproject.toml | 11 ++++---- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md index 0b515e2..af5fdfb 100644 --- a/docs/managers/account-move.md +++ b/docs/managers/account-move.md @@ -97,7 +97,15 @@ and caches it for subsequent accesses. invoice_date: date ``` -Date associated with the account move (invoice). +The invoicing date for the account move (invoice). + +### `invoice_date_due` + +```python +invoice_date_due: date +``` + +The due date that the account move (invoice) must be paid by. ### `invoice_line_ids` diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index bfa860c..09fae84 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -36,7 +36,6 @@ ) from typing_extensions import ( - Annotated, Self, get_args as get_type_args, get_origin as get_type_origin, @@ -48,7 +47,6 @@ DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, get_mapped_field, - is_subclass, ) from .record import ModelRef, RecordBase @@ -59,7 +57,7 @@ from .. import client Record = TypeVar("Record", bound=RecordBase) -FilterCriteria = Union[Tuple[str, str, Any], Sequence[Any], str] +FilterCriterion = Union[Tuple[str, str, Any], Sequence[Any], str] class RecordManagerBase(Generic[Record]): @@ -353,7 +351,7 @@ def get( @overload def search( self, - filters: Optional[Sequence[FilterCriteria]] = ..., + filters: Optional[Sequence[FilterCriterion]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: Literal[False] = ..., @@ -363,7 +361,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[FilterCriteria]] = ..., + filters: Optional[Sequence[FilterCriterion]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -374,7 +372,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[FilterCriteria]] = ..., + filters: Optional[Sequence[FilterCriterion]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: Literal[False] = ..., @@ -385,7 +383,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[FilterCriteria]] = ..., + filters: Optional[Sequence[FilterCriterion]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., *, @@ -396,7 +394,7 @@ def search( @overload def search( self, - filters: Optional[Sequence[FilterCriteria]] = ..., + filters: Optional[Sequence[FilterCriterion]] = ..., fields: Optional[Iterable[str]] = ..., order: Optional[str] = ..., as_id: bool = ..., @@ -405,7 +403,7 @@ def search( def search( self, - filters: Optional[Sequence[FilterCriteria]] = None, + filters: Optional[Sequence[FilterCriterion]] = None, fields: Optional[Iterable[str]] = None, order: Optional[str] = None, as_id: bool = False, @@ -509,7 +507,7 @@ def search( def _encode_filters( self, - filters: Sequence[FilterCriteria], + filters: Sequence[FilterCriterion], ) -> List[Union[str, Tuple[str, str, Any]]]: _filters: List[Union[str, Tuple[str, str, Any]]] = [] for f in filters: @@ -589,9 +587,6 @@ def _encode_filter_field(self, field: str) -> Tuple[Any, str]: if local_field not in self._record_type_hints: return (Any, remote_field) type_hint = self._record_type_hints[local_field] - # If the type hint is annotated, get the original data type. - if get_type_origin(type_hint) is Annotated: - return (get_type_args(type_hint)[0], remote_field) return (type_hint, remote_field) def create(self, **fields) -> int: @@ -861,11 +856,9 @@ def _encode_value(self, type_hint: Any, value: Any) -> Any: value_types = ( get_type_args(type_hint) if type_origin is Union else [type_origin] ) + is_model_ref = ModelRef.is_annotated(type_hint) for value_type in value_types: - if is_subclass(value_type, RecordBase) and isinstance( - value, - RecordBase, - ): + if is_model_ref and isinstance(value, RecordBase): return value.id if value_type is date and isinstance(value, date): return value.strftime(DEFAULT_SERVER_DATE_FORMAT) diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index 4e12a5e..5785519 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -45,7 +45,10 @@ class AccountMove(RecordBase): """ invoice_date: date - """Date associated with the account move (invoice).""" + """The invoicing date for the account move (invoice).""" + + invoice_date_due: date + """The due date that the account move (invoice) must be paid by.""" invoice_line_ids: Annotated[ List[int], diff --git a/pyproject.toml b/pyproject.toml index 358ddbf..60fefc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ keywords = [ license = {text = "Apache-2.0"} classifiers = [ "Development Status :: 4 - Beta", - "Intended Audience :: System Administrators", + "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Operating System :: OS Independent", @@ -28,7 +28,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Topic :: System :: Systems Administration", + "Topic :: Software Development :: Libraries", "Typing :: Typed", ] requires-python = ">=3.8" @@ -40,11 +40,11 @@ dependencies = [ dynamic = ["version"] [project.urls] -Homepage = "https://github.com/catalyst-cloud/python-openstack-odooclient" -Documentation = "https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/index.md" +Homepage = "https://catalyst-cloud.github.io/python-openstack-odooclient" +Documentation = "https://catalyst-cloud.github.io/python-openstack-odooclient" Repository = "https://github.com/catalyst-cloud/python-openstack-odooclient" Issues = "https://github.com/catalyst-cloud/python-openstack-odooclient/issues" -Changelog = "https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/changelog.md" +Changelog = "https://catalyst-cloud.github.io/python-openstack-odooclient/latest/changelog.html" [tool.setuptools_scm] @@ -62,7 +62,6 @@ docs = [ [tool.pdm.scripts] lint = {cmd = "ruff check"} format = {cmd = "ruff format"} -update-changelog = {cmd = "scriv collect --keep"} [tool.ruff] fix = true From 60c0f9ac0e4eecb087dcd387f97a3e3495a5ae32 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 10:55:09 +1200 Subject: [PATCH 76/87] Move _resolve_alias into RecordManagerBase (the only place it is used), make _record_type_hints read-only, reduce duplicate get_type_hints calls, add/improve docstrings --- openstack_odooclient/base/client.py | 10 ++- openstack_odooclient/base/record.py | 85 +++++-------------- openstack_odooclient/base/record_manager.py | 74 ++++++++++++++-- .../base/record_manager_with_unique_field.py | 2 +- 4 files changed, 95 insertions(+), 76 deletions(-) diff --git a/openstack_odooclient/base/client.py b/openstack_odooclient/base/client.py index 2400987..e41ef41 100644 --- a/openstack_odooclient/base/client.py +++ b/openstack_odooclient/base/client.py @@ -169,14 +169,16 @@ def __init__( opener=opener, ) self._odoo.login(database, username, password) - # Create an internal mapping between record classes and their managers. - # This is populated by the manager classes themselves when created, - # and used when converting model references on record objects into - # new record objects. self._record_manager_mapping: Dict[ Type[RecordBase], RecordManagerBase, ] = {} + """An internal mapping between record classes and their managers. + + This is populated by the manager classes themselves when created, + and used when converting model references on record objects into + new record objects. + """ # Create record managers defined in the type hints. for attr_name, attr_type in get_type_hints(type(self)).items(): if is_subclass(attr_type, RecordManagerBase): diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index 6081a41..ced409b 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -24,12 +24,10 @@ TYPE_CHECKING, Any, Dict, - List, Literal, Mapping, Optional, Sequence, - Set, Type, Union, ) @@ -39,7 +37,6 @@ Self, get_args as get_type_args, get_origin as get_type_origin, - get_type_hints, ) from ..util import is_subclass @@ -215,6 +212,10 @@ def _env(self) -> Environment: """The OdooRPC environment object this record was created from.""" return self._manager._env + @property + def _type_hints(self) -> MappingProxyType[str, Any]: + return self._manager._record_type_hints + @classmethod def from_record_obj(cls, record_obj: RecordBase) -> Self: """Create a record object of this class's type @@ -299,36 +300,6 @@ def _get_field(self, name: str) -> Any: except KeyError as err: raise AttributeError(str(err)) from None - @classmethod - def _resolve_alias(cls, field: str) -> str: - type_hints = get_type_hints(cls, include_extras=True) - if field not in type_hints: - return field - # NOTE(callumdickinson): Continually resolve field aliases - # until we get to a field that is not an alias. - resolved_aliases: Set[str] = set() - alias_chain: List[str] = [] - annotation = FieldAlias.get(type_hints[field]) - while annotation: - # Check if field aliases loop back on each other. - if field in resolved_aliases: - raise ValueError( - ( - "Found recursive field alias definitions " - f"on {cls.__name__}: {' -> '.join(alias_chain)}" - ), - ) - resolved_aliases.add(field) - alias_chain.append(field) - # Resolve the target field from the alias annotation, - # and try to fetch the target field's annotation to check - # if it is also an alias. - field = annotation.field - if field not in type_hints: - break - annotation = FieldAlias.get(type_hints[field]) - return field - def __getattr__(self, name: str) -> Any: # If the field value has already been decoded, # return the cached value. @@ -336,42 +307,28 @@ def __getattr__(self, name: str) -> Any: return self._values[name] # NOTE(callumdickinson): Use the type hint to coerce # the field value returned in the record dict into the expected type. - type_hints = get_type_hints(type(self), include_extras=True) # First, check if the field has a type hint defined at all. # If not, just cache the value as is and return it. - if name not in type_hints: + if name not in self._type_hints: self._values[name] = self._get_field(name) return self._values[name] # We know we have a type hint to decode for the field. - type_hint = type_hints[name] - # Check if the field is annotated. - # There are special code paths for handling fields - # with specific annotations added to them. - if get_type_origin(type_hint) is Annotated: - type_args = get_type_args(type_hint) - attr_type: Type[Any] = type_args[0] - annotations = type_args[1:] - if len(annotations) == 1: - annotation = annotations[0] - # If this field is a field alias, - # recursively fetch the value for the target field. - if isinstance(annotation, FieldAlias): - self._values[name] = getattr(self, annotation.field) - return self._values[name] - # If this field is a model ref, resolve the model ref - # and return the intended value. - if isinstance(annotation, ModelRef): - self._values[name] = self._getattr_model_ref( - attr_type=attr_type, - model_ref=annotation, - ) - return self._values[name] - raise ValueError( - ( - f"Unsupported annotation for field '{name}': " - f"{annotation}" - ), - ) + type_hint = self._type_hints[name] + # If this field is a field alias, recursively fetch + # the value for the target field. + field_alias = FieldAlias.get(type_hint) + if field_alias: + self._values[name] = getattr(self, field_alias.field) + return self._values[name] + # If this field is a model ref, resolve the model ref + # and return the intended value. + model_ref = ModelRef.get(type_hint) + if model_ref: + self._values[name] = self._getattr_model_ref( + attr_type=get_type_args(type_hint)[0], + model_ref=model_ref, + ) + return self._values[name] # Base case: Decode the value according to the field's type hint, # cache the value, and return it. self._values[name] = self._decode_value( diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 09fae84..d3e7333 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -16,6 +16,7 @@ from __future__ import annotations from datetime import date, datetime +from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, @@ -48,7 +49,7 @@ DEFAULT_SERVER_DATETIME_FORMAT, get_mapped_field, ) -from .record import ModelRef, RecordBase +from .record import FieldAlias, ModelRef, RecordBase if TYPE_CHECKING: from odoorpc import ODOO # type: ignore[import] @@ -61,6 +62,37 @@ class RecordManagerBase(Generic[Record]): + """A generic record manager base class. + + This is the class that is subclassed create a record manager + for querying, creating and managing record objects. + + To define a record manager for your custom record class: + + 1. Set ``RecordManagerBase`` as the superclass, and pass the + record class as the type argument to configure type hinting + for record manager methods. + 2. Set the ``env_name`` class attribute on the record manager + to the Odoo model name for the record type. + 3. Set the ``record_class`` class attribute on the record manager + to define the record class that will be used to create record objects. + + >>> from openstack_odooclient import Client, RecordBase, RecordManagerBase + >>> class CustomRecord(RecordBase): + ... name: str + >>> class CustomRecordManager(RecordManager[CustomRecord]): + ... env_name = "custom.record" + ... record_class = CustomRecord + + Once you have your manager class, subclass the ``Client`` + class and add a type hint for your custom record manager. + This will allow you to use custom record managers on your + Odoo client objects. + + >>> class CustomClient(Client): + ... custom_records: CustomRecordManager + """ + env_name: str """The Odoo environment (model) name to manage.""" @@ -80,9 +112,11 @@ def __init__(self, client: client.Client) -> None: # Assign this record manager object as the manager # responsible for the configured record class in the client. self._client._record_manager_mapping[self.record_class] = self - self._record_type_hints = get_type_hints( - self.record_class, - include_extras=True, + self._record_type_hints = MappingProxyType( + get_type_hints( + self.record_class, + include_extras=True, + ), ) """The type hints for the fields defined in the record class.""" self._field_mapping_reverse = { @@ -498,7 +532,7 @@ def search( ids, fields=fields, as_dict=as_dict, - # A race condition might occur where a record is eleted + # A race condition might occur where a record is deleted # after finding the ID but before querying the contents of it. # If this happens, silently drop the record ID from the result. optional=True, @@ -842,8 +876,34 @@ def _get_local_field(self, field: str) -> str: return self._model_ref_mapping[local_field] return local_field - def _resolve_alias(self, alias: str) -> str: - return self.record_class._resolve_alias(alias) + def _resolve_alias(self, field: str) -> str: + if field not in self._record_type_hints: + return field + # NOTE(callumdickinson): Continually resolve field aliases + # until we get to a field that is not an alias. + resolved_aliases: Set[str] = set() + alias_chain: List[str] = [] + annotation = FieldAlias.get(self._record_type_hints[field]) + while annotation: + # Check if field aliases loop back on each other. + if field in resolved_aliases: + raise ValueError( + ( + "Found recursive field alias definitions " + f"on {self.record_class.__name__}: " + f"{' -> '.join(alias_chain)}" + ), + ) + resolved_aliases.add(field) + alias_chain.append(field) + # Resolve the target field from the alias annotation, + # and try to fetch the target field's annotation to check + # if it is also an alias. + field = annotation.field + if field not in self._record_type_hints: + break + annotation = FieldAlias.get(self._record_type_hints[field]) + return field def _decode_field(self, field: str) -> str: return self._get_local_field(self._resolve_alias(field)) diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index e95b563..7879b84 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -53,7 +53,7 @@ class RecordManagerWithUniqueFieldBase( >>> class CustomRecord(RecordBase): ... name: str >>> class CustomRecordManager( - ... RecordManagerWithUniqueFieldBase[Record, str], + ... RecordManagerWithUniqueFieldBase[CustomRecord, str], ... ): ... env_name = "custom.record" ... record_class = CustomRecord From 6f8b6ef8b77bed931cea9642d075decd94a8e61f Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 11:49:50 +1200 Subject: [PATCH 77/87] Change Client type hints in base classes to ClientBase, add proper type hinting for `_manager`, add typos pre-commit hook, other grammar and docs fixes --- .github/workflows/main.yml | 2 +- .github/workflows/tag.yml | 2 +- .pre-commit-config.yaml | 4 + docs/index.md | 4 +- docs/managers/credit-type.md | 2 +- docs/managers/custom.md | 91 +++++++++++-------- docs/managers/grant-type.md | 2 +- docs/managers/index.md | 1 - openstack_odooclient/base/record.py | 29 +++--- openstack_odooclient/base/record_manager.py | 8 +- .../base/record_manager_with_unique_field.py | 8 +- openstack_odooclient/exceptions.py | 1 + openstack_odooclient/managers/account_move.py | 2 +- .../managers/account_move_line.py | 2 +- openstack_odooclient/managers/company.py | 2 +- openstack_odooclient/managers/credit.py | 2 +- .../managers/credit_transaction.py | 2 +- openstack_odooclient/managers/credit_type.py | 2 +- openstack_odooclient/managers/currency.py | 2 +- .../managers/customer_group.py | 2 +- openstack_odooclient/managers/grant.py | 2 +- openstack_odooclient/managers/grant_type.py | 2 +- openstack_odooclient/managers/partner.py | 2 +- .../managers/partner_category.py | 2 +- openstack_odooclient/managers/pricelist.py | 4 +- openstack_odooclient/managers/product.py | 2 +- .../managers/product_category.py | 2 +- openstack_odooclient/managers/project.py | 2 +- .../managers/project_contact.py | 2 +- .../managers/referral_code.py | 2 +- openstack_odooclient/managers/reseller.py | 2 +- .../managers/reseller_tier.py | 2 +- openstack_odooclient/managers/sale_order.py | 6 +- .../managers/sale_order_line.py | 2 +- .../managers/support_subscription.py | 2 +- .../managers/support_subscription_type.py | 2 +- openstack_odooclient/managers/tax.py | 2 +- openstack_odooclient/managers/tax_group.py | 2 +- .../managers/term_discount.py | 2 +- openstack_odooclient/managers/trial.py | 2 +- openstack_odooclient/managers/uom.py | 2 +- openstack_odooclient/managers/uom_category.py | 2 +- openstack_odooclient/managers/user.py | 2 +- .../managers/volume_discount_range.py | 2 +- openstack_odooclient/managers/voucher_code.py | 2 +- openstack_odooclient/util.py | 10 +- 46 files changed, 138 insertions(+), 98 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7c1abe6..d9ecfaf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ on: - main jobs: - publish-github-pages-develop: + publish-github-pages: runs-on: ubuntu-22.04 steps: - name: Clone full tree, and checkout branch diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 9b08a1f..ff60434 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -78,7 +78,7 @@ jobs: files: dist/* fail_on_unmatched_files: true - publish-github-pages-latest: + publish-github-pages: runs-on: ubuntu-22.04 steps: - name: Clone full tree, and checkout branch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7e308d..533b873 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,10 @@ repos: - id: detect-private-key - id: check-added-large-files - id: check-merge-conflict + - repo: https://github.com/crate-ci/typos + rev: "v1.22.9" + hooks: + - id: typos - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.4.8" hooks: diff --git a/docs/index.md b/docs/index.md index e06b4c8..5b89761 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,9 @@ changes between Odoo versions. The OpenStack Odoo Client library supports Python 3.8 and later. -To install the library package, simply install the `openstack-odooclient` package using `pip`. +To install the library package, simply install the +[`openstack-odooclient`](https://pypi.org/project/openstack-odooclient) +package using `pip`. ```bash python -m pip install openstack-odooclient diff --git a/docs/managers/credit-type.md b/docs/managers/credit-type.md index b0d4652..3804a3d 100644 --- a/docs/managers/credit-type.md +++ b/docs/managers/credit-type.md @@ -138,7 +138,7 @@ product_id: int The ID of the [product](product.md) to use when applying the credit to invoices. -### `product_nane` +### `product_name` ```python product_name: str diff --git a/docs/managers/custom.md b/docs/managers/custom.md index c12cb62..b0796b5 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -22,7 +22,9 @@ support for custom Odoo add-ons. Odoo records are represented by **record classes** in the Odoo Client library. Record classes are implementations (subclasses) of the `RecordBase` class, -which the record manager uses to create immutable objects for the record model. +which the [record manager](#managers) uses to create immutable objects for the record model. +The name of the record manager (as defined in the Python source file) should +be passed as the generic type argument for `RecordBase`, as a string. Record fields are defined as type hints on the record class. These type hints are parsed by the Odoo Client library, @@ -34,11 +36,20 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" ``` +!!! note + + Make sure not to forget passing the name of the record manager class + as the generic type argument for `RecordBase`. + + If this is not done there are no issues from a functional perspective, + but type hinting for the `_manager` attribute will not work correctly + when creating custom [record methods](#record-methods). + ### Field Types The following basic field types from Odoo are supported. @@ -52,7 +63,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: bool """Description of the field.""" ``` @@ -66,7 +77,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: int """Description of the field.""" ``` @@ -80,7 +91,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" ``` @@ -94,7 +105,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: int """Description of the field.""" ``` @@ -110,7 +121,7 @@ from datetime import date from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: date """Description of the field.""" ``` @@ -126,7 +137,7 @@ from datetime import datetime from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: date """Description of the field.""" ``` @@ -144,7 +155,7 @@ from typing import Literal from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: Literal["value1", "value2", "value3"] """Description of the field. @@ -173,7 +184,7 @@ from typing import Literal, Union from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: Union[str, Literal[False]] """Description of the field.""" ``` @@ -190,7 +201,7 @@ from typing import Optional from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: Optional[str] """Description of the field.""" ``` @@ -215,7 +226,7 @@ from __future__ import annotations from openstack_odooclient import FieldAlias, RecordBase from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -257,7 +268,7 @@ from __future__ import annotations from openstack_odooclient import ModelRef, RecordBase, User from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): user_id: Annotated[int, ModelRef("user_id", User)] """ID for the user that owns this record.""" ``` @@ -270,7 +281,7 @@ from __future__ import annotations from openstack_odooclient import ModelRef, RecordBase, User from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): user_name: Annotated[str, ModelRef("user_id", User)] """Name of the user that owns this record.""" ``` @@ -283,7 +294,7 @@ from __future__ import annotations from openstack_odooclient import ModelRef, RecordBase, User from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): user: Annotated[User, ModelRef("user_id", User)] """The user that owns this record. @@ -307,7 +318,7 @@ from __future__ import annotations from openstack_odooclient import ModelRef, RecordBase, User from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): user_id: Annotated[int, ModelRef("user_id", User)] """ID for the user that owns this record.""" @@ -339,7 +350,7 @@ from typing import Optional from openstack_odooclient import ModelRef, RecordBase, User from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): user_id: Annotated[Optional[int], ModelRef("user_id", User)] """ID for the user that owns this record.""" @@ -365,7 +376,7 @@ from typing import Optional from openstack_odooclient import ModelRef, RecordBase from typing_extensions import Annotated, Self -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): record_id: Annotated[Optional[int], ModelRef("user_id", Self)] """ID for the record related to this one, if set..""" @@ -402,7 +413,7 @@ from typing import List from openstack_odooclient import ModelRef, RecordBase, Product from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): product_ids: Annotated[List[int], ModelRef("product_id", Product)] """The list of IDs for the products to use.""" ``` @@ -417,7 +428,7 @@ from typing import List from openstack_odooclient import ModelRef, RecordBase, Product from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): products: Annotated[List[Product], ModelRef("product_id", Product)] """The list of products to use. @@ -443,7 +454,7 @@ from typing import List from openstack_odooclient import ModelRef, RecordBase, Product from typing_extensions import Annotated -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): product_ids: Annotated[List[int], ModelRef("product_id", Product)] """The list of IDs for the products to use.""" @@ -472,7 +483,7 @@ from typing import List from openstack_odooclient import ModelRef, RecordBase from typing_extensions import Annotated, Self -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): child_ids: Annotated[List[int], ModelRef("child_id", Self)] """The list of IDs for the child records.""" @@ -502,10 +513,10 @@ from __future__ import annotations from typing import List -from openstack_odooclient import ModelRef, RecordBase +from openstack_odooclient import ModelRef, RecordBase, RecordManagerBase from typing_extensions import Annotated -class Parent(RecordBase): +class Parent(RecordBase["ParentManager"]): child_ids: Annotated[List[int], ModelRef("child_id", Child)] """The list of IDs for the children records.""" @@ -516,6 +527,10 @@ class Parent(RecordBase): and caches them for subsequent accesses. """ +class ParentManager(RecordManagerBase[Parent]): + env_name = "custom.parent" + record_class = Parent + from .child import Child # noqa: E402 ``` @@ -524,10 +539,10 @@ from __future__ import annotations from typing import Optional -from openstack_odooclient import ModelRef, RecordBase +from openstack_odooclient import ModelRef, RecordBase, RecordManagerBase from typing_extensions import Annotated -class Child(RecordBase): +class Child(RecordBase["ChildManager"]): parent_id: Annotated[Optional[int], ModelRef("parent_id", Parent)] """ID for the parent record, if it has one.""" @@ -541,6 +556,10 @@ class Child(RecordBase): and caches it for subsequent accesses. """ +class Child(RecordManagerBase[Child]): + env_name = "custom.child" + record_class = child + from .parent import Parent # noqa: E402 ``` @@ -568,7 +587,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -609,7 +628,7 @@ from __future__ import annotations from openstack_odooclient import RecordBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -621,7 +640,7 @@ In addition to all of the Odoo fields defined on the record class, the following internal attributes are also available for use in object methods: * `_client` ([`Client`](../index.md#connecting-to-odoo)) - The Odoo client object the record was created from -* `_manager` (`RecordManagerBase`) - The manager object the record was created from +* `_manager` (`RecordManager`) - The manager object the record was created from (correctly type hinted so methods can be called) * `_records` (`MappingProxyType[str, Any]`) - The raw record fields from OdooRPC * `_fields` (`tuple[str, ...] | None`) - The fields that were selected during the query (or `None` for all fields) * `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object @@ -643,11 +662,11 @@ for implementing the query methods for the record class. ### Creating a Manager Class Manager classes are subclasses of the generic `RecordManagerBase` class, -specifying the record class the generic type argument, +specifying the record class as the generic type argument, and defining the following class attributes: * `env_name` (`str`) - The name of the Odoo environment (database model) for the record class -* `record_class` (`Type[T]`) - The record class to use to create record objects (**must** be the same class as the one specified in the generic subclass definition) +* `record_class` (`Type[Record]`) - The record class to use to create record objects (**must** be the same class as the one specified in the generic subclass definition) The following optional class attributes are also available: @@ -663,7 +682,7 @@ from typing import List, Union from openstack_odooclient import RecordBase, RecordManagerBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -687,7 +706,7 @@ from typing import List, Union from openstack_odooclient import Client, RecordBase, RecordManagerBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -713,7 +732,7 @@ from typing import List, Union from openstack_odooclient import RecordBase, RecordManagerBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -745,7 +764,7 @@ from typing import List, Union from openstack_odooclient import RecordBase, RecordManagerBase -class CustomRecord(RecordBase): +class CustomRecord(RecordBase["CustomRecordManager"]): custom_field: str """Description of the field.""" @@ -769,7 +788,7 @@ class CustomRecordManager(RecordManagerBase[CustomRecord]): The following internal attributes are also available for use in methods: * `env_name` (`str`) - The name of the Odoo environment (database model) for the record class -* `record_class` (`Type[T]`) - The record class object +* `record_class` (`Type[Record]`) - The record class object * `default_fields` (`tuple[str, ...] | None`) - The default list of fields to fetch on queries (or `None` to fetch all) * `_client` ([`Client`](../index.md#connecting-to-odoo)) - The Odoo client object the record manager uses * `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object diff --git a/docs/managers/grant-type.md b/docs/managers/grant-type.md index e8e7f23..a948ffa 100644 --- a/docs/managers/grant-type.md +++ b/docs/managers/grant-type.md @@ -145,7 +145,7 @@ product_id: int The ID of the [product](product.md) to use when applying the grant to invoices. -### `product_nane` +### `product_name` ```python product_name: str diff --git a/docs/managers/index.md b/docs/managers/index.md index 7002da6..fa58d9d 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -872,7 +872,6 @@ The managers for these record types have additional methods for querying records * [Partner Categories](partner-category.md) * [Pricelists](pricelist.md) * [Product Categories](product-category.md) -* [OpenStack Projects](project.md) * [OpenStack Reseller Tiers](reseller-tier.md) * [Sale Orders](sale-order.md) * [OpenStack Support Subscription Types](support-subscription-type.md) diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index ced409b..8447714 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -24,11 +24,13 @@ TYPE_CHECKING, Any, Dict, + Generic, Literal, Mapping, Optional, Sequence, Type, + TypeVar, Union, ) @@ -45,8 +47,9 @@ from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from ..client import Client - from .record_manager import RecordManagerBase + from .client import ClientBase + +RecordManager = TypeVar("RecordManager", bound="RecordManagerBase") class AnnotationBase: @@ -95,7 +98,7 @@ class FieldAlias(AnnotationBase): >>> from typing_extensions import Annotated >>> from openstack_odooclient import FieldAlias, RecordBase - >>> class CustomRecord(RecordBase): + >>> class CustomRecord(RecordBase["CustomRecordManager"]): ... name: str ... name_alias: Annotated[str, FieldAlias("name")] """ @@ -115,7 +118,7 @@ class ModelRef(AnnotationBase): >>> from typing_extensions import Annotated >>> from openstack_odooclient import ModelRef, RecordBase, User - >>> class CustomRecord(RecordBase): + >>> class CustomRecord(RecordBase["CustomRecordManager"]): ... user_id: Annotated[int, ModelRef("user_id", User)] ... user_name: Annotated[str, ModelRef("user_id", User)] ... user: Annotated[User, ModelRef("user_id", User)] @@ -128,10 +131,12 @@ class ModelRef(AnnotationBase): record_class: Any -class RecordBase: - """The base class for records. +class RecordBase(Generic[RecordManager]): + """The generic base class for records. - Subclass this class to implement the record class for custom record types. + Subclass this class to implement the record class for custom record types, + specifying the name of the manager class (string), as available in the + Python source file, as the generic type argument. """ id: int @@ -184,7 +189,7 @@ class RecordBase: def __init__( self, - client: Client, + client: ClientBase, record: Mapping[str, Any], fields: Optional[Sequence[str]], ) -> None: @@ -198,9 +203,10 @@ def __init__( """The cache for the processed record field values.""" @property - def _manager(self) -> RecordManagerBase: + def _manager(self) -> RecordManager: """The manager object responsible for this record.""" - return self._client._record_manager_mapping[type(self)] + mapping = self._client._record_manager_mapping + return mapping[type(self)] # type: ignore[return-value] @property def _odoo(self) -> ODOO: @@ -364,7 +370,7 @@ def _getattr_model_ref( return field_value raise ValueError( ( - "Unsupported field value typefor model ref list: " + "Unsupported field value type for model ref list: " f"{value_type}" ), ) @@ -487,3 +493,4 @@ def __repr__(self) -> str: # NOTE(callumdickinson): Import here to avoid circular imports. from ..managers.user import User # noqa: E402 +from .record_manager import RecordManagerBase # noqa: E402 diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index d3e7333..c6c556d 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -55,7 +55,7 @@ from odoorpc import ODOO # type: ignore[import] from odoorpc.env import Environment # type: ignore[import] - from .. import client + from .client import ClientBase Record = TypeVar("Record", bound=RecordBase) FilterCriterion = Union[Tuple[str, str, Any], Sequence[Any], str] @@ -78,7 +78,7 @@ class RecordManagerBase(Generic[Record]): to define the record class that will be used to create record objects. >>> from openstack_odooclient import Client, RecordBase, RecordManagerBase - >>> class CustomRecord(RecordBase): + >>> class CustomRecord(RecordBase["CustomRecordManager"]): ... name: str >>> class CustomRecordManager(RecordManager[CustomRecord]): ... env_name = "custom.record" @@ -97,7 +97,7 @@ class and add a type hint for your custom record manager. """The Odoo environment (model) name to manage.""" record_class: Type[Record] - """The record object type to instatiate using this manager.""" + """The record object type to instantiate using this manager.""" default_fields: Optional[Tuple[str, ...]] = None """List of fields to fetch by default if a field list is not supplied @@ -106,7 +106,7 @@ class and add a type hint for your custom record manager. By default, all fields on the model will be fetched. """ - def __init__(self, client: client.Client) -> None: + def __init__(self, client: ClientBase) -> None: self._client = client """The Odoo client object the manager uses.""" # Assign this record manager object as the manager diff --git a/openstack_odooclient/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py index 7879b84..c6360f2 100644 --- a/openstack_odooclient/base/record_manager_with_unique_field.py +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -50,7 +50,7 @@ class RecordManagerWithUniqueFieldBase( ... RecordBase, ... RecordManagerWithUniqueFieldBase, ... ) - >>> class CustomRecord(RecordBase): + >>> class CustomRecord(RecordBase["CustomRecordManager"]): ... name: str >>> class CustomRecordManager( ... RecordManagerWithUniqueFieldBase[CustomRecord, str], @@ -58,7 +58,6 @@ class RecordManagerWithUniqueFieldBase( ... env_name = "custom.record" ... record_class = CustomRecord - Once you have your manager class, you can define methods that use the provided ``_get_by_unique_field`` method to implement custom search functionality according to your needs. @@ -196,6 +195,11 @@ def _get_by_unique_field( A number of parameters are available to configure the return type, and what happens when a result is not found. + Additional filters can be added to the search query using the + ``filters`` parameter. If defined, these filters will be appended + to the unique field search filter. Filters should be defined + using the same format that ``search`` uses. + By default all fields available on the record model will be selected, but this can be filtered using the ``fields`` parameter. diff --git a/openstack_odooclient/exceptions.py b/openstack_odooclient/exceptions.py index f1e3ea0..3f8af82 100644 --- a/openstack_odooclient/exceptions.py +++ b/openstack_odooclient/exceptions.py @@ -12,6 +12,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. + from __future__ import annotations diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index 5785519..e2004a2 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -24,7 +24,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class AccountMove(RecordBase): +class AccountMove(RecordBase["AccountMoveManager"]): amount_total: float """Total (taxed) amount charged on the account move (invoice).""" diff --git a/openstack_odooclient/managers/account_move_line.py b/openstack_odooclient/managers/account_move_line.py index 4b71d1a..ead00eb 100644 --- a/openstack_odooclient/managers/account_move_line.py +++ b/openstack_odooclient/managers/account_move_line.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class AccountMoveLine(RecordBase): +class AccountMoveLine(RecordBase["AccountMoveLineManager"]): currency_id: Annotated[int, ModelRef("currency_id", Currency)] """The ID for the currency used in this account move (invoice) line. diff --git a/openstack_odooclient/managers/company.py b/openstack_odooclient/managers/company.py index a48c981..22d57c7 100644 --- a/openstack_odooclient/managers/company.py +++ b/openstack_odooclient/managers/company.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class Company(RecordBase): +class Company(RecordBase["CompanyManager"]): active: bool """Whether or not this company is active (enabled).""" diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py index 795f8b1..c09409c 100644 --- a/openstack_odooclient/managers/credit.py +++ b/openstack_odooclient/managers/credit.py @@ -24,7 +24,7 @@ from ..base.record_manager import RecordManagerBase -class Credit(RecordBase): +class Credit(RecordBase["CreditManager"]): credit_type_id: Annotated[int, ModelRef("credit_type", CreditType)] """The ID of the type of this credit.""" diff --git a/openstack_odooclient/managers/credit_transaction.py b/openstack_odooclient/managers/credit_transaction.py index 76d1324..a8c3eca 100644 --- a/openstack_odooclient/managers/credit_transaction.py +++ b/openstack_odooclient/managers/credit_transaction.py @@ -21,7 +21,7 @@ from ..base.record_manager import RecordManagerBase -class CreditTransaction(RecordBase): +class CreditTransaction(RecordBase["CreditTransactionManager"]): credit_id: Annotated[int, ModelRef("credit", Credit)] """The ID of the credit this transaction was made against.""" diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py index e139d8d..decde0a 100644 --- a/openstack_odooclient/managers/credit_type.py +++ b/openstack_odooclient/managers/credit_type.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class CreditType(RecordBase): +class CreditType(RecordBase["CreditTypeManager"]): credit_ids: Annotated[List[int], ModelRef("credits", Credit)] """A list of IDs for the credits which are of this credit type.""" diff --git a/openstack_odooclient/managers/currency.py b/openstack_odooclient/managers/currency.py index 9dd0ab6..8393ac8 100644 --- a/openstack_odooclient/managers/currency.py +++ b/openstack_odooclient/managers/currency.py @@ -22,7 +22,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class Currency(RecordBase): +class Currency(RecordBase["CurrencyManager"]): active: bool """Whether or not this currency is active (enabled).""" diff --git a/openstack_odooclient/managers/customer_group.py b/openstack_odooclient/managers/customer_group.py index 255ea15..2208e5b 100644 --- a/openstack_odooclient/managers/customer_group.py +++ b/openstack_odooclient/managers/customer_group.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class CustomerGroup(RecordBase): +class CustomerGroup(RecordBase["CustomerGroupManager"]): name: str """The name of the customer group.""" diff --git a/openstack_odooclient/managers/grant.py b/openstack_odooclient/managers/grant.py index 5df03c1..1669cd9 100644 --- a/openstack_odooclient/managers/grant.py +++ b/openstack_odooclient/managers/grant.py @@ -24,7 +24,7 @@ from ..base.record_manager import RecordManagerBase -class Grant(RecordBase): +class Grant(RecordBase["GrantManager"]): expiry_date: date """The date the grant expires.""" diff --git a/openstack_odooclient/managers/grant_type.py b/openstack_odooclient/managers/grant_type.py index 80e2040..032b384 100644 --- a/openstack_odooclient/managers/grant_type.py +++ b/openstack_odooclient/managers/grant_type.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class GrantType(RecordBase): +class GrantType(RecordBase["GrantTypeManager"]): grant_ids: Annotated[List[int], ModelRef("grants", Grant)] """A list of IDs for the grants which are of this grant type.""" diff --git a/openstack_odooclient/managers/partner.py b/openstack_odooclient/managers/partner.py index ad49226..ed92cc8 100644 --- a/openstack_odooclient/managers/partner.py +++ b/openstack_odooclient/managers/partner.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class Partner(RecordBase): +class Partner(RecordBase["PartnerManager"]): active: bool """Whether or not this partner is active (enabled).""" diff --git a/openstack_odooclient/managers/partner_category.py b/openstack_odooclient/managers/partner_category.py index a03b24d..762b5cf 100644 --- a/openstack_odooclient/managers/partner_category.py +++ b/openstack_odooclient/managers/partner_category.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class PartnerCategory(RecordBase): +class PartnerCategory(RecordBase["PartnerCategoryManager"]): active: bool """Whether or not the partner category is active (enabled).""" diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 471cd98..0051fb8 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class Pricelist(RecordBase): +class Pricelist(RecordBase["PricelistManager"]): active: bool """Whether or not the pricelist is active.""" @@ -75,7 +75,7 @@ def get_price(self, product: Union[int, Product], qty: float) -> float: :return: Price to charge :rtype: float """ - return self._client.pricelists.get_price( + return self._manager.get_price( pricelist=self, product=product, qty=qty, diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py index e3e7e15..17c6eee 100644 --- a/openstack_odooclient/managers/product.py +++ b/openstack_odooclient/managers/product.py @@ -34,7 +34,7 @@ ) -class Product(RecordBase): +class Product(RecordBase["ProductManager"]): categ_id: Annotated[int, ModelRef("categ_id", ProductCategory)] """The ID for the category this product is under.""" diff --git a/openstack_odooclient/managers/product_category.py b/openstack_odooclient/managers/product_category.py index a5848be..3ae0753 100644 --- a/openstack_odooclient/managers/product_category.py +++ b/openstack_odooclient/managers/product_category.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class ProductCategory(RecordBase): +class ProductCategory(RecordBase["ProductCategoryManager"]): child_id: Annotated[List[int], ModelRef("child_id", Self)] """A list of IDs for the child categories.""" diff --git a/openstack_odooclient/managers/project.py b/openstack_odooclient/managers/project.py index 258237b..def5762 100644 --- a/openstack_odooclient/managers/project.py +++ b/openstack_odooclient/managers/project.py @@ -34,7 +34,7 @@ ) -class Project(RecordBase): +class Project(RecordBase["ProjectManager"]): billing_type: Literal["customer", "internal"] """Billing type for this project. diff --git a/openstack_odooclient/managers/project_contact.py b/openstack_odooclient/managers/project_contact.py index d381f9e..887c6e0 100644 --- a/openstack_odooclient/managers/project_contact.py +++ b/openstack_odooclient/managers/project_contact.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class ProjectContact(RecordBase): +class ProjectContact(RecordBase["ProjectContactManager"]): contact_type: Literal[ "primary", "billing", diff --git a/openstack_odooclient/managers/referral_code.py b/openstack_odooclient/managers/referral_code.py index 407d8bb..e18b908 100644 --- a/openstack_odooclient/managers/referral_code.py +++ b/openstack_odooclient/managers/referral_code.py @@ -23,7 +23,7 @@ from ..base.record_manager_coded import CodedRecordManagerBase -class ReferralCode(RecordBase): +class ReferralCode(RecordBase["ReferralCodeManager"]): allowed_uses: int """The number of allowed uses of this referral code. diff --git a/openstack_odooclient/managers/reseller.py b/openstack_odooclient/managers/reseller.py index 30f48c7..490620f 100644 --- a/openstack_odooclient/managers/reseller.py +++ b/openstack_odooclient/managers/reseller.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class Reseller(RecordBase): +class Reseller(RecordBase["ResellerManager"]): alternative_billing_url: Optional[str] """The URL to the cloud billing page for the reseller, if available.""" diff --git a/openstack_odooclient/managers/reseller_tier.py b/openstack_odooclient/managers/reseller_tier.py index 15abd4e..ebb345d 100644 --- a/openstack_odooclient/managers/reseller_tier.py +++ b/openstack_odooclient/managers/reseller_tier.py @@ -21,7 +21,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class ResellerTier(RecordBase): +class ResellerTier(RecordBase["ResellerTierManager"]): discount_percent: float """The maximum discount percentage for this reseller tier (0-100).""" diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 1dda3c0..291da10 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -24,7 +24,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class SaleOrder(RecordBase): +class SaleOrder(RecordBase["SaleOrderManager"]): amount_untaxed: float """The untaxed total cost of the sale order.""" @@ -149,11 +149,11 @@ class SaleOrder(RecordBase): def action_confirm(self) -> None: """Confirm the sale order.""" - self._client.sale_orders.action_confirm(self) + self._manager.action_confirm(self) def create_invoices(self) -> None: """Create invoices from this sale order.""" - self._client.sale_orders.create_invoices(self) + self._manager.create_invoices(self) class SaleOrderManager(NamedRecordManagerBase[SaleOrder]): diff --git a/openstack_odooclient/managers/sale_order_line.py b/openstack_odooclient/managers/sale_order_line.py index edd6c92..adf7019 100644 --- a/openstack_odooclient/managers/sale_order_line.py +++ b/openstack_odooclient/managers/sale_order_line.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class SaleOrderLine(RecordBase): +class SaleOrderLine(RecordBase["SaleOrderLineManager"]): company_id: Annotated[int, ModelRef("company_id", Company)] """The ID for the company this sale order line was generated for. diff --git a/openstack_odooclient/managers/support_subscription.py b/openstack_odooclient/managers/support_subscription.py index 3c4506c..80b4cf3 100644 --- a/openstack_odooclient/managers/support_subscription.py +++ b/openstack_odooclient/managers/support_subscription.py @@ -24,7 +24,7 @@ from ..base.record_manager import RecordManagerBase -class SupportSubscription(RecordBase): +class SupportSubscription(RecordBase["SupportSubscriptionManager"]): billing_type: Literal["paid", "complimentary"] """The method of billing for the support subscription. diff --git a/openstack_odooclient/managers/support_subscription_type.py b/openstack_odooclient/managers/support_subscription_type.py index 4a4a986..82640fc 100644 --- a/openstack_odooclient/managers/support_subscription_type.py +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class SupportSubscriptionType(RecordBase): +class SupportSubscriptionType(RecordBase["SupportSubscriptionTypeManager"]): billing_type: Literal["paid", "complimentary"] """The type of support subscription.""" diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py index 2e050f6..1fda300 100644 --- a/openstack_odooclient/managers/tax.py +++ b/openstack_odooclient/managers/tax.py @@ -23,7 +23,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class Tax(RecordBase): +class Tax(RecordBase["TaxManager"]): active: bool """Whether or not this tax is active (enabled).""" diff --git a/openstack_odooclient/managers/tax_group.py b/openstack_odooclient/managers/tax_group.py index afe369f..1691fe0 100644 --- a/openstack_odooclient/managers/tax_group.py +++ b/openstack_odooclient/managers/tax_group.py @@ -19,7 +19,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class TaxGroup(RecordBase): +class TaxGroup(RecordBase["TaxGroupManager"]): name: str """Name of the tax group.""" diff --git a/openstack_odooclient/managers/term_discount.py b/openstack_odooclient/managers/term_discount.py index b78b893..377c9b4 100644 --- a/openstack_odooclient/managers/term_discount.py +++ b/openstack_odooclient/managers/term_discount.py @@ -24,7 +24,7 @@ from ..base.record_manager import RecordManagerBase -class TermDiscount(RecordBase): +class TermDiscount(RecordBase["TermDiscountManager"]): discount_percent: float """The maximum discount percentage for this term discount (0-100).""" diff --git a/openstack_odooclient/managers/trial.py b/openstack_odooclient/managers/trial.py index 280dcee..a81c030 100644 --- a/openstack_odooclient/managers/trial.py +++ b/openstack_odooclient/managers/trial.py @@ -24,7 +24,7 @@ from ..base.record_manager import RecordManagerBase -class Trial(RecordBase): +class Trial(RecordBase["TrialManager"]): account_suspended_date: Union[date, Literal[False]] """The date the account was suspended, following the end of the trial.""" diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py index 201f8ca..dbb50c9 100644 --- a/openstack_odooclient/managers/uom.py +++ b/openstack_odooclient/managers/uom.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class Uom(RecordBase): +class Uom(RecordBase["UomManager"]): active: bool """Whether or not this Unit of Measure is active (enabled).""" diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py index 677f475..963327c 100644 --- a/openstack_odooclient/managers/uom_category.py +++ b/openstack_odooclient/managers/uom_category.py @@ -21,7 +21,7 @@ from ..base.record_manager import RecordManagerBase -class UomCategory(RecordBase): +class UomCategory(RecordBase["UomCategoryManager"]): measure_type: Literal[ "unit", "weight", diff --git a/openstack_odooclient/managers/user.py b/openstack_odooclient/managers/user.py index 94d98c0..3013822 100644 --- a/openstack_odooclient/managers/user.py +++ b/openstack_odooclient/managers/user.py @@ -21,7 +21,7 @@ from ..base.record_manager import RecordManagerBase -class User(RecordBase): +class User(RecordBase["UserManager"]): active: bool """Whether or not this user is active (enabled).""" diff --git a/openstack_odooclient/managers/volume_discount_range.py b/openstack_odooclient/managers/volume_discount_range.py index 8b81756..38b94f0 100644 --- a/openstack_odooclient/managers/volume_discount_range.py +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -23,7 +23,7 @@ from ..base.record_manager import RecordManagerBase -class VolumeDiscountRange(RecordBase): +class VolumeDiscountRange(RecordBase["VolumeDiscountRangeManager"]): customer_group_id: Annotated[ Optional[int], ModelRef("customer_group", CustomerGroup), diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py index 09c6e4b..a2dfcef 100644 --- a/openstack_odooclient/managers/voucher_code.py +++ b/openstack_odooclient/managers/voucher_code.py @@ -24,7 +24,7 @@ from ..base.record_manager_named import NamedRecordManagerBase -class VoucherCode(RecordBase): +class VoucherCode(RecordBase["VoucherCodeManager"]): claimed: bool """Whether or not this voucher code has been claimed.""" diff --git a/openstack_odooclient/util.py b/openstack_odooclient/util.py index 5e8a654..640a58b 100644 --- a/openstack_odooclient/util.py +++ b/openstack_odooclient/util.py @@ -15,7 +15,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar + +from typing_extensions import TypeGuard if TYPE_CHECKING: from typing import Any, Mapping, Optional, Tuple, Type, Union @@ -27,6 +29,8 @@ f"{DEFAULT_SERVER_DATE_FORMAT} {DEFAULT_SERVER_TIME_FORMAT}" ) +T = TypeVar("T") + def get_mapped_field( field_mapping: Mapping[Optional[str], Mapping[str, str]], @@ -61,8 +65,8 @@ def get_mapped_field( def is_subclass( type_obj: Type[Any], - classes: Union[Type[Any], Tuple[Type[Any]]], -) -> bool: + classes: Union[Type[T], Tuple[Type[T]]], +) -> TypeGuard[Type[T]]: """Check whether or not the given type is a subclass of any of the given classes (single class, or tuple of one or more classes). From 95783d1bc607ebac4c07b2ac7252e900c6d05c22 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 11:58:39 +1200 Subject: [PATCH 78/87] Last batch of fixes --- docs/managers/partner.md | 2 -- docs/managers/sale-order.md | 4 ++-- openstack_odooclient/managers/sale_order.py | 2 +- openstack_odooclient/util.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/managers/partner.md b/docs/managers/partner.md index d2f08d2..17ff4b8 100644 --- a/docs/managers/partner.md +++ b/docs/managers/partner.md @@ -178,8 +178,6 @@ os_referral_id: int | None The ID for the [referral code](referral-code.md) the partner used on sign-up, if one was used. -return self._get_ref_id("os_referral", optional=True) - ### `os_referral_name` ```python diff --git a/docs/managers/sale-order.md b/docs/managers/sale-order.md index 4e26fca..7d12bc0 100644 --- a/docs/managers/sale-order.md +++ b/docs/managers/sale-order.md @@ -93,8 +93,8 @@ Create invoices from the given sale order. #### Parameters -| Name | Type | Description | Default | -|--------------|--------------------|----------------------------------------|------------| +| Name | Type | Description | Default | +|--------------|-------------------|----------------------------------------|------------| | `sale_order` | `int | SaleOrder` | The sale order to create invoices from | (required) | ## Record diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 291da10..03a816d 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -148,7 +148,7 @@ class SaleOrder(RecordBase["SaleOrderManager"]): """ def action_confirm(self) -> None: - """Confirm the sale order.""" + """Confirm this sale order.""" self._manager.action_confirm(self) def create_invoices(self) -> None: diff --git a/openstack_odooclient/util.py b/openstack_odooclient/util.py index 640a58b..ae0be04 100644 --- a/openstack_odooclient/util.py +++ b/openstack_odooclient/util.py @@ -65,7 +65,7 @@ def get_mapped_field( def is_subclass( type_obj: Type[Any], - classes: Union[Type[T], Tuple[Type[T]]], + classes: Union[Type[T], Tuple[Type[T], ...]], ) -> TypeGuard[Type[T]]: """Check whether or not the given type is a subclass of any of the given classes (single class, or tuple of one or more classes). From aedae399ba9a413cd405e5a1a2cedac5462b582a Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 12:58:12 +1200 Subject: [PATCH 79/87] Add missing docs, add tips about when not to use _manager --- docs/managers/account-move.md | 65 +++++++++++++++++++ docs/managers/custom.md | 12 +++- docs/managers/index.md | 6 +- openstack_odooclient/base/record_manager.py | 2 +- openstack_odooclient/managers/account_move.py | 62 +++++++++++++++++- openstack_odooclient/managers/pricelist.py | 33 +++++++--- openstack_odooclient/managers/sale_order.py | 4 +- 7 files changed, 165 insertions(+), 19 deletions(-) diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md index af5fdfb..a21b103 100644 --- a/docs/managers/account-move.md +++ b/docs/managers/account-move.md @@ -33,6 +33,47 @@ AccountMove(record={'id': 1234, ...}, fields=None) For more information on how to use managers, refer to [Managers](index.md). +The following manager methods are also available, in addition to the standard methods. + +### `action_post` + +```python +action_post(*account_moves: int, | AccountMove | Iterable[int | AccountMove]) -> None +``` + +Change one or more draft account moves (invoices) +into "posted" state. + +This method accepts either a record object or ID, or an iterable of +either of those types. Multiple positional arguments are allowed. + +All specified records will be processed in a single request. + +#### Parameters + +| Name | Type | Description | Default | +|------------------|---------------------------------------------------|---------------------------------------------|------------| +| `*account_moves` | `int | AccountMove | Iterable[int | AccountMove]` | Record objects, IDs, or record/ID iterables | (required) | + +### `send_openstack_invoice_email` + +```python +send_openstack_invoice_email( + account_move: int | AccountMove, + email_ctx: Optional[Mapping[str, Any]] = None, +) -> None +``` + +Send an OpenStack invoice email for the given +account move (invoice). + +#### Parameters + +| Name | Type | Description | Default | +|----------------|----------------------------|-------------------------------------------------|------------| +| `account_move` | `int | AccountMove` | The account move (invoice) to send an email for | (required) | +| `email_ctx` | `Mapping[str, Any] | None` | Optional email context | `None` | + ## Record The account move (invoice) manager returns `AccountMove` record objects. @@ -237,3 +278,27 @@ Values: * ``draft`` - Draft invoice * ``posted`` - Posted (finalised) invoice * ``cancel`` - Cancelled invoice + +### `action_post` + +```python +action_post() -> None +``` + +Change this draft account move (invoice) into "posted" state. + +### `send_openstack_invoice_email` + +```python +send_openstack_invoice_email( + email_ctx: Optional[Mapping[str, Any]] = None, +) -> None +``` + +Send an OpenStack invoice email for this account move (invoice). + +#### Parameters + +| Name | Type | Description | Default | +|-------------|----------------------------|------------------------|---------| +| `email_ctx` | `Mapping[str, Any] | None` | Optional email context | `None` | diff --git a/docs/managers/custom.md b/docs/managers/custom.md index b0796b5..fbe314b 100644 --- a/docs/managers/custom.md +++ b/docs/managers/custom.md @@ -648,8 +648,16 @@ the following internal attributes are also available for use in object methods: !!! note - Record objects are intended to be immutable. Custom methods should not change - the internal state, or the fields, of the record object. + It is recommended to follow these guidelines when defining custom + record methods: + + * Record objects are intended to be immutable. Custom methods should not + change the internal state, or the fields, of the record object. + * If you are releasing your custom record types in a publicly available + library, **do not** rely on custom manager methods through the `_manager` + attribute. If a depending package subclasses your custom record types, + **the manager for that record subclass will not have your custom manager + methods defined on it.** ## Managers diff --git a/docs/managers/index.md b/docs/managers/index.md index fa58d9d..085fec3 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -854,9 +854,9 @@ All specified records will be deleted in a single request. #### Parameters -| Name | Type | Description | Default | -|------------|--------------------------------------------|------------------------------------------------------------------------------|------------| -| `*records` | `Record | int | Iterable[Record | int]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | +| Name | Type | Description | Default | +|------------|-----------------------------------------|------------------------------------------------------------------------------|------------| +| `*records` | `int | Record | Iterable[int | Record]` | The records to delete (object, ID, or record/ID list) (positional arguments) | (required) | ## Named Record Managers diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index c6c556d..86e8c0d 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -807,7 +807,7 @@ def _encode_create_field( def unlink( self, - *records: Union[Record, int, Iterable[Union[Record, int]]], + *records: Union[int, Record, Iterable[Union[int, Record]]], ) -> None: """Delete one or more records from Odoo. diff --git a/openstack_odooclient/managers/account_move.py b/openstack_odooclient/managers/account_move.py index e2004a2..f2d3283 100644 --- a/openstack_odooclient/managers/account_move.py +++ b/openstack_odooclient/managers/account_move.py @@ -16,7 +16,7 @@ from __future__ import annotations from datetime import date -from typing import Any, List, Literal, Mapping, Optional, Union +from typing import Any, Iterable, List, Literal, Mapping, Optional, Union from typing_extensions import Annotated @@ -157,7 +157,7 @@ class AccountMove(RecordBase["AccountMoveManager"]): } def action_post(self) -> None: - """Change a draft account move (invoice) into "posted" state.""" + """Change this draft account move (invoice) into "posted" state.""" self._env.action_post(self.id) def send_openstack_invoice_email( @@ -179,6 +179,64 @@ class AccountMoveManager(NamedRecordManagerBase[AccountMove]): env_name = "account.move" record_class = AccountMove + def action_post( + self, + *account_moves: Union[ + int, + AccountMove, + Iterable[Union[int, AccountMove]], + ], + ) -> None: + """Change one or more draft account moves (invoices) + into "posted" state. + + This method accepts either a record object or ID, or an iterable of + either of those types. Multiple positional arguments are allowed. + + All specified records will be processed in a single request. + + :param account_moves: Record objects, IDs, or record/ID iterables + :type account_moves: int | AccountMove | Iterable[int | AccountMove] + """ + _ids: List[int] = [] + for ids in account_moves: + if isinstance(ids, int): + _ids.append(ids) + elif isinstance(ids, AccountMove): + _ids.append(ids.id) + else: + _ids.extend( + ( + (i.id if isinstance(i, AccountMove) else i) + # FIXME(callumdickinson): This should not be + # giving an error. Suspecting there's a bug in mypy. + for i in ids # type: ignore[union-attr] + ), + ) + self._env.action_post(_ids) + + def send_openstack_invoice_email( + self, + account_move: Union[int, AccountMove], + email_ctx: Optional[Mapping[str, Any]] = None, + ) -> None: + """Send an OpenStack invoice email for the given + account move (invoice). + + :param account_move: The account move (invoice) to send an email for + :type account_move: int | AccountMove + :param email_ctx: Optional email context, defaults to None + :type email_ctx: Optional[Mapping[str, Any]], optional + """ + self._env.send_openstack_invoice_email( + ( + account_move.id + if isinstance(account_move, AccountMove) + else account_move + ), + email_ctx=dict(email_ctx) if email_ctx else None, + ) + # NOTE(callumdickinson): Import here to make sure circular imports work. from .account_move_line import AccountMoveLine # noqa: E402 diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py index 0051fb8..ef44149 100644 --- a/openstack_odooclient/managers/pricelist.py +++ b/openstack_odooclient/managers/pricelist.py @@ -75,7 +75,8 @@ def get_price(self, product: Union[int, Product], qty: float) -> float: :return: Price to charge :rtype: float """ - return self._manager.get_price( + return get_price( + manager=self._manager, pricelist=self, product=product, qty=qty, @@ -104,15 +105,29 @@ def get_price( :return: Price to charge :rtype: float """ - pricelist_id = ( - pricelist.id if isinstance(pricelist, Pricelist) else pricelist + return get_price( + manager=self, + pricelist=pricelist, + product=product, + qty=qty, ) - price = self._env.price_get( - pricelist_id, - (product.id if isinstance(product, Product) else product), - max(qty, 0), - )[str(pricelist_id)] - return price if qty >= 0 else -price + + +def get_price( + manager: PricelistManager, + pricelist: Union[int, Pricelist], + product: Union[int, Product], + qty: float, +) -> float: + pricelist_id = ( + pricelist.id if isinstance(pricelist, Pricelist) else pricelist + ) + price = manager._env.price_get( + pricelist_id, + (product.id if isinstance(product, Product) else product), + max(qty, 0), + )[str(pricelist_id)] + return price if qty >= 0 else -price # NOTE(callumdickinson): Import here to make sure circular imports work. diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py index 03a816d..a4f71a2 100644 --- a/openstack_odooclient/managers/sale_order.py +++ b/openstack_odooclient/managers/sale_order.py @@ -149,11 +149,11 @@ class SaleOrder(RecordBase["SaleOrderManager"]): def action_confirm(self) -> None: """Confirm this sale order.""" - self._manager.action_confirm(self) + self._env.action_confirm(self.id) def create_invoices(self) -> None: """Create invoices from this sale order.""" - self._manager.create_invoices(self) + self._env.create_invoices(self.id) class SaleOrderManager(NamedRecordManagerBase[SaleOrder]): From 7be869b083a6e040195e475909431f3c29f18e75 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 18:12:24 +1200 Subject: [PATCH 80/87] Improve model ref handling in search filters, make create_uid/write_uid optional fields --- docs/managers/index.md | 14 ++++----- openstack_odooclient/base/record.py | 12 ++++---- openstack_odooclient/base/record_manager.py | 33 +++++++++++++++++---- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/docs/managers/index.md b/docs/managers/index.md index 085fec3..7019502 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -1292,7 +1292,7 @@ The time the record was created. #### `create_uid` ```python -create_uid: int +create_uid: int | None ``` The ID of the [user](user.md) that created this record. @@ -1300,7 +1300,7 @@ The ID of the [user](user.md) that created this record. #### `create_name` ```python -create_name: str +create_name: str | None ``` The name of the [user](user.md) that created this record. @@ -1308,7 +1308,7 @@ The name of the [user](user.md) that created this record. #### `create_user` ```python -create_user: User +create_user: User | None ``` The [user](user.md) that created this record. @@ -1319,7 +1319,7 @@ and caches it for subsequent accesses. #### `write_date` ```python -write_date: datetime +write_date: datetime | None ``` The time the record was last modified. @@ -1327,7 +1327,7 @@ The time the record was last modified. #### `write_uid` ```python -write_uid: int +write_uid: int | None ``` The ID of the [user](user.md) that last modified this record. @@ -1335,7 +1335,7 @@ The ID of the [user](user.md) that last modified this record. #### `write_name` ```python -write_name: str +write_name: str | None ``` The name of the [user](user.md) that modified this record. @@ -1343,7 +1343,7 @@ The name of the [user](user.md) that modified this record. #### `write_user` ```python -write_user: User +write_user: User | None ``` The [user](user.md) that last modified this record. diff --git a/openstack_odooclient/base/record.py b/openstack_odooclient/base/record.py index 8447714..1da33da 100644 --- a/openstack_odooclient/base/record.py +++ b/openstack_odooclient/base/record.py @@ -145,13 +145,13 @@ class RecordBase(Generic[RecordManager]): create_date: datetime """The time the record was created.""" - create_uid: Annotated[int, ModelRef("create_uid", User)] + create_uid: Annotated[Optional[int], ModelRef("create_uid", User)] """The ID of the user that created this record.""" - create_name: Annotated[str, ModelRef("create_uid", User)] + create_name: Annotated[Optional[str], ModelRef("create_uid", User)] """The name of the user that created this record.""" - create_user: Annotated[User, ModelRef("create_uid", User)] + create_user: Annotated[Optional[User], ModelRef("create_uid", User)] """The user that created this record. This fetches the full record from Odoo once, @@ -161,13 +161,13 @@ class RecordBase(Generic[RecordManager]): write_date: datetime """The time the record was last modified.""" - write_uid: Annotated[int, ModelRef("write_uid", User)] + write_uid: Annotated[Optional[int], ModelRef("write_uid", User)] """The ID for the user that last modified this record.""" - write_name: Annotated[str, ModelRef("write_uid", User)] + write_name: Annotated[Optional[str], ModelRef("write_uid", User)] """The name of the user that last modified this record.""" - write_user: Annotated[User, ModelRef("write_uid", User)] + write_user: Annotated[Optional[User], ModelRef("write_uid", User)] """The user that last modified this record. This fetches the full record from Odoo once, diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 86e8c0d..a46747f 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -37,6 +37,7 @@ ) from typing_extensions import ( + Annotated, Self, get_args as get_type_args, get_origin as get_type_origin, @@ -912,14 +913,36 @@ def _encode_field(self, field: str) -> str: return self._get_remote_field(self._resolve_alias(field)) def _encode_value(self, type_hint: Any, value: Any) -> Any: - type_origin = get_type_origin(type_hint) or type_hint + # Field aliases should be parsed before we get to this point. + # Handle model refs specially. + is_model_ref = ModelRef.is_annotated(type_hint) + if is_model_ref: + attr_type = get_type_origin(get_type_args(type_hint)[0]) + if attr_type is list and isinstance(value, (list, set, tuple)): + return [ + (record.id if isinstance(record, RecordBase) else record) + for record in value + ] + if isinstance(value, RecordBase): + return value.id + # Should be a record ID (int). + return value + # For every other field type, parse the possible value types + # from the type hint. + type_hint_origin = get_type_origin(type_hint) or type_hint + attr_type = ( + get_type_args(type_hint)[0] + if type_hint_origin is Annotated + else type_hint_origin + ) + attr_type_origin = get_type_origin(attr_type) or attr_type value_types = ( - get_type_args(type_hint) if type_origin is Union else [type_origin] + get_type_args(attr_type) + if attr_type_origin is Union + else [attr_type_origin] ) - is_model_ref = ModelRef.is_annotated(type_hint) + # Recursively handle the types that need to be serialised. for value_type in value_types: - if is_model_ref and isinstance(value, RecordBase): - return value.id if value_type is date and isinstance(value, date): return value.strftime(DEFAULT_SERVER_DATE_FORMAT) if value_type is datetime and isinstance(value, datetime): From ddcdc3b6e9c3fc783ad72442b3b860141e16a550 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 18:14:13 +1200 Subject: [PATCH 81/87] write_date is still required --- docs/managers/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/index.md b/docs/managers/index.md index 7019502..d177f93 100644 --- a/docs/managers/index.md +++ b/docs/managers/index.md @@ -1319,7 +1319,7 @@ and caches it for subsequent accesses. #### `write_date` ```python -write_date: datetime | None +write_date: datetime ``` The time the record was last modified. From c3f8a0abb7f9a250916f1fd8207b20e1d4f5aca0 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 19:03:44 +1200 Subject: [PATCH 82/87] Fix incorrect comma --- docs/managers/account-move.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managers/account-move.md b/docs/managers/account-move.md index a21b103..e30cbe4 100644 --- a/docs/managers/account-move.md +++ b/docs/managers/account-move.md @@ -38,7 +38,7 @@ The following manager methods are also available, in addition to the standard me ### `action_post` ```python -action_post(*account_moves: int, | AccountMove | Iterable[int | AccountMove]) -> None +action_post(*account_moves: int | AccountMove | Iterable[int | AccountMove]) -> None ``` Change one or more draft account moves (invoices) From e8d9332c1e5420d6704fdf3d6ed87132ca531df4 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 19:09:07 +1200 Subject: [PATCH 83/87] Update release date --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index cb5bba0..5108264 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ -## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) - 2024-06-27 +## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) - 2024-07-02 ### Added From 890184bf62179f2b4c84fd28b063c1739375a921 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 19:11:39 +1200 Subject: [PATCH 84/87] Move missing changes over to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99f200a..ee270e1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ changes between Odoo versions. The OpenStack Odoo Client library supports Python 3.8 and later. -To install the library package, simply install the `openstack-odooclient` package using `pip`. +To install the library package, simply install the +[`openstack-odooclient`](https://pypi.org/project/openstack-odooclient) +package using `pip`. ```bash python -m pip install openstack-odooclient From d27d77c7369997f05437b3137f11a357e5266695 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Mon, 1 Jul 2024 19:13:47 +1200 Subject: [PATCH 85/87] Change README links to point to GitHub Pages --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee270e1..8b0d982 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ For example, performing a simple search query would look something like this: ``` For more information on the available managers and their functions, -check the [Managers](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/managers/index.md) page in the documentation. +check the [Managers](https://catalyst-cloud.github.io/python-openstack-odooclient/latest/managers/index.html) page in the documentation. ## Records @@ -138,4 +138,4 @@ User(record={'id': 1234, ...}, fields=None) ``` For more information on the available managers and their functions, -check the [Records](https://github.com/catalyst-cloud/python-openstack-odooclient/blob/main/docs/managers/index.md#records) section in the documentation. +check the [Records](https://catalyst-cloud.github.io/python-openstack-odooclient/latest/managers/index.html#records) section in the documentation. From 1318642672a83f0a12ff0ef7494017cd97bb7b7d Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 2 Jul 2024 09:20:38 +1200 Subject: [PATCH 86/87] Make sure optional/empty model refs are encoded to the expected values --- openstack_odooclient/base/record_manager.py | 24 +++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index a46747f..47ed3d8 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -918,14 +918,26 @@ def _encode_value(self, type_hint: Any, value: Any) -> Any: is_model_ref = ModelRef.is_annotated(type_hint) if is_model_ref: attr_type = get_type_origin(get_type_args(type_hint)[0]) - if attr_type is list and isinstance(value, (list, set, tuple)): - return [ - (record.id if isinstance(record, RecordBase) else record) - for record in value - ] + if attr_type is list: + # False, None or empty structures are expected here. + if not value: + return [] + if isinstance(value, (list, set, tuple)): + return [ + ( + record.id + if isinstance(record, RecordBase) + else record + ) + for record in value + ] if isinstance(value, RecordBase): return value.id - # Should be a record ID (int). + # None is our internal representation of "no value". + # Odoo generally expects False. + if value is None: + return False + # Should be a record ID (int), or False. return value # For every other field type, parse the possible value types # from the type hint. From e5b4107eb498f433f701c55cbc096b8c4d1c28c8 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Tue, 2 Jul 2024 09:27:56 +1200 Subject: [PATCH 87/87] Simplify is_model_ref var --- openstack_odooclient/base/record_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openstack_odooclient/base/record_manager.py b/openstack_odooclient/base/record_manager.py index 47ed3d8..490fd92 100644 --- a/openstack_odooclient/base/record_manager.py +++ b/openstack_odooclient/base/record_manager.py @@ -915,8 +915,7 @@ def _encode_field(self, field: str) -> str: def _encode_value(self, type_hint: Any, value: Any) -> Any: # Field aliases should be parsed before we get to this point. # Handle model refs specially. - is_model_ref = ModelRef.is_annotated(type_hint) - if is_model_ref: + if ModelRef.is_annotated(type_hint): attr_type = get_type_origin(get_type_args(type_hint)[0]) if attr_type is list: # False, None or empty structures are expected here.