Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

117
pricelist_cache/README.rst Executable file
View File

@@ -0,0 +1,117 @@
===============
Pricelist Cache
===============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:39feac382c97a1d62a2021eb330fb9404a69dcc7e8846cb8a4d71797b4d3efee
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/18.0/pricelist_cache
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-pricelist_cache
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
Provides a cron task who caches prices for all products and all
pricelists. The goal is to be able to generate a whole catalog of prices
and products for a given customer in a decent time.
Everyday, the cron task will trash the previous day's cache, and rebuild
it from scratch. It means that at any moment, the prices stored in the
cache are those of the current day, and will not be recomputed before
the next day.
However, new prices will be cached in the following cases:
- new product is created
- new pricelist is created
- new pricelist item is created
**Table of contents**
.. contents::
:local:
Known issues / Roadmap
======================
- Use job dependencies. If pricelist a is based on b, then job a should
depend on job b.
- In tests do not depend on odoo demo data which might change anytime
and break the tests
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/sale-workflow/issues/new?body=module:%20pricelist_cache%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Camptocamp
Contributors
------------
- Telmo Santos <telmo.santos@camptocamp.com>
- Matthieu Méquignon <matthieu.mequignon@camptocamp.com>
- Simone Orsi <simahawk@gmail.com>
- Thierry Ducrest <thierry.ducrest@camptocamp.com>
- Sébastien Alix <sebastien.alix@camptocamp.com>
- `Trobz <https://trobz.com>`__:
- Hai Lang <hailn@trobz.com>
Other credits
-------------
**Financial support**
- Cosanum
- Camptocamp R&D
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/pricelist_cache>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
pricelist_cache/__init__.py Executable file
View File

@@ -0,0 +1,3 @@
from . import models
from . import wizards
from .hooks import set_default_partner_product_filter

35
pricelist_cache/__manifest__.py Executable file
View File

@@ -0,0 +1,35 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Pricelist Cache",
"summary": """
Provide a new model to cache price lists and update it,
to make it easier to retrieve them.
""",
"version": "18.0.1.0.0",
"category": "Hidden",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"depends": [
"partner_pricelist_search",
"product",
"sale",
"queue_job",
],
"website": "https://github.com/OCA/sale-workflow",
"data": [
"security/ir.model.access.csv",
"data/ir_cron.xml",
"data/ir_filters_data.xml",
"data/queue_job.xml",
"views/res_partner.xml",
"views/product_pricelist.xml",
"views/product_pricelist_cache.xml",
"wizards/pricelist_cache_wizard.xml",
],
"demo": [
"data/demo.xml",
],
"installable": True,
"post_init_hook": "set_default_partner_product_filter",
}

137
pricelist_cache/data/demo.xml Executable file
View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!--root pricelist 0-->
<record id="list0" model="product.pricelist">
<field name="name">Pricelist 0</field>
</record>
<record id="item2" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list0" />
<field name="product_id" ref="product.product_product_6" />
<field name="fixed_price">100.0</field>
</record>
<record id="item3" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list0" />
<field name="product_id" ref="product.product_product_8" />
<field name="fixed_price">100.0</field>
</record>
<!--child pricelist 1, based on pricelist 0-->
<record id="list1" model="product.pricelist">
<field name="name">Pricelist 1</field>
<field name="sequence">2</field>
</record>
<record id="item4" model="product.pricelist.item">
<field name="pricelist_id" ref="list1" />
<field name="applied_on">3_global</field>
<field name="compute_price">formula</field>
<field name="base">pricelist</field>
<field name="base_pricelist_id" ref="list0" />
</record>
<record id="item5" model="product.pricelist.item">
<field name="pricelist_id" ref="list1" />
<field name="applied_on">0_product_variant</field>
<field name="base">list_price</field>
<field name="product_id" ref="product.product_product_6" />
<field name="fixed_price">75.0</field>
</record>
<!--child pricelist 2, based on pricelist 1-->
<record id="list2" model="product.pricelist">
<field name="name">Pricelist 2</field>
<field name="sequence">3</field>
</record>
<record id="item6" model="product.pricelist.item">
<field name="pricelist_id" ref="list2" />
<field name="applied_on">3_global</field>
<field name="compute_price">formula</field>
<field name="base">pricelist</field>
<field name="base_pricelist_id" ref="list1" />
</record>
<record id="item7" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list2" />
<field name="product_id" ref="product.product_product_6" />
<field name="fixed_price">50.0</field>
<field name="date_start">2021-03-01</field>
<field name="date_end">2021-04-01</field>
</record>
<!--child pricelist 3, based on pricelist 2-->
<record id="list3" model="product.pricelist">
<field name="name">Pricelist 3</field>
<field name="sequence">4</field>
</record>
<record id="item8" model="product.pricelist.item">
<field name="pricelist_id" ref="list3" />
<field name="applied_on">3_global</field>
<field name="compute_price">formula</field>
<field name="base">pricelist</field>
<field name="base_pricelist_id" ref="list2" />
</record>
<record id="item9" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list3" />
<field name="product_id" ref="product.product_product_6" />
<field name="fixed_price">25.0</field>
</record>
<!--root pricelist 4, based on list0-->
<record id="list4" model="product.pricelist">
<field name="name">Pricelist 4</field>
<field name="sequence">5</field>
</record>
<record id="item10" model="product.pricelist.item">
<field name="pricelist_id" ref="list4" />
<field name="applied_on">3_global</field>
<field name="compute_price">formula</field>
<field name="base">pricelist</field>
<field name="base_pricelist_id" ref="list0" />
</record>
<record id="item11" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list4" />
<field name="product_id" ref="product.product_product_6" />
<field name="fixed_price">15.0</field>
</record>
<record id="item12" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="applied_on">0_product_variant</field>
<field name="pricelist_id" ref="list4" />
<field name="product_id" ref="product.product_product_7" />
<field name="fixed_price">50</field>
</record>
<!-- factor pricelist 5, based on pricelist 3-->
<record id="list5" model="product.pricelist">
<field name="name">Pricelist 5</field>
<field name="sequence">6</field>
</record>
<record id="item13" model="product.pricelist.item">
<field name="pricelist_id" ref="list5" />
<field name="applied_on">3_global</field>
<field name="compute_price">formula</field>
<field name="base">pricelist</field>
<field name="base_pricelist_id" ref="list3" />
<field name="price_surcharge">20</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record model="ir.cron" id="cron_reset_pricelist_cache">
<field name='name'>Reset pricelist cache</field>
<field name='interval_number'>1</field>
<field name='interval_type'>days</field>
<field name="active" eval="False" />
<field name="model_id" ref="pricelist_cache.model_product_pricelist_cache" />
<field name="state">code</field>
<field name="code">model.cron_reset_pricelist_cache()</field>
<field
name="nextcall"
eval="(DateTime.now().replace(hour=1, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"
/>
</record>
</odoo>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="product_filter_default" model="ir.filters">
<field name="name">Pricelist cache default product filter for partner</field>
<field name="model_id">product.product</field>
<field name="domain" eval="[('sale_ok', '=', True)]" />
<field name="user_id" eval="False" />
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="channel_pricelist_cache" model="queue.job.channel">
<field name="name">pricelist_cache</field>
<field name="parent_id" ref="queue_job.channel_root" />
</record>
<record id="job_function_pricelist_cache_update" model="queue.job.function">
<field name="model_id" ref="pricelist_cache.model_product_pricelist_cache" />
<field name="method">update_product_pricelist_cache</field>
<field name="channel_id" ref="channel_pricelist_cache" />
</record>
</odoo>

20
pricelist_cache/hooks.py Executable file
View File

@@ -0,0 +1,20 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
def set_default_partner_product_filter(env):
"""This hook is here because we couldn't set the default filter
as a default value for partners.
When the module is installed, Odoo creates the new field and at the
same time tries to set the default value for all existing records in
the DB. However the XML data (and thus 'product_filter_default' filter)
is still not created at this stage.
"""
partners_to_update = (
env["res.partner"]
.with_context(active_test=False)
.search([("pricelist_cache_product_filter_id", "=", False)])
)
default_filter = env.ref("pricelist_cache.product_filter_default")
partners_to_update.write({"pricelist_cache_product_filter_id": default_filter.id})

331
pricelist_cache/i18n/es.po Executable file
View File

@@ -0,0 +1,331 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pricelist_cache
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-04-18 11:36+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Apply On"
msgstr "Aplicar Sobre"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_cache_line_ids
msgid "Cached prices"
msgstr "Precios en caché"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Cancel"
msgstr "Cancelar"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__company_id
msgid "Company"
msgstr "Companía"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_res_partner
msgid "Contact"
msgstr "Contacto"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_date
msgid "Created on"
msgstr "Creado el"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.view_partner_property_form
msgid "Display Customer Prices"
msgstr "Mostrar Precios de Clientes"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_product__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__display_name
msgid "Display Name"
msgstr "Mostrar Nombre"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_view
msgid "Display Pricelist Prices"
msgstr "Mostrar Lista de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_end
msgid "End Date"
msgstr "Fecha Final"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_end
msgid ""
"Ending datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
"Fecha y hora de finalización de la validación del artículo de la lista de "
"precios\n"
"El valor mostrado depende de la zona horaria configurada en sus preferencias."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__min_quantity
msgid ""
"For the rule to apply, bought/sold quantity must be greater than or equal to "
"the minimum quantity specified in this field.\n"
"Expressed in the default unit of measure of the product."
msgstr ""
"Para que se aplique la regla, la cantidad comprada / vendida debe ser mayor "
"o igual a la cantidad mínima especificada en este campo.\n"
"Expresado en la unidad de medida predeterminada del producto."
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_product__id
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__id
msgid "ID"
msgstr "ID"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__is_pricelist_cache_available
msgid "Is Pricelist Cache Available"
msgstr "Está Disponible la Lista de Precios en Caché"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_computed
msgid "Is Pricelist Cache Computed"
msgstr "Se Computa la Caché de la Lista de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist____last_update
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache____last_update
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard____last_update
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item____last_update
#: model:ir.model.fields,field_description:pricelist_cache.field_product_product____last_update
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner____last_update
msgid "Last Modified on"
msgstr "Última Modificación el"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_uid
msgid "Last Updated by"
msgstr "Última Actualización por"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_date
msgid "Last Updated on"
msgstr "Última Actualización el"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__min_quantity
msgid "Min. Quantity"
msgstr "Cantidad Mín"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Open"
msgstr "Abrir"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__base_pricelist_id
msgid "Other Pricelist"
msgstr "Otras Listas de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__parent_pricelist_ids
msgid "Parent Pricelist"
msgstr "Lista de Precios para Padres"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__partner_id
msgid "Partner"
msgstr "Socio"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__price
msgid "Price"
msgstr "Precio"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__pricelist_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__pricelist_id
msgid "Pricelist"
msgstr "Lista de Precios"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list0
msgid "Pricelist 0"
msgstr "Lista de precios 0"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list1
msgid "Pricelist 1"
msgstr "Lista de precios 1"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list2
msgid "Pricelist 2"
msgstr "Lista de precios 2"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list3
msgid "Pricelist 3"
msgstr "Lista de precios 3"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list4
msgid "Pricelist 4"
msgstr "Lista de precios 4"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list5
msgid "Pricelist 5"
msgstr "Lista de precios 5"
#. module: pricelist_cache
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_action
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_wizard_action
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache
#: model:ir.ui.menu,name:pricelist_cache.menuitem_pricelist_cache_wizard
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Pricelist Cache"
msgstr "Caché de Listas de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__pricelist_cache_product_filter_id
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__pricelist_cache_product_filter_id
msgid "Pricelist Cache Product Filter"
msgstr "Filtro de Producto de Caché de Lista de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__pricelist_cache_update_skipped
msgid "Pricelist Cache Update Skipped"
msgstr "Se ha Omitido la Actualización de la Caché de Listas de Precios"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Pricelist Item applicable on selected option"
msgstr "Elemento de tarifa aplicable en la opción seleccionada"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_item
msgid "Pricelist Rule"
msgstr "Regla de la Lista de Precios"
#. module: pricelist_cache
#: model:ir.filters,name:pricelist_cache.product_filter_default
msgid "Pricelist cache default product filter for partner"
msgstr ""
"Filtro de productos por defecto de la caché de listas de precios para socios"
#. module: pricelist_cache
#: code:addons/pricelist_cache/models/res_partner.py:0
#, python-format
msgid "Pricelist caching in progress. Retry later"
msgstr "Lista de precios en caché en curso. Reintentar más tarde"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_product
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__product_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid "Product"
msgstr "Producto"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__categ_id
msgid "Product Category"
msgstr "Categoría de Producto"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__product_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_id
msgid "Product Variant"
msgstr "Variante del Producto"
#. module: pricelist_cache
#: model:ir.actions.server,name:pricelist_cache.cron_reset_pricelist_cache_ir_actions_server
#: model:ir.cron,cron_name:pricelist_cache.cron_reset_pricelist_cache
#: model:ir.cron,name:pricelist_cache.cron_reset_pricelist_cache
msgid "Reset pricelist cache"
msgstr "Restablecer caché de listas de precios"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__categ_id
msgid ""
"Specify a product category if this rule only applies to products belonging "
"to this category or its children categories. Keep empty otherwise."
msgstr ""
"Especifique una categoría de producto si esta regla solo se aplica a los "
"productos que pertenecen a esta categoría o sus categorías secundarias. "
"Mantener vacío de lo contrario."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_id
msgid ""
"Specify a product if this rule only applies to one product. Keep empty "
"otherwise."
msgstr ""
"Especifique un producto si esta regla solo se aplica a un producto. Mantener "
"vacío de lo contrario."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid ""
"Specify a template if this rule only applies to one product template. Keep "
"empty otherwise."
msgstr ""
"Especifique una plantilla si esta regla solo se aplica a una plantilla de "
"producto. Mantener vacío de lo contrario."
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_start
msgid "Start Date"
msgstr "Fecha de Comienzo"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_start
msgid ""
"Starting datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
"Fecha y hora de inicio de la validación de la lista de precios.\n"
"El valor mostrado depende de la zona horaria configurada en sus preferencias."
#. module: pricelist_cache
#: model:base.automation,name:pricelist_cache.automation_update_product_pricelist_cache
#: model:ir.actions.server,name:pricelist_cache.automation_update_product_pricelist_cache_ir_actions_server
msgid "Update Product Pricelist Cache"
msgstr "Actualizar la Caché de la Lista de Precios de Productos"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache_wizard
msgid "Wizard for pricelist cache"
msgstr "Asistente para la caché de listas de precios"

301
pricelist_cache/i18n/it.po Executable file
View File

@@ -0,0 +1,301 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pricelist_cache
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-04-16 10:23+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Apply On"
msgstr "Applica a"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_cache_line_ids
msgid "Cached prices"
msgstr "Prezzi memorizzati"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Cancel"
msgstr "Annulla"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__categ_id
msgid "Category"
msgstr "Categoria"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__company_id
msgid "Company"
msgstr "Azienda"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_res_partner
msgid "Contact"
msgstr "Contatto"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_date
msgid "Created on"
msgstr "Creato il"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.view_partner_property_form
msgid "Display Customer Prices"
msgstr "Visualizza prezzi cliente"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_view
msgid "Display Pricelist Prices"
msgstr "Visualizza prezzi listino"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_end
msgid "End Date"
msgstr "Data fine"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_end
msgid ""
"Ending datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
"Data di fine per la validazione riga di listino\n"
"Il valore mostrato dipende dal fuso orario impostato nelle tue preferenze."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__min_quantity
msgid ""
"For the rule to apply, bought/sold quantity must be greater than or equal to the minimum quantity specified in this field.\n"
"Expressed in the default unit of measure of the product."
msgstr ""
"Per rendere applicabile la regola, la quantità acquistata/venduta deve "
"essere superiore o uguale a quella minima specificata \n"
"in questo campo, espressa nell'unità di misura predefinita del prodotto."
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__id
msgid "ID"
msgstr "ID"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__is_pricelist_cache_available
msgid "Is Pricelist Cache Available"
msgstr "La memoria listino è disponibile"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_computed
msgid "Is Pricelist Cache Computed"
msgstr "La memoria listino è calcolata"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__min_quantity
msgid "Min. Quantity"
msgstr "Quantità minima"
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Open"
msgstr "Apri"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__base_pricelist_id
msgid "Other Pricelist"
msgstr "Altro listino"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__parent_pricelist_ids
msgid "Parent Pricelist"
msgstr "Listino padre"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__partner_id
msgid "Partner"
msgstr "Partner"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__price
msgid "Price"
msgstr "Prezzo"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__pricelist_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__pricelist_id
msgid "Pricelist"
msgstr "Listino prezzi"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list0
msgid "Pricelist 0"
msgstr "Listino prezzi 0"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list1
msgid "Pricelist 1"
msgstr "Listino prezzi 1"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list2
msgid "Pricelist 2"
msgstr "Listino prezzi 2"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list3
msgid "Pricelist 3"
msgstr "Listino prezzi 3"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list4
msgid "Pricelist 4"
msgstr "Listino prezzi 4"
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list5
msgid "Pricelist 5"
msgstr "Listino prezzi 5"
#. module: pricelist_cache
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_action
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_wizard_action
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache
#: model:ir.ui.menu,name:pricelist_cache.menuitem_pricelist_cache_wizard
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Pricelist Cache"
msgstr "Memoria listino"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__pricelist_cache_product_filter_id
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__pricelist_cache_product_filter_id
msgid "Pricelist Cache Product Filter"
msgstr "Filtro prodotto memoria listino"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__pricelist_cache_update_skipped
msgid "Pricelist Cache Update Skipped"
msgstr "Aggiornamento memoria listino saltata"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Pricelist Item applicable on selected option"
msgstr "Riga listino applicabile all'opzione selezionata"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_item
msgid "Pricelist Rule"
msgstr "Regola listino prezzi"
#. module: pricelist_cache
#. odoo-python
#: code:addons/pricelist_cache/models/res_partner.py:0
msgid "Pricelist caching in progress. Retry later"
msgstr "Memorizzazione listino in corso. Riprovare più tardi"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__product_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid "Product"
msgstr "Prodotto"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_product
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__product_id
msgid "Product Variant"
msgstr "Variante prodotto"
#. module: pricelist_cache
#: model:ir.actions.server,name:pricelist_cache.cron_reset_pricelist_cache_ir_actions_server
msgid "Reset pricelist cache"
msgstr "Resetta memoria listino"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__categ_id
msgid ""
"Specify a product category if this rule only applies to products belonging "
"to this category or its children categories. Keep empty otherwise."
msgstr ""
"Specificare una categoria prodotto se questa regola si applica solo ai "
"prodotti appartenenti a questa categoria o alle sue categorie figlie, "
"altrimenti lasciare vuoto."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_id
msgid ""
"Specify a product if this rule only applies to one product. Keep empty "
"otherwise."
msgstr ""
"Specificare un prodotto se questa regola si applica solo ad un prodotto, "
"altrimenti lasciare vuoto."
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid ""
"Specify a template if this rule only applies to one product template. Keep "
"empty otherwise."
msgstr ""
"Specificare un modello prodotto se questa regola si applica solo ad un "
"modello prodotto, altrimenti lasciare vuoto."
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_start
msgid "Start Date"
msgstr "Data inizio"
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_start
msgid ""
"Starting datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
"Data di inizio per la validazione riga di listino\n"
"Il valore mostrato dipende dal fuso orario impostato nelle tue preferenze."
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_id
msgid "Variant"
msgstr "Variante"
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache_wizard
msgid "Wizard for pricelist cache"
msgstr "Procedura guidata per memoria listino"

View File

@@ -0,0 +1,284 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pricelist_cache
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Apply On"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_cache_line_ids
msgid "Cached prices"
msgstr ""
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Cancel"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__categ_id
msgid "Category"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__company_id
msgid "Company"
msgstr ""
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_res_partner
msgid "Contact"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_uid
msgid "Created by"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__create_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__create_date
msgid "Created on"
msgstr ""
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.view_partner_property_form
msgid "Display Customer Prices"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__display_name
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__display_name
msgid "Display Name"
msgstr ""
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_view
msgid "Display Pricelist Prices"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_end
msgid "End Date"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_end
msgid ""
"Ending datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__min_quantity
msgid ""
"For the rule to apply, bought/sold quantity must be greater than or equal to the minimum quantity specified in this field.\n"
"Expressed in the default unit of measure of the product."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__id
msgid "ID"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__is_pricelist_cache_available
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__is_pricelist_cache_available
msgid "Is Pricelist Cache Available"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__is_pricelist_cache_computed
msgid "Is Pricelist Cache Computed"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_uid
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_uid
msgid "Last Updated by"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__write_date
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__write_date
msgid "Last Updated on"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__min_quantity
msgid "Min. Quantity"
msgstr ""
#. module: pricelist_cache
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Open"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__base_pricelist_id
msgid "Other Pricelist"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist__parent_pricelist_ids
msgid "Parent Pricelist"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__partner_id
msgid "Partner"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__price
msgid "Price"
msgstr ""
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__pricelist_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__pricelist_id
msgid "Pricelist"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list0
msgid "Pricelist 0"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list1
msgid "Pricelist 1"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list2
msgid "Pricelist 2"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list3
msgid "Pricelist 3"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list4
msgid "Pricelist 4"
msgstr ""
#. module: pricelist_cache
#: model:product.pricelist,name:pricelist_cache.list5
msgid "Pricelist 5"
msgstr ""
#. module: pricelist_cache
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_action
#: model:ir.actions.act_window,name:pricelist_cache.product_pricelist_cache_wizard_action
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache
#: model:ir.ui.menu,name:pricelist_cache.menuitem_pricelist_cache_wizard
#: model_terms:ir.ui.view,arch_db:pricelist_cache.product_pricelist_cache_wizard_view_form
msgid "Pricelist Cache"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_res_partner__pricelist_cache_product_filter_id
#: model:ir.model.fields,field_description:pricelist_cache.field_res_users__pricelist_cache_product_filter_id
msgid "Pricelist Cache Product Filter"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__pricelist_cache_update_skipped
msgid "Pricelist Cache Update Skipped"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__applied_on
msgid "Pricelist Item applicable on selected option"
msgstr ""
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_item
msgid "Pricelist Rule"
msgstr ""
#. module: pricelist_cache
#. odoo-python
#: code:addons/pricelist_cache/models/res_partner.py:0
msgid "Pricelist caching in progress. Retry later"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache_wizard__product_id
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid "Product"
msgstr ""
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_product
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_cache__product_id
msgid "Product Variant"
msgstr ""
#. module: pricelist_cache
#: model:ir.actions.server,name:pricelist_cache.cron_reset_pricelist_cache_ir_actions_server
msgid "Reset pricelist cache"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__categ_id
msgid ""
"Specify a product category if this rule only applies to products belonging "
"to this category or its children categories. Keep empty otherwise."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_id
msgid ""
"Specify a product if this rule only applies to one product. Keep empty "
"otherwise."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__product_tmpl_id
msgid ""
"Specify a template if this rule only applies to one product template. Keep "
"empty otherwise."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__date_start
msgid "Start Date"
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,help:pricelist_cache.field_product_pricelist_item__date_start
msgid ""
"Starting datetime for the pricelist item validation\n"
"The displayed value depends on the timezone set in your preferences."
msgstr ""
#. module: pricelist_cache
#: model:ir.model.fields,field_description:pricelist_cache.field_product_pricelist_item__product_id
msgid "Variant"
msgstr ""
#. module: pricelist_cache
#: model:ir.model,name:pricelist_cache.model_product_pricelist_cache_wizard
msgid "Wizard for pricelist cache"
msgstr ""

View File

@@ -0,0 +1,5 @@
from . import product_pricelist
from . import product_pricelist_item
from . import product_pricelist_cache
from . import product_product
from . import res_partner

View File

@@ -0,0 +1,195 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from datetime import date
from odoo import api, fields, models
class Pricelist(models.Model):
_inherit = "product.pricelist"
parent_pricelist_ids = fields.Many2many(
"product.pricelist",
relation="product_pricelist_cache__parent_pricelist_ids_rel",
column1="pricelist_id",
column2="parent_pricelist_id",
compute="_compute_parent_pricelist_ids",
store=True,
)
is_pricelist_cache_computed = fields.Boolean()
is_pricelist_cache_available = fields.Boolean(
compute="_compute_is_pricelist_cache_available"
)
@api.depends(
"item_ids", "item_ids.applied_on", "item_ids.base", "item_ids.base_pricelist_id"
)
def _compute_parent_pricelist_ids(self):
for record in self:
record.parent_pricelist_ids = record._get_parent_pricelists()
def _compute_is_pricelist_cache_available(self):
for record in self:
parents = record._get_parent_list_tree()
record.is_pricelist_cache_available = all(
parents.mapped("is_pricelist_cache_computed")
)
def _get_parent_list_tree(self):
self.ensure_one()
query = """
WITH RECURSIVE parent_pricelist AS (
SELECT id
FROM product_pricelist
WHERE id = %(pricelist_id)s
UNION SELECT item.base_pricelist_id AS id
FROM product_pricelist_item item
INNER JOIN parent_pricelist parent
ON item.pricelist_id = parent.id
)
SELECT id FROM parent_pricelist;
"""
self.env.flush_all()
self.env.cr.execute(query, {"pricelist_id": self.id})
return self.search([("id", "in", [row[0] for row in self.env.cr.fetchall()])])
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
for record in res:
if record._is_factor_pricelist() or record._is_global_pricelist():
product_ids_to_cache = None
else:
product_ids_to_cache = record.item_ids.mapped("product_id").ids
cache_model = self.env["product.pricelist.cache"].with_delay()
cache_model.update_product_pricelist_cache(
product_ids=product_ids_to_cache, pricelist_ids=record.ids
)
return res
def _get_product_prices(self, product_ids):
self.ensure_one()
# Search instead of browse, since products could have been unlinked
# between the time where records have been created / modified
# and the time this method is executed.
products = self.env["product.product"].search([("id", "in", product_ids)])
results = self._compute_price_rule(products, 1, date=date.today())
product_prices = {prod: price[0] for prod, price in results.items()}
return product_prices
def _get_root_pricelist_ids(self):
"""Returns the id of all root pricelists.
A root pricelist have no item referencing another pricelist.
"""
no_parent_query = """
SELECT id
FROM product_pricelist pp
WHERE id NOT IN (
SELECT pricelist_id
FROM product_pricelist_item
WHERE (
base_pricelist_id IS NOT NULL
AND base = 'pricelist'
)
)
AND active = TRUE;
"""
self.env.flush_all()
self.env.cr.execute(no_parent_query)
return [row[0] for row in self.env.cr.fetchall()]
def _get_factor_pricelist_ids(self):
"""Returns the id of all factor pricelists.
A factor pricelist have an item referencing a pricelist,
altering the price via price_discount or price_surcharge
"""
factor_pricelist_query = """
SELECT id
FROM product_pricelist
WHERE id IN (
SELECT pricelist_id
FROM product_pricelist_item
WHERE (
base_pricelist_id IS NOT NULL
AND base = 'pricelist'
AND (
price_discount != 0.0
OR price_surcharge != 0.0
)
)
)
AND active = TRUE;
"""
self.env.flush_all()
self.env.cr.execute(factor_pricelist_query)
return [row[0] for row in self.env.cr.fetchall()]
def _get_global_pricelist_ids(self):
"""Return factor pricelists and pricelists with no parents."""
global_pricelist_ids = self._get_root_pricelist_ids()
factor_pricelist_ids = self._get_factor_pricelist_ids()
return global_pricelist_ids + factor_pricelist_ids
def _get_parent_pricelists(self):
"""Returns the parent pricelists.
The parent pricelist is defined on a pricelist_item when it's applied
globally, and based on another pricelist
"""
self.ensure_one()
query = """
SELECT base_pricelist_id
FROM product_pricelist_item
WHERE applied_on = '3_global'
AND base = 'pricelist'
AND base_pricelist_id IS NOT NULL
AND pricelist_id = %(pricelist_id)s
"""
self.env.cr.execute(query, {"pricelist_id": self.id})
return self.browse([row[0] for row in self.env.cr.fetchall()])
def _is_factor_pricelist(self):
"""Returns whether a pricelist is a factor pricelist.
A factor pricelist is applied globally and refers to another pricelist.
It also alters the "parent's price" by applying a discount or a surcharge
on it.
"""
self.ensure_one()
parent_pricelist_items = self.item_ids.filtered(
lambda i: (
i.applied_on == "3_global"
and i.base == "pricelist"
and i.base_pricelist_id
and (i.price_discount or i.price_surcharge)
)
)
return bool(parent_pricelist_items)
def _is_global_pricelist(self):
"""Returns whether a pricelist is a factor global.
A factor pricelist is applied globally and refers to another pricelist.
It also alters the "parent's price" by applying a discount or a surcharge
on it.
"""
self.ensure_one()
return bool(not self._get_parent_pricelists())
def _recursive_get_items(self, product):
"""Recursively searches on parent pricelists for items applied on product."""
item_ids = self.item_ids.filtered(lambda i: i.product_id == product).ids
for parent_pricelist in self._get_parent_pricelists():
parent_items = parent_pricelist._recursive_get_items(product)
item_ids.extend(parent_items.ids)
return self.env["product.pricelist.item"].browse(item_ids)
def button_open_pricelist_cache_tree(self):
cache_model = self.env["product.pricelist.cache"]
products = self.env["product.product"].search([])
prices = cache_model.get_cached_prices_for_pricelist(self, products)
domain = [("id", "in", prices.ids)]
return cache_model._get_tree_view(domain)

View File

@@ -0,0 +1,268 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from psycopg2 import sql
from odoo import fields, models, tools
class PricelistCache(models.Model):
"""This model aims to store all product prices depending on all pricelist.
Price cache is updated or created in the following cases:
- Product price is created / modified;
-> entrypoint "product_product.py::{create,write}"
- Pricelist item is created / modified;
-> entrypoint "product_pricelist_item.py::update_product_pricelist_cache"
- Pricelist is created;
-> entrypoint "product_pricelist.py::create"
There's also a daily cron task that updates cache prices
that have been skipped during the day:
- see "cron_reset_pricelist_cache" for the cron method
- see "product_pricelist_item.py::update_product_pricelist_cache"
for skip conditions
Every call to PricelistCache.update_product_pricelist_cache
should be made in a job as computation might be slow, depending on the case.
"""
_name = "product.pricelist.cache"
_description = "Pricelist Cache"
_rec_name = "pricelist_id"
pricelist_id = fields.Many2one(
"product.pricelist",
string="Pricelist",
required=True,
index=True,
ondelete="cascade",
)
product_id = fields.Many2one(
"product.product", string="Product Variant", index=True
)
price = fields.Float()
def _update_existing_records(self, product_prices):
"""Update existing records with provided prices.
Args:
- self : The recordset of cache records to update
- product_prices : The new prices to apply
"""
# Write everything in single transaction
values = [
sql.SQL(", ").join(
map(sql.Literal, (record.id, product_prices[record.product_id.id]))
)
for record in self
]
query = sql.SQL(
"""
UPDATE
product_pricelist_cache AS pricelist_cache
SET
price = c.price
FROM (VALUES ({}))
AS c(id, price)
WHERE
c.id = pricelist_cache.id;
"""
).format(sql.SQL("), (").join(values))
self.env.flush_all()
self.env.cr.execute(query)
self.invalidate_model(["price"])
self.env.flush_all()
def _create_cache_records(self, pricelist_id, product_ids, product_prices):
"""Create price cache records for a given pricelist, applied to a list of
product ids.
args:
- pricelist_id : The pricelist id on which prices are applied
- product_ids : A list of product ids to cache
- product_prices : A dict containing the prices for each product
"""
values = [
sql.SQL(", ").join(
map(sql.Literal, (p_id, pricelist_id, product_prices[p_id]))
)
for p_id in product_ids
]
if values:
# create_everything from a single transaction
query = sql.SQL(
"""
INSERT INTO product_pricelist_cache (product_id, pricelist_id, price)
VALUES ({});
"""
).format(sql.SQL("), (").join(values))
self.env.flush_all()
self.env.cr.execute(query)
def _update_pricelist_cache(self, pricelist_id, product_prices):
"""Updates the cache, for a given pricelist, and product prices.
Args:
- pricelist: a product.pricelist record
- product_prices: A dictionnary,
with product.product id as keys, and prices as values
"""
product_ids = list(product_prices.keys())
# First, update existing records
existing_records = self.search(
[
("pricelist_id", "=", pricelist_id),
("product_id", "in", product_ids),
]
)
if existing_records:
existing_records._update_existing_records(product_prices)
# Then, create missing records with provided prices
# Diff between products and already created records
not_cached_product_ids = set(product_ids)
if existing_records:
not_cached_product_ids -= set(existing_records.mapped("product_id").ids)
if not_cached_product_ids:
self._create_cache_records(
pricelist_id, not_cached_product_ids, product_prices
)
def _get_product_ids_to_update(self, pricelist, product_ids):
"""Returns a list of product_ids that are already cached
for the given pricelist.
Args:
- pricelist: The pricelist record on which new prices are applied
- product_prices: The list of products to check
"""
product_ids_to_update = []
# We need to be sure to not waste resources while updating the cache.
# To do that, we ensure that prices are not coming from a parent
# pricelist.
if pricelist._get_parent_pricelists():
# If this is a factor pricelist, then everything
# have to be updated
if pricelist._is_factor_pricelist():
product_ids_to_update = product_ids
# Otherwise, prices are fetched from parent pricelist
# and only products in items have to be updated
else:
product_item_ids = pricelist.item_ids.filtered(
lambda i: i.product_id.id in product_ids
)
product_ids_to_update = product_item_ids.mapped("product_id").ids
else:
# No parent (for instance public pricelist), then update everything
product_ids_to_update = product_ids
return product_ids_to_update
def update_product_pricelist_cache(self, product_ids=None, pricelist_ids=None):
"""
Updates price list cache given a product.product recordset and a pricelist,
if specified.
"""
if not product_ids:
product_ids = self.env["product.product"].search([]).ids
if not pricelist_ids:
pricelists = self.env["product.pricelist"].search([])
else:
# Search instead of browse, since pricelists could have been unlinked
# between the time where records have been created / modified
# and the time this method is executed.
pricelists = self.env["product.pricelist"].search(
[("id", "in", pricelist_ids)]
)
for pricelist in pricelists:
product_ids_to_update = self._get_product_ids_to_update(
pricelist, product_ids
)
product_prices = pricelist._get_product_prices(product_ids_to_update)
self._update_pricelist_cache(pricelist.id, product_prices)
# Once this is done, set pricelist cache as computed on pricelist
pricelist.is_pricelist_cache_computed = True
def _update_pricelist_items_cache(self, pricelist_items):
"""Updates cache for a given recordset of pricelist items, then update
the items skipped state to False.
"""
pricelist_products = pricelist_items._get_pricelist_products_group()
for pricelist_id, product_ids in pricelist_products.items():
self.with_delay().update_product_pricelist_cache(
product_ids=product_ids, pricelist_ids=[pricelist_id]
)
pricelist_items.write({"pricelist_cache_update_skipped": False})
def create_full_cache(self):
"""Creates cache for all prices applied to all pricelists."""
pricelist_model = self.env["product.pricelist"]
# Here, we split price computation in 2.
# Huge pricelists (root pricelists and factor pricelists) are computed
# on their own, in order to avoid long sql transactions.pricelist_model
# Smaller pricelists can be computed 3 by 3, since they are taking
# less time to process.
global_list_ids = pricelist_model._get_global_pricelist_ids()
# Belt and braces. Just to ensure higher level lists are executed first
global_list_ids.sort()
for list_id in global_list_ids:
self.with_delay().update_product_pricelist_cache(pricelist_ids=[list_id])
regular_list_ids = self.env["product.pricelist"].search(
[("id", "not in", global_list_ids)]
)
pricelist_ids = regular_list_ids.ids
# Spawn a job every 3 pricelists (reduce the number of jobs created)
for chunk_ids in tools.misc.split_every(3, pricelist_ids):
self.with_delay().update_product_pricelist_cache(pricelist_ids=chunk_ids)
def flush_pricelist_cache(self):
# flush table
flush_query = "TRUNCATE TABLE product_pricelist_cache CASCADE;"
self.env.cr.execute(flush_query)
# reset sequence
sequence_query = """
ALTER SEQUENCE product_pricelist_cache_id_seq RESTART WITH 1;
"""
self.env.cr.execute(sequence_query)
self.env["product.pricelist"].search([]).is_pricelist_cache_computed = False
def cron_reset_pricelist_cache(self):
"""Recreates the whole price list cache."""
self.flush_pricelist_cache()
# Re-create everything
self.create_full_cache()
def get_cached_prices_for_pricelist(self, pricelist, products):
"""Retrieves product prices for a given pricelist."""
# As some items might have been skipped during product_pricelist_item
# updates, some cached prices might be wrong, since those records
# will be updated during a daily cron task.
# If any of those prices is queried here, update cache before retrieving it
need_update_items = self.env["product.pricelist.item"].search(
[
("pricelist_id", "=", pricelist.id),
("product_id", "in", products.ids),
("pricelist_cache_update_skipped", "=", True),
]
)
self._update_pricelist_items_cache(need_update_items)
# Retrieve cache for the current pricelist first
cached_prices = self.search(
[
("pricelist_id", "=", pricelist.id),
("product_id", "in", products.ids),
]
)
# Then, retrieves prices from parent pricelists
remaining_products = products - cached_prices.mapped("product_id")
parent_pricelists = pricelist._get_parent_pricelists()
# There shouldn't be multiple parents for a pricelist, but it's possible…
for parent_pricelist in parent_pricelists:
cached_prices |= self.get_cached_prices_for_pricelist(
parent_pricelist, remaining_products
)
return cached_prices
def _get_tree_view(self, domain=None):
xmlid = "pricelist_cache.product_pricelist_cache_action"
action = self.env["ir.actions.act_window"]._for_xml_id(xmlid)
if domain is not None:
action["domain"] = domain
return action

View File

@@ -0,0 +1,69 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from collections import defaultdict
from odoo import api, fields, models
class PricelistItem(models.Model):
_inherit = "product.pricelist.item"
base_pricelist_id = fields.Many2one(index=True)
product_tmpl_id = fields.Many2one(index=True)
product_id = fields.Many2one(index=True)
date_start = fields.Datetime(index=True)
date_end = fields.Datetime(index=True)
applied_on = fields.Selection(index=True)
categ_id = fields.Many2one(index=True)
min_quantity = fields.Float(index=True)
company_id = fields.Many2one(index=True)
pricelist_cache_update_skipped = fields.Boolean()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.update_product_pricelist_cache()
return res
def _has_date_range(self):
"""Returns whether any of the item records in recordset is based on dates."""
return any(bool(record.date_start or record.date_end) for record in self)
def _get_pricelist_products_group(self):
"""Returns a mapping of products grouped by pricelist.
Result:
keys: product.pricelist id
values: product.product list of ids
"""
pricelist_products = defaultdict(list)
for item in self:
pricelist_products[item.pricelist_id.id].append(item.product_id.id)
return pricelist_products
def update_product_pricelist_cache(self):
"""Executed when a product item is modified. Filters items not based
on variants or based on dates, then updates the cache.
"""
# Filter items applied on variants
items = self.filtered(lambda i: i.applied_on == "0_product_variant")
# Filter items based on dates
item_ids_to_update = []
for item in items:
product_item_tree = item.pricelist_id._recursive_get_items(item.product_id)
if product_item_tree._has_date_range():
# skip if any of the item in the tree is date based
item.pricelist_cache_update_skipped = True
continue
item_ids_to_update.append(item.id)
items_to_update = self.env["product.pricelist.item"].browse(item_ids_to_update)
# Group per pricelist
pricelist_products = items_to_update._get_pricelist_products_group()
# Update cache
cache_object = self.env["product.pricelist.cache"]
for pricelist_id, product_ids in pricelist_products.items():
cache_object.with_delay().update_product_pricelist_cache(
product_ids=product_ids, pricelist_ids=[pricelist_id]
)

View File

@@ -0,0 +1,23 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import api, models
class ProductProduct(models.Model):
_inherit = "product.product"
@api.model_create_multi
def create(self, vals):
"""Create a cache record for each newly created product, for each global
pricelist.
"""
res = super().create(vals)
pricelist_model = self.env["product.pricelist"]
global_pricelist_ids = pricelist_model._get_global_pricelist_ids()
if global_pricelist_ids and res:
cache_model = self.env["product.pricelist.cache"]
cache_model.with_delay().update_product_pricelist_cache(
res.ids, global_pricelist_ids
)
return res

View File

@@ -0,0 +1,58 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import exceptions, fields, models
class Partner(models.Model):
_inherit = "res.partner"
is_pricelist_cache_available = fields.Boolean(
related="property_product_pricelist.is_pricelist_cache_available"
)
def _default_pricelist_cache_product_filter_id(self):
# When the module is installed, Odoo creates the new field and at the
# same time tries to set the default value for all existing records in
# the DB. However the XML data (and thus 'product_filter_default' filter)
# is still not created at this stage.
# In order to get the module installed, the 'raise_if_not_found' parameter
# has been added, and to set the default value on existing partners
# the post_init_hook 'set_default_partner_product_filter' has been defined.
return self.env.ref(
"pricelist_cache.product_filter_default", raise_if_not_found=False
)
pricelist_cache_product_filter_id = fields.Many2one(
comodel_name="ir.filters",
domain=[("model_id", "=", "product.product")],
default=lambda o: o._default_pricelist_cache_product_filter_id(),
)
def _pricelist_cache_get_prices(self):
if not self.is_pricelist_cache_available:
raise exceptions.UserError(
self.env._("Pricelist caching in progress. Retry later")
)
pricelist = self._pricelist_cache_get_pricelist()
products = self._pricelist_cache_get_products()
cache_model = self.env["product.pricelist.cache"]
return cache_model.get_cached_prices_for_pricelist(pricelist, products)
def _pricelist_cache_get_pricelist(self):
return self.property_product_pricelist
def _pricelist_cache_get_products(self):
domain = self._pricelist_cache_product_domain()
return self.env["product.product"].search(domain)
def _pricelist_cache_product_domain(self):
if self.pricelist_cache_product_filter_id:
return self.pricelist_cache_product_filter_id._get_eval_domain()
return []
def button_open_pricelist_cache_tree(self):
prices = self._pricelist_cache_get_prices()
cache_model = self.env["product.pricelist.cache"]
domain = [("id", "in", prices.ids)]
return cache_model._get_tree_view(domain)

3
pricelist_cache/pyproject.toml Executable file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,12 @@
- Telmo Santos \<telmo.santos@camptocamp.com\>
- Matthieu Méquignon \<matthieu.mequignon@camptocamp.com\>
- Simone Orsi \<simahawk@gmail.com\>
- Thierry Ducrest \<thierry.ducrest@camptocamp.com\>
- Sébastien Alix \<sebastien.alix@camptocamp.com\>
- [Trobz](https://trobz.com):
- Hai Lang \<hailn@trobz.com\>

View File

@@ -0,0 +1,4 @@
**Financial support**
- Cosanum
- Camptocamp R&D

View File

@@ -0,0 +1,14 @@
Provides a cron task who caches prices for all products and all
pricelists. The goal is to be able to generate a whole catalog of prices
and products for a given customer in a decent time.
Everyday, the cron task will trash the previous day's cache, and rebuild
it from scratch. It means that at any moment, the prices stored in the
cache are those of the current day, and will not be recomputed before
the next day.
However, new prices will be cached in the following cases:
- new product is created
- new pricelist is created
- new pricelist item is created

View File

@@ -0,0 +1,3 @@
- Use job dependencies. If pricelist a is based on b, then job a should
depend on job b.
- In tests do not depend on odoo demo data which might change anytime and break the tests

View File

@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_pricelist_cache_user,access_product_pricelist_cache_user,model_product_pricelist_cache,base.group_user,1,0,0,0
access_product_pricelist_cache_system,access_product_pricelist_cache_system,model_product_pricelist_cache,base.group_system,1,1,1,1
access_product_pricelist_cache_wizard_user,access_product_pricelist_cache_wizard,model_product_pricelist_cache_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_pricelist_cache_user access_product_pricelist_cache_user model_product_pricelist_cache base.group_user 1 0 0 0
3 access_product_pricelist_cache_system access_product_pricelist_cache_system model_product_pricelist_cache base.group_system 1 1 1 1
4 access_product_pricelist_cache_wizard_user access_product_pricelist_cache_wizard model_product_pricelist_cache_wizard base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,462 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Pricelist Cache</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="pricelist-cache">
<h1 class="title">Pricelist Cache</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:39feac382c97a1d62a2021eb330fb9404a69dcc7e8846cb8a4d71797b4d3efee
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/18.0/pricelist_cache"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-pricelist_cache"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Provides a cron task who caches prices for all products and all
pricelists. The goal is to be able to generate a whole catalog of prices
and products for a given customer in a decent time.</p>
<p>Everyday, the cron task will trash the previous days cache, and rebuild
it from scratch. It means that at any moment, the prices stored in the
cache are those of the current day, and will not be recomputed before
the next day.</p>
<p>However, new prices will be cached in the following cases:</p>
<ul class="simple">
<li>new product is created</li>
<li>new pricelist is created</li>
<li>new pricelist item is created</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-1">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-1">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Use job dependencies. If pricelist a is based on b, then job a should
depend on job b.</li>
<li>In tests do not depend on odoo demo data which might change anytime
and break the tests</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20pricelist_cache%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Telmo Santos &lt;<a class="reference external" href="mailto:telmo.santos&#64;camptocamp.com">telmo.santos&#64;camptocamp.com</a>&gt;</li>
<li>Matthieu Méquignon &lt;<a class="reference external" href="mailto:matthieu.mequignon&#64;camptocamp.com">matthieu.mequignon&#64;camptocamp.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simahawk&#64;gmail.com">simahawk&#64;gmail.com</a>&gt;</li>
<li>Thierry Ducrest &lt;<a class="reference external" href="mailto:thierry.ducrest&#64;camptocamp.com">thierry.ducrest&#64;camptocamp.com</a>&gt;</li>
<li>Sébastien Alix &lt;<a class="reference external" href="mailto:sebastien.alix&#64;camptocamp.com">sebastien.alix&#64;camptocamp.com</a>&gt;</li>
<li><a class="reference external" href="https://trobz.com">Trobz</a>:<ul>
<li>Hai Lang &lt;<a class="reference external" href="mailto:hailn&#64;trobz.com">hailn&#64;trobz.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-6">Other credits</a></h2>
<p><strong>Financial support</strong></p>
<ul class="simple">
<li>Cosanum</li>
<li>Camptocamp R&amp;D</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/18.0/pricelist_cache">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
from . import test_pricelist_cache
from . import test_partner_pricelist_cache
from . import test_methods

154
pricelist_cache/tests/common.py Executable file
View File

@@ -0,0 +1,154 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from functools import wraps
from odoo.addons.base.tests.common import BaseCommon
LIST_PRICES_MAPPING = {
"pricelist_cache.list0": [
{"id": 17, "price": 100.0},
{"id": 18, "price": 15.8},
{"id": 19, "price": 100.0},
{"id": 20, "price": 47.0},
],
"pricelist_cache.list1": [
{"id": 17, "price": 75.0},
{"id": 18, "price": 15.8},
{"id": 19, "price": 100.0},
{"id": 20, "price": 47.0},
],
"pricelist_cache.list2": [
{"id": 17, "price": 50.0},
{"id": 18, "price": 15.8},
{"id": 19, "price": 100.0},
{"id": 20, "price": 47.0},
],
"pricelist_cache.list3": [
{"id": 17, "price": 25.0},
{"id": 18, "price": 15.8},
{"id": 19, "price": 100.0},
{"id": 20, "price": 47.0},
],
"pricelist_cache.list4": [
{"id": 17, "price": 15.0},
{"id": 18, "price": 50.0},
{"id": 19, "price": 100.0},
{"id": 20, "price": 47.0},
],
"pricelist_cache.list5": [
{"id": 17, "price": 45.0},
{"id": 18, "price": 35.8},
{"id": 19, "price": 120.0},
{"id": 20, "price": 67.0},
],
}
def check_duplicates(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
duplicates_query = """
SELECT product_id, pricelist_id, count(*)
FROM product_pricelist_cache
GROUP BY product_id, pricelist_id
HAVING count(*) > 1;
"""
self.env.cr.execute(duplicates_query)
res = self.env.cr.fetchall()
self.assertFalse(res)
return wrapper
class TestPricelistCacheCommon(BaseCommon):
@classmethod
def setUpClassBaseCache(cls):
cls.cache_model.cron_reset_pricelist_cache()
cls.env["product.pricelist"].invalidate_model(["is_pricelist_cache_available"])
@classmethod
def set_currency(cls):
"""Sets currency everywhere, as the sale dependency breaks every unit test."""
usd = cls.env.ref("base.USD")
cls.env.user.company_id.currency_id = usd
cls.products.write({"currency_id": usd.id})
cls.lists.write({"currency_id": usd.id})
cls.pricelist_items.write({"currency_id": usd.id})
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(
context=dict(
cls.env.context,
queue_job__no_delay=True,
)
)
cls.cache_model = cls.env["product.pricelist.cache"]
# root pricelists
cls.list0 = cls.env.ref("pricelist_cache.list0")
# child 1, based on list0
cls.list1 = cls.env.ref("pricelist_cache.list1")
# child 2, based on list1
cls.list2 = cls.env.ref("pricelist_cache.list2")
# child 3, based on list2
cls.list3 = cls.env.ref("pricelist_cache.list3")
# child 4, based on list0
cls.list4 = cls.env.ref("pricelist_cache.list4")
# factor list 5, based on list3
cls.list5 = cls.env.ref("pricelist_cache.list5")
cls.lists = cls.env["product.pricelist"].browse(
[
cls.list0.id,
cls.list1.id,
cls.list2.id,
cls.list3.id,
cls.list4.id,
cls.list5.id,
]
)
# products
# TODO: create products instead of using odoo demo data
cls.p6 = cls.env.ref("product.product_product_6")
cls.p7 = cls.env.ref("product.product_product_7")
cls.p8 = cls.env.ref("product.product_product_8")
# P9 not in any pricelist
cls.p9 = cls.env.ref("product.product_product_9")
cls.products = cls.env["product.product"].browse(
[cls.p6.id, cls.p7.id, cls.p8.id, cls.p9.id]
)
cls.pricelist_items = cls.env["product.pricelist.item"]
cls.pricelist_items |= cls.list0.item_ids
cls.pricelist_items |= cls.list1.item_ids
cls.pricelist_items |= cls.list2.item_ids
cls.pricelist_items |= cls.list3.item_ids
cls.pricelist_items |= cls.list4.item_ids
cls.set_currency()
cls.setUpClassBaseCache()
cls.partner = cls.env.ref("base.res_partner_12")
def _flush_cache(self):
self.cache_model.flush_pricelist_cache()
self.env["res.partner"].invalidate_model(["is_pricelist_cache_available"])
self.env["product.pricelist"].invalidate_model(["is_pricelist_cache_available"])
def _update_cache(self, pricelist_ids=None, product_ids=None):
self.cache_model.update_product_pricelist_cache(
product_ids=product_ids, pricelist_ids=pricelist_ids
)
self.env["res.partner"].invalidate_model(["is_pricelist_cache_available"])
self.env["product.pricelist"].invalidate_model(["is_pricelist_cache_available"])
def assert_cache_available(self, lists):
self.assertTrue(all(lists.mapped("is_pricelist_cache_available")))
def assert_cache_computed(self, lists):
self.assertTrue(all(lists.mapped("is_pricelist_cache_computed")))
def assert_cache_not_available(self, lists):
self.assertFalse(any(lists.mapped("is_pricelist_cache_available")))
def assert_cache_not_computed(self, lists):
self.assertFalse(any(lists.mapped("is_pricelist_cache_computed")))

View File

@@ -0,0 +1,38 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from .common import TestPricelistCacheCommon
class TestPricelistCacheModels(TestPricelistCacheCommon):
def test_get_parent_lists_tree(self):
list0 = self.list0
list1 = self.list1
list2 = self.list2
list3 = self.list3
list4 = self.list4
list5 = self.list5
# list0 has no parent list, its tree should be itself only
list0_parents_tree = list0._get_parent_list_tree()
expected_list0_tree = list0
self.assertEqual(list0_parents_tree, expected_list0_tree)
# list1 parent is list0, tree should be list0|list1
list1_parents_tree = list1._get_parent_list_tree()
expected_list1_tree = expected_list0_tree | list1
self.assertEqual(list1_parents_tree, expected_list1_tree)
# list2 parent is list1, tree should be list0|list1|list2
list2_parents_tree = list2._get_parent_list_tree()
expected_list2_tree = expected_list1_tree | list2
self.assertEqual(list2_parents_tree, expected_list2_tree)
# list3 parent is list2, tree should be list0|list1|list2|list3
list3_parents_tree = list3._get_parent_list_tree()
expected_list3_tree = expected_list2_tree | list3
self.assertEqual(list3_parents_tree, expected_list3_tree)
# list4 parent is list0, tree should be list0|list4
list4_parents_tree = list4._get_parent_list_tree()
expected_list4_tree = expected_list0_tree | list4
self.assertEqual(list4_parents_tree, expected_list4_tree)
# list 5 parent is list3, tree should be list0|list1|list2|list3|list5
list5_parents_tree = list5._get_parent_list_tree()
expected_list5_tree = expected_list3_tree | list5
self.assertEqual(list5_parents_tree, expected_list5_tree)

View File

@@ -0,0 +1,108 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from freezegun import freeze_time
from odoo.exceptions import UserError
from .common import LIST_PRICES_MAPPING, TestPricelistCacheCommon
@freeze_time("2021-03-15")
class TestPricelistCache(TestPricelistCacheCommon):
def test_partner_pricelists(self):
partner = self.partner
for pricelist_xmlid, expected_result in LIST_PRICES_MAPPING.items():
partner.property_product_pricelist = self.env.ref(pricelist_xmlid)
price_list = partner._pricelist_cache_get_prices()
# for test purposes, only test products referenced in demo data
# Since cache is created for more or less products, depending
# on the modules installed
price_list = price_list.filtered(lambda p: p.product_id in self.products)
result = [{"id": c.product_id.id, "price": c.price} for c in price_list]
result.sort(key=lambda r: r["id"])
self.assertEqual(result, expected_result)
def assert_partner_cache_not_available(self):
regex = r"Pricelist caching in progress. Retry later"
with self.assertRaisesRegex(UserError, regex):
self.partner._pricelist_cache_get_prices()
def assert_partner_cache_available(self):
self.partner._pricelist_cache_get_prices()
def test_partner_inconsistent_cache(self):
# Initialize
partner = self.partner
list3 = self.list3
list2 = self.list2
list1 = self.list1
list0 = self.list0
all_lists = list0 | list1 | list2 | list3
partner.property_product_pricelist = list3
# ### No pricelist cached
# - all_lists computed -> False
# - all_lists available -> False
self._flush_cache()
self.assert_cache_not_available(all_lists)
self.assert_cache_not_computed(all_lists)
self.assert_partner_cache_not_available()
# ### list2 cached
# Availability
# - all_lists -> False
# Computation
# - list0, list1, list3 -> False
# - list2 -> True
self._update_cache(pricelist_ids=list2.ids)
self.assert_cache_computed(list2)
self.assert_cache_not_computed(list3 | list1 | list0)
# No cache is available, because list2 depends on pricelists that
# haven't been cached.
self.assert_cache_not_available(all_lists)
# Therefore, trying to get prices for partner should raise an exception
self.assert_partner_cache_not_available()
# ### list1 and list2 cached
# Availability
# - all_lists -> False
# Computation
# - list0, list3 -> False
# - list1, list2 -> True
self._update_cache(pricelist_ids=list1.ids)
self.assert_cache_computed(list2 | list1)
self.assert_cache_not_computed(list3 | list0)
# No cache is available, because list1 and list2 depends on list0 that
# haven't been cached.
self.assert_cache_not_available(all_lists)
# Therefore, trying to get prices for partner should raise an exception
self.assert_partner_cache_not_available()
# ### list0, list1, list2 cached
# Availability
# - list0, list1, list2 -> True
# - list3 -> False
# Computation
# - list0, list1, list2 -> True
# - list3 -> False
self._update_cache(pricelist_ids=list0.ids)
self.assert_cache_computed(list0 | list1 | list2)
self.assert_cache_not_computed(list3)
# Now, all list0-2 are available, because none of them have a parent pricelist
# that isn't cached
self.assert_cache_available(list0 | list1 | list2)
# But list3 itself isnt cached
self.assert_cache_not_available(list3)
# And partner cache still cannot be retrieved
self.assert_partner_cache_not_available()
# ### All lists cached
# Availability
# - all_lists -> True
# Computation
# - all_lists -> True
self._update_cache(pricelist_ids=list3.ids)
self.assert_cache_available(all_lists)
self.assert_cache_computed(all_lists)
self.assert_partner_cache_available()

View File

@@ -0,0 +1,496 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from freezegun import freeze_time
from .common import TestPricelistCacheCommon, check_duplicates
@freeze_time("2021-03-15")
class TestPricelistCache(TestPricelistCacheCommon):
@check_duplicates
def test_base_caching(self):
cache_model = self.cache_model
# product 6, list 0: cached, price 100.0
p6_list0_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list0.id),
]
)
self.assertTrue(p6_list0_cache)
self.assertEqual(p6_list0_cache.price, 100.0)
# product 6, list 1: cached, price 75
p6_list1_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list1.id),
]
)
self.assertTrue(p6_list1_cache)
self.assertEqual(p6_list1_cache.price, 75.0)
# product 6, list 2 : Now cached, price 50.0
p6_list2_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list2.id),
]
)
self.assertTrue(p6_list2_cache)
self.assertEqual(p6_list2_cache.price, 50.0)
# product 6, list 3 : Cached, price 25.0
p6_list3_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list3.id),
]
)
self.assertTrue(p6_list3_cache)
self.assertEqual(p6_list3_cache.price, 25.0)
# product 6, list 4 : Cached, price 25.0
p6_list4_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list4.id),
]
)
self.assertTrue(p6_list4_cache)
self.assertEqual(p6_list4_cache.price, 15.0)
# product 6, list 5 : Cached, list3 price + 20
p6_list3_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list3.id),
]
)
expected_price = p6_list3_cache.price + 20.0
p6_list5_cache = cache_model.search(
[
("product_id", "=", self.p6.id),
("pricelist_id", "=", self.list5.id),
]
)
self.assertTrue(p6_list5_cache)
self.assertEqual(p6_list5_cache.price, expected_price)
# product 7, list 3: cached, price 50.0
p7_list4_cache = cache_model.search(
[
("product_id", "=", self.p7.id),
("pricelist_id", "=", self.list4.id),
]
)
self.assertTrue(p7_list4_cache)
self.assertEqual(p7_list4_cache.price, 50.0)
# product 7, list 5 : Cached, list0 price + 20
p7_list0_cache = cache_model.search(
[
("product_id", "=", self.p7.id),
("pricelist_id", "=", self.list0.id),
]
)
expected_price = p7_list0_cache.price + 20.0
p7_list5_cache = cache_model.search(
[
("product_id", "=", self.p7.id),
("pricelist_id", "=", self.list5.id),
]
)
self.assertTrue(p7_list5_cache)
self.assertEqual(p7_list5_cache.price, expected_price)
# product 8, list 0: cached price 100.0
p8_list0_cache = cache_model.search(
[
("product_id", "=", self.p8.id),
("pricelist_id", "=", self.list0.id),
]
)
self.assertTrue(p8_list0_cache)
self.assertEqual(p8_list0_cache.price, 100.0)
# Since we do not handle pricelist item updates anymore,
# these three tests will be adapted and re-enabled afterwards.
# test_update_pricelist_item, test_update_product_price,
# and test_retrieve_skipped_cache
# @check_duplicates
# def test_update_pricelist_item(self):
# cache_model = self.cache_model
# # case 1, product price is not set on a parent pricelist
# p7_list4_item = self.env.ref("pricelist_cache.item12")
# p7_list4_item.fixed_price = 42.0
# p7_cache = cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list4.id),
# ]
# )
# self.assertEqual(p7_cache.price, 42.0)
# # case 2, product price is set on the parent pricelist
# p6_list4_item = self.env.ref("pricelist_cache.item11")
# p6_list4_item.fixed_price = 52.0
# p6_cache = cache_model.search(
# [
# ("product_id", "=", self.p6.id),
# ("pricelist_id", "=", self.list4.id),
# ]
# )
# self.assertEqual(p6_cache.price, 52.0)
# # case 3, dates are set on the item, price unchanged
# p6_list2_item = self.env.ref("pricelist_cache.item7")
# p6_list2_item.fixed_price = 62.0
# p6_cache = cache_model.search(
# [
# ("product_id", "=", self.p6.id),
# ("pricelist_id", "=", self.list2.id),
# ]
# )
# self.assertEqual(p6_cache.price, 50.0)
# # case 4, dates are set on the parent's pricelist item: price unchanged
# p6_list3_item = self.env.ref("pricelist_cache.item9")
# p6_list3_item.fixed_price = 72.0
# p6_cache = cache_model.search(
# [
# ("product_id", "=", self.p6.id),
# ("pricelist_id", "=", self.list3.id),
# ]
# )
# self.assertEqual(p6_cache.price, 25.0)
# @check_duplicates
# def test_update_product_price(self):
# self.p7.write({"list_price": 42})
# # p6 should be updated only for list0 and list5
# p7_l0_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list0.id),
# ]
# )
# self.assertEqual(p7_l0_cache.price, 42)
# p7_l1_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list1.id),
# ]
# )
# self.assertFalse(p7_l1_cache)
# p7_l2_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list2.id),
# ]
# )
# self.assertFalse(p7_l2_cache)
# p7_l3_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list3.id),
# ]
# )
# self.assertFalse(p7_l3_cache)
# p7_l4_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list4.id),
# ]
# )
# self.assertEqual(p7_l4_cache.price, 50)
# p7_l5_cache = self.cache_model.search(
# [
# ("product_id", "=", self.p7.id),
# ("pricelist_id", "=", self.list5.id),
# ]
# )
# self.assertEqual(p7_l5_cache.price, 62)
@check_duplicates
def test_retrieve_price_list(self):
products = self.products
cache_model = self.cache_model
# list0 cache
l0_cache = cache_model.get_cached_prices_for_pricelist(self.list0, products)
self.assertEqual(len(l0_cache), 4)
l0_p6_cache = l0_cache.filtered(lambda c: c.product_id == self.p6)
self.assertEqual(l0_p6_cache.price, 100.0)
l0_p8_cache = l0_cache.filtered(lambda c: c.product_id == self.p8)
self.assertEqual(l0_p6_cache.price, 100.0)
# list1 cache
l1_cache = cache_model.get_cached_prices_for_pricelist(self.list1, products)
self.assertEqual(len(l1_cache), 4)
l1_p6_cache = l1_cache.filtered(lambda c: c.product_id == self.p6)
self.assertEqual(l1_p6_cache.price, 75.0)
# p8 price should have been fetched from list0 cache.
l1_p8_cache = l1_cache.filtered(lambda c: c.product_id == self.p8)
self.assertEqual(l0_p8_cache, l1_p8_cache)
# list2 cache
l2_cache = cache_model.get_cached_prices_for_pricelist(self.list2, products)
self.assertEqual(len(l2_cache), 4)
l2_p6_cache = l2_cache.filtered(lambda c: c.product_id == self.p6)
self.assertEqual(l2_p6_cache.price, 50.0)
# p8 price should have been fetched from list0 cache.
l2_p8_cache = l2_cache.filtered(lambda c: c.product_id == self.p8)
self.assertEqual(l0_p8_cache, l2_p8_cache)
# list3 cache
l3_cache = cache_model.get_cached_prices_for_pricelist(self.list3, products)
self.assertEqual(len(l3_cache), 4)
l3_p6_cache = l3_cache.filtered(lambda c: c.product_id == self.p6)
self.assertEqual(l3_p6_cache.price, 25.0)
# p8 price should have been fetched from list0 cache.
l3_p8_cache = l3_cache.filtered(lambda c: c.product_id == self.p8)
self.assertEqual(l0_p8_cache, l3_p8_cache)
# list4 cache
l4_cache = cache_model.get_cached_prices_for_pricelist(self.list4, products)
self.assertEqual(len(l4_cache), 4)
l4_p6_cache = l4_cache.filtered(lambda c: c.product_id == self.p6)
self.assertEqual(l4_p6_cache.price, 15.0)
l4_p7_cache = l4_cache.filtered(lambda c: c.product_id == self.p7)
self.assertEqual(l4_p7_cache.price, 50.0)
# p8 price should have been fetched from list0 cache.
l4_p8_cache = l4_cache.filtered(lambda c: c.product_id == self.p8)
self.assertEqual(l0_p8_cache, l4_p8_cache)
# @check_duplicates
# @freeze_time("2021-04-15")
# def test_retrieve_skipped_cache(self):
# # When a pricelist item is updated, if it's based on dates, then the
# # cache update is skipped until the next cron cache update.
# # If one of those prices have to be retrieved, then the price would
# # be wrong in the cache. This tests ensures that calling
# # `get_cached_prices_for_pricelist` updates cache prices that
# # have been skipped
# item9 = self.env.ref("pricelist_cache.item9")
# item9.fixed_price = 32.0
# self.assertTrue(item9.pricelist_cache_update_skipped)
# item9_cache = self.env["product.pricelist.cache"].search(
# [
# ("product_id", "=", item9.product_id.id),
# ("pricelist_id", "=", item9.pricelist_id.id),
# ]
# )
# # item has been skipped, since parent item (item7) is based on dates
# self.assertEqual(item9_cache.price, 25.0)
# item9_cache2 = self.cache_model.get_cached_prices_for_pricelist(
# item9.pricelist_id, item9.product_id
# )
# # Since cache update was previously skipped, get_cache_prices_for_pricelist
# # should have updated it "on the fly"
# self.assertEqual(item9_cache2.price, 32.0)
# self.assertFalse(item9.pricelist_cache_update_skipped)
@check_duplicates
def test_pricelist_methods(self):
# test _get_root_pricelist_ids
pricelist_model = self.env["product.pricelist"]
expected_root_pricelist_ids = self.list0.ids
# This pricelist is created when stock module is installed. No other
# way is found yet to identify it.
pl = pricelist_model.search([("name", "=", "Default USD pricelist")])
if pl:
expected_root_pricelist_ids.append(pl.id)
expected_root_pricelist_ids.sort()
root_pricelist_ids = pricelist_model._get_root_pricelist_ids()
root_pricelist_ids.sort()
self.assertEqual(root_pricelist_ids, expected_root_pricelist_ids)
# test _get_factor_pricelist_ids
expected_factor_pricelist_ids = self.list5.ids
factor_pricelist_ids = pricelist_model._get_factor_pricelist_ids()
self.assertEqual(factor_pricelist_ids, expected_factor_pricelist_ids)
# test _get_parent_pricelists
list_5_parent = self.list5._get_parent_pricelists()
self.assertEqual(list_5_parent, self.list3)
list_4_parent = self.list4._get_parent_pricelists()
self.assertEqual(list_4_parent, self.list0)
list_3_parent = self.list3._get_parent_pricelists()
self.assertEqual(list_3_parent, self.list2)
list_2_parent = self.list2._get_parent_pricelists()
self.assertEqual(list_2_parent, self.list1)
list_1_parent = self.list1._get_parent_pricelists()
self.assertEqual(list_1_parent, self.list0)
list_0_parent = self.list0._get_parent_pricelists()
self.assertFalse(list_0_parent)
# test _is_factor_pricelist
factor_pricelist = pricelist_model.browse(factor_pricelist_ids)
self.assertTrue(factor_pricelist._is_factor_pricelist())
root_pricelists = pricelist_model.browse(root_pricelist_ids)
self.assertEqual(len(root_pricelists), len(expected_root_pricelist_ids))
for pricelist in root_pricelists:
self.assertFalse(pricelist._is_factor_pricelist())
# test _recursive_get_items
expected_item_ids = [
self.env.ref("pricelist_cache.item2").id,
self.env.ref("pricelist_cache.item5").id,
self.env.ref("pricelist_cache.item7").id,
self.env.ref("pricelist_cache.item9").id,
]
expected_item_ids.sort()
items = self.list3._recursive_get_items(self.p6)
item_ids = items.ids
item_ids.sort()
self.assertEqual(item_ids, expected_item_ids)
# test _has_date_range
self.assertTrue(items._has_date_range())
items -= self.env.ref("pricelist_cache.item7")
self.assertFalse(items._has_date_range())
# test _get_pricelist_products_group
expected_groups = {
self.list0.id: self.p6.ids,
self.list1.id: self.p6.ids,
self.list3.id: self.p6.ids,
}
groups = items._get_pricelist_products_group()
for (
expected_pricelist_id,
expected_product_ids,
) in expected_groups.items():
self.assertEqual(expected_product_ids, groups[expected_pricelist_id])
@check_duplicates
@freeze_time("2021-04-15")
def test_cache_at_product_create(self):
"""Ensures that cache prices are created at product creation on each global
pricelist."""
# TODO: Add the required dependencies in a future release
# Stock is a dependency for the creation of this product
new_product = self.env["product.product"].create(
{"name": "Dehydrated Water", "list_price": 42}
)
# global pricelist, cache created, regular price
list0_cache = self.cache_model.search(
[
("product_id", "=", new_product.id),
("pricelist_id", "=", self.list0.id),
]
)
self.assertTrue(list0_cache)
self.assertEqual(list0_cache.price, 42)
# Not a global pricelist, not defined
global_ids = [self.list1.id, self.list2.id, self.list3.id, self.list4.id]
not_global_lists_cache = self.cache_model.search(
[
("product_id", "=", new_product.id),
("pricelist_id", "in", global_ids),
]
)
self.assertFalse(not_global_lists_cache)
# Factor pricelist, defined, price +20
list5_cache = self.cache_model.search(
[
("product_id", "=", new_product.id),
("pricelist_id", "=", self.list5.id),
]
)
self.assertTrue(list5_cache)
self.assertEqual(list5_cache.price, 62)
@check_duplicates
@freeze_time("2021-04-15")
def test_cache_at_pricelist_create(self):
# create pricelist child of list0, no item, no cache created
pricelist_model = self.env["product.pricelist"]
pricelist = pricelist_model.create(
{
"name": "test1",
"item_ids": [
(
0,
0,
{
"applied_on": "3_global",
"compute_price": "formula",
"base": "pricelist",
"base_pricelist_id": self.list0.id,
},
)
],
}
)
cached_prices = self.cache_model.search([("pricelist_id", "=", pricelist.id)])
self.assertFalse(cached_prices)
# create pricelist child of list0, 1 item, 1 cache create
pricelist = pricelist_model.create(
{
"name": "test2",
"item_ids": [
(
0,
0,
{
"applied_on": "3_global",
"compute_price": "formula",
"base": "pricelist",
"base_pricelist_id": self.list0.id,
},
),
(
0,
0,
{
"applied_on": "0_product_variant",
"base": "list_price",
"product_id": self.p6.id,
"fixed_price": 16.0,
},
),
],
}
)
cached_prices = self.cache_model.search([("pricelist_id", "=", pricelist.id)])
self.assertEqual(len(cached_prices), 1)
self.assertEqual(cached_prices.price, 16.0)
# create factor pricelist +30, compare price with parent pricelist
pricelist = pricelist_model.create(
{
"name": "test3",
"item_ids": [
(
0,
0,
{
"applied_on": "3_global",
"compute_price": "formula",
"base": "pricelist",
"base_pricelist_id": self.list0.id,
"price_surcharge": 30,
},
)
],
}
)
self.assertTrue(pricelist._is_factor_pricelist())
for product in self.products:
l0_cache = self.cache_model.search(
[
("pricelist_id", "=", self.list0.id),
("product_id", "=", product.id),
]
)
cache = self.cache_model.search(
[
("pricelist_id", "=", pricelist.id),
("product_id", "=", product.id),
]
)
self.assertEqual(l0_cache.price + 30, cache.price)
# create root pricelist, prices should be the same than those returned
# by pricelist._compute_price_rule()
pricelist = pricelist_model.create({"name": "test4"})
self.assertTrue(pricelist._is_global_pricelist())
product_prices = pricelist._get_product_prices(self.products.ids)
for product_id, price in product_prices.items():
cache = self.cache_model.search(
[
("product_id", "=", product_id),
("pricelist_id", "=", pricelist.id),
]
)
self.assertEqual(cache.price, price)
@check_duplicates
def test_reset_cache(self):
"""Ensures that the sequence is reset when cache is reset, to avoid reaching
the limit of ids, since the id is an int, with hard limit to 2,147,483,627.
"""
old_max_cache_id = max(self.cache_model.search([]).ids)
self.cache_model.cron_reset_pricelist_cache()
new_max_cache_id = max(self.cache_model.search([]).ids)
self.assertEqual(new_max_cache_id, old_max_cache_id)

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="product_pricelist_view" model="ir.ui.view">
<field name="name">product.pricelist.form.inherit</field>
<field name="model">product.pricelist</field>
<field name="inherit_id" ref="product.product_pricelist_view" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header>
<button
name="button_open_pricelist_cache_tree"
string="Display Pricelist Prices"
class="btn-primary"
type="object"
/>
</header>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="product_pricelist_cache_view_tree" model="ir.ui.view">
<field name="name">product.pricelist.cache.tree</field>
<field name="model">product.pricelist.cache</field>
<field name="arch" type="xml">
<list create="false" edit="false">
<field name="product_id" />
<field name="price" />
</list>
</field>
</record>
<record id="product_pricelist_cache_action" model="ir.actions.act_window">
<field name="name">Pricelist Cache</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.pricelist.cache</field>
<field name="view_mode">list</field>
<field name="view_id" ref="product_pricelist_cache_view_tree" />
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_partner_property_form" model="ir.ui.view">
<field name="name">res.partner.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="product.view_partner_property_form" />
<field name="arch" type="xml">
<xpath expr="//group[@name='sale']" position="inside">
<field name="pricelist_cache_product_filter_id" />
</xpath>
<xpath expr="//div[@name='button_box']" postion="inside">
<button
name="button_open_pricelist_cache_tree"
string="Display Customer Prices"
type="object"
class="oe_stat_button"
icon="fa-dollar"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
from . import pricelist_cache_wizard

View File

@@ -0,0 +1,59 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import api, fields, models
class PricelistCacheWizard(models.TransientModel):
_name = "product.pricelist.cache.wizard"
_description = "Wizard for pricelist cache"
partner_id = fields.Many2one("res.partner")
pricelist_id = fields.Many2one("product.pricelist")
product_id = fields.Many2one("product.product")
display_cache_line_ids = fields.Many2many(
"product.pricelist.cache", string="Cached prices"
)
@api.onchange("partner_id")
def _onchange_partner_id(self):
if self.partner_id:
self.pricelist_id = self.partner_id.property_product_pricelist
else:
self.pricelist_id = False
@api.onchange("pricelist_id", "product_id")
def _onchange_product_pricelist(self):
pricelist = self.pricelist_id
product = self.product_id
partner = self.partner_id
if not pricelist:
self.display_cache_line_ids = False
return
if product:
products = product
else:
products = self.env["product.product"].search([])
if partner and not partner.property_product_pricelist == pricelist:
partner = False
cache_model = self.env["product.pricelist.cache"]
cache_selfs = cache_model.get_cached_prices_for_pricelist(
self.pricelist_id, products
)
# TODO fields are flushed when get_cached_prices_for_pricelist
# is called. I wonder is this is because of the use of flush()
# in the code.
# There's maybe a better way to keep those fields values.
data = {
"display_cache_line_ids": [(6, 0, cache_selfs.ids)],
"pricelist_id": pricelist.id,
"product_id": product.id if product else False,
"partner_id": partner.id if partner else False,
}
self.update(data)
def open_in_tree_view(self):
prices = self.display_cache_line_ids
cache_model = self.env["product.pricelist.cache"]
domain = [("id", "in", prices.ids)]
return cache_model._get_tree_view(domain)

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2021 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="product_pricelist_cache_wizard_view_form" model="ir.ui.view">
<field name="name">product.pricelist.cache.wizard.form</field>
<field name="model">product.pricelist.cache.wizard</field>
<field name="arch" type="xml">
<form string="Pricelist Cache">
<sheet>
<group>
<field name="partner_id" />
<field name="pricelist_id" />
<field name="product_id" />
<field name="display_cache_line_ids" mode="list">
<list limit="80">
<field name="product_id" />
<field name="price" />
</list>
</field>
</group>
</sheet>
<footer>
<button
name="open_in_tree_view"
type="object"
string="Open"
class="btn-primary"
invisible="not display_cache_line_ids"
/>
<button special="cancel" string="Cancel" class="btn-default" />
</footer>
</form>
</field>
</record>
<record id="product_pricelist_cache_wizard_action" model="ir.actions.act_window">
<field name="name">Pricelist Cache</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.pricelist.cache.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="product_pricelist_cache_wizard_view_form" />
<field
name="context"
>{"withControlPanel": False, "no_breadcrumbs": True}</field>
</record>
<menuitem
action="product_pricelist_cache_wizard_action"
id="menuitem_pricelist_cache_wizard"
parent="sale.prod_config_main"
sequence="20"
/>
</odoo>