diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d9ecfaf --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +--- + +name: main + +on: + push: + branches: + - main + +jobs: + publish-github-pages: + 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 docs to GitHub Pages + run: pdm run mike deploy --push develop diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..ff60434 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,96 @@ +name: tag + +on: + push: + tags: + - "*.*.*" + +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.16.1" + - 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.16.1" + - 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: Extract changelog + id: extract-changelog + uses: sean0x42/markdown-extract@v2 + with: + 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: + 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 docs to GitHub Pages + run: pdm run mike deploy --push --update-aliases ${{ github.ref_name }} latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a0b8caa --- /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.16.1" + - 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.16.1" + # - 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/.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/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..533b873 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +--- +# .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/crate-ci/typos + rev: "v1.22.9" + hooks: + - id: typos + - 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.16.1" + hooks: + - id: pdm-lock-check diff --git a/README.md b/README.md index 5cbe30c..8b0d982 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,141 @@ -# python-openstack-odooclient -Python client library for Odoo and the OpenStack integration add-on. +# 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 +[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 well-defined API, without having to take into account considerations such as backward-incompatible +changes between Odoo versions. + +## Installation + +The OpenStack Odoo Client library supports Python 3.8 and later. + +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 +``` + +## 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 | str | Path = 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, +check the [Managers](https://catalyst-cloud.github.io/python-openstack-odooclient/latest/managers/index.html) page in the documentation. + +## 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 the available managers and their functions, +check the [Records](https://catalyst-cloud.github.io/python-openstack-odooclient/latest/managers/index.html#records) section in the documentation. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..5108264 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,10 @@ +# Changelog + + + +## [0.1.0](https://github.com/catalyst-cloud/python-openstack-odooclient/releases/tag/0.1.0) - 2024-07-02 + + +### Added + +- Create the OpenStack Odoo Client Library for Python ([#1](https://github.com/catalyst-cloud/python-openstack-odooclient/pull/1)) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5b89761 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,139 @@ +# 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 +[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 well-defined API, without having to take into account considerations such as backward-incompatible +changes between Odoo versions. + +## Installation + +The OpenStack Odoo Client library supports Python 3.8 and later. + +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 +``` + +## 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 | str | Path = 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/account-move-line.md b/docs/managers/account-move-line.md new file mode 100644 index 0000000..d6416f8 --- /dev/null +++ b/docs/managers/account-move-line.md @@ -0,0 +1,243 @@ +# Account Move (Invoice) Lines + +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` +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. + +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 +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. + +### `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 +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..e30cbe4 --- /dev/null +++ b/docs/managers/account-move.md @@ -0,0 +1,304 @@ +# Account Moves (Invoices) + +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` +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). + +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. + +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. + +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 +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 +``` + +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` + +```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 + +### `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/company.md b/docs/managers/company.md new file mode 100644 index 0000000..4e6b321 --- /dev/null +++ b/docs/managers/company.md @@ -0,0 +1,149 @@ +# Companies + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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..c93943e --- /dev/null +++ b/docs/managers/credit-transaction.md @@ -0,0 +1,91 @@ +# OpenStack Credit Transactions + +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` +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. + +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 +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..3804a3d --- /dev/null +++ b/docs/managers/credit-type.md @@ -0,0 +1,167 @@ +# OpenStack Credit Types + +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` +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. + +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 +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)/[`only_for_product_categories`](#only_for_product_categories). +If none of these values 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_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. + +### `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)/[`only_for_products`](#only_for_products). +If none of these values are specified, the credit applies to all products. + +### `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_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. + +### `product_id` + +```python +product_id: int +``` + +The ID of the [product](product.md) to use when applying +the credit to invoices. + +### `product_name` + +```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..6bff3d8 --- /dev/null +++ b/docs/managers/credit.md @@ -0,0 +1,167 @@ +# OpenStack Credits + +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` +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. + +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 +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/currency.md b/docs/managers/currency.md new file mode 100644 index 0000000..cd79548 --- /dev/null +++ b/docs/managers/currency.md @@ -0,0 +1,137 @@ +# Currencies + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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 age of the set currency rate. + +### `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/custom.md b/docs/managers/custom.md new file mode 100644 index 0000000..fbe314b --- /dev/null +++ b/docs/managers/custom.md @@ -0,0 +1,850 @@ +# 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. + +!!! 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](#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, +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["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. + +#### `bool` + +Corresponds to the `Boolean` field type in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + 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["CustomRecordManager"]): + custom_field: int + """Description of the field.""" +``` + +#### `str` + +Corresponds to the `Char` or `Text` field types in Odoo. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + custom_field: date + """Description of the field.""" +``` + +#### `Literal` + +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["CustomRecordManager"]): + 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 +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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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 (Many2one) + +Singular record model refs correspond to the `Many2one` relationship type in Odoo. +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 +(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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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 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). + +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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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 IDs directly as a list of integers. + +```python +from __future__ import annotations + +from typing import List + +from openstack_odooclient import ModelRef, RecordBase, Product +from typing_extensions import Annotated + +class CustomRecord(RecordBase["CustomRecordManager"]): + 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 expose the records 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["CustomRecordManager"]): + 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["CustomRecordManager"]): + 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 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. + +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["CustomRecordManager"]): + 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 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 + 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, RecordManagerBase +from typing_extensions import Annotated + +class Parent(RecordBase["ParentManager"]): + 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. + """ + +class ParentManager(RecordManagerBase[Parent]): + env_name = "custom.parent" + record_class = Parent + +from .child import Child # noqa: E402 +``` + +```python title="child.py" +from __future__ import annotations + +from typing import Optional + +from openstack_odooclient import ModelRef, RecordBase, RecordManagerBase +from typing_extensions import Annotated + +class Child(RecordBase["ChildManager"]): + 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. + """ + +class Child(RecordManagerBase[Child]): + env_name = "custom.child" + record_class = child + +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["CustomRecordManager"]): + 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. + +```python +from __future__ import annotations + +from openstack_odooclient import RecordBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + 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` ([`Client`](../index.md#connecting-to-odoo)) - The Odoo client 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 +* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model + +!!! note + + 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 + +**Manager classes** are used to provide query methods and other functionality +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 `RecordManagerBase` class, +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[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: + +* `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. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import RecordBase, RecordManagerBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(RecordManagerBase[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 +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 by calling them on the manager object. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import Client, RecordBase, RecordManagerBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(RecordManagerBase[CustomRecord]): + env_name = "custom.record" + record_class = CustomRecord + +odoo_client = Client(...) +custom_records = CustomRecordManager(odoo_client) +``` + +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 +custom manager class. + +```python +from __future__ import annotations + +from typing import List, Union + +from openstack_odooclient import RecordBase, RecordManagerBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(RecordManagerBase[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 RecordBase, RecordManagerBase + +class CustomRecord(RecordBase["CustomRecordManager"]): + custom_field: str + """Description of the field.""" + +class CustomRecordManager(RecordManagerBase[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 custom_record + ), + ) +``` + +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[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 +* `_env` (`odoorpc.env.Environment`) - 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, RecordManagerBase, User, UserManager + +class CustomUser(User): + custom_field: str + """Description of the field.""" + +class CustomUserManager(RecordManagerBase[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/customer-group.md b/docs/managers/customer-group.md new file mode 100644 index 0000000..1c88fb1 --- /dev/null +++ b/docs/managers/customer-group.md @@ -0,0 +1,106 @@ +# OpenStack Customer Groups + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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..a948ffa --- /dev/null +++ b/docs/managers/grant-type.md @@ -0,0 +1,166 @@ +# OpenStack Grant Types + +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` +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. + +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 +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_name` + +```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..ea5d01c --- /dev/null +++ b/docs/managers/grant.md @@ -0,0 +1,138 @@ +# OpenStack Grants + +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` +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. + +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 +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..d177f93 --- /dev/null +++ b/docs/managers/index.md @@ -0,0 +1,1447 @@ +# 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) +* [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](user.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, + optional: bool = False, +) -> list[Record] +``` + +```python +list( + ids: int | Iterable[int], + fields: Iterable[str] | None = None, + as_dict: bool = True, + optional: bool = False, +) -> 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, ...}] +``` + +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 +>>> 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) +[] +``` + +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` | +| `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 `optional` is `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[Tuple[str, str, Any] | Sequence[Any] | str] | 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[Tuple[str, str, Any] | Sequence[Any] | str] | 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[Tuple[str, str, Any] | Sequence[Any] | str] | 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[Tuple[str, str, Any] | Sequence[Any] | str] | 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[Tuple[str, str, Any] | Sequence[Any] | str] | 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[Tuple[str, str, Any] | Sequence[Any] | str] | 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 +[ORM API search domain](https://www.odoo.com/documentation/14.0/developer/reference/addons/orm.html#search-domains) +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)` + 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](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 + 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), ...] +``` + +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``). + +```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[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 + +| 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_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 +``` + +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 +>>> 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 +``` + +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. + +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( +... hostname="localhost", +... port=8069, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.sales_orders.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_orders.list( +... odoo_client.sales_orders.create_multi({...}, {...}), +... ) +[SaleOrder(record={'id': 1234, ...}, fields=None), SaleOrder(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` | `int | Record | Iterable[int | Record]` | 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 (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 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` + +```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`) | + +## Coded Record Managers + +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. + +* [OpenStack Referral Codes](referral-code.md) +* [OpenStack Voucher Codes](voucher-code.md) + +### `get_by_code` + +```python +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, +... protocol="jsonrpc", +... database="odoodb", +... user="test-user", +... password="", +... ) +>>> odoo_client.voucher_codes.get_by_code("OSCODE123") +VoucherCode(record={'id': 1234, 'code': 'OSCODE123', ...}, 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.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 +>>> 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, ...} +``` + +When `optional` is `True`, `None` is returned if a record +with the given code 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.voucher_codes.get_by_code("non-existent", optional=True) +None +``` + +#### 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 +>>> 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 + +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 +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 | None +``` + +The ID of the [user](user.md) that created this record. + +#### `create_name` + +```python +create_name: str | None +``` + +The name of the [user](user.md) that created this record. + +#### `create_user` + +```python +create_user: User | None +``` + +The [user](user.md) 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 | None +``` + +The ID of the [user](user.md) that last modified this record. + +#### `write_name` + +```python +write_name: str | None +``` + +The name of the [user](user.md) that modified this record. + +#### `write_user` + +```python +write_user: User | None +``` + +The [user](user.md) that last modified this record. + +This fetches the full record from Odoo once, +and caches it for subsequence accesses. + +#### `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 +``` + +## 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/partner-category.md b/docs/managers/partner-category.md new file mode 100644 index 0000000..7780a4a --- /dev/null +++ b/docs/managers/partner-category.md @@ -0,0 +1,157 @@ +# Partner Categories + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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..17ff4b8 --- /dev/null +++ b/docs/managers/partner.md @@ -0,0 +1,385 @@ +# Partners + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + +### `active` + +```python +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 +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. + +### `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..a02db62 --- /dev/null +++ b/docs/managers/pricelist.md @@ -0,0 +1,221 @@ +# Pricelists + +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` +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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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..e2476f6 --- /dev/null +++ b/docs/managers/product-category.md @@ -0,0 +1,138 @@ +# Product Categories + +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` +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. + +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 +child_id: list[int] +``` + +A list of IDs for the child categories. + +### `child_ids` + +```python +child_ids: list[int] +``` + +An alias for [`child_id`](#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..0dc38c4 --- /dev/null +++ b/docs/managers/product.md @@ -0,0 +1,383 @@ +# Products + +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` +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. + +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 +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..9030bc9 --- /dev/null +++ b/docs/managers/project-contact.md @@ -0,0 +1,125 @@ +# OpenStack Project Contacts + +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` +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. + +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 +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 new file mode 100644 index 0000000..dcd59af --- /dev/null +++ b/docs/managers/project.md @@ -0,0 +1,423 @@ +# OpenStack Projects + +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` +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). + +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. + +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. + +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 +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 new file mode 100644 index 0000000..9a69b24 --- /dev/null +++ b/docs/managers/referral-code.md @@ -0,0 +1,190 @@ +# OpenStack Referral Codes + +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` +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. + +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 +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-tier.md b/docs/managers/reseller-tier.md new file mode 100644 index 0000000..0bbaec2 --- /dev/null +++ b/docs/managers/reseller-tier.md @@ -0,0 +1,147 @@ +# OpenStack 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. + +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 +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 new file mode 100644 index 0000000..1ba7c44 --- /dev/null +++ b/docs/managers/reseller.md @@ -0,0 +1,172 @@ +# OpenStack Resellers + +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` +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. + +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 +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/docs/managers/sale-order-line.md b/docs/managers/sale-order-line.md new file mode 100644 index 0000000..57ccb5e --- /dev/null +++ b/docs/managers/sale-order-line.md @@ -0,0 +1,553 @@ +# 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). + +## 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 +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 [account move (invoice) lines](account-move-line.md) created +from this sale order line. + +### `invoice_lines` + +```python +invoice_lines: list[AccountMoveLine] +``` + +The [account move (invoice) lines](account-move-line.md) 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 new file mode 100644 index 0000000..7d12bc0 --- /dev/null +++ b/docs/managers/sale-order.md @@ -0,0 +1,382 @@ +# 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. + +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 +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/docs/managers/support-subscription-type.md b/docs/managers/support-subscription-type.md new file mode 100644 index 0000000..dd86e77 --- /dev/null +++ b/docs/managers/support-subscription-type.md @@ -0,0 +1,135 @@ +# 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. + +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 +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 new file mode 100644 index 0000000..027c0a4 --- /dev/null +++ b/docs/managers/support-subscription.md @@ -0,0 +1,175 @@ +# 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_subscriptions` +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. + +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 +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/docs/managers/tax-group.md b/docs/managers/tax-group.md new file mode 100644 index 0000000..f3c2e90 --- /dev/null +++ b/docs/managers/tax-group.md @@ -0,0 +1,57 @@ +# 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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-and-methods). + +### `name` + +```python +name: str +``` + +Name of the tax group. diff --git a/docs/managers/tax.md b/docs/managers/tax.md new file mode 100644 index 0000000..3ecf28f --- /dev/null +++ b/docs/managers/tax.md @@ -0,0 +1,198 @@ +# 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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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/docs/managers/term-discount.md b/docs/managers/term-discount.md new file mode 100644 index 0000000..a58d349 --- /dev/null +++ b/docs/managers/term-discount.md @@ -0,0 +1,187 @@ +# 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. + +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 +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 new file mode 100644 index 0000000..41f8be1 --- /dev/null +++ b/docs/managers/trial.md @@ -0,0 +1,117 @@ +# 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. + +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 +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-category.md b/docs/managers/uom-category.md new file mode 100644 index 0000000..4ef3490 --- /dev/null +++ b/docs/managers/uom-category.md @@ -0,0 +1,81 @@ +# 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. + +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 +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 new file mode 100644 index 0000000..f2b63da --- /dev/null +++ b/docs/managers/uom.md @@ -0,0 +1,150 @@ +# 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), Product | +| 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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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/docs/managers/user.md b/docs/managers/user.md new file mode 100644 index 0000000..eef6f01 --- /dev/null +++ b/docs/managers/user.md @@ -0,0 +1,127 @@ +# 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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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 new file mode 100644 index 0000000..1ccb853 --- /dev/null +++ b/docs/managers/volume-discount-range.md @@ -0,0 +1,195 @@ +# 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. + +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 +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 new file mode 100644 index 0000000..2709000 --- /dev/null +++ b/docs/managers/voucher-code.md @@ -0,0 +1,279 @@ +# 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. + +For more information on attributes and methods common to all record types, +see [Record Attributes and Methods](index.md#attributes-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/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..2b6251f --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,195 @@ +# 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 method to limit the selected fields 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 +``` + +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, +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](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 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 +``` + +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/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f2ecb21 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,107 @@ +--- + +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 + +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 + +extra: + version: + provider: mike + +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 + - performance.md + - changelog.md + +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/__init__.py b/openstack_odooclient/__init__.py new file mode 100644 index 0000000..d981426 --- /dev/null +++ b/openstack_odooclient/__init__.py @@ -0,0 +1,160 @@ +# 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.client import ClientBase +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, + MultipleRecordsFoundError, + RecordNotFoundError, +) +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", + "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", + "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", + "UserManager", + "VolumeDiscountRange", + "VolumeDiscountRangeManager", + "VoucherCode", + "VoucherCodeManager", + "ClientError", + "MultipleRecordsFoundError", + "RecordNotFoundError", +] 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/base/client.py b/openstack_odooclient/base/client.py new file mode 100644 index 0000000..e41ef41 --- /dev/null +++ b/openstack_odooclient/base/client.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 + +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_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: + """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 + 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, str, Path] + :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, str, Path] = ..., + 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, str, Path] = ..., + 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, str, Path] = ..., + 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, str, Path] = 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) + 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): + 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.""" + 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/base/record.py b/openstack_odooclient/base/record.py new file mode 100644 index 0000000..1da33da --- /dev/null +++ b/openstack_odooclient/base/record.py @@ -0,0 +1,496 @@ +# 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 dataclasses import dataclass +from datetime import date, datetime +from types import MappingProxyType +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + Literal, + Mapping, + Optional, + Sequence, + Type, + TypeVar, + Union, +) + +from typing_extensions import ( + Annotated, + Self, + get_args as get_type_args, + get_origin as get_type_origin, +) + +from ..util import is_subclass + +if TYPE_CHECKING: + from odoorpc import ODOO # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + + from .client import ClientBase + +RecordManager = TypeVar("RecordManager", bound="RecordManagerBase") + + +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 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["CustomRecordManager"]): + ... name: str + ... name_alias: Annotated[str, FieldAlias("name")] + """ + + field: str + + +@dataclass(frozen=True) +class ModelRef(AnnotationBase): + """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["CustomRecordManager"]): + ... 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 + record_class: Any + + +class RecordBase(Generic[RecordManager]): + """The generic base class for records. + + 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 + """The record's ID in Odoo.""" + + create_date: datetime + """The time the record was created.""" + + create_uid: Annotated[Optional[int], ModelRef("create_uid", User)] + """The ID of the user that created this record.""" + + create_name: Annotated[Optional[str], ModelRef("create_uid", User)] + """The name of the user that created this record.""" + + create_user: Annotated[Optional[User], ModelRef("create_uid", User)] + """The user that created this record. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + write_date: datetime + """The time the record was last modified.""" + + write_uid: Annotated[Optional[int], ModelRef("write_uid", User)] + """The ID for the user that last modified this record.""" + + write_name: Annotated[Optional[str], ModelRef("write_uid", User)] + """The name of the user that last modified this record.""" + + write_user: Annotated[Optional[User], ModelRef("write_uid", User)] + """The user that last modified this record. + + This fetches the full record from Odoo once, + and caches it for subsequence accesses. + """ + + _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. + """ + + def __init__( + self, + client: ClientBase, + record: Mapping[str, Any], + fields: Optional[Sequence[str]], + ) -> None: + self._client = client + """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) -> RecordManager: + """The manager object responsible for this record.""" + mapping = self._client._record_manager_mapping + return mapping[type(self)] # type: ignore[return-value] + + @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 + + @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 + 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, + 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 from OdooRPC, defaults to False + :type raw: bool, optional + :return: Record dictionary + :rtype: Dict[str, Any] + """ + return ( + copy.deepcopy(dict(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. + + 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 + """ + return type(self)( + client=self._client, + 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 + + 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] + # NOTE(callumdickinson): Use the type hint to coerce + # the field value returned in the record dict into the expected type. + # 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 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 = 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( + 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] + # 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): + 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( + ( + "Unsupported field value type for 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 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: " + f"{value_type}" + ), + ) + + @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) + # 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__}(" + f"record={dict(self._record)}" + f", fields={list(self._fields) if self._fields else None}" + ")" + ) + + def __repr__(self) -> str: + return str(self) + + +# 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 new file mode 100644 index 0000000..490fd92 --- /dev/null +++ b/openstack_odooclient/base/record_manager.py @@ -0,0 +1,964 @@ +# 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 types import MappingProxyType +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, + Self, + get_args as get_type_args, + get_origin as get_type_origin, + get_type_hints, +) + +from ..exceptions import RecordNotFoundError +from ..util import ( + DEFAULT_SERVER_DATE_FORMAT, + DEFAULT_SERVER_DATETIME_FORMAT, + get_mapped_field, +) +from .record import FieldAlias, ModelRef, RecordBase + +if TYPE_CHECKING: + from odoorpc import ODOO # type: ignore[import] + from odoorpc.env import Environment # type: ignore[import] + + from .client import ClientBase + +Record = TypeVar("Record", bound=RecordBase) +FilterCriterion = Union[Tuple[str, str, Any], Sequence[Any], str] + + +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["CustomRecordManager"]): + ... 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.""" + + record_class: Type[Record] + """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 + in queries. + + By default, all fields on the model will be fetched. + """ + + def __init__(self, client: ClientBase) -> None: + self._client = client + """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 + 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 = { + 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() + ) + } + """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: + """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]] = ..., + 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( + self, + 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. + + 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. + + 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. + 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. + + :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 + :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]] + """ + 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._encode_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: + res_dicts = [ + { + self._get_local_field(field): value + for field, value in record_dict.items() + } + for record_dict in records + ] + else: + res_objs = [ + self.record_class( + client=self._client, + 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) + ) + 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( + 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, + optional=True, + )[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[FilterCriterion]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + ) -> List[Record]: ... + + @overload + def search( + self, + filters: Optional[Sequence[FilterCriterion]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[False] = ..., + ) -> List[int]: ... + + @overload + def search( + self, + filters: Optional[Sequence[FilterCriterion]] = ..., + 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[FilterCriterion]] = ..., + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[True], + ) -> List[int]: ... + + @overload + def search( + self, + filters: Optional[Sequence[FilterCriterion]] = ..., + 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[FilterCriterion]] = 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 ORM API search domain + 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)`` + 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. + + 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``). + + 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: Union[Tuple[str, str, Any], Sequence[Any], str] | None + :param fields: Fields to select, defaults to ``None`` (select all) + :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`` + :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, + # 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, + ) + return [] # type: ignore[return-value] + + def _encode_filters( + self, + filters: Sequence[FilterCriterion], + ) -> List[Union[str, Tuple[str, str, Any]]]: + _filters: List[Union[str, Tuple[str, str, Any]]] = [] + for f in filters: + if isinstance(f, str): + _filters.append(f) + else: + field_type, field_name = self._encode_filter_field(field=f[0]) + operator: str = f[1] + 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], + ) + _filters.append((field_name, operator, value)) + return _filters + + 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 + # 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 self._record_type_hints: + return (Any, f"{remote_field}.{'.'.join(field_refs[1:])}") + type_hint: Any = self._record_type_hints[local_field] + model_ref = ModelRef.get(type_hint) + if model_ref: + 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[ + record_class # type: ignore[index] + ]._encode_filter_field( + 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 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 self._record_type_hints: + return (Any, remote_field) + type_hint = self._record_type_hints[local_field] + return (type_hint, remote_field) + + 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. + + 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. + + :return: The ID of the newly created record + :rtype: int + """ + 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, + 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. + + :return: The IDs of the newly created records + :rtype: List[int] + """ + res: Union[int, List[int]] = self._env.create( + [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] = {} + for field, value in fields.items(): + remote_field, remote_value = self._encode_create_field( + 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, + field: str, + value: Any, + ) -> Tuple[str, Any]: + # Fetch the local and remote representations of the given field. + # 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 self._record_type_hints: + return (remote_field, value) + # Fetch the type hint for parsing. + 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: + # 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. + 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: + if not value: + return (remote_field, []) + remote_values: List[ + Union[ + Tuple[int, int], + Tuple[int, int, Dict[str, Any]], + ], + ] = [] + for v in value: + 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 + if model_ref.record_class is Self + else self._client._record_manager_mapping[ + model_ref.record_class + ] + ) + 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 (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 (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 (remote_field, value.id) + # 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}' " + f"when creating record: {value}" + ), + ) + # For regular fields, encode the value based on its type hint. + return ( + remote_field, + self._encode_value(type_hint=type_hint, value=value), + ) + + def unlink( + self, + *records: Union[int, Record, Iterable[Union[int, Record]]], + ) -> 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: + # 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, + field=field, + ) + + def _get_local_field(self, field: str) -> str: + # 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, 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)) + + 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: + # Field aliases should be parsed before we get to this point. + # Handle model refs specially. + 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. + 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 + # 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. + 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(attr_type) + if attr_type_origin is Union + else [attr_type_origin] + ) + # Recursively handle the types that need to be serialised. + for value_type in value_types: + 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): + 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/base/record_manager_coded.py b/openstack_odooclient/base/record_manager_coded.py new file mode 100644 index 0000000..607b75f --- /dev/null +++ b/openstack_odooclient/base/record_manager_coded.py @@ -0,0 +1,200 @@ +# 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 .record_manager_with_unique_field import ( + Record, + RecordManagerWithUniqueFieldBase, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + Literal, + Optional, + Union, + ) + + +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 + 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. + 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. + """ + + @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[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 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/base/record_manager_named.py b/openstack_odooclient/base/record_manager_named.py new file mode 100644 index 0000000..ed79532 --- /dev/null +++ b/openstack_odooclient/base/record_manager_named.py @@ -0,0 +1,200 @@ +# 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 .record_manager_with_unique_field import ( + Record, + RecordManagerWithUniqueFieldBase, +) + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + Iterable, + Literal, + Optional, + Union, + ) + + +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 + 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. + """ + + @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[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 + :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/base/record_manager_with_unique_field.py b/openstack_odooclient/base/record_manager_with_unique_field.py new file mode 100644 index 0000000..c6360f2 --- /dev/null +++ b/openstack_odooclient/base/record_manager_with_unique_field.py @@ -0,0 +1,266 @@ +# 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 .record_manager 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], +): + """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["CustomRecordManager"]): + ... name: str + >>> class CustomRecordManager( + ... RecordManagerWithUniqueFieldBase[CustomRecord, 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, + 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. + + 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. + + 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 + :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) + :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 + :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/client.py b/openstack_odooclient/client.py new file mode 100644 index 0000000..fd41852 --- /dev/null +++ b/openstack_odooclient/client.py @@ -0,0 +1,192 @@ +# 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.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): + """A client 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, str, Path] + :param version: Server version, defaults to ``None`` (auto-detect) + :type version: Optional[str], optional + """ + + account_moves: AccountMoveManager + """Account move (invoice) manager.""" + + account_move_lines: AccountMoveLineManager + """Account move (invoice) line manager.""" + + companies: CompanyManager + """Company manager.""" + + credits: CreditManager + """OpenStack credit manager.""" + + credit_transactions: CreditTransactionManager + """OpenStack credit transaction manager.""" + + credit_types: CreditTypeManager + """OpenStack credit type manager.""" + + currencies: CurrencyManager + """Currency manager.""" + + 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 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/exceptions.py b/openstack_odooclient/exceptions.py new file mode 100644 index 0000000..3f8af82 --- /dev/null +++ b/openstack_odooclient/exceptions.py @@ -0,0 +1,36 @@ +# 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..f2d3283 --- /dev/null +++ b/openstack_odooclient/managers/account_move.py @@ -0,0 +1,244 @@ +# 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 typing import Any, Iterable, List, Literal, Mapping, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class AccountMove(RecordBase["AccountMoveManager"]): + 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, ModelRef("currency_id", Currency)] + """The ID for the currency used in this account move (invoice).""" + + currency_name: Annotated[str, ModelRef("currency_id", Currency)] + """The name of the currency used in this account move (invoice).""" + + currency: Annotated[Currency, ModelRef("currency_id", 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 + """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], + 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[AccountMoveLine], + ModelRef("invoice_line_ids", 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: 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", + ] + """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: Union[str, Literal[False]] + """Name assigned to the account move (invoice), if posted.""" + + 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], 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], ModelRef("os_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. + """ + + 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: 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", + }, + } + + def action_post(self) -> None: + """Change this 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(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 +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 new file mode 100644 index 0000000..ead00eb --- /dev/null +++ b/openstack_odooclient/managers/account_move_line.py @@ -0,0 +1,144 @@ +# 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, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class AccountMoveLine(RecordBase["AccountMoveLineManager"]): + currency_id: Annotated[int, ModelRef("currency_id", Currency)] + """The ID for the currency used in this + account move (invoice) line. + """ + + currency_name: Annotated[int, ModelRef("currency_id", Currency)] + """The name of the currency used in this + account move (invoice) line. + """ + + currency: Annotated[Currency, ModelRef("currency_id", 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 + """Amount charged in tax on the account move (invoice) line.""" + + move_id: Annotated[int, ModelRef("move_id", AccountMove)] + """The ID for the account move (invoice) this line is part of.""" + + move_name: Annotated[str, ModelRef("move_id", AccountMove)] + """The name of the account move (invoice) this line is part of.""" + + move: Annotated[AccountMove, ModelRef("move_id", AccountMove)] + """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.""" + + 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], ModelRef("os_project", Project)] + """The name of the OpenStack project this account move (invoice) line + was generated for. + """ + + os_project: Annotated[Optional[Project], ModelRef("os_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. + """ + + os_region: Union[str, Literal[False]] + """The OpenStack region the account move (invoice) line + was created from. + """ + + os_resource_id: Union[str, Literal[False]] + """The OpenStack resource ID for the resource that generated + this account move (invoice) line. + """ + + 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 a compute instance, this would be set to the instance's flavour name. + """ + + os_resource_type: Union[str, Literal[False]] + """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.""" + + product_id: Annotated[int, ModelRef("product_id", Product)] + """The ID for the product charged on the + account move (invoice) line. + """ + + product_name: Annotated[str, ModelRef("product_id", Product)] + """The name of the product charged on the + account move (invoice) line. + """ + + product: Annotated[Product, ModelRef("product_id", 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 + """Quantity of product charged on the account move (invoice) line.""" + + +class AccountMoveLineManager(RecordManagerBase[AccountMoveLine]): + env_name = "account.move.line" + record_class = 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/company.py b/openstack_odooclient/managers/company.py new file mode 100644 index 0000000..22d57c7 --- /dev/null +++ b/openstack_odooclient/managers/company.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 typing import List, Literal, Optional, Union + +from typing_extensions import Annotated, Self + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class Company(RecordBase["CompanyManager"]): + active: bool + """Whether or not this company is active (enabled).""" + + child_ids: Annotated[List[int], ModelRef("child_ids", Self)] + """A list of IDs for the child companies.""" + + children: Annotated[List[Self], ModelRef("child_ids", Self)] + """The list of child companies. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + name: str + """Company name, set from the partner name.""" + + 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], ModelRef("parent_id", Self)] + """The name of the parent company, if this company + is the child of another company. + """ + + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] + """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.""" + + partner_id: Annotated[int, ModelRef("partner_id", Partner)] + """The ID for the partner for the company.""" + + partner_name: Annotated[str, ModelRef("partner_id", Partner)] + """The name of the partner for the company.""" + + partner: Annotated[Partner, ModelRef("partner_id", Partner)] + """The partner for the company. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class CompanyManager(NamedRecordManagerBase[Company]): + env_name = "res.company" + record_class = Company + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/credit.py b/openstack_odooclient/managers/credit.py new file mode 100644 index 0000000..c09409c --- /dev/null +++ b/openstack_odooclient/managers/credit.py @@ -0,0 +1,110 @@ +# 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 typing import List, Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Credit(RecordBase["CreditManager"]): + credit_type_id: Annotated[int, ModelRef("credit_type", CreditType)] + """The ID of the type of this credit.""" + + credit_type_name: Annotated[str, ModelRef("credit_type", CreditType)] + """The name of the type of this credit.""" + + credit_type: Annotated[CreditType, ModelRef("credit_type", CreditType)] + """The type of this credit. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + 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.""" + + transaction_ids: Annotated[ + List[int], + ModelRef("transactions", CreditTransaction), + ] + """A list of IDs for the transactions that have been made + using this credit. + """ + + transactions: Annotated[ + List[CreditTransaction], + ModelRef("transactions", 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: Annotated[ + Optional[int], + ModelRef("voucher_code", VoucherCode), + ] + """The ID of the voucher code used when applying for the credit, + if one was supplied. + """ + + voucher_code_name: Annotated[ + Optional[str], + ModelRef("voucher_code", VoucherCode), + ] + """The name of the voucher code used when applying for the credit, + if one was supplied. + """ + + voucher_code: Annotated[ + Optional[VoucherCode], + ModelRef("voucher_code", 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. + """ + + +class CreditManager(RecordManagerBase[Credit]): + env_name = "openstack.credit" + record_class = Credit + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..a8c3eca --- /dev/null +++ b/openstack_odooclient/managers/credit_transaction.py @@ -0,0 +1,51 @@ +# 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_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class CreditTransaction(RecordBase["CreditTransactionManager"]): + credit_id: Annotated[int, ModelRef("credit", Credit)] + """The ID of the credit this transaction was made against.""" + + credit_name: Annotated[str, ModelRef("credit", Credit)] + """The name of the credit this transaction was made against.""" + + credit: Annotated[Credit, ModelRef("credit", Credit)] + """The credit this transaction was made against. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + description: str + """A description of this credit transaction.""" + + value: float + """The value of the credit transaction.""" + + +class CreditTransactionManager(RecordManagerBase[CreditTransaction]): + env_name = "openstack.credit.transaction" + record_class = CreditTransaction + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from .credit import Credit # noqa: E402 diff --git a/openstack_odooclient/managers/credit_type.py b/openstack_odooclient/managers/credit_type.py new file mode 100644 index 0000000..decde0a --- /dev/null +++ b/openstack_odooclient/managers/credit_type.py @@ -0,0 +1,117 @@ +# 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 List + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +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.""" + + 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, + and caches them for subsequent accesses. + """ + + name: str + """Name of the Credit Type.""" + + only_for_product_ids: Annotated[ + List[int], + ModelRef("only_for_products", Product), + ] + """A list of IDs for the products this credit applies to. + + 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[ + List[Product], + ModelRef("only_for_products", Product), + ] + """A list of products which this credit applies to. + + 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. + """ + + only_for_product_category_ids: Annotated[ + List[int], + ModelRef("only_for_product_categories", ProductCategory), + ] + """A list of IDs for the product categories this credit applies to. + + 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[ + List[ProductCategory], + ModelRef("only_for_product_categories", ProductCategory), + ] + """A list of product categories which this credit applies to. + + 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. + """ + + product_id: Annotated[int, ModelRef("product", Product)] + """The ID of the product to use when applying + the credit to invoices. + """ + + product_name: Annotated[str, ModelRef("product", Product)] + """The name of the product to use when applying + the credit to invoices. + """ + + product: Annotated[Product, ModelRef("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: bool + """Whether or not the credit is refundable.""" + + +class CreditTypeManager(NamedRecordManagerBase[CreditType]): + env_name = "openstack.credit.type" + record_class = 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/currency.py b/openstack_odooclient/managers/currency.py new file mode 100644 index 0000000..8393ac8 --- /dev/null +++ b/openstack_odooclient/managers/currency.py @@ -0,0 +1,69 @@ +# 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 ..base.record import RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class Currency(RecordBase["CurrencyManager"]): + 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 age of the set currency rate.""" + + 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(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..2208e5b --- /dev/null +++ b/openstack_odooclient/managers/customer_group.py @@ -0,0 +1,70 @@ +# 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 List, Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class CustomerGroup(RecordBase["CustomerGroupManager"]): + name: str + """The name of the customer group.""" + + 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], 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], ModelRef("pricelist", Pricelist)] + """The ID for the pricelist this customer group uses, + if not the default one. + """ + + 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], + ModelRef("pricelist", 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(NamedRecordManagerBase[CustomerGroup]): + env_name = "openstack.customer_group" + record_class = CustomerGroup + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..1669cd9 --- /dev/null +++ b/openstack_odooclient/managers/grant.py @@ -0,0 +1,88 @@ +# 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 typing import Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Grant(RecordBase["GrantManager"]): + expiry_date: date + """The date the grant expires.""" + + grant_type_id: Annotated[int, ModelRef("grant_type", GrantType)] + """The ID of the type of this grant.""" + + grant_type_name: Annotated[str, ModelRef("grant_type", GrantType)] + """The name of the type of this grant.""" + + grant_type: Annotated[GrantType, ModelRef("grant_type", GrantType)] + """The type of this grant. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + 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.""" + + voucher_code_id: Annotated[ + Optional[int], + ModelRef("voucher_code", VoucherCode), + ] + """The ID of the voucher code used when applying for the grant, + if one was supplied. + """ + + voucher_code_name: Annotated[ + Optional[str], + ModelRef("voucher_code", VoucherCode), + ] + """The name of the voucher code used when applying for the grant, + if one was supplied. + """ + + voucher_code: Annotated[ + Optional[VoucherCode], + ModelRef("voucher_code", 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. + """ + + +class GrantManager(RecordManagerBase[Grant]): + env_name = "openstack.grant" + record_class = Grant + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..032b384 --- /dev/null +++ b/openstack_odooclient/managers/grant_type.py @@ -0,0 +1,125 @@ +# 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 List + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +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.""" + + 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, + and caches them for subsequent accesses. + """ + + name: str + """Name of the Grant Type.""" + + only_for_product_ids: Annotated[ + List[int], + ModelRef("only_for_products", Product), + ] + """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. + """ + + only_for_products: Annotated[ + List[Product], + ModelRef("only_for_products", Product), + ] + """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. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + only_for_product_category_ids: Annotated[ + List[int], + ModelRef( + "only_for_product_categories", + ProductCategory, + ), + ] + """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. + """ + + only_for_product_categories: Annotated[ + List[ProductCategory], + ModelRef( + "only_for_product_categories", + ProductCategory, + ), + ] + """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. + + 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. + """ + + product_id: Annotated[int, ModelRef("product", Product)] + """The ID of the product to use when applying + the grant to invoices. + """ + + product_name: Annotated[str, ModelRef("product", Product)] + """The name of the product to use when applying + the grant to invoices. + """ + + product: Annotated[Product, ModelRef("product", 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(NamedRecordManagerBase[GrantType]): + env_name = "openstack.grant.type" + record_class = GrantType + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..ed92cc8 --- /dev/null +++ b/openstack_odooclient/managers/partner.py @@ -0,0 +1,289 @@ +# 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 List, Literal, Optional, Union + +from typing_extensions import Annotated, Self + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Partner(RecordBase["PartnerManager"]): + active: bool + """Whether or not this partner is active (enabled).""" + + company_id: Annotated[int, ModelRef("company_id", Company)] + """The ID for the company this partner is owned by.""" + + company_name: Annotated[str, ModelRef("company_id", Company)] + """The name of the company this partner is owned by.""" + + company: Annotated[Company, ModelRef("company_id", Company)] + """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.""" + + os_customer_group_id: Annotated[ + Optional[int], + ModelRef("os_customer_group", CustomerGroup), + ] + """The ID for the customer group this partner is part of, + if it is part of one. + """ + + os_customer_group_name: Annotated[ + Optional[str], + 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[CustomerGroup], + ModelRef("os_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. + """ + + 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], ModelRef("os_projects", Project)] + """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], + ModelRef("os_project_contacts", ProjectContact), + ] + """A list of IDs for the project contacts that are associated + with this partner. + """ + + os_project_contacts: Annotated[ + List[ProjectContact], + ModelRef("os_project_contacts", ProjectContact), + ] + """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], + ModelRef("os_referral", ReferralCode), + ] + """The ID for the referral code the partner used on sign-up, + if one was used. + """ + + os_referral_name: Annotated[ + Optional[str], + ModelRef("os_referral", ReferralCode), + ] + """The name of the referral code the partner used on sign-up, + if one was used. + """ + + os_referral: Annotated[ + Optional[ReferralCode], + ModelRef("os_referral", 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. + """ + + os_referral_code_ids: Annotated[ + List[int], + ModelRef("os_referral_codes", ReferralCode), + ] + """A list of IDs for the referral codes the partner has used.""" + + os_referral_codes: Annotated[ + List[ReferralCode], + ModelRef("os_referral_codes", ReferralCode), + ] + """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], + ModelRef("os_reseller", Reseller), + ] + """The ID for the reseller for this partner, if this partner + is billed through a reseller. + """ + + os_reseller_name: Annotated[ + Optional[str], + 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], + ModelRef("os_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. + """ + + 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], 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], ModelRef("os_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. + """ + + parent_id: Annotated[ + Optional[int], + ModelRef("parent_id", Self), + ] + """The ID for the parent partner of this partner, + if it has a parent. + """ + + parent_name: Annotated[ + Optional[str], + ModelRef("parent_id", Self), + ] + """The name of the parent partner of this partner, + if it has a parent. + """ + + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] + """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], + ModelRef("property_product_pricelist", 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], + ModelRef("property_product_pricelist", 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], + ModelRef("property_product_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. + """ + + stripe_customer_id: Union[str, Literal[False]] + """The Stripe customer ID for this partner, if one has been assigned.""" + + 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], ModelRef("user_id", User)] + """The name of the internal user associated with this partner, + if one is assigned. + """ + + user: Annotated[Optional[User], ModelRef("user_id", 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. + """ + + +class PartnerManager(RecordManagerBase[Partner]): + env_name = "res.partner" + record_class = 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 +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 new file mode 100644 index 0000000..762b5cf --- /dev/null +++ b/openstack_odooclient/managers/partner_category.py @@ -0,0 +1,87 @@ +# 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 List, Literal, Optional, Union + +from typing_extensions import Annotated, Self + +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class PartnerCategory(RecordBase["PartnerCategoryManager"]): + active: bool + """Whether or not the partner category is active (enabled).""" + + child_ids: Annotated[List[int], ModelRef("child_id", Self)] + """A list of IDs for the child categories.""" + + children: Annotated[List[Self], ModelRef("child_id", Self)] + """The list of child categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + color: int + """Colour index for the partner category.""" + + colour: Annotated[int, FieldAlias("color")] + """Alias for ``color``.""" + + name: str + """The name of the partner category.""" + + 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], ModelRef("parent_id", Self)] + """The name of the parent partner category, if this category + is the child of another category. + """ + + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] + """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.""" + + partner_ids: Annotated[List[int], ModelRef("partner_id", Partner)] + """A list of IDs for the partners in this category.""" + + partners: Annotated[List[Partner], ModelRef("partner_id", Partner)] + """The list of partners in this category. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + +class PartnerCategoryManager(NamedRecordManagerBase[PartnerCategory]): + env_name = "res.partner.category" + record_class = PartnerCategory + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/pricelist.py b/openstack_odooclient/managers/pricelist.py new file mode 100644 index 0000000..ef44149 --- /dev/null +++ b/openstack_odooclient/managers/pricelist.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 typing import Literal, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class Pricelist(RecordBase["PricelistManager"]): + active: bool + """Whether or not the pricelist is active.""" + + company_id: Annotated[Optional[int], ModelRef("company_id", Company)] + """The ID for the company for this pricelist, if set.""" + + company_name: Annotated[Optional[str], ModelRef("company_id", Company)] + """The name of the company for this pricelist, if set.""" + + 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, ModelRef("currency_id", Currency)] + """The ID for the currency used in this pricelist.""" + + currency_name: Annotated[str, ModelRef("currency_id", Currency)] + """The name of the currency used in this pricelist.""" + + currency: Annotated[Currency, ModelRef("currency_id", Currency)] + """The currency used in this pricelist. + + 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. + + Values: + + * ``with_discount`` - Discount included in the price + * ``without_discount`` - Show public price & discount to the customer + """ + + name: str + """The name of this pricelist.""" + + 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) + :type product: int or Product + :param qty: Quantity to charge for + :type qty: float + :return: Price to charge + :rtype: float + """ + return get_price( + manager=self._manager, + pricelist=self, + product=product, + qty=qty, + ) + + +class PricelistManager(NamedRecordManagerBase[Pricelist]): + env_name = "product.pricelist" + record_class = Pricelist + + def get_price( + self, + pricelist: Union[int, Pricelist], + product: Union[int, Product], + qty: float, + ) -> float: + """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 + :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 get_price( + manager=self, + pricelist=pricelist, + product=product, + qty=qty, + ) + + +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. +from .company import Company # noqa: E402 +from .currency import Currency # noqa: E402 +from .product import Product # noqa: E402 diff --git a/openstack_odooclient/managers/product.py b/openstack_odooclient/managers/product.py new file mode 100644 index 0000000..17c6eee --- /dev/null +++ b/openstack_odooclient/managers/product.py @@ -0,0 +1,364 @@ +# 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 ( + Any, + Dict, + Iterable, + List, + Literal, + Optional, + Union, + overload, +) + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_with_unique_field import ( + RecordManagerWithUniqueFieldBase, +) + + +class Product(RecordBase["ProductManager"]): + categ_id: Annotated[int, ModelRef("categ_id", ProductCategory)] + """The ID for the category this product is under.""" + + categ_name: Annotated[str, ModelRef("categ_id", ProductCategory)] + """The name of the category this product is under.""" + + 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], ModelRef("company_id", Company)] + """The ID for the company that owns this product, if set.""" + + company_name: Annotated[Optional[str], ModelRef("company_id", Company)] + """The name of the company that owns this product, if set.""" + + company: Annotated[Optional[Company], ModelRef("company_id", Company)] + """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. + + 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.""" + + uom_id: Annotated[int, ModelRef("uom_id", Uom)] + """The ID for the Unit of Measure for this product.""" + + uom_name: Annotated[str, ModelRef("uom_id", Uom)] + """The name of the Unit of Measure for this product.""" + + uom: Annotated[Uom, ModelRef("uom_id", Uom)] + """The Unit of Measure for this product. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class ProductManager(RecordManagerWithUniqueFieldBase[Product, str]): + env_name = "product.product" + record_class = Product + + @overload + def get_sellable_company_products( + self, + company: Union[int, Company], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[False] = ..., + ) -> List[Product]: ... + + @overload + def get_sellable_company_products( + self, + company: Union[int, Company], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[True], + as_dict: Literal[False] = ..., + ) -> List[int]: ... + + @overload + def get_sellable_company_products( + self, + company: Union[int, Company], + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + *, + as_id: Literal[True], + as_dict: Literal[True], + ) -> List[int]: ... + + @overload + def get_sellable_company_products( + self, + company: Union[int, Company], + *, + fields: Optional[Iterable[str]] = ..., + order: Optional[str] = ..., + as_id: Literal[False] = ..., + as_dict: Literal[True], + ) -> List[Dict[str, Any]]: ... + + @overload + def get_sellable_company_products( + self, + company: Union[int, Company], + *, + 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_company_products( + self, + company: Union[int, Company], + 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: 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[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 + :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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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[int, Company], + 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. + + 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 fields: Fields to select, defaults to ``None`` (select all) + :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 + :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, + ) + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..3ae0753 --- /dev/null +++ b/openstack_odooclient/managers/product_category.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 typing import List, Literal, Optional, Union + +from typing_extensions import Annotated, Self + +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class ProductCategory(RecordBase["ProductCategoryManager"]): + child_id: Annotated[List[int], ModelRef("child_id", Self)] + """A list of IDs for the child categories.""" + + child_ids: Annotated[List[int], FieldAlias("child_id")] + """An alias for ``child_id``.""" + + children: Annotated[List[Self], ModelRef("child_id", Self)] + """The list of child categories. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + complete_name: str + """The complete product category tree.""" + + name: str + """Name of the product category.""" + + 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], ModelRef("parent_id", Self)] + """The name of the parent product category, if this category + is the child of another category. + """ + + parent: Annotated[Optional[Self], ModelRef("parent_id", Self)] + """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.""" + + product_count: int + """The number of products under this category.""" + + +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 new file mode 100644 index 0000000..def5762 --- /dev/null +++ b/openstack_odooclient/managers/project.py @@ -0,0 +1,364 @@ +# 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 ( + Any, + Dict, + Iterable, + List, + Literal, + Optional, + Union, + overload, +) + +from typing_extensions import Annotated, Self + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_with_unique_field import ( + RecordManagerWithUniqueFieldBase, +) + + +class Project(RecordBase["ProjectManager"]): + 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. + """ + + owner_id: Annotated[int, ModelRef("owner", Partner)] + """The ID for the partner that owns this project.""" + + owner_name: Annotated[str, ModelRef("owner", Partner)] + """The name of the partner that owns this project.""" + + 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], ModelRef("parent", Self)] + """The ID for the parent project, if this project + is the child of another project. + """ + + 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], ModelRef("parent", Self)] + """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: 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).""" + + project_contact_ids: Annotated[ + List[int], + ModelRef("project_contacts", ProjectContact), + ] + """A list of IDs for the contacts for this project.""" + + project_contacts: Annotated[ + List[ProjectContact], + ModelRef("project_contacts", ProjectContact), + ] + """The contacts for this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + project_credit_ids: Annotated[ + List[int], + ModelRef("project_credits", Credit), + ] + """A list of IDs for the credits that apply to this project.""" + + project_credits: Annotated[ + List[Credit], + ModelRef("project_credits", Credit), + ] + """The credits that apply to this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + 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], ModelRef("project_grants", Grant)] + """The grants that apply to this project. + + 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 + 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: Annotated[ + Optional[int], + ModelRef("support_subscription", SupportSubscription), + ] + """The ID for the support subscription for this project, + if the project has one. + """ + + support_subscription_name: Annotated[ + Optional[str], + ModelRef("support_subscription", SupportSubscription), + ] + """The name of the support subscription for this project, + if the project has one. + """ + + support_subscription: Annotated[ + Optional[SupportSubscription], + ModelRef("support_subscription", 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. + """ + + term_discount_ids: Annotated[ + List[int], + ModelRef("term_discounts", TermDiscount), + ] + """A list of IDs for the term discounts that apply to this project.""" + + term_discounts: Annotated[ + List[TermDiscount], + ModelRef("term_discounts", TermDiscount), + ] + """The term discounts that apply to this project. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + +class ProjectManager(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. + + 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[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 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]]] + """ + return self._get_by_unique_field( + field="os_id", + value=os_id, + fields=fields, + as_id=as_id, + as_dict=as_dict, + optional=optional, + ) + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..887c6e0 --- /dev/null +++ b/openstack_odooclient/managers/project_contact.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 typing import Literal, Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class ProjectContact(RecordBase["ProjectContactManager"]): + contact_type: Literal[ + "primary", + "billing", + "technical", + "legal", + "reseller customer", + ] + """The contact type to assign the partner as on the project.""" + + inherit: bool + """Whether or not this contact should be inherited by child projects.""" + + partner_id: Annotated[int, ModelRef("partner", Partner)] + """The ID for the partner linked to this project contact.""" + + partner_name: Annotated[str, ModelRef("partner", Partner)] + """The name of the partner linked to this project contact.""" + + 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], ModelRef("project", Project)] + """The ID for the project this contact is linked to, if set.""" + + project_name: Annotated[Optional[str], ModelRef("project", Project)] + """The name of the project this contact is linked to, if set.""" + + project: Annotated[Optional[Project], ModelRef("project", 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(RecordManagerBase[ProjectContact]): + env_name = "openstack.project_contact" + record_class = ProjectContact + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +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 new file mode 100644 index 0000000..e18b908 --- /dev/null +++ b/openstack_odooclient/managers/referral_code.py @@ -0,0 +1,120 @@ +# 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 List + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_coded import CodedRecordManagerBase + + +class ReferralCode(RecordBase["ReferralCodeManager"]): + 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.""" + + 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], ModelRef("referrals", Partner)] + """The partners that signed up using this referral code. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + referral_credit_amount: float + """Initial balance for the referral credit.""" + + referral_credit_duration: int + """Duration of the referral credit, in days.""" + + referral_credit_type_id: Annotated[ + int, + ModelRef("referral_credit_type", CreditType), + ] + """The ID of the credit type to use for the referral credit.""" + + referral_credit_type_name: Annotated[ + str, + ModelRef("referral_credit_type", CreditType), + ] + """The name of the credit type to use for the referral credit.""" + + referral_credit_type: Annotated[ + CreditType, + ModelRef("referral_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. + """ + + reward_credit_amount: float + """Initial balance for the reward credit.""" + + reward_credit_duration: int + """Duration of the reward credit, in days.""" + + reward_credit_type_id: Annotated[ + int, + ModelRef("reward_credit_type", CreditType), + ] + """The ID of the credit type to use for the reward credit.""" + + reward_credit_type_name: Annotated[ + str, + ModelRef("reward_credit_type", CreditType), + ] + """The name of the credit type to use for the reward credit.""" + + reward_credit_type: Annotated[ + CreditType, + ModelRef("reward_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. + """ + + +class ReferralCodeManager(CodedRecordManagerBase[ReferralCode]): + env_name = "openstack.referral_code" + record_class = ReferralCode + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..490620f --- /dev/null +++ b/openstack_odooclient/managers/reseller.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 typing import Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Reseller(RecordBase["ResellerManager"]): + 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.""" + + demo_project_id: Annotated[ + Optional[int], + ModelRef("demo_project", Project), + ] + """The ID for the optional demo project belonging to the reseller.""" + + demo_project_name: Annotated[ + Optional[str], + ModelRef("demo_project", Project), + ] + """The name of the optional demo project belonging to the reseller.""" + + demo_project: Annotated[ + Optional[Project], + ModelRef("demo_project", 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.""" + + 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. + """ + + partner_id: Annotated[int, ModelRef("partner", Partner)] + """The ID for the reseller partner.""" + + partner_name: Annotated[str, ModelRef("partner", Partner)] + """The name of the reseller 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, ModelRef("tier", ResellerTier)] + """The ID for the tier this reseller is under.""" + + tier_name: Annotated[str, ModelRef("tier", ResellerTier)] + """The name of the tier this reseller is under.""" + + tier: Annotated[ResellerTier, ModelRef("tier", ResellerTier)] + """The tier this reseller is under. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class ResellerManager(RecordManagerBase[Reseller]): + env_name = "openstack.reseller" + record_class = Reseller + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..ebb345d --- /dev/null +++ b/openstack_odooclient/managers/reseller_tier.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 typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class ResellerTier(RecordBase["ResellerTierManager"]): + discount_percent: float + """The maximum discount percentage for this reseller tier (0-100).""" + + discount_product_id: Annotated[ + int, + ModelRef("discount_product", Product), + ] + """The ID of the discount product for the reseller tier.""" + + discount_product_name: Annotated[ + str, + ModelRef("discount_product", Product), + ] + """The name of the discount product for the reseller tier.""" + + discount_product: Annotated[ + Product, + ModelRef("discount_product", Product), + ] + """The discount product for the reseller tier. + + 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.""" + + free_monthly_credit_product_id: Annotated[ + int, + ModelRef("free_monthly_credit_product", 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, + 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, + ModelRef("free_monthly_credit_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. + """ + + free_support_hours: int + """The amount of free support hours the reseller is entitled to + under this tier. + """ + + name: str + """Reseller tier name.""" + + min_usage_threshold: float + """The minimum required usage amount for the reseller tier.""" + + +class ResellerTierManager(NamedRecordManagerBase[ResellerTier]): + env_name = "openstack.reseller.tier" + record_class = ResellerTier + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from .product import Product # noqa: E402 diff --git a/openstack_odooclient/managers/sale_order.py b/openstack_odooclient/managers/sale_order.py new file mode 100644 index 0000000..a4f71a2 --- /dev/null +++ b/openstack_odooclient/managers/sale_order.py @@ -0,0 +1,196 @@ +# 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 List, Literal, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import FieldAlias, ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class SaleOrder(RecordBase["SaleOrderManager"]): + 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.""" + + currency_id: Annotated[int, ModelRef("currency_id", Currency)] + """The ID for the currency used in this sale order.""" + + currency_name: Annotated[str, ModelRef("currency_id", Currency)] + """The name of the currency used in this sale order.""" + + currency: Annotated[Currency, ModelRef("currency_id", Currency)] + """The currency used in this sale order. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + 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. + """ + + order_line_ids: Annotated[ + List[int], + ModelRef("order_line", SaleOrderLine), + ] + """A list of IDs for the lines added to the sale order.""" + + order_line: Annotated[ + List[SaleOrderLine], + ModelRef("order_line", SaleOrderLine), + ] + """The lines added to the sale order. + + This fetches the full records from Odoo once, + and caches them for subsequent accesses. + """ + + order_lines: Annotated[List[SaleOrderLine], FieldAlias("order_line")] + """An alias for ``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. + """ + + 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], ModelRef("os_project", Project)] + """The name of the the OpenStack project this sale order was + was generated for. + """ + + os_project: Annotated[Optional[Project], ModelRef("os_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. + """ + + partner_id: Annotated[int, ModelRef("partner_id", Partner)] + """The ID for the recipient partner for the sale order.""" + + partner_name: Annotated[str, ModelRef("partner_id", Partner)] + """The name of the recipient partner for the sale order.""" + + partner: Annotated[Partner, ModelRef("partner_id", Partner)] + """The recipient partner for the sale order. + + 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. + + 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 + """ + + def action_confirm(self) -> None: + """Confirm this sale order.""" + self._env.action_confirm(self.id) + + def create_invoices(self) -> None: + """Create invoices from this sale order.""" + self._env.create_invoices(self.id) + + +class SaleOrderManager(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: The 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 the given sale order. + + :param sale_order: The sale order to create invoices from + :type sale_order: Union[int, SaleOrder] + """ + self._env.create_invoices( + ( + sale_order.id + if isinstance(sale_order, SaleOrder) + else sale_order + ), + ) + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..adf7019 --- /dev/null +++ b/openstack_odooclient/managers/sale_order_line.py @@ -0,0 +1,309 @@ +# 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 List, Literal, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class SaleOrderLine(RecordBase["SaleOrderLineManager"]): + company_id: Annotated[int, ModelRef("company_id", Company)] + """The ID for the company this sale order line + was generated for. + """ + + company_name: Annotated[str, ModelRef("company_id", Company)] + """The name of the company this sale order line + was generated for. + """ + + company: Annotated[Company, ModelRef("company_id", Company)] + """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, ModelRef("currency_id", Currency)] + """The ID for the currency used in this sale order line.""" + + currency_name: Annotated[str, ModelRef("currency_id", Currency)] + """The name of the currency used in this sale order line.""" + + currency: Annotated[Currency, ModelRef("currency_id", Currency)] + """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).""" + + display_name: str + """Display name for the sale order line in the sale order.""" + + invoice_line_ids: Annotated[ + List[int], + ModelRef("invoice_lines", AccountMoveLine), + ] + """A list of IDs for the account move (invoice) lines created + from this sale order line. + """ + + invoice_lines: Annotated[ + List[AccountMoveLine], + ModelRef("invoice_lines", AccountMoveLine), + ] + """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. + """ + + 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. + """ + + order_id: Annotated[int, ModelRef("order_id", SaleOrder)] + """The ID for the sale order this line is linked to.""" + + order_name: Annotated[str, ModelRef("order_id", SaleOrder)] + """The name of the sale order this line is linked to.""" + + 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, ModelRef("order_partner_id", Partner)] + """The ID for the recipient partner for the sale order.""" + + order_partner_name: Annotated[str, ModelRef("order_partner_id", Partner)] + """The name of the recipient partner for the sale order.""" + + 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], 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], ModelRef("os_project", Project)] + """The name of the the OpenStack project this sale order line was + was generated for. + """ + + os_project: Annotated[Optional[Project], ModelRef("os_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. + """ + + os_region: Union[str, Literal[False]] + """The OpenStack region the sale order line was created from.""" + + os_resource_id: Union[str, Literal[False]] + """The OpenStack resource ID for the resource that generated + this sale order line. + """ + + 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 sale order line for a compute instance, + this would be set to the instance's flavour name. + """ + + os_resource_type: Union[str, Literal[False]] + """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_taxexcl: 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.""" + + product_id: Annotated[int, ModelRef("product_id", Product)] + """The ID of the product charged on this sale order line.""" + + product_name: Annotated[str, ModelRef("product_id", Product)] + """The name of the product charged on this sale order line.""" + + 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, 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, 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, ModelRef("product_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. + """ + + 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.""" + + salesman_id: Annotated[int, ModelRef("salesman_id", Partner)] + """The ID for the salesperson partner assigned + to this sale order line. + """ + + salesman_name: Annotated[str, ModelRef("salesman_id", Partner)] + """The name of the salesperson partner assigned + to this sale order line. + """ + + salesman: Annotated[Partner, ModelRef("salesman_id", Partner)] + """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. + + 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: Annotated[int, ModelRef("tax_id", Tax)] + """The ID for the tax used on this sale order line.""" + + tax_name: Annotated[str, ModelRef("tax_id", Tax)] + """The name of the tax used on this sale order line.""" + + tax: Annotated[Tax, ModelRef("tax_id", Tax)] + """The tax used on this sale order line. + + 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 + has already been invoiced. + """ + + untaxed_amount_to_invoice: float + """The balance, excluding tax, on the sale order line that + still needs to be invoiced. + """ + + +class SaleOrderLineManager(RecordManagerBase[SaleOrderLine]): + env_name = "sale.order.line" + record_class = SaleOrderLine + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..80b4cf3 --- /dev/null +++ b/openstack_odooclient/managers/support_subscription.py @@ -0,0 +1,119 @@ +# 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 typing import Literal, Optional + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class SupportSubscription(RecordBase["SupportSubscriptionManager"]): + 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.""" + + 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. + + Support subscriptions linked to a partner + cover all projects the partner owns. + """ + + partner_name: Annotated[Optional[str], ModelRef("partner", Partner)] + """The name of 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: Annotated[Optional[Partner], ModelRef("partner", 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], 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], 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], ModelRef("project", 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.""" + + support_subscription_type_id: Annotated[ + int, + ModelRef("support_subscription_type", SupportSubscriptionType), + ] + """The ID of the type of the support subscription.""" + + support_subscription_type_name: Annotated[ + str, + ModelRef("support_subscription_type", SupportSubscriptionType), + ] + """The name of the type of the support subscription.""" + + support_subscription_type: Annotated[ + SupportSubscriptionType, + ModelRef("support_subscription_type", SupportSubscriptionType), + ] + """The type of the support subscription. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class SupportSubscriptionManager(RecordManagerBase[SupportSubscription]): + env_name = "openstack.support_subscription" + record_class = SupportSubscription + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..82640fc --- /dev/null +++ b/openstack_odooclient/managers/support_subscription_type.py @@ -0,0 +1,86 @@ +# 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 List, Literal + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class SupportSubscriptionType(RecordBase["SupportSubscriptionTypeManager"]): + billing_type: Literal["paid", "complimentary"] + """The type of support subscription.""" + + name: str + """The name of the support subscription type.""" + + product_id: Annotated[int, ModelRef("product", Product)] + """The ID for the product to use to invoice + the support subscription. + """ + + product_name: Annotated[str, ModelRef("product", Product)] + """The name of the product to use to invoice + the support subscription. + """ + + product: Annotated[Product, ModelRef("product", 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).""" + + support_subscription_ids: Annotated[ + List[int], + ModelRef("support_subscription", SupportSubscription), + ] + """A list of IDs for the support subscriptions of this type.""" + + support_subscription: Annotated[ + List[SupportSubscription], + ModelRef("support_subscription", SupportSubscription), + ] + """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[SupportSubscription], + ModelRef("support_subscription", SupportSubscription), + ] + """An alias for ``support_subscription``.""" + + +class SupportSubscriptionTypeManager( + NamedRecordManagerBase[SupportSubscriptionType], +): + env_name = "openstack.support_subscription.type" + record_class = SupportSubscriptionType + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from .product import Product # noqa: E402 +from .support_subscription import SupportSubscription # noqa: E402 diff --git a/openstack_odooclient/managers/tax.py b/openstack_odooclient/managers/tax.py new file mode 100644 index 0000000..1fda300 --- /dev/null +++ b/openstack_odooclient/managers/tax.py @@ -0,0 +1,109 @@ +# 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 typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class Tax(RecordBase["TaxManager"]): + 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"] + """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: 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: Annotated[int, ModelRef("company_id", Company)] + """The ID for the company this tax is owned by.""" + + company_name: Annotated[str, ModelRef("company_id", Company)] + """The name of the company this tax is owned by.""" + + company: Annotated[Company, ModelRef("company_id", Company)] + """The company this tax is owned by. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + 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 + """Name of the tax.""" + + 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 + """ + + 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, ModelRef("tax_group_id", TaxGroup)] + """The name of the tax group this tax is categorised under.""" + + 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, + and caches it for subsequent accesses. + """ + + +class TaxManager(NamedRecordManagerBase[Tax]): + env_name = "account.tax" + record_class = Tax + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..1691fe0 --- /dev/null +++ b/openstack_odooclient/managers/tax_group.py @@ -0,0 +1,29 @@ +# 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.record import RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class TaxGroup(RecordBase["TaxGroupManager"]): + name: str + """Name of the tax group.""" + + +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 new file mode 100644 index 0000000..377c9b4 --- /dev/null +++ b/openstack_odooclient/managers/term_discount.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 datetime import date +from typing import Optional + +from typing_extensions import Annotated, Self + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class TermDiscount(RecordBase["TermDiscountManager"]): + 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.""" + + partner_id: Annotated[int, ModelRef("partner", Partner)] + """The ID for the partner that receives this term discount.""" + + partner_name: Annotated[str, ModelRef("partner", Partner)] + """The name of the partner that receives this term discount.""" + + 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], ModelRef("project", 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], ModelRef("project", 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], ModelRef("project", 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.""" + + superseded_by_id: Annotated[ + Optional[int], + ModelRef("superseded_by", Self), + ] + """The ID for the term discount that supersedes this one, + if superseded. + """ + + superseded_by_name: Annotated[ + Optional[str], + ModelRef("superseded_by", Self), + ] + """The name of the term discount that supersedes this one, + if superseded. + """ + + superseded_by: Annotated[ + Optional[Self], + ModelRef("superseded_by", Self), + ] + """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(RecordManagerBase[TermDiscount]): + env_name = "openstack.term_discount" + record_class = TermDiscount + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 new file mode 100644 index 0000000..a81c030 --- /dev/null +++ b/openstack_odooclient/managers/trial.py @@ -0,0 +1,65 @@ +# 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 typing import Literal, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Trial(RecordBase["TrialManager"]): + 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.""" + + partner_id: Annotated[int, ModelRef("partner", Partner)] + """The ID for the target partner for this trial.""" + + partner_name: Annotated[str, ModelRef("partner", Partner)] + """The name of the target partner for this trial.""" + + partner: Annotated[Partner, ModelRef("partner", Partner)] + """The target partner for this trial. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + start_date: date + """The start date of this trial.""" + + +class TrialManager(RecordManagerBase[Trial]): + env_name = "openstack.trial" + record_class = Trial + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from .partner import Partner # noqa: E402 diff --git a/openstack_odooclient/managers/uom.py b/openstack_odooclient/managers/uom.py new file mode 100644 index 0000000..dbb50c9 --- /dev/null +++ b/openstack_odooclient/managers/uom.py @@ -0,0 +1,95 @@ +# 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 typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class Uom(RecordBase["UomManager"]): + active: bool + """Whether or not this Unit of Measure is active (enabled).""" + + category_id: Annotated[int, ModelRef("category_id", UomCategory)] + """The ID for the category this Unit of Measure is classified as.""" + + category_name: Annotated[str, ModelRef("category_id", UomCategory)] + """The name of the category this Unit of Measure is classified as.""" + + category: Annotated[UomCategory, ModelRef("category_id", UomCategory)] + """The category this Unit of Measure is classified as. + + 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 + 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 + """ + + +class UomManager(RecordManagerBase[Uom]): + env_name = "uom.uom" + record_class = Uom + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from .uom_category import UomCategory # noqa: E402 diff --git a/openstack_odooclient/managers/uom_category.py b/openstack_odooclient/managers/uom_category.py new file mode 100644 index 0000000..963327c --- /dev/null +++ b/openstack_odooclient/managers/uom_category.py @@ -0,0 +1,51 @@ +# 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 ..base.record import RecordBase +from ..base.record_manager import RecordManagerBase + + +class UomCategory(RecordBase["UomCategoryManager"]): + 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 + """The name of the Unit of Measure (UoM) category.""" + + +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 new file mode 100644 index 0000000..3013822 --- /dev/null +++ b/openstack_odooclient/managers/user.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 typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class User(RecordBase["UserManager"]): + active: bool + """Whether or not this user is active (enabled).""" + + active_partner: bool + """Whether or not the partner this user is associated with is active.""" + + company_id: Annotated[int, ModelRef("company_id", Company)] + """The ID for the default company this user is logged in as.""" + + company_name: Annotated[str, ModelRef("company_id", Company)] + """The name of the default company this user is logged in as.""" + + company: Annotated[Company, ModelRef("company_id", Company)] + """The default company this user is logged in as. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + name: str + """User name.""" + + partner_id: Annotated[int, ModelRef("partner_id", Partner)] + """The ID for the partner that this user is associated with.""" + + partner_name: Annotated[str, ModelRef("partner_id", Partner)] + """The name of the partner that this user is associated with.""" + + partner: Annotated[Partner, ModelRef("partner_id", Partner)] + """The partner that this user is associated with. + + This fetches the full record from Odoo once, + and caches it for subsequent accesses. + """ + + +class UserManager(RecordManagerBase[User]): + env_name = "res.users" + record_class = User + + +# NOTE(callumdickinson): Import here to make sure circular imports work. +from .company import Company # 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 new file mode 100644 index 0000000..38b94f0 --- /dev/null +++ b/openstack_odooclient/managers/volume_discount_range.py @@ -0,0 +1,127 @@ +# 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 List, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager import RecordManagerBase + + +class VolumeDiscountRange(RecordBase["VolumeDiscountRangeManager"]): + customer_group_id: Annotated[ + Optional[int], + ModelRef("customer_group", CustomerGroup), + ] + """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[ + Optional[str], + ModelRef("customer_group", CustomerGroup), + ] + """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[ + Optional[CustomerGroup], + ModelRef("customer_group", CustomerGroup), + ] + """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. + """ + + 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.""" + + +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, CustomerGroup]] = 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 ``None`` + (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 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 + :return: Highest percentage applicable discount range (if found) + :rtype: Optional[VolumeDiscountRange] + """ + ranges = self.search( + [("customer_group", "=", 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] + + +# NOTE(callumdickinson): Import here to avoid circular imports. +from .customer_group import CustomerGroup # noqa: E402 diff --git a/openstack_odooclient/managers/voucher_code.py b/openstack_odooclient/managers/voucher_code.py new file mode 100644 index 0000000..a2dfcef --- /dev/null +++ b/openstack_odooclient/managers/voucher_code.py @@ -0,0 +1,201 @@ +# 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 typing import List, Literal, Optional, Union + +from typing_extensions import Annotated + +from ..base.record import ModelRef, RecordBase +from ..base.record_manager_named import NamedRecordManagerBase + + +class VoucherCode(RecordBase["VoucherCodeManager"]): + claimed: bool + """Whether or not this voucher code has been claimed.""" + + code: str + """The code string for this voucher code.""" + + 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. + """ + + credit_type_id: Annotated[ + Optional[int], + ModelRef("credit_type", 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], + 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[CreditType], + ModelRef("credit_type", 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. + """ + + credit_duration: Union[int, Literal[False]] + """The duration of the credit, in days, if a credit is to be + created by the voucher code. + """ + + customer_group_id: Annotated[ + Optional[int], + ModelRef("customer_group", CustomerGroup), + ] + """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 to add the customer to, if set.""" + + customer_group: Annotated[ + Optional[CustomerGroup], + ModelRef("customer_group", CustomerGroup), + ] + """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: Union[date, Literal[False]] + """The date the voucher code expires.""" + + grant_duration: Union[int, Literal[False]] + """The duration of the grant, in days, if a grant is to be + created by the voucher code. + """ + + 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], + 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[GrantType], + ModelRef("grant_type", 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. + """ + + grant_value: Union[float, Literal[False]] + """The value of the grant, 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_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. + """ + + sales_person_id: Annotated[ + Optional[int], + ModelRef("sales_person", Partner), + ] + """The ID for the salesperson partner responsible for this + voucher code, if assigned. + """ + + sales_person_name: Annotated[ + Optional[str], + ModelRef("sales_person", Partner), + ] + """The name of the salesperson partner responsible for this + voucher code, if assigned. + """ + + sales_person: Annotated[ + Optional[Partner], + ModelRef("sales_person", Partner), + ] + """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], 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[PartnerCategory], ModelRef("tags", 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. + """ + + +class VoucherCodeManager(NamedRecordManagerBase[VoucherCode]): + env_name = "openstack.voucher_code" + record_class = VoucherCode + + +# NOTE(callumdickinson): Import here to avoid circular imports. +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 diff --git a/openstack_odooclient/py.typed b/openstack_odooclient/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/openstack_odooclient/util.py b/openstack_odooclient/util.py new file mode 100644 index 0000000..ae0be04 --- /dev/null +++ b/openstack_odooclient/util.py @@ -0,0 +1,88 @@ +# 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, TypeVar + +from typing_extensions import TypeGuard + +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" +DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S" +DEFAULT_SERVER_DATETIME_FORMAT = ( + f"{DEFAULT_SERVER_DATE_FORMAT} {DEFAULT_SERVER_TIME_FORMAT}" +) + +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 is_subclass( + type_obj: Type[Any], + 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). + + 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 diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..90ac863 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,888 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "docs", "lint"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.2" +content_hash = "sha256:95c41758d35d5104a0d319482ed751666ca4dc8c619221953114a9d12b06cb98" + +[[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"] +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 = "importlib-resources" +version = "6.4.0" +requires_python = ">=3.8" +summary = "Read resources from Python packages" +groups = ["docs"] +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" +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 = "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" +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" +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", "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 = "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" +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" +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 = "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" +requires_python = ">=3.7" +summary = "A lil' TOML parser" +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" +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"}, +] + +[[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 = "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" +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"] +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 new file mode 100644 index 0000000..60fefc9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,170 @@ +[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 :: Developers", + "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 :: Software Development :: Libraries", + "Typing :: Typed", +] +requires-python = ">=3.8" +dependencies = [ + "OdooRPC>=0.9.0", + "packaging", + "typing-extensions>=4.0.0", +] +dynamic = ["version"] + +[project.urls] +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://catalyst-cloud.github.io/python-openstack-odooclient/latest/changelog.html" + +[tool.setuptools_scm] + +[tool.pdm.dev-dependencies] +lint = [ + "mypy==1.10.0", + "ruff==0.4.8", +] +docs = [ + "mkdocs-material>=9.5.27", + "towncrier>=23.11.0", + "mike>=2.1.2", +] + +[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 + +[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