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..28991bc49 --- /dev/null +++ b/connector_oxigesti/components/deleter.py @@ -0,0 +1,59 @@ +# 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, domain=None): + if not domain: + domain = [] + # Run the synchronization + record_ids = self.backend_adapter.search(domain) # canviar per external:ids + for record_id in record_ids: + self._export_delete_record(record_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, relation): + self.model.export_delete_record(self.backend_record, relation) + + +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/components/exporter.py b/connector_oxigesti/components/exporter.py index 026ab8ef6..8ef56ff95 100644 --- a/connector_oxigesti/components/exporter.py +++ b/connector_oxigesti/components/exporter.py @@ -88,7 +88,10 @@ def _run(self, fields=None): # link if already exists on backend if not self.external_id: - computed_external_id = self.binder._get_external_id(self.binding) + extra_vals = {x: self.binding[x] for x in self.binder._odoo_extra_fields} + computed_external_id = self.binder._get_external_id( + self.binding.odoo_id, extra_vals=extra_vals + ) if computed_external_id: external_record = self.backend_adapter.read(computed_external_id) if external_record: diff --git a/connector_oxigesti/components_custom/binder.py b/connector_oxigesti/components_custom/binder.py index face852b6..2aa0e785b 100644 --- a/connector_oxigesti/components_custom/binder.py +++ b/connector_oxigesti/components_custom/binder.py @@ -251,10 +251,13 @@ def bind(self, external_id, binding): } ) - def _get_external_id(self, binding): + def _get_external_id(self, relation, extra_vals=None): return None def to_json(self, value): return { self._external_field: value, } + + def _get_internal_record_domain(self, values): + return [(k, "=", v) for k, v in values.items()] 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 577636a98..d6fe1628f 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..ff37a9f59 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): @@ -35,7 +15,7 @@ class OxigestiBinding(models.AbstractModel): _inherit = "external.binding" _description = "oxigesti Binding (abstract)" - active = fields.Boolean(default=True) + # active = fields.Boolean(default=True) backend_id = fields.Many2one( comodel_name="oxigesti.backend", @@ -94,7 +74,10 @@ def _compute_external_id_hash(self): if other: with other.backend_id.work_on(other._name) as work: binder = work.component(usage="binder") - other_computed_external_id = binder._get_external_id(other) + extra_vals = {x: other[x] for x in binder._odoo_extra_fields} + other_computed_external_id = binder._get_external_id( + other, extra_vals=extra_vals + ) other_computed_external_id_hash = ( other_computed_external_id and idhash(other_computed_external_id) @@ -151,6 +134,29 @@ def _compute_external_id_hash(self): ), ] + def get_external_ids_domain_by_backend(self): + ext_ids_by_backend = {} + for rec in self: + with rec.backend_id.work_on(rec._name) as work: + binder = work.component(usage="binder") + external_id = binder.to_external(rec.id) + if external_id: + adapter = work.component(usage="backend.adapter") + external_dict = adapter.id2dict(external_id) + ext_ids_by_backend.setdefault(rec.backend_id, []).append( + external_dict + ) + res = {} + for backend, external_dicts in ext_ids_by_backend.items(): + domain = ["|"] * (len(external_dicts) - 1) + for external_dict in external_dicts: + ext_domain = ["&"] * (len(external_dict) - 1) + for k, v in external_dict.items(): + ext_domain.append((k, "=", v)) + domain += ext_domain + res[backend] = domain + return res + @api.model def import_data(self, backend, since_date): """Default method, it should be overridden by subclasses""" @@ -213,3 +219,33 @@ 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 import_delete_record(self, backend, relation): + """Deleter Odoo record""" + raise NotImplementedError + + @api.model + def export_delete_batch(self, backend, domain=None): + """Prepare the batch export of records modified on Odoo""" + if not domain: + domain = [] + # 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(domain=domain) + + @api.model + def import_delete_batch(self, backend, domain=None): + """Prepare the batch import of records modified on Oxigesti""" + raise NotImplementedError diff --git a/connector_oxigesti/models/product_buyerinfo/binder.py b/connector_oxigesti/models/product_buyerinfo/binder.py index e3cd1fb83..d9b2b6af5 100644 --- a/connector_oxigesti/models/product_buyerinfo/binder.py +++ b/connector_oxigesti/models/product_buyerinfo/binder.py @@ -10,21 +10,16 @@ class ProductBuyerinfoBinder(Component): _apply_on = "oxigesti.product.buyerinfo" - def _get_external_id(self, binding): - if not self._is_binding(binding): - raise Exception("The source object %s must be a binding" % binding._name) - + def _get_external_id(self, relation, extra_vals=None): partner_adapter = self.component( usage="backend.adapter", model_name="oxigesti.res.partner" ) partner_binder = self.binder_for("oxigesti.res.partner") - partner_external_id = partner_binder.to_external( - binding.odoo_id.partner_id, wrap=True - ) + partner_external_id = partner_binder.to_external(relation.partner_id, wrap=True) external_id = None if partner_external_id: - codigo_articulo = binding.odoo_id.product_id.default_code + codigo_articulo = relation.product_id.default_code codigo_mutua = partner_adapter.id2dict(partner_external_id)["Codigo_Mutua"] if codigo_articulo and codigo_mutua: external_id = [codigo_articulo, codigo_mutua] diff --git a/connector_oxigesti/models/product_category/__init__.py b/connector_oxigesti/models/product_category/__init__.py index 98026c285..1d62de855 100644 --- a/connector_oxigesti/models/product_category/__init__.py +++ b/connector_oxigesti/models/product_category/__init__.py @@ -5,5 +5,4 @@ from . import export_mapper from . import binder from . import binding -from . import listener from . import product_category diff --git a/connector_oxigesti/models/product_category/adapter.py b/connector_oxigesti/models/product_category/adapter.py index 0bff1cfd1..e50b404ef 100644 --- a/connector_oxigesti/models/product_category/adapter.py +++ b/connector_oxigesti/models/product_category/adapter.py @@ -28,8 +28,4 @@ class ProductCategoryAdapter(Component): values (%(phvalues)s) """ - _sql_delete = """delete from %(schema)s.Odoo_Articulos_Categorias - where Id = %%(Id)s - """ - _id = ("Id",) diff --git a/connector_oxigesti/models/product_category/binder.py b/connector_oxigesti/models/product_category/binder.py index 356badd05..2363fb61f 100644 --- a/connector_oxigesti/models/product_category/binder.py +++ b/connector_oxigesti/models/product_category/binder.py @@ -13,22 +13,16 @@ class ProductCategoryBinder(Component): _apply_on = "oxigesti.product.category" - def _get_external_id(self, binding): - if not self._is_binding(binding): - raise Exception("The source object %s must be a binding" % binding._name) - + def _get_external_id(self, relation, extra_vals=None): adapter = self.component( usage="backend.adapter", model_name="oxigesti.product.category" ) - external_ids = adapter.search([("IdCategoriaOdoo", "=", binding.odoo_id.id)]) + external_ids = adapter.search([("IdCategoriaOdoo", "=", relation.id)]) if not external_ids: return None if len(external_ids) > 1: raise ValidationError( - _( - "More than one Category with ID '%i' on the backend" - % (binding.odoo_id.id,) - ) + _("More than one Category with ID '%i' on the backend" % (relation.id,)) ) return external_ids[0] diff --git a/connector_oxigesti/models/product_category/listener.py b/connector_oxigesti/models/product_category/listener.py deleted file mode 100644 index 6625b2166..000000000 --- a/connector_oxigesti/models/product_category/listener.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright NuoBiT Solutions - Eric Antones -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo.addons.component.core import Component - - -class ProductCategoryListener(Component): - _name = "oxigesti.product.category.listener" - _inherit = "oxigesti.event.listener" - - _apply_on = "product.category" diff --git a/connector_oxigesti/models/product_category/product_category.py b/connector_oxigesti/models/product_category/product_category.py index 1f022c6d3..bbfa8dcec 100644 --- a/connector_oxigesti/models/product_category/product_category.py +++ b/connector_oxigesti/models/product_category/product_category.py @@ -14,15 +14,3 @@ class ProductCategory(models.Model): inverse_name="odoo_id", string="Oxigesti Bindings", ) - - def unlink(self): - to_remove = {} - for record in self: - to_remove[record.id] = [ - (binding.backend_id.id, binding._name, binding.external_id) - for binding in record.oxigesti_bind_ids - ] - result = super(ProductCategory, self).unlink() - for bindings_data in to_remove.values(): - self._event("on_record_post_unlink").notify(bindings_data) - return result diff --git a/connector_oxigesti/models/product_pricelist/__init__.py b/connector_oxigesti/models/product_pricelist/__init__.py new file mode 100644 index 000000000..9430369c2 --- /dev/null +++ b/connector_oxigesti/models/product_pricelist/__init__.py @@ -0,0 +1 @@ +from . import listener diff --git a/connector_oxigesti/models/product_pricelist/listener.py b/connector_oxigesti/models/product_pricelist/listener.py new file mode 100644 index 000000000..b5a478a5f --- /dev/null +++ b/connector_oxigesti/models/product_pricelist/listener.py @@ -0,0 +1,18 @@ +# 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.product.pricelist.listener" + _inherit = "oxigesti.event.listener" + + _apply_on = "product.pricelist" + + def on_record_unlink(self, relation): + bindings = ( + relation.sudo().with_context(active_test=False).item_ids.oxigesti_bind_ids + ) + for backend, domain in bindings.get_external_ids_domain_by_backend().items(): + bindings.with_delay().export_delete_batch(backend, domain=domain) diff --git a/connector_oxigesti/models/product_pricelist_item/__init__.py b/connector_oxigesti/models/product_pricelist_item/__init__.py index 43d4796fa..fd680d3b4 100644 --- a/connector_oxigesti/models/product_pricelist_item/__init__.py +++ b/connector_oxigesti/models/product_pricelist_item/__init__.py @@ -1,5 +1,9 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + from . import exporter from . import adapter 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..fdaae7b5b 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 """ @@ -28,4 +28,9 @@ class ProductPricelistItemAdapter(Component): values (%(phvalues)s) """ + _sql_delete = """delete from %(schema)s.Odoo_Articulos_Generales_x_Cliente + where CodigoArticulo = %%(CodigoArticulo)s + and Codigo_Mutua = %%(Codigo_Mutua)s + """ + _id = ("CodigoArticulo", "Codigo_Mutua") diff --git a/connector_oxigesti/models/product_pricelist_item/binder.py b/connector_oxigesti/models/product_pricelist_item/binder.py index 2991e8bc8..c59e53923 100644 --- a/connector_oxigesti/models/product_pricelist_item/binder.py +++ b/connector_oxigesti/models/product_pricelist_item/binder.py @@ -12,21 +12,18 @@ class ProductPricelistItemBinder(Component): _odoo_extra_fields = ["odoo_partner_id"] - def _get_external_id(self, binding): - if not self._is_binding(binding): - raise Exception("The source object %s must be a binding" % binding._name) - + def _get_external_id(self, relation, extra_vals=None): partner_adapter = self.component( usage="backend.adapter", model_name="oxigesti.res.partner" ) partner_binder = self.binder_for("oxigesti.res.partner") partner_external_id = partner_binder.to_external( - binding.odoo_partner_id, wrap=True + extra_vals["odoo_partner_id"], wrap=True, wrapped_model="res.partner" ) external_id = None if partner_external_id: - codigo_articulo = binding.odoo_id.product_tmpl_id.default_code + codigo_articulo = relation.product_tmpl_id.default_code codigo_mutua = partner_adapter.id2dict(partner_external_id)["Codigo_Mutua"] if codigo_articulo and codigo_mutua: external_id = [codigo_articulo, codigo_mutua] diff --git a/connector_oxigesti/models/product_pricelist_item/binding.py b/connector_oxigesti/models/product_pricelist_item/binding.py index e11179a6e..86b45f752 100644 --- a/connector_oxigesti/models/product_pricelist_item/binding.py +++ b/connector_oxigesti/models/product_pricelist_item/binding.py @@ -1,19 +1,110 @@ # Copyright NuoBiT Solutions - Eric Antones # Copyright NuoBiT Solutions - Kilian Niubo +# Copyright NuoBiT Solutions - Frank Cespedes # 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): _inherit = "product.pricelist.item" + partner_ids = fields.Many2many( + comodel_name="res.partner", + compute="_compute_partner_ids", + ) + + def _compute_partner_ids(self): + for rec in self: + rec.partner_ids = ( + self.env["res.partner"] + .search( + [ + ("company_id", "", rec.company_id.id), + ] + ) + .filtered(lambda x: x.property_product_pricelist == rec.pricelist_id) + ) + + oxigesti_write_date = fields.Datetime( + compute="_compute_oxigesti_write_date", + store=True, + required=True, + ) + + @api.depends( + "partner_ids.property_product_pricelist", + "oxigesti_bind_ids", + "oxigesti_bind_ids.oxigesti_write_date", + "compute_price", + "applied_on", + "fixed_price", + ) + def _compute_oxigesti_write_date(self): + for rec in self: + rec.oxigesti_write_date = ( + max( + rec.mapped("write_date") + + rec.oxigesti_bind_ids.mapped("oxigesti_write_date") + + rec.oxigesti_bind_ids.odoo_partner_id.mapped("write_date") + ) + or False + ) + oxigesti_bind_ids = fields.One2many( comodel_name="oxigesti.product.pricelist.item", inverse_name="odoo_id", 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." + ) + ) + + def is_deprecated(self, partner): + self.ensure_one() + return ( + partner.property_product_pricelist != self.pricelist_id + or not self.active + or not self.product_tmpl_id.active + or not partner.active + ) + class ProductPricelistItemBinding(models.Model): _name = "oxigesti.product.pricelist.item" @@ -21,16 +112,78 @@ class ProductPricelistItemBinding(models.Model): _inherits = {"product.pricelist.item": "odoo_id"} _description = "Product pricelist item binding" + oxigesti_write_date = fields.Datetime( + compute="_compute_oxigesti_write_date", + store=True, + required=True, + default=fields.Datetime.now, + ) + + @api.depends("deprecated") + def _compute_oxigesti_write_date(self): + for rec in self: + rec.oxigesti_write_date = rec.write_date + odoo_id = fields.Many2one( comodel_name="product.pricelist.item", string="Product pricelist item", + compute="_compute_odoo_id", + store=True, + readonly=False, required=True, ondelete="cascade", ) + @api.depends("odoo_partner_id.property_product_pricelist") + def _compute_odoo_id(self): + for rec in self: + record = rec.odoo_partner_id.property_product_pricelist.item_ids.filtered( + lambda r: r.product_tmpl_id == rec.product_tmpl_id + ) + if len(record) > 1: + raise ValidationError( + _( + "There are more than one pricelist item with the same " + "product and the same pricelist for the customer %s" + ) + % rec.odoo_partner_id.name + ) + if record: + rec.odoo_id = record + odoo_partner_id = fields.Many2one( comodel_name="res.partner", string="Partner", required=True, ondelete="cascade" ) + + odoo_fixed_price = fields.Float( + compute="_compute_odoo_fixed_price", + store=True, + required=True, + ) + + @api.depends("odoo_id.fixed_price", "deprecated") + def _compute_odoo_fixed_price(self): + for rec in self: + if not rec.deprecated: + rec.odoo_fixed_price = rec.odoo_id.fixed_price + + deprecated = fields.Boolean( + required=True, + compute="_compute_deprecated", + store=True, + ) + + @api.depends( + "odoo_id", + "odoo_id.active", + "product_tmpl_id.active", + "odoo_partner_id.property_product_pricelist", + "odoo_partner_id.active", + ) + def _compute_deprecated(self): + for rec in self: + rec.deprecated = rec.odoo_id.is_deprecated(rec.odoo_partner_id) + _sql_constraints = [ ( "oxigesti_external_uniq", @@ -46,9 +199,13 @@ class ProductPricelistItemBinding(models.Model): @api.model def export_data(self, backend, since_date): - domain = [("company_id", "in", (backend.company_id.id, False))] + domain = [ + ("company_id", "in", (backend.company_id.id, False)), + ("compute_price", "=", "fixed"), + ("applied_on", "=", "1_product"), + ] if since_date: - domain += [("write_date", ">", since_date)] + domain += [("oxigesti_write_date", ">", since_date)] self.with_delay().export_batch(backend, domain=domain) def resync(self): 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..b6768456b --- /dev/null +++ b/connector_oxigesti/models/product_pricelist_item/deleter.py @@ -0,0 +1,29 @@ +# 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 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..0012df3de 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,11 @@ def Codigo_Mutua(self, record): return {"Codigo_Mutua": external_id[0]} + @mapping + def Importe(self, record): + if not record.deprecated: + 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..78a350186 100644 --- a/connector_oxigesti/models/product_pricelist_item/exporter.py +++ b/connector_oxigesti/models/product_pricelist_item/exporter.py @@ -5,8 +5,79 @@ from odoo.addons.component.core import Component +# class ProductPricelistItemBatchExporter(AbstractComponent): +# """Export the Oxigesti Product prices. +# +# For every product in the list, a delayed job is created. +# """ +# +# _name = "oxigesti.product.pricelist.item.batch.exporter" +# +# def run_common(self, domain=None): +# if not domain: +# domain = [] +# # Run the batch synchronization +# parent_domain = domain + [ +# ("is_company", "=", True), +# ("customer_rank", ">=", 0), +# ] +# since_date = None +# domain = [] +# for e in parent_domain: +# field, operator, value = e +# if field == "write_date": +# since_date = value +# else: +# domain.append(e) +# partner_adapter = self.component( +# usage="backend.adapter", model_name="oxigesti.res.partner" +# ) +# partner_binder = self.binder_for("oxigesti.res.partner") +# binder = self.binder_for(self.model._name) +# for p in self.env["res.partner"].search(domain): +# pricelist_items = p.property_product_pricelist.item_ids.filtered( +# lambda x: ( +# not since_date +# or x.write_date > since_date +# or p.write_date > since_date +# ) +# and x.applied_on == "1_product" +# and x.compute_price == "fixed" +# and ( +# not x.sudo().product_tmpl_id.company_id +# or x.sudo().product_tmpl_id.company_id == p.company_id +# ) +# ) +# if pricelist_items: +# # create/update binding data +# partner_external_id = partner_binder.to_external(p, wrap=True) +# for pl in pricelist_items: +# if pl.product_tmpl_id.default_code and partner_external_id: +# external_id = [ +# pl.product_tmpl_id.default_code, +# partner_adapter.id2dict(partner_external_id)[ +# "Codigo_Mutua" +# ], +# ] +# binding = binder.to_internal(external_id) +# if binding and binding.odoo_id != pl: +# old_binding = binder._find_binding( +# pl, binding_extra_vals={"odoo_partner_id": p.id} +# ) +# if old_binding: +# old_binding.unlink() +# binding.odoo_id = pl +# binding = binder.wrap_binding( +# pl, +# binding_extra_vals={ +# "odoo_partner_id": p.id, +# "deprecated": pl.is_deprecated(p), +# }, +# ) +# self._export_record(binding) -class ProductPricelistItemBatchExporter(Component): + +class ProductPricelistItemDelayedBatchExporter(Component): """Export the Oxigesti Product prices. For every product in the list, a delayed job is created. @@ -14,65 +85,22 @@ class ProductPricelistItemBatchExporter(Component): _name = "oxigesti.product.pricelist.item.delayed.batch.exporter" _inherit = "oxigesti.delayed.batch.exporter" - _apply_on = "oxigesti.product.pricelist.item" - def run(self, domain=None): - if not domain: - domain = [] - # Run the batch synchronization - parent_domain = domain + [ - ("is_company", "=", True), - ("customer_rank", ">=", 0), - ] - since_date = None - domain = [] - for e in parent_domain: - field, operator, value = e - if field == "write_date": - since_date = value - else: - domain.append(e) - partner_adapter = self.component( - usage="backend.adapter", model_name="oxigesti.res.partner" - ) - partner_binder = self.binder_for("oxigesti.res.partner") - binder = self.binder_for(self.model._name) - for p in self.env["res.partner"].search(domain): - partner_external_id = partner_binder.to_external(p, wrap=True) - for pl in p.property_product_pricelist.item_ids.filtered( - lambda x: ( - not since_date - or x.write_date > since_date - or p.write_date > since_date - ) - and p.customer_rank >= p.supplier_rank - and x.applied_on == "1_product" - and x.compute_price == "fixed" - and ( - not x.sudo().product_tmpl_id.company_id - or x.sudo().product_tmpl_id.company_id == p.company_id - ) - ): - if pl.product_tmpl_id.default_code and partner_external_id: - external_id = [ - pl.product_tmpl_id.default_code, - partner_adapter.id2dict(partner_external_id)["Codigo_Mutua"], - ] - binding = binder.to_internal(external_id) - if binding and binding.odoo_id != pl: - old_binding = binder._find_binding( - pl, binding_extra_vals={"odoo_partner_id": p.id} - ) - if old_binding: - old_binding.unlink() - binding.odoo_id = pl - binding = binder.wrap_binding( - pl, - binding_extra_vals={ - "odoo_partner_id": p.id, - }, - ) - self._export_record(binding) + # def run(self, domain=None): + # super().run_common(domain=domain) + + +class ProductPricelistItemDirectBatchExporter(Component): + """Export the Oxigesti Product prices. + + For every product in the list, a direct job is created. + """ + + _name = "oxigesti.product.pricelist.item.direct.batch.exporter" + _inherit = "oxigesti.direct.batch.exporter" + + # def run(self, domain=None): + # super().run_common(domain=domain) class ProductPricelistItemExporter(Component): @@ -81,6 +109,84 @@ class ProductPricelistItemExporter(Component): _apply_on = "oxigesti.product.pricelist.item" + def run(self, relation, *args, **kwargs): + partners = ( + self.env["res.partner"] + .search( + [ + ("company_id", "=?", self.backend_record.company_id.id), + ] + ) + .filtered(lambda x: x.property_product_pricelist == relation.pricelist_id) + ) + if partners: + bindings = self.env[relation.oxigesti_bind_ids._name] + for p in partners: + bindings |= self.binder.wrap_binding( + relation, + binding_extra_vals={ + "odoo_partner_id": p.id, + "deprecated": relation.is_deprecated(p), + "odoo_fixed_price": relation.fixed_price, + }, + ) + else: + bindings = relation.oxigesti_bind_ids.filtered( + lambda x: x.deprecated + and x.odoo_partner_id.property_product_pricelist + != relation.pricelist_id + ) + for binding in bindings: + super().run(binding, *args, **kwargs) + + # def run(self, relation, *args, **kwargs): + # partners = ( + # self.env["res.partner"] + # .search( + # [ + # ("company_id", "=?", self.backend_record.company_id.id), + # ] + # ) + # .filtered(lambda x: x.property_product_pricelist == relation.pricelist_id) + # ) + # for p in partners: + # computed_external_id = self.binder._get_external_id( + # relation, extra_vals={"odoo_partner_id": p.id} + # ) + # binding = self.binder.to_internal(computed_external_id) + # if binding and binding.odoo_id != relation: + # # old_binding = self.binder._find_binding( + # # relation, binding_extra_vals={"odoo_partner_id": p.id} + # # ) + # # if old_binding: + # # old_binding.unlink() + # binding.odoo_id = relation + # # computed_external_id_hash = idhash(computed_external_id) + # # binding = self.env["oxigesti.product.pricelist.item"].search( + # # [ + # # ("backend_id", "=", self.backend_record.id), + # # ("external_id_hash", "=", computed_external_id_hash), + # # ("odoo_partner_id", "=", p.id), + # # ("odoo_id", "!=", relation.id), + # # ] + # # ) + # # if binding: + # # binding.odoo_id = relation + # binding = self.binder.wrap_binding( + # relation, + # binding_extra_vals={ + # "odoo_partner_id": p.id, + # "deprecated": relation.is_deprecated(p), + # "odoo_fixed_price": relation.fixed_price, + # }, + # ) + # super().run(binding, *args, **kwargs) + # if not partners: + # for binding in relation.oxigesti_bind_ids.filtered( + # lambda x: x.deprecated + # ): + # super().run(binding, *args, **kwargs) + def _export_dependencies(self): # partner binder = self.binder_for("oxigesti.res.partner") 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..58607e54c --- /dev/null +++ b/connector_oxigesti/models/product_pricelist_item/listener.py @@ -0,0 +1,15 @@ +# 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 + for backend, domain in bindings.get_external_ids_domain_by_backend().items(): + bindings.with_delay().export_delete_batch(backend, domain=domain) diff --git a/connector_oxigesti/models/product_product/__init__.py b/connector_oxigesti/models/product_product/__init__.py index 655cad79f..7bdde7bb4 100644 --- a/connector_oxigesti/models/product_product/__init__.py +++ b/connector_oxigesti/models/product_product/__init__.py @@ -5,4 +5,3 @@ from . import export_mapper from . import binder from . import binding -from . import listener diff --git a/connector_oxigesti/models/product_product/adapter.py b/connector_oxigesti/models/product_product/adapter.py index af1ef919c..0a2d4ecad 100644 --- a/connector_oxigesti/models/product_product/adapter.py +++ b/connector_oxigesti/models/product_product/adapter.py @@ -27,8 +27,4 @@ class ProductProductAdapter(Component): values (%(phvalues)s) """ - _sql_delete = """delete from %(schema)s.Odoo_Articulos_Generales - where CodigoArticulo = %%(CodigoArticulo)s - """ - _id = ("CodigoArticulo",) diff --git a/connector_oxigesti/models/product_product/binder.py b/connector_oxigesti/models/product_product/binder.py index 042537bed..5dd37c711 100644 --- a/connector_oxigesti/models/product_product/binder.py +++ b/connector_oxigesti/models/product_product/binder.py @@ -19,12 +19,9 @@ class ProductProductBinder(Component): _apply_on = "oxigesti.product.product" - def _get_external_id(self, binding): - if not self._is_binding(binding): - raise Exception("The source object %s must be a binding" % binding._name) - + def _get_external_id(self, relation, extra_vals=None): external_id = None - if binding.odoo_id.default_code: - external_id = [binding.odoo_id.default_code] + if relation.default_code: + external_id = [relation.default_code] return external_id diff --git a/connector_oxigesti/models/product_product/binding.py b/connector_oxigesti/models/product_product/binding.py index bc324fcb3..a50d90b45 100644 --- a/connector_oxigesti/models/product_product/binding.py +++ b/connector_oxigesti/models/product_product/binding.py @@ -14,18 +14,6 @@ class ProductProduct(models.Model): string="Oxigesti Bindings", ) - def unlink(self): - to_remove = {} - for record in self: - to_remove[record.id] = [ - (binding.backend_id.id, binding._name, binding.external_id) - for binding in record.oxigesti_bind_ids - ] - result = super(ProductProduct, self).unlink() - for bindings_data in to_remove.values(): - self._event("on_record_post_unlink").notify(bindings_data) - return result - class ProductProductBinding(models.Model): _name = "oxigesti.product.product" diff --git a/connector_oxigesti/models/product_product/listener.py b/connector_oxigesti/models/product_product/listener.py deleted file mode 100644 index d01997641..000000000 --- a/connector_oxigesti/models/product_product/listener.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright NuoBiT Solutions - Eric Antones -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo.addons.component.core import Component - - -class ProductProductListener(Component): - _name = "oxigesti.product.product.listener" - _inherit = "oxigesti.event.listener" - - _apply_on = "product.product" diff --git a/connector_oxigesti/models/product_template/__init__.py b/connector_oxigesti/models/product_template/__init__.py index 209806a0a..9430369c2 100644 --- a/connector_oxigesti/models/product_template/__init__.py +++ b/connector_oxigesti/models/product_template/__init__.py @@ -1,4 +1 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from . import binding from . import listener diff --git a/connector_oxigesti/models/product_template/binding.py b/connector_oxigesti/models/product_template/binding.py deleted file mode 100644 index 4ce56784c..000000000 --- a/connector_oxigesti/models/product_template/binding.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright NuoBiT Solutions - Eric Antones -# Copyright NuoBiT Solutions - Kilian Niubo -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo import models - - -class ProductTemplate(models.Model): - _inherit = "product.template" - - def unlink(self): - to_remove = {} - for record in self: - to_remove[record.id] = [ - (binding.backend_id.id, binding._name, binding.external_id) - for binding in record.product_variant_ids.mapped("oxigesti_bind_ids") - ] - result = super(ProductTemplate, self).unlink() - for bindings_data in to_remove.values(): - self._event("on_record_post_unlink").notify(bindings_data) - return result diff --git a/connector_oxigesti/models/product_template/listener.py b/connector_oxigesti/models/product_template/listener.py index 6e161534c..9ad1dd934 100644 --- a/connector_oxigesti/models/product_template/listener.py +++ b/connector_oxigesti/models/product_template/listener.py @@ -1,11 +1,22 @@ # 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 ProductTemplateListener(Component): +class ProductPricelistItemListener(Component): _name = "oxigesti.product.template.listener" _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 + ) + for backend, domain in bindings.get_external_ids_domain_by_backend().items(): + self.env[ + "oxigesti.product.pricelist.item" + ].with_delay().export_delete_batch(backend, domain) 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/listener.py b/connector_oxigesti/models/res_partner/listener.py new file mode 100644 index 000000000..339292c15 --- /dev/null +++ b/connector_oxigesti/models/res_partner/listener.py @@ -0,0 +1,20 @@ +# 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 + ) + for backend, domain in bindings.get_external_ids_domain_by_backend().items(): + bindings.with_delay().export_delete_batch(backend, domain=domain) diff --git a/connector_oxigesti/models/stock_production_lot/binder.py b/connector_oxigesti/models/stock_production_lot/binder.py index 6418275dc..549d74647 100644 --- a/connector_oxigesti/models/stock_production_lot/binder.py +++ b/connector_oxigesti/models/stock_production_lot/binder.py @@ -10,14 +10,9 @@ class StockProductionLotBinder(Component): _apply_on = "oxigesti.stock.production.lot" - def _get_external_id(self, binding): - if not self._is_binding(binding): - raise Exception("The source object %s must be a binding" % binding._name) - + def _get_external_id(self, relation, extra_vals=None): product_binder = self.binder_for("oxigesti.product.product") - product_external_id = product_binder.to_external( - binding.odoo_id.product_id, wrap=True - ) + product_external_id = product_binder.to_external(relation.product_id, wrap=True) external_id = None if product_external_id: @@ -27,6 +22,6 @@ def _get_external_id(self, binding): codigo_articulo = product_adapter.id2dict(product_external_id)[ "CodigoArticulo" ] - external_id = [codigo_articulo, binding.name] + external_id = [codigo_articulo, relation.name] return external_id 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 @@ +