diff --git a/sale_configurator_base/models/__init__.py b/sale_configurator_base/models/__init__.py index a6fee4e2..ef2dca3d 100644 --- a/sale_configurator_base/models/__init__.py +++ b/sale_configurator_base/models/__init__.py @@ -1,3 +1,4 @@ +from . import configurable_mixin from . import sale from . import ir_ui_view from . import account_move_line diff --git a/sale_configurator_base/models/account_move_line.py b/sale_configurator_base/models/account_move_line.py index 1e1c3753..be81d6ed 100644 --- a/sale_configurator_base/models/account_move_line.py +++ b/sale_configurator_base/models/account_move_line.py @@ -4,12 +4,59 @@ from odoo import api, fields, models -class AccountMoveLine(models.Model): - _inherit = "account.move.line" +class AccountMove(models.Model): + _name = "account.move" + _inherit = ["configurable.mixin", "account.move"] + + @property + def _lines_name(self): + return "line_ids" + + @api.depends("line_ids") + def _onchange_children_sequence(self): + super()._onchange_children_sequence() - @api.depends("sale_line_ids") - def _compute_has_parent(self): + def _rebuild_parent_configuration_from_sale(self): for rec in self: - rec.has_parent = any([line.parent_id for line in rec.sale_line_ids]) + lines = rec.line_ids + mapping_sale_line_to_invoice_line = { + line.sale_line_ids.id: line.id for line in lines + } + for line in lines: + sale_line_parent = line.sale_line_ids.parent_id + if sale_line_parent: + line.parent_id = mapping_sale_line_to_invoice_line[ + sale_line_parent.id + ] + + +class AccountMoveLine(models.Model): + _name = "account.move.line" + _inherit = ["account.move.line", "configurable.line.mixin"] + + parent_id = fields.Many2one( + "account.move.line", "Parent Line", ondelete="cascade", index=True + ) + child_ids = fields.One2many("account.move.line", "parent_id", "Children Lines") + + @api.depends( + "price_subtotal", + "price_total", + "child_ids.price_subtotal", + "child_ids.price_total", + "parent_id", + ) + def _compute_config_amount(self): + return super()._compute_config_amount() + + @api.depends("product_id") + def _compute_is_configurable(self): + return super()._compute_is_configurable() + + @api.depends("price_unit", "child_ids") + def _compute_report_line_is_empty_parent(self): + return super()._compute_report_line_is_empty_parent() - has_parent = fields.Boolean(compute="_compute_has_parent", store=True) + @property + def _parent_container_name(self): + return "move_id" diff --git a/sale_configurator_base/models/configurable_mixin.py b/sale_configurator_base/models/configurable_mixin.py new file mode 100644 index 00000000..0c20d960 --- /dev/null +++ b/sale_configurator_base/models/configurable_mixin.py @@ -0,0 +1,178 @@ +# Copyright 2021 Akretion (http://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.tools import float_compare + + +class ConfigurableMixin(models.AbstractModel): + """ + Justification for this model is that we want identical functionality + on sale orders and invoices. We can't implement everything out of the + box, so some re-implementing is necessary when inheriting the mixin. + To implement, define: + - @api.depends functions + - _lines_name property + """ + + _name = "configurable.mixin" + _description = "Configurable Mixin" + + @property + def _lines_name(self): + raise NotImplementedError + + @property + def _lines(self): + return getattr(self, self._lines_name) + + def sync_sequence(self): + for record in self: + done = [] + lines = record._lines.sorted("sequence") + for line in lines: + if not line.parent_id: + line.sequence = len(done) + done.append(line) + line._sort_children_line(done) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records.sync_sequence() + return records + + def write(self, vals): + super().write(vals) + if self._lines_name in vals: + self.sync_sequence() + return True + + # @api.depends("lines") + def _onchange_children_sequence(self): + """Implement using @api.depends + .line""" + self.sync_sequence() + + +class ConfigurableLineMixin(models.AbstractModel): + """ + To implement, define: + - parent_id + - child_ids + - _parent_container property + - @api.depends functions + TODO cleanup???: + - reimplement price_config_subtotal and total as fields.Monetary + """ + + _name = "configurable.line.mixin" + _description = "Configurable Line Mixin" + + child_type = fields.Selection([]) + # Monetary fields require a currency, so reimplement it + # TODO do something more elegant + price_config_subtotal = fields.Float( + compute="_compute_config_amount", + string="Config Subtotal", + readonly=True, + store=True, + ) + price_config_total = fields.Float( + compute="_compute_config_amount", + string="Config Total", + readonly=True, + store=True, + ) + + is_configurable = fields.Boolean( + "Line is a configurable Product ?", + compute="_compute_is_configurable", + ) + report_line_is_empty_parent = fields.Boolean( + compute="_compute_report_line_is_empty_parent", + help="Technical field used in the report to hide subtotals" + " and taxes in case a parent line (with children lines) " + "has no price by itself", + ) + + @property + def _parent_container(self): + return getattr(self, self._parent_container_name) + + @property + def _parent_container_name(self): + raise NotImplementedError + + def _get_child_type_sort(self): + return [] + + def _sort_children_line(self, done): + types = self._get_child_type_sort() + types.sort() + for _position, child_type in types: + for line in self.child_ids.sorted("sequence"): + if line.child_type == child_type: + line.sequence = len(done) + done.append(line) + + # @api.depends("price_unit", "child_ids") + def _compute_report_line_is_empty_parent(self): + for rec in self: + rec.report_line_is_empty_parent = False + price_unit_like_zero = ( + float_compare(rec.price_unit, 0.00, precision_digits=2) == 0 + ) + if rec.child_ids and price_unit_like_zero: + rec.report_line_is_empty_parent = True + + # @api.depends("product_id") + def _compute_is_configurable(self): + for record in self: + record.is_configurable = record._is_line_configurable() + + def _is_line_configurable(self): + raise NotImplementedError + + # @api.depends( + # "price_subtotal", + # "price_total", + # "child_ids.price_subtotal", + # "child_ids.price_total", + # "parent_id", + # ) + def _compute_config_amount(self): + """ + Compute the config amounts of the line. + Implement using @api.depends: + - price_subtotal + - price_total + - child_ids.price_subtotal + - child_ids.price_total + - parent_id + """ + for line in self: + line.update(line._get_price_config()) + + def _get_price_config(self): + self.ensure_one() + if self.parent_id: + return { + "price_config_subtotal": 0, + "price_config_total": 0, + } + else: + return { + "price_config_subtotal": self.price_subtotal + + sum(self.child_ids.mapped("price_subtotal")), + "price_config_total": self.price_total + + sum(self.child_ids.mapped("price_total")), + } + + @api.model_create_multi + def create(self, vals_list): + parent_name = self._parent_container_name + for vals in vals_list: + if vals.get("parent_id") and parent_name not in vals: + vals[parent_name] = self.browse(vals["parent_id"])._parent_container.id + return super().create(vals_list) diff --git a/sale_configurator_base/models/sale.py b/sale_configurator_base/models/sale.py index 78311784..b200069b 100644 --- a/sale_configurator_base/models/sale.py +++ b/sale_configurator_base/models/sale.py @@ -8,7 +8,6 @@ from odoo import _, api, fields, models from odoo.osv import expression -from odoo.tools import float_compare # TODO put this in a box tool module @@ -20,32 +19,16 @@ def update_attrs(node, add_attrs): class SaleOrder(models.Model): - _inherit = "sale.order" - - def sync_sequence(self): - for record in self: - done = [] - for line in record.order_line.sorted("sequence"): - if not line.parent_id: - line.sequence = len(done) - done.append(line) - line._sort_children_line(done) - - @api.model_create_multi - def create(self, vals_list): - records = super().create(vals_list) - records.sync_sequence() - return records - - def write(self, vals): - super().write(vals) - if "order_line" in vals: - self.sync_sequence() - return True - - @api.onchange("order_line") - def onchange_sale_line_sequence(self): - self.sync_sequence() + _name = "sale.order" + _inherit = ["configurable.mixin", "sale.order"] + + @property + def _lines_name(self): + return "order_line" + + @api.depends("order_line") + def _onchange_children_sequence(self): + super()._onchange_children_sequence() @api.model def _fields_view_get( @@ -74,38 +57,37 @@ def _fields_view_get( ) if field.get("name") == "product_id": field.set( - "class", field.get("class", "") + " configurator_option_padding" + "class", field.get("class", "") + " configurator_child_padding" ) if field.get("name") == "name": field.set( "class", field.get("class", "") - + " description configurator_option_padding", + + " description configurator_child_padding", ) res["arch"] = etree.tostring(doc, pretty_print=True).decode("utf-8") return res + def _create_invoices(self, grouped=False, final=False, date=None): + """ + _create_invoices doesn't have the right hooks for us + to directly build the parent/child relationships, + thus we rebuild them at the end + """ + result = super()._create_invoices(grouped, final, date) + for invoice in result: + invoice._rebuild_parent_configuration_from_sale() + return result + class SaleOrderLine(models.Model): - _inherit = "sale.order.line" + _name = "sale.order.line" + _inherit = ["configurable.line.mixin", "sale.order.line"] parent_id = fields.Many2one( "sale.order.line", "Parent Line", ondelete="cascade", index=True ) child_ids = fields.One2many("sale.order.line", "parent_id", "Children Lines") - child_type = fields.Selection([]) - price_config_subtotal = fields.Monetary( - compute="_compute_config_amount", - string="Config Subtotal", - readonly=True, - store=True, - ) - price_config_total = fields.Monetary( - compute="_compute_config_amount", - string="Config Total", - readonly=True, - store=True, - ) pricelist_id = fields.Many2one(related="order_id.pricelist_id", string="Pricelist") # There is already an order_partner_id in the sale line class # but we want to make the view as much compatible between child view @@ -113,44 +95,6 @@ class SaleOrderLine(models.Model): # with the child line (but in that case the parent is a sale order line partner_id = fields.Many2one(related="order_id.partner_id", string="Customer") - is_configurable = fields.Boolean( - "Line is a configurable Product ?", - compute="_compute_is_configurable", - ) - report_line_is_empty_parent = fields.Boolean( - compute="_compute_report_line_is_empty_parent", - help="Technical field used in the report to hide subtotals" - " and taxes in case a parent line (with children lines) " - "has no price by itself", - ) - - def _get_child_type_sort(self): - return [] - - def _sort_children_line(self, done): - types = self._get_child_type_sort() - types.sort() - for _position, child_type in types: - for line in self.child_ids.sorted("sequence"): - if line.child_type == child_type: - line.sequence = len(done) - done.append(line) - - @api.depends("price_unit", "child_ids") - def _compute_report_line_is_empty_parent(self): - for rec in self: - rec.report_line_is_empty_parent = False - price_unit_like_zero = ( - float_compare(rec.price_unit, 0.00, precision_digits=2) == 0 - ) - if rec.child_ids and price_unit_like_zero: - rec.report_line_is_empty_parent = True - - @api.depends("product_id") - def _compute_is_configurable(self): - for record in self: - record.is_configurable = record._is_line_configurable() - def _is_line_configurable(self): return False @@ -184,30 +128,16 @@ def open_sale_line_config_base(self): "parent_id", ) def _compute_config_amount(self): - """ - Compute the config amounts of the SO line. - """ - for line in self: - line.update(line._get_price_config()) - - def _get_price_config(self): - self.ensure_one() - if self.parent_id: - return { - "price_config_subtotal": 0, - "price_config_total": 0, - } - else: - return { - "price_config_subtotal": self.price_subtotal - + sum(self.child_ids.mapped("price_subtotal")), - "price_config_total": self.price_total - + sum(self.child_ids.mapped("price_total")), - } - - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get("parent_id") and "order_id" not in vals: - vals["order_id"] = self.browse(vals["parent_id"]).order_id.id - return super().create(vals_list) + return super()._compute_config_amount() + + @api.depends("product_id") + def _compute_is_configurable(self): + return super()._compute_is_configurable() + + @api.depends("price_unit", "child_ids") + def _compute_report_line_is_empty_parent(self): + return super()._compute_report_line_is_empty_parent() + + @property + def _parent_container_name(self): + return "order_id" diff --git a/sale_configurator_base/static/src/scss/sale_order.scss b/sale_configurator_base/static/src/scss/child_line.scss similarity index 50% rename from sale_configurator_base/static/src/scss/sale_order.scss rename to sale_configurator_base/static/src/scss/child_line.scss index fedcc95e..f05d424e 100644 --- a/sale_configurator_base/static/src/scss/sale_order.scss +++ b/sale_configurator_base/static/src/scss/child_line.scss @@ -1,9 +1,9 @@ /* Different field types will use td or span */ -tr.text-it > td > span.configurator_option_padding { +tr.text-it > td > span.configurator_child_padding { padding-left: 30px; } -tr.text-it > td.configurator_option_padding { +tr.text-it > td.configurator_child_padding { padding-left: 30px; } diff --git a/sale_configurator_base/templates/account_invoice_templates.xml b/sale_configurator_base/templates/account_invoice_templates.xml index ff4935cd..1b5ce316 100644 --- a/sale_configurator_base/templates/account_invoice_templates.xml +++ b/sale_configurator_base/templates/account_invoice_templates.xml @@ -7,17 +7,131 @@ expr="//t[@name='account_invoice_line_accountable']//span[@t-field='line.name']" position="replace" > - + - + + + + + + + + + + Sub-total + + + + + Total + + + + + + + + Total + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sale_configurator_base/templates/sale_report_templates.xml b/sale_configurator_base/templates/sale_report_templates.xml index 2061f56e..e4b883ef 100644 --- a/sale_configurator_base/templates/sale_report_templates.xml +++ b/sale_configurator_base/templates/sale_report_templates.xml @@ -21,7 +21,7 @@