From 59b9c0dd197636653f8b396a51e61490e4e3b99b Mon Sep 17 00:00:00 2001 From: FrankC013 Date: Thu, 28 Nov 2024 10:09:25 +0100 Subject: [PATCH] [IMP] connector_oxigesti: logic to mark general items as deprecated per customer --- connector_oxigesti/common/tools.py | 112 ++++++++++++++++++ connector_oxigesti/components/__init__.py | 1 + connector_oxigesti/components/adapter.py | 51 +++----- connector_oxigesti/components/deleter.py | 66 +++++++++++ .../data/queue_job_function_data.xml | 24 ++++ connector_oxigesti/models/__init__.py | 1 + .../models/oxigesti_binding/common.py | 42 +++---- .../models/product_pricelist/__init__.py | 1 + .../product_pricelist/product_pricelist.py | 22 ++++ .../models/product_pricelist_item/__init__.py | 2 + .../models/product_pricelist_item/adapter.py | 2 +- .../models/product_pricelist_item/binding.py | 63 +++++++++- .../models/product_pricelist_item/deleter.py | 36 ++++++ .../product_pricelist_item/export_mapper.py | 10 +- .../models/product_pricelist_item/exporter.py | 20 +++- .../models/product_pricelist_item/listener.py | 18 +++ .../models/product_template/binding.py | 11 ++ .../models/product_template/listener.py | 14 +++ .../models/res_partner/__init__.py | 1 + .../models/res_partner/binding.py | 9 ++ .../models/res_partner/listener.py | 25 ++++ .../views/product_pricelist_item_view.xml | 19 +++ 22 files changed, 484 insertions(+), 66 deletions(-) create mode 100644 connector_oxigesti/common/tools.py create mode 100644 connector_oxigesti/components/deleter.py create mode 100644 connector_oxigesti/models/product_pricelist/__init__.py create mode 100644 connector_oxigesti/models/product_pricelist/product_pricelist.py create mode 100644 connector_oxigesti/models/product_pricelist_item/deleter.py create mode 100644 connector_oxigesti/models/product_pricelist_item/listener.py create mode 100644 connector_oxigesti/models/res_partner/listener.py diff --git a/connector_oxigesti/common/tools.py b/connector_oxigesti/common/tools.py new file mode 100644 index 000000000..f06910856 --- /dev/null +++ b/connector_oxigesti/common/tools.py @@ -0,0 +1,112 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Frank Cespedes +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) +import hashlib + +from odoo import _ +from odoo.exceptions import ValidationError +from odoo.osv.expression import normalize_domain + +OP_MAP = { + "&": "AND", + "|": "OR", + "!": "NOT", +} + + +def domain_prefix_to_infix(domain): + stack = [] + i = len(domain) - 1 + while i >= 0: + item = domain[i] + if item in OP_MAP: + if item == "!": + stack.append((item, stack.pop())) + else: + stack.append((stack.pop(), item, stack.pop())) + else: + if not isinstance(item, (tuple, list)): + raise ValidationError(_("Unexpected domain clause %s") % item) + stack.append(item) + i -= 1 + return stack.pop() + + +def domain_infix_to_where(domain): + def _convert_operator(operator, value): + if value is None: + if operator == "=": + operator = "is" + elif operator == "!=": + operator = "is not" + else: + raise ValidationError( + _("Operator '%s' is not implemented on NULL values") % operator + ) + return operator + + def _domain_infix_to_where_raw(domain, values): + if not isinstance(domain, (list, tuple)): + raise ValidationError(_("Invalid domain format %s") % domain) + if len(domain) == 2: + operator, expr = domain + if operator not in OP_MAP: + raise ValidationError( + _("Invalid format, operator not supported %s on domain %s") + % (operator, domain) + ) + values_r, right = _domain_infix_to_where_raw(expr, values) + return values_r, f"{OP_MAP[operator]} ({right})" + elif len(domain) == 3: + expr_l, operator, expr_r = domain + if operator in OP_MAP: + values_l, left = _domain_infix_to_where_raw(expr_l, values) + values_r, right = _domain_infix_to_where_raw(expr_r, values) + return {**values_l, **values_r}, f"({left} {OP_MAP[operator]} {right})" + field, operator, value = domain + # field and values + if field not in values: + values[field] = {"next": 1, "values": {}} + field_n = f"{field}{values[field]['next']}" + if field_n in values[field]["values"]: + raise ValidationError(_("Unexpected!! Field %s already used") % field) + values[field]["values"][field_n] = value + values[field]["next"] += 1 + # operator and nulls values + operator = _convert_operator(operator, value) + return values, f"{field} {operator} %({field_n})s" + else: + raise ValidationError(_("Invalid domain format %s") % domain) + + values, where = _domain_infix_to_where_raw(domain, {}) + values_norm = {} + for _k, v in values.items(): + values_norm.update(v["values"]) + return values_norm, where + + +def domain_to_where(domain): + domain_norm = normalize_domain(domain) + domain_infix = domain_prefix_to_infix(domain_norm) + return domain_infix_to_where(domain_infix) + + +def idhash(external_id): + if not isinstance(external_id, (tuple, list)): + raise ValidationError(_("external id must be list or tuple")) + external_id_hash = hashlib.sha256() + for e in external_id: + if isinstance(e, int): + e9 = str(e) + if int(e9) != e: + raise Exception("Unexpected") + elif isinstance(e, str): + e9 = e + elif e is None: + pass + else: + raise Exception("Unexpected type for a key: type %s" % type(e)) + + external_id_hash.update(e9.encode("utf8")) + + return external_id_hash.hexdigest() diff --git a/connector_oxigesti/components/__init__.py b/connector_oxigesti/components/__init__.py index d1e50e2df..2789d7a61 100644 --- a/connector_oxigesti/components/__init__.py +++ b/connector_oxigesti/components/__init__.py @@ -8,3 +8,4 @@ from . import import_mapper from . import export_mapper from . import listener +from . import deleter diff --git a/connector_oxigesti/components/adapter.py b/connector_oxigesti/components/adapter.py index a597e189a..2efb8d316 100644 --- a/connector_oxigesti/components/adapter.py +++ b/connector_oxigesti/components/adapter.py @@ -19,6 +19,8 @@ from odoo.addons.component.core import AbstractComponent from odoo.addons.connector.exception import NetworkRetryableError +from ..common.tools import domain_to_where + try: import pymssql except ImportError: @@ -153,7 +155,7 @@ def _escape(self, s): def _exec_sql(self, sql, params, as_dict=False, commit=False): # Convert params - params = self._convert_tuple(params, to_backend=True) + params = self._convert_dict(params, to_backend=True) # Execute sql conn = self.conn() cr = conn.cursor(as_dict=as_dict) @@ -179,7 +181,7 @@ def _exec_query(self, filters=None, fields=None, as_dict=True): filters = [] # check if schema exists to avoid injection schema_exists = self._exec_sql( - "select 1 from sys.schemas where name=%s", (self.schema,) + "select 1 from sys.schemas where name=%(schema)s", dict(schema=self.schema) ) if not schema_exists: raise pymssql.InternalError("The schema %s does not exist" % self.schema) @@ -201,26 +203,12 @@ def _exec_query(self, filters=None, fields=None, as_dict=True): sql_l.append("select %s from t" % (", ".join(fields_l),)) if filters: - where = [] - for k, operator, v in filters: - if v is None: - if operator == "=": - operator = "is" - elif operator == "!=": - operator = "is not" - else: - raise Exception( - "Operator '%s' is not implemented on NULL values" - % operator - ) - - where.append("%s %s %%s" % (k, operator)) - values.append(v) - sql_l.append("where %s" % (" and ".join(where),)) + values, where = domain_to_where(filters) + sql_l.append("where %s" % where) sql = " ".join(sql_l) - res = self._exec_sql(sql, tuple(values), as_dict=as_dict) + res = self._exec_sql(sql, values, as_dict=as_dict) filter_keys_s = {e[0] for e in filters} if self._id and set(self._id).issubset(filter_keys_s): @@ -291,7 +279,7 @@ def write(self, _id, values_d): # pylint: disable=W8106 # check if schema exists to avoid injection schema_exists = self._exec_sql( - "select 1 from sys.schemas where name=%s", (self.schema,) + "select 1 from sys.schemas where name=%(schema)s", dict(schema=self.schema) ) if not schema_exists: raise pymssql.InternalError("The schema %s does not exist" % self.schema) @@ -357,22 +345,16 @@ def create(self, values_d): # pylint: disable=W8106 # check if schema exists to avoid injection schema_exists = self._exec_sql( - "select 1 from sys.schemas where name=%s", (self.schema,) + "select 1 from sys.schemas where name=%(schema)s", dict(schema=self.schema) ) if not schema_exists: raise pymssql.InternalError("The schema %s does not exist" % self.schema) # build the sql parts - fields, params, phvalues = [], [], [] - for k, v in values_d.items(): + fields, phvalues = [], [] + for k in values_d.keys(): fields.append(k) - params.append(v) - if v is None or isinstance(v, (str, datetime.date, datetime.datetime)): - phvalues.append("%s") - elif isinstance(v, (int, float)): - phvalues.append("%d") - else: - raise NotImplementedError("Type %s" % type(v)) + phvalues.append(f"%({k})s") # build retvalues retvalues = ["inserted.%s" % x for x in self._id] @@ -386,9 +368,8 @@ def create(self, values_d): # pylint: disable=W8106 ) # executem la insercio - res = None try: - res = self._exec_sql(sql, tuple(params), commit=True) + res = self._exec_sql(sql, values_d, commit=True) except pymssql.IntegrityError as e: # Workaround: Because of Microsoft SQL Server # removes the spaces on varchars on comparisions @@ -431,12 +412,16 @@ def delete(self, _id): # check if schema exists to avoid injection schema_exists = self._exec_sql( - "select 1 from sys.schemas where name=%s", (self.schema,) + "select 1 from sys.schemas where name=%(schema)s", dict(schema=self.schema) ) if not schema_exists: raise pymssql.InternalError("The schema %s does not exist" % self.schema) # prepare the sql with base strucrture + if not hasattr(self, "_sql_delete"): + raise ValidationError( + _("The model %s does not have a delete SQL defined") % (self._name,) + ) sql = self._sql_delete % dict(schema=self.schema) # get id fieldnames and values diff --git a/connector_oxigesti/components/deleter.py b/connector_oxigesti/components/deleter.py new file mode 100644 index 000000000..f4f698be4 --- /dev/null +++ b/connector_oxigesti/components/deleter.py @@ -0,0 +1,66 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Frank Cespedes +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + + +class OxigestiExportDeleter(AbstractComponent): + """Base Export Deleter for Oxigesti""" + + _name = "oxigesti.export.deleter" + _inherit = ["base.deleter", "base.oxigesti.connector"] + + _usage = "record.export.deleter" + + def run(self, external_id): + adapter = self.component(usage="backend.adapter") + adapter.delete(external_id) + + +class OxigestiBatchExportDeleter(AbstractComponent): + _name = "oxigesti.batch.export.deleter" + _inherit = ["base.exporter", "base.oxigesti.connector"] + + def run(self, bindings=None): + if not bindings: + return + # Run the synchronization + external_ids = [] + for binding in bindings: + with binding.backend_id.work_on(binding._name) as work: + binder = work.component(usage="binder") + external_id = binder.to_external(binding) + adapter = work.component(usage="backend.adapter") + if adapter.read(external_id): + external_ids.append(external_id) + for ext_id in external_ids: + self._export_delete_record(ext_id) + + def _export_delete_record(self, external_id): + raise NotImplementedError + + +class OxigestiDirectBatchExportDeleter(AbstractComponent): + _name = "oxigesti.direct.batch.export.deleter" + _inherit = "oxigesti.batch.export.deleter" + + _usage = "direct.batch.export.deleter" + + def _export_delete_record(self, external_id): + self.model.export_delete_record(self.backend_record, external_id) + + +class OxigestiDelayedBatchExportDeleter(AbstractComponent): + _name = "oxigesti.delayed.batch.export.deleter" + _inherit = "oxigesti.batch.export.deleter" + + _usage = "delayed.batch.export.deleter" + + def _export_delete_record(self, external_id, job_options=None): + delayable = self.model.with_delay(**job_options or {}) + delayable.export_delete_record(self.backend_record, external_id) diff --git a/connector_oxigesti/data/queue_job_function_data.xml b/connector_oxigesti/data/queue_job_function_data.xml index 01a51c63b..49ef310ad 100644 --- a/connector_oxigesti/data/queue_job_function_data.xml +++ b/connector_oxigesti/data/queue_job_function_data.xml @@ -209,4 +209,28 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)--> + + + export_delete_record + + + + + + export_delete_batch + + + diff --git a/connector_oxigesti/models/__init__.py b/connector_oxigesti/models/__init__.py index 85685fccb..879415304 100644 --- a/connector_oxigesti/models/__init__.py +++ b/connector_oxigesti/models/__init__.py @@ -5,6 +5,7 @@ from . import product_product from . import product_category from . import product_buyerinfo +from . import product_pricelist from . import product_pricelist_item from . import stock_production_lot from . import sale_order_line diff --git a/connector_oxigesti/models/oxigesti_binding/common.py b/connector_oxigesti/models/oxigesti_binding/common.py index 6e614cdad..275248c4a 100644 --- a/connector_oxigesti/models/oxigesti_binding/common.py +++ b/connector_oxigesti/models/oxigesti_binding/common.py @@ -2,32 +2,12 @@ # Copyright NuoBiT Solutions - Kilian Niubo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -import hashlib import json from odoo import _, api, fields, models from odoo.exceptions import ValidationError - -def idhash(external_id): - if not isinstance(external_id, (tuple, list)): - raise ValidationError(_("external id must be list or tuple")) - external_id_hash = hashlib.sha256() - for e in external_id: - if isinstance(e, int): - e9 = str(e) - if int(e9) != e: - raise Exception("Unexpected") - elif isinstance(e, str): - e9 = e - elif e is None: - pass - else: - raise Exception("Unexpected type for a key: type %s" % type(e)) - - external_id_hash.update(e9.encode("utf8")) - - return external_id_hash.hexdigest() +from ...common.tools import idhash class OxigestiBinding(models.AbstractModel): @@ -213,3 +193,23 @@ def export_record(self, backend, relation): with backend.work_on(self._name) as work: exporter = work.component(usage="record.exporter") return exporter.run(relation) + + @api.model + def export_delete_record(self, backend, external_id): + """Deleter Oxigesti record""" + if not external_id: + raise ValidationError(_("The external_id of the binding is null")) + binding_name = self._name + with backend.work_on(binding_name) as work: + deleter = work.component(usage="record.export.deleter") + deleter.run(external_id) + + @api.model + def export_delete_batch(self, backend, bindings=None): + """Prepare the batch export of records modified on Odoo""" + if not bindings: + return + # Prepare the batch export of records modified on Odoo + with backend.work_on(self._name) as work: + exporter = work.component(usage="delayed.batch.export.deleter") + return exporter.run(bindings=bindings) diff --git a/connector_oxigesti/models/product_pricelist/__init__.py b/connector_oxigesti/models/product_pricelist/__init__.py new file mode 100644 index 000000000..9880677a7 --- /dev/null +++ b/connector_oxigesti/models/product_pricelist/__init__.py @@ -0,0 +1 @@ +from . import product_pricelist diff --git a/connector_oxigesti/models/product_pricelist/product_pricelist.py b/connector_oxigesti/models/product_pricelist/product_pricelist.py new file mode 100644 index 000000000..d19a5024a --- /dev/null +++ b/connector_oxigesti/models/product_pricelist/product_pricelist.py @@ -0,0 +1,22 @@ +# Copyright NuoBiT Solutions - Frank Cespedes +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, models +from odoo.exceptions import ValidationError + + +class Pricelist(models.Model): + _inherit = "product.pricelist" + + def unlink(self): + for rec in self: + if rec.item_ids.oxigesti_bind_ids: + raise ValidationError( + _( + "You can't delete the pricelist %s that has been exported " + "the product prices by customer to Oxigesti. If you want to" + "delete it, you must first delete the items of the pricelist." + ) + % rec.name + ) + return super().unlink() diff --git a/connector_oxigesti/models/product_pricelist_item/__init__.py b/connector_oxigesti/models/product_pricelist_item/__init__.py index 43d4796fa..e7119be37 100644 --- a/connector_oxigesti/models/product_pricelist_item/__init__.py +++ b/connector_oxigesti/models/product_pricelist_item/__init__.py @@ -3,3 +3,5 @@ from . import export_mapper from . import binder from . import binding +from . import listener +from . import deleter diff --git a/connector_oxigesti/models/product_pricelist_item/adapter.py b/connector_oxigesti/models/product_pricelist_item/adapter.py index fb6c7293e..0eb795fea 100644 --- a/connector_oxigesti/models/product_pricelist_item/adapter.py +++ b/connector_oxigesti/models/product_pricelist_item/adapter.py @@ -11,7 +11,7 @@ class ProductPricelistItemAdapter(Component): _apply_on = "oxigesti.product.pricelist.item" - _sql = """select b.CodigoArticulo, b.Codigo_Mutua, b.Importe + _sql = """select b.CodigoArticulo, b.Codigo_Mutua, b.Importe, b.Deprecated from %(schema)s.Odoo_Articulos_Generales_x_Cliente b """ diff --git a/connector_oxigesti/models/product_pricelist_item/binding.py b/connector_oxigesti/models/product_pricelist_item/binding.py index e11179a6e..9396f3c53 100644 --- a/connector_oxigesti/models/product_pricelist_item/binding.py +++ b/connector_oxigesti/models/product_pricelist_item/binding.py @@ -2,7 +2,8 @@ # Copyright NuoBiT Solutions - Kilian Niubo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class ProductPricelistItem(models.Model): @@ -14,6 +15,44 @@ class ProductPricelistItem(models.Model): string="Oxigesti Bindings", ) + @api.constrains("compute_price", "applied_on") + def _check_binding(self): + for rec in self: + if rec.oxigesti_bind_ids: + if rec.compute_price != "fixed": + raise ValidationError( + _( + "You can't change the price calculation method of a " + "pricelist item because they have been exported the " + "product prices by customer to Oxigesti.\nIf you need to " + "change the price calculation method, you can delete the " + "pricelist item and create a new one." + ) + ) + if rec.applied_on != "1_product": + raise ValidationError( + _( + "You can't change the applied on field of a pricelist " + "item because they have been exported the product prices " + "by customer to Oxigesti.\nIf you need to change the " + "applied on field, you can delete the pricelist item and " + "create a new one." + ) + ) + + @api.constrains("product_tmpl_id") + def _check_product_tmpl_id(self): + for rec in self: + if rec.oxigesti_bind_ids: + raise ValidationError( + _( + "You can't change the product of a pricelist item " + "because they have been exported the product prices by " + "customer to Oxigesti.\nIf you need to change the product, " + "you can delete the pricelist item and create a new one." + ) + ) + class ProductPricelistItemBinding(models.Model): _name = "oxigesti.product.pricelist.item" @@ -27,10 +66,21 @@ class ProductPricelistItemBinding(models.Model): required=True, ondelete="cascade", ) - odoo_partner_id = fields.Many2one( comodel_name="res.partner", string="Partner", required=True, ondelete="cascade" ) + deprecated = fields.Boolean(default=False) + odoo_fixed_price = fields.Float( + compute="_compute_odoo_fixed_price", + store=True, + ) + + @api.depends("odoo_id.fixed_price", "deprecated", "external_id") + def _compute_odoo_fixed_price(self): + for rec in self: + if not rec.deprecated or not rec.external_id: + rec.odoo_fixed_price = rec.odoo_id.fixed_price + _sql_constraints = [ ( "oxigesti_external_uniq", @@ -51,6 +101,15 @@ def export_data(self, backend, since_date): domain += [("write_date", ">", since_date)] self.with_delay().export_batch(backend, domain=domain) + def is_deprecated(self): + self.ensure_one() + return ( + self.odoo_partner_id.property_product_pricelist != self.odoo_id.pricelist_id + or not self.odoo_id.active + or not self.odoo_id.product_tmpl_id.active + or not self.odoo_partner_id.active + ) + def resync(self): for record in self: func = record.export_record diff --git a/connector_oxigesti/models/product_pricelist_item/deleter.py b/connector_oxigesti/models/product_pricelist_item/deleter.py new file mode 100644 index 000000000..f311a4727 --- /dev/null +++ b/connector_oxigesti/models/product_pricelist_item/deleter.py @@ -0,0 +1,36 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Frank Cespedes +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class OxigestiExportDeleter(Component): + """Base Export Deleter for Oxigesti""" + + _name = "oxigesti.product.pricelist.item.export.deleter" + _inherit = "oxigesti.export.deleter" + + _apply_on = "oxigesti.product.pricelist.item" + + def run(self, external_id): + adapter = self.component(usage="backend.adapter") + adapter.write(external_id, {"deprecated": True}) + + +class OxigestiDirectBatchExportDeleter(Component): + _name = "oxigesti.product.pricelist.item.direct.batch.export.deleter" + _inherit = "oxigesti.direct.batch.export.deleter" + + _apply_on = "oxigesti.product.pricelist.item" + + +class OxigestiDelayedBatchExportDeleter(Component): + _name = "oxigesti.product.pricelist.item.delayed.batch.export.deleter" + _inherit = "oxigesti.delayed.batch.export.deleter" + + _apply_on = "oxigesti.product.pricelist.item" diff --git a/connector_oxigesti/models/product_pricelist_item/export_mapper.py b/connector_oxigesti/models/product_pricelist_item/export_mapper.py index dc324b212..85ff9b790 100644 --- a/connector_oxigesti/models/product_pricelist_item/export_mapper.py +++ b/connector_oxigesti/models/product_pricelist_item/export_mapper.py @@ -12,10 +12,6 @@ class ProductPricelistItemExportMapper(Component): _apply_on = "oxigesti.product.pricelist.item" - direct = [ - ("fixed_price", "Importe"), - ] - @only_create @mapping def CodigoArticulo(self, record): @@ -61,6 +57,10 @@ def Codigo_Mutua(self, record): return {"Codigo_Mutua": external_id[0]} + @mapping + def Importe(self, record): + return {"Importe": record.odoo_fixed_price} + @mapping def Deprecated(self, record): - return {"Deprecated": 0} + return {"Deprecated": record.deprecated and 1 or 0} diff --git a/connector_oxigesti/models/product_pricelist_item/exporter.py b/connector_oxigesti/models/product_pricelist_item/exporter.py index 40a1e8770..03e2eb3dc 100644 --- a/connector_oxigesti/models/product_pricelist_item/exporter.py +++ b/connector_oxigesti/models/product_pricelist_item/exporter.py @@ -43,7 +43,7 @@ def run(self, domain=None): lambda x: ( not since_date or x.write_date > since_date - or p.write_date > since_date + or p.oxigesti_pricelist_write_date > since_date ) and p.customer_rank >= p.supplier_rank and x.applied_on == "1_product" @@ -68,11 +68,23 @@ def run(self, domain=None): binding.odoo_id = pl binding = binder.wrap_binding( pl, - binding_extra_vals={ - "odoo_partner_id": p.id, - }, + binding_extra_vals={"odoo_partner_id": p.id}, ) + binding.deprecated = binding.is_deprecated() self._export_record(binding) + if p.oxigesti_pricelist_write_date > since_date: + oxigesti_pricelist = ( + self.env["oxigesti.product.pricelist.item"] + .search( + [ + ("odoo_partner_id", "=", p.id), + ] + ) + .filtered(lambda x: x.is_deprecated() and not x.deprecated) + ) + for pl in oxigesti_pricelist: + pl.deprecated = True + self._export_record(pl) class ProductPricelistItemExporter(Component): diff --git a/connector_oxigesti/models/product_pricelist_item/listener.py b/connector_oxigesti/models/product_pricelist_item/listener.py new file mode 100644 index 000000000..b9b8c0ed4 --- /dev/null +++ b/connector_oxigesti/models/product_pricelist_item/listener.py @@ -0,0 +1,18 @@ +# Copyright NuoBiT Solutions - Frank Cespedes +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.addons.component.core import Component + + +class ProductPricelistItemListener(Component): + _name = "oxigesti.product.pricelist.item.listener" + _inherit = "oxigesti.event.listener" + + _apply_on = "product.pricelist.item" + + def on_record_unlink(self, relation): + bindings = relation.sudo().oxigesti_bind_ids + bindings_by_backend = {} + for binding in bindings: + bindings_by_backend.setdefault(binding.backend_id, []).append(binding) + for backend, backend_bindings in bindings_by_backend.items(): + self.with_delay().export_delete_batch(backend, bindings=backend_bindings) diff --git a/connector_oxigesti/models/product_template/binding.py b/connector_oxigesti/models/product_template/binding.py index 8be8c4fb2..8d8bd2380 100644 --- a/connector_oxigesti/models/product_template/binding.py +++ b/connector_oxigesti/models/product_template/binding.py @@ -33,6 +33,17 @@ def write(self, vals): "template that has variants binded to oxigesti" ) ) + if "active" in vals: + for rec in self: + if rec.oxigesti_product_variant_bind_ids: + pricelist_items = self.env["product.pricelist.item"].search( + [ + ("product_tmpl_id", "=", rec.id), + ("oxigesti_bind_ids", "!=", False), + ] + ) + if pricelist_items: + pricelist_items.write({"write_date": fields.Datetime.now()}) return super(ProductTemplate, self).write(vals) def unlink(self): diff --git a/connector_oxigesti/models/product_template/listener.py b/connector_oxigesti/models/product_template/listener.py index 6e161534c..e67e31290 100644 --- a/connector_oxigesti/models/product_template/listener.py +++ b/connector_oxigesti/models/product_template/listener.py @@ -9,3 +9,17 @@ class ProductTemplateListener(Component): _inherit = "oxigesti.event.listener" _apply_on = "product.template" + + def on_record_unlink(self, relation): + bindings = ( + relation.sudo() + .with_context(active_test=False) + .product_variant_ids.oxigesti_bind_ids + ) + bindings_by_backend = {} + for binding in bindings: + bindings_by_backend.setdefault(binding.backend_id, []).append(binding) + for backend, backend_bindings in bindings_by_backend.items(): + self.env[ + "oxigesti.product.pricelist.item" + ].with_delay().export_delete_batch(backend, bindings=backend_bindings) diff --git a/connector_oxigesti/models/res_partner/__init__.py b/connector_oxigesti/models/res_partner/__init__.py index 3e96d846c..87d8b1370 100644 --- a/connector_oxigesti/models/res_partner/__init__.py +++ b/connector_oxigesti/models/res_partner/__init__.py @@ -5,3 +5,4 @@ from . import import_mapper from . import binder from . import binding +from . import listener diff --git a/connector_oxigesti/models/res_partner/binding.py b/connector_oxigesti/models/res_partner/binding.py index 6df54bc3c..e38d978ab 100644 --- a/connector_oxigesti/models/res_partner/binding.py +++ b/connector_oxigesti/models/res_partner/binding.py @@ -13,6 +13,15 @@ class ResPartner(models.Model): inverse_name="odoo_id", string="Oxigesti Bindings", ) + oxigesti_pricelist_write_date = fields.Datetime( + default=fields.Datetime.now, + required=True, + ) + + def write(self, vals): + if "property_product_pricelist" in vals: + vals["oxigesti_pricelist_write_date"] = fields.Datetime.now() + return super().write(vals) class ResPartnerBinding(models.Model): diff --git a/connector_oxigesti/models/res_partner/listener.py b/connector_oxigesti/models/res_partner/listener.py new file mode 100644 index 000000000..8c700cb39 --- /dev/null +++ b/connector_oxigesti/models/res_partner/listener.py @@ -0,0 +1,25 @@ +# Copyright NuoBiT Solutions - Eric Antones +# Copyright NuoBiT Solutions - Frank Cespedes +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.addons.component.core import Component + + +class ProductPricelistItemListener(Component): + _name = "oxigesti.res.partner.listener" + _inherit = "oxigesti.event.listener" + + _apply_on = "res.partner" + + def on_record_unlink(self, relation): + bindings = ( + relation.sudo() + .with_context(active_test=False) + .property_product_pricelist.item_ids.oxigesti_bind_ids.filtered( + lambda x: x.odoo_partner_id == relation + ) + ) + bindings_by_backend = {} + for binding in bindings: + bindings_by_backend.setdefault(binding.backend_id, []).append(binding) + for backend, backend_bindings in bindings_by_backend.items(): + self.with_delay().export_delete_batch(backend, bindings=backend_bindings) diff --git a/connector_oxigesti/views/product_pricelist_item_view.xml b/connector_oxigesti/views/product_pricelist_item_view.xml index fc3fa56f4..5a590a613 100644 --- a/connector_oxigesti/views/product_pricelist_item_view.xml +++ b/connector_oxigesti/views/product_pricelist_item_view.xml @@ -2,6 +2,23 @@ + + product.pricelist.item.oxigesti.connector.form + product.pricelist.item + + + + + + + + + + + + + + oxigesti.product.pricelist.item.form oxigesti.product.pricelist.item @@ -13,6 +30,7 @@ + @@ -28,6 +46,7 @@ +