diff --git a/sale_margin_delivered/README.rst b/sale_margin_delivered/README.rst new file mode 100644 index 000000000..b02cd9e7a --- /dev/null +++ b/sale_margin_delivered/README.rst @@ -0,0 +1,138 @@ +===================== +Sale Margin Delivered +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0b09ede2e638f86ca5633af540546efdea6a1511f82c70e98678201db8657d54 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmargin--analysis-lightgray.png?logo=github + :target: https://github.com/OCA/margin-analysis/tree/17.0/sale_margin_delivered + :alt: OCA/margin-analysis +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/margin-analysis-17-0/margin-analysis-17-0-sale_margin_delivered + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/margin-analysis&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Computes sale order lines margins for the delivered items. + +This module takes in consideration Outgoing and Returns. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module has been developed to be able to show delivered margins on +sale order lines. + +If you need this module for those reasons, these might interest you too: + +- sale_margin_delivered_dropshipping +- sale_report_delivered + +Configuration +============= + +To grant Sales Margin view privileges to a user, go to *Settings > Users +& Companies > Users*, select the user and set *Show Sale Margin* on. + +Usage +===== + +1. Go to *Sales > Orders > Quotations*. +2. Create a new *Sales Order* and add a line with an stockable product. + Set a quantity higher than one. +3. Confirm the *Sales Order* and deliver just a partial amount of + product in the picking. +4. Go to *Sales > Reporting > Sales* and unfold the *Order Reference* + dimension and the *Margin* and *Margin Delivered* to compare them. + +For example: + +In an order line with a product at a cost of 10 and a sell price of 25 +we deliver 2 of 3 units. Then, the reported margins would be: + +\`margin\`: 45 (3 \* 15) \`margin_delivered\`: 30 (2 \* 15) + +Additionally, you can check the margin and the margin percent in the +sales order line. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Sergio Teruel + - David Vidal + - Carlos Roca + - Pilar Vargas + +- Eduardo de Miguel (`Moduon `__) +- Rafael Blasco (`Moduon `__) +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sergio-teruel| image:: https://github.com/sergio-teruel.png?size=40px + :target: https://github.com/sergio-teruel + :alt: sergio-teruel +.. |maintainer-Shide| image:: https://github.com/Shide.png?size=40px + :target: https://github.com/Shide + :alt: Shide + +Current `maintainers `__: + +|maintainer-sergio-teruel| |maintainer-Shide| + +This module is part of the `OCA/margin-analysis `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_margin_delivered/__init__.py b/sale_margin_delivered/__init__.py new file mode 100644 index 000000000..55ec7fc9a --- /dev/null +++ b/sale_margin_delivered/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import reports diff --git a/sale_margin_delivered/__manifest__.py b/sale_margin_delivered/__manifest__.py new file mode 100644 index 000000000..b9d590d7e --- /dev/null +++ b/sale_margin_delivered/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2018 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Sale Margin Delivered", + "version": "17.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/margin-analysis", + "category": "Sales", + "license": "AGPL-3", + "depends": ["sale_stock", "sale_margin"], + "data": ["views/sale_margin_delivered_view.xml"], + "installable": True, + "development_status": "Production/Stable", + "maintainers": ["sergio-teruel", "Shide"], +} diff --git a/sale_margin_delivered/i18n/es.po b/sale_margin_delivered/i18n/es.po new file mode 100644 index 000000000..c2801fe6d --- /dev/null +++ b/sale_margin_delivered/i18n/es.po @@ -0,0 +1,129 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_margin_delivered +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-19 07:32+0000\n" +"PO-Revision-Date: 2024-06-19 09:33+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.4\n" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "" +"Average Unit Cost of delivered products.\n" +"\n" +"Formula: Value Delivered / Quantity Delivered\n" +"\n" +"When using the FIFO method, the value of this field may not match the actual " +"cost of the product delivered.\n" +"There may also be differences with the costing of the Sales from Deliveries " +"report, because when the sales order is created, it is not known exactly " +"which units will actually be delivered to calculate their cost.\n" +"This is because when the sales order is created, it is not known which units " +"will actually be delivered to calculate their actual cost. You do not have " +"this information until you validate the corresponding delivery note." +msgstr "" +"Promedio del Coste Unitario de los productos entregados.\n" +"\n" +"Fórmula: Valor entregado / Cantidad Entregada\n" +"\n" +"Cuando se usa el método FIFO el valor de este campo puede no coincidir con " +"el coste real del producto entregado.\n" +"Puede haber también diferencias con el cálculo del coste del informe " +"\"Ventas desde albaranes\".\n" +"El motivo es que al hacer el pedido de venta no se sabe todavía qué unidades " +"serán las realmente entregadas para calcular su coste real. No se tendrá " +"este dato hasta validar el albarán de entrega correspondiente." + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Cost Price dlvd." +msgstr "Coste entr." + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_report__margin_delivered +msgid "Margin Delivered" +msgstr "Margen entr." + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "Margin Delivered Percent" +msgstr "Margen entr. (%)" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd." +msgstr "Margen entr." + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd. (%)" +msgstr "Margen entr. (%)" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "" +"Margin percent between the Unit Price with discounts and Delivered Unit " +"Cost.\n" +"\n" +"Formula: ((Unit Price with Discounts - Average Unit Cost of delivered " +"products) / Unit Price with Discounts)" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "Purchase Price Delivery" +msgstr "Coste entr." + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_report +msgid "Sales Analysis Report" +msgstr "Informe de análisis de ventas" + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered +msgid "" +"Total Margin of all delivered products.\n" +"\n" +"Formula: Delivered Quantities * (Unit Price with Discounts - Average Unit " +"Cost of Delivered Products)\n" +"\n" +"Value may differ from Cost Price because Stock Valuation Layers are used " +"instead of Cost on line." +msgstr "" +"Margen Total de todos los productos entregados.\n" +"\n" +"Fórmula: Cantidades Entregadas * (Precio Unitario con Descuentos - Promedio " +"del Coste Unitario de los productos entregados)\n" +"\n" +"El valor del Coste puede cambiar porque se utiliza la Valoración de Stock en " +"vez del Coste de la línea." + +#~ msgid "" +#~ "Margin percent between the Unit Price with discounts and Delivered Unit " +#~ "Cost.\n" +#~ "\n" +#~ "Formula: ((Unit Price with Discounts - Average Unit Cost of delivered " +#~ "products) / Unit Price with Discounts) * 100.0" +#~ msgstr "" +#~ "Margen porcentual entre el Precio Unitario con Descuentos y el Coste " +#~ "Unitario entregado.\n" +#~ "\n" +#~ "Fórmula: ((Precio Unitario con descuentos - Promedio del coste unitario " +#~ "de los productos entregados) / Precio Unitario con descuentos) * 100.0" diff --git a/sale_margin_delivered/i18n/it.po b/sale_margin_delivered/i18n/it.po new file mode 100644 index 000000000..073797e18 --- /dev/null +++ b/sale_margin_delivered/i18n/it.po @@ -0,0 +1,153 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_margin_delivered +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-24 14:36+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "" +"Average Unit Cost of delivered products.\n" +"\n" +"Formula: Value Delivered / Quantity Delivered\n" +"\n" +"When using the FIFO method, the value of this field may not match the actual " +"cost of the product delivered.\n" +"There may also be differences with the costing of the Sales from Deliveries " +"report, because when the sales order is created, it is not known exactly " +"which units will actually be delivered to calculate their cost.\n" +"This is because when the sales order is created, it is not known which units " +"will actually be delivered to calculate their actual cost. You do not have " +"this information until you validate the corresponding delivery note." +msgstr "" +"Costo unitario medio dei prodotti consegnati.\n" +"\n" +"Formula: valore consegnato / quantità consegnata\n" +"\n" +"Quando si utilizza il metodo FIFO, il valore di questo campo potrebbe non " +"corrispondere al costo effettivo del prodotto consegnato.\n" +"Potrebbero inoltre esserci differenze con la determinazione dei costi del " +"resoconto vendite da consegne, poiché quando viene creato l'ordine cliente, " +"non è noto esattamente quali unità verranno effettivamente consegnate per " +"calcolarne il costo.\n" +"Questo perché quando viene creato l'ordine cliente, non è noto quali unità " +"verranno effettivamente consegnate per calcolarne il costo effettivo. Non si " +"avranno queste informazioni finché non si convaliderà il DDT corrispondente." + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Cost Price dlvd." +msgstr "Prezzo costo consegnato" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_report__margin_delivered +msgid "Margin Delivered" +msgstr "Margine consegnato" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "Margin Delivered Percent" +msgstr "Percentuale margine consegnato" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd." +msgstr "Margine consegnato" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd. (%)" +msgstr "Margine consegnato (%)" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "" +"Margin percent between the Unit Price with discounts and Delivered Unit " +"Cost.\n" +"\n" +"Formula: ((Unit Price with Discounts - Average Unit Cost of delivered " +"products) / Unit Price with Discounts)" +msgstr "" +"Percentuale di margine tra il prezzo unitario con sconti e il costo unitario " +"di consegna.\n" +"\n" +"Formula: ((prezzo unitario con sconti - costo medio unitario dei prodotti " +"consegnati) / prezzo unitario con sconti)" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "Purchase Price Delivery" +msgstr "Prezzo consegna acquisto" + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_report +msgid "Sales Analysis Report" +msgstr "Resoconto analisi vendite" + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered +msgid "" +"Total Margin of all delivered products.\n" +"\n" +"Formula: Delivered Quantities * (Unit Price with Discounts - Average Unit " +"Cost of Delivered Products)\n" +"\n" +"Value may differ from Cost Price because Stock Valuation Layers are used " +"instead of Cost on line." +msgstr "" +"Margine totale di tutti i prodotti consegnati.\n" +"\n" +"Formula: quantità consegnate * (prezzo unitario con sconti - costo unitario " +"medio dei prodotti consegnati)\n" +"\n" +"Il valore può differire dal prezzo di costo poiché vengono utilizzati i " +"livelli di valutazione delle scorte anziché il costo in linea." + +#~ msgid "" +#~ "Average Unit Cost of delivered products.\n" +#~ "\n" +#~ "Formula: Value Delivered / Quantity Delivered" +#~ msgstr "" +#~ "Costo unitario medio dei prodotti consegnati.\n" +#~ "\n" +#~ "Formula: valore consegnato / quantità consegnata" + +#~ msgid "" +#~ "Margin percent between the Unit Price with discounts and Delivered Unit " +#~ "Cost.\n" +#~ "\n" +#~ "Formula: (Margin Dlvd. / Unit Price with Discounts) * 100.0" +#~ msgstr "" +#~ "Margine percentuale tra il prezzo unitario scontato e il costo unitario " +#~ "di consegna.\n" +#~ "\n" +#~ "Formula: (margine consegna / prezzo unitario con sconti) * 100.0" + +#~ msgid "" +#~ "Total Margin of all delivered products.\n" +#~ "\n" +#~ "Formula: Delivered Quantities * (Unit Price with Discounts - Average Unit " +#~ "Cost of Delivered Products)" +#~ msgstr "" +#~ "Margine totale dei prodotti consegnati.\n" +#~ "\n" +#~ "Formula: quantità consegnata * (prezzo unitario con sconto - costo medio " +#~ "unitario dei prodotti consegnati)" diff --git a/sale_margin_delivered/i18n/sale_margin_delivered.pot b/sale_margin_delivered/i18n/sale_margin_delivered.pot new file mode 100644 index 000000000..692c43791 --- /dev/null +++ b/sale_margin_delivered/i18n/sale_margin_delivered.pot @@ -0,0 +1,85 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_margin_delivered +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "" +"Average Unit Cost of delivered products.\n" +"\n" +"Formula: Value Delivered / Quantity Delivered\n" +"\n" +"When using the FIFO method, the value of this field may not match the actual cost of the product delivered.\n" +"There may also be differences with the costing of the Sales from Deliveries report, because when the sales order is created, it is not known exactly which units will actually be delivered to calculate their cost.\n" +"This is because when the sales order is created, it is not known which units will actually be delivered to calculate their actual cost. You do not have this information until you validate the corresponding delivery note." +msgstr "" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Cost Price dlvd." +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_report__margin_delivered +msgid "Margin Delivered" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "Margin Delivered Percent" +msgstr "" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd." +msgstr "" + +#. module: sale_margin_delivered +#: model_terms:ir.ui.view,arch_db:sale_margin_delivered.view_order_form +msgid "Margin dlvd. (%)" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered_percent +msgid "" +"Margin percent between the Unit Price with discounts and Delivered Unit Cost.\n" +"\n" +"Formula: ((Unit Price with Discounts - Average Unit Cost of delivered products) / Unit Price with Discounts)" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,field_description:sale_margin_delivered.field_sale_order_line__purchase_price_delivery +msgid "Purchase Price Delivery" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_report +msgid "Sales Analysis Report" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model,name:sale_margin_delivered.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_margin_delivered +#: model:ir.model.fields,help:sale_margin_delivered.field_sale_order_line__margin_delivered +msgid "" +"Total Margin of all delivered products.\n" +"\n" +"Formula: Delivered Quantities * (Unit Price with Discounts - Average Unit Cost of Delivered Products)\n" +"\n" +"Value may differ from Cost Price because Stock Valuation Layers are used instead of Cost on line." +msgstr "" diff --git a/sale_margin_delivered/models/__init__.py b/sale_margin_delivered/models/__init__.py new file mode 100644 index 000000000..7df041a90 --- /dev/null +++ b/sale_margin_delivered/models/__init__.py @@ -0,0 +1 @@ +from . import sale_margin diff --git a/sale_margin_delivered/models/sale_margin.py b/sale_margin_delivered/models/sale_margin.py new file mode 100644 index 000000000..d70e289fc --- /dev/null +++ b/sale_margin_delivered/models/sale_margin.py @@ -0,0 +1,113 @@ +# Copyright 2018 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, tools + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + margin_delivered = fields.Float( + compute="_compute_margin_delivered", + store=True, + help="Total Margin of all delivered products.\n\n" + "Formula: Delivered Quantities * (Unit Price with Discounts - " + "Average Unit Cost of Delivered Products)\n\nValue may differ from " + "Cost Price because Stock Valuation Layers are used instead of Cost on line.", + ) + margin_delivered_percent = fields.Float( + compute="_compute_margin_delivered", + store=True, + readonly=True, + help="Margin percent between the Unit Price with discounts and " + "Delivered Unit Cost.\n\n" + "Formula: ((Unit Price with Discounts - Average Unit Cost of " + "delivered products) / Unit Price with Discounts)", + ) + purchase_price_delivery = fields.Float( + compute="_compute_margin_delivered", + store=True, + help="Average Unit Cost of delivered products.\n\n" + "Formula: Value Delivered / Quantity Delivered\n\n" + "When using the FIFO method, the value of this field may not match the " + "actual cost of the product delivered.\n" + "There may also be differences with the costing of the Sales from " + "Deliveries report, because when the sales order is created, it is not known " + "exactly which units will actually be delivered to calculate their cost.\n" + "This is because when the sales order is created, it is not known which " + "units will actually be delivered to calculate their actual cost. You do not " + "have this information until you validate the corresponding delivery note.", + ) + + def _get_delivered_margin_valuation_layers(self): + """Gets all Valuation Layers that should be considered for + Delivered Margin calculation.""" + self.ensure_one() + valuation_layers = self.env["stock.valuation.layer"] + for move in self.move_ids.filtered(lambda m: m.state == "done"): + if move.picking_code == "outgoing": + # Outgoing moves have 1 valuation layer and are always negative + valuation_layers |= move.stock_valuation_layer_ids + elif move.picking_code == "incoming" and move.to_refund: + # Incoming moves have 2 valuation layers. Use positive one + valuation_layers |= move.stock_valuation_layer_ids.filtered( + lambda vl: vl.quantity > 0 + ) + return valuation_layers + + @api.depends( + "margin", + "qty_delivered", + "product_uom_qty", + "move_ids.stock_valuation_layer_ids.value", + "move_ids.stock_valuation_layer_ids.quantity", + ) + def _compute_margin_delivered(self): + """Computes the Delivered Margin of the Lines. + + It is calculated based on the Valuation Layers of each Line. + """ + digits = self.env["decimal.precision"].precision_get("Product Price") + self.margin_delivered = 0.0 + self.margin_delivered_percent = 0.0 + self.purchase_price_delivery = 0.0 + for line in self.filtered("qty_delivered"): + # we need to compute the price reduce to avoid rounding issues + # the one stored in the line is rounded to the product price precision + price_reduce_taxexcl = ( + line.price_subtotal / line.product_uom_qty + if line.product_uom_qty + else 0.0 + ) + + if line.product_id.type != "product": + currency = ( + line.order_id.pricelist_id.currency_id + or line.company_id.currency_id + ) + price = line.purchase_price + line.margin_delivered = currency.round( + line.price_subtotal - (price * line.qty_delivered) + ) + line.purchase_price_delivery = price + else: + valuation_layers = line._get_delivered_margin_valuation_layers() + value_delivered = sum(valuation_layers.mapped("value")) + qty_delivered = ( + sum(valuation_layers.mapped("quantity")) or -line.qty_delivered + ) + # purchase_price_delivery always will be positive + # because division of same signs + line.purchase_price_delivery = tools.float_round( + value_delivered / qty_delivered, precision_digits=digits + ) + # Inverse qty_delivered because outgoing quantities are negative + line.margin_delivered = -qty_delivered * ( + price_reduce_taxexcl - line.purchase_price_delivery + ) + # compute percent margin based on delivered quantities or ordered + # quantities + if price_reduce_taxexcl: + line.margin_delivered_percent = ( + price_reduce_taxexcl - line.purchase_price_delivery + ) / price_reduce_taxexcl diff --git a/sale_margin_delivered/pyproject.toml b/sale_margin_delivered/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/sale_margin_delivered/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_margin_delivered/readme/CONFIGURE.md b/sale_margin_delivered/readme/CONFIGURE.md new file mode 100644 index 000000000..5fcb0aaa2 --- /dev/null +++ b/sale_margin_delivered/readme/CONFIGURE.md @@ -0,0 +1,3 @@ +To grant Sales Margin view privileges to a user, go to *Settings \> +Users & Companies \> Users*, select the user and set *Show Sale Margin* +on. diff --git a/sale_margin_delivered/readme/CONTEXT.md b/sale_margin_delivered/readme/CONTEXT.md new file mode 100644 index 000000000..aec0017f1 --- /dev/null +++ b/sale_margin_delivered/readme/CONTEXT.md @@ -0,0 +1,7 @@ +This module has been developed to be able to show delivered margins on +sale order lines. + +If you need this module for those reasons, these might interest you too: + +- sale_margin_delivered_dropshipping +- sale_report_delivered diff --git a/sale_margin_delivered/readme/CONTRIBUTORS.md b/sale_margin_delivered/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..2eb9d7548 --- /dev/null +++ b/sale_margin_delivered/readme/CONTRIBUTORS.md @@ -0,0 +1,9 @@ +- [Tecnativa](https://www.tecnativa.com): + - Sergio Teruel + - David Vidal + - Carlos Roca + - Pilar Vargas +- Eduardo de Miguel ([Moduon](https://www.moduon.team/)) +- Rafael Blasco ([Moduon](https://www.moduon.team/)) +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/sale_margin_delivered/readme/DESCRIPTION.md b/sale_margin_delivered/readme/DESCRIPTION.md new file mode 100644 index 000000000..55a53496d --- /dev/null +++ b/sale_margin_delivered/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +Computes sale order lines margins for the delivered items. + +This module takes in consideration Outgoing and Returns. diff --git a/sale_margin_delivered/readme/USAGE.md b/sale_margin_delivered/readme/USAGE.md new file mode 100644 index 000000000..66db69637 --- /dev/null +++ b/sale_margin_delivered/readme/USAGE.md @@ -0,0 +1,17 @@ +1. Go to *Sales \> Orders \> Quotations*. +2. Create a new *Sales Order* and add a line with an stockable product. + Set a quantity higher than one. +3. Confirm the *Sales Order* and deliver just a partial amount of + product in the picking. +4. Go to *Sales \> Reporting \> Sales* and unfold the *Order Reference* + dimension and the *Margin* and *Margin Delivered* to compare them. + +For example: + +In an order line with a product at a cost of 10 and a sell price of 25 +we deliver 2 of 3 units. Then, the reported margins would be: + +\`margin\`: 45 (3 \* 15) \`margin_delivered\`: 30 (2 \* 15) + +Additionally, you can check the margin and the margin percent in the +sales order line. diff --git a/sale_margin_delivered/reports/__init__.py b/sale_margin_delivered/reports/__init__.py new file mode 100644 index 000000000..cd23411b8 --- /dev/null +++ b/sale_margin_delivered/reports/__init__.py @@ -0,0 +1 @@ +from . import sale_report diff --git a/sale_margin_delivered/reports/sale_report.py b/sale_margin_delivered/reports/sale_report.py new file mode 100644 index 000000000..08490ae7d --- /dev/null +++ b/sale_margin_delivered/reports/sale_report.py @@ -0,0 +1,18 @@ +# Copyright 2018 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleReport(models.Model): + _inherit = "sale.report" + + margin_delivered = fields.Float(readonly=True) + + def _select_additional_fields(self): + res = super()._select_additional_fields() + res["margin_delivered"] = f"""SUM(l.margin_delivered + / {self._case_value_or_one('s.currency_rate')} + * {self._case_value_or_one('currency_table.rate')}) + """ + return res diff --git a/sale_margin_delivered/static/description/icon.png b/sale_margin_delivered/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/sale_margin_delivered/static/description/icon.png differ diff --git a/sale_margin_delivered/static/description/index.html b/sale_margin_delivered/static/description/index.html new file mode 100644 index 000000000..a48db94b8 --- /dev/null +++ b/sale_margin_delivered/static/description/index.html @@ -0,0 +1,474 @@ + + + + + +Sale Margin Delivered + + + +
+

Sale Margin Delivered

+ + +

Production/Stable License: AGPL-3 OCA/margin-analysis Translate me on Weblate Try me on Runboat

+

Computes sale order lines margins for the delivered items.

+

This module takes in consideration Outgoing and Returns.

+

Table of contents

+ +
+

Use Cases / Context

+

This module has been developed to be able to show delivered margins on +sale order lines.

+

If you need this module for those reasons, these might interest you too:

+
    +
  • sale_margin_delivered_dropshipping
  • +
  • sale_report_delivered
  • +
+
+
+

Configuration

+

To grant Sales Margin view privileges to a user, go to Settings > Users +& Companies > Users, select the user and set Show Sale Margin on.

+
+
+

Usage

+
    +
  1. Go to Sales > Orders > Quotations.
  2. +
  3. Create a new Sales Order and add a line with an stockable product. +Set a quantity higher than one.
  4. +
  5. Confirm the Sales Order and deliver just a partial amount of +product in the picking.
  6. +
  7. Go to Sales > Reporting > Sales and unfold the Order Reference +dimension and the Margin and Margin Delivered to compare them.
  8. +
+

For example:

+

In an order line with a product at a cost of 10 and a sell price of 25 +we deliver 2 of 3 units. Then, the reported margins would be:

+

`margin`: 45 (3 * 15) `margin_delivered`: 30 (2 * 15)

+

Additionally, you can check the margin and the margin percent in the +sales order line.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

sergio-teruel Shide

+

This module is part of the OCA/margin-analysis project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sale_margin_delivered/tests/__init__.py b/sale_margin_delivered/tests/__init__.py new file mode 100644 index 000000000..42412e220 --- /dev/null +++ b/sale_margin_delivered/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl +from . import test_sale_margin_delivered diff --git a/sale_margin_delivered/tests/test_sale_margin_delivered.py b/sale_margin_delivered/tests/test_sale_margin_delivered.py new file mode 100644 index 000000000..76c066170 --- /dev/null +++ b/sale_margin_delivered/tests/test_sale_margin_delivered.py @@ -0,0 +1,219 @@ +# Copyright 2019 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl +from odoo import Command +from odoo.tests import Form + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT, BaseCommon + + +class TestSaleMarginDelivered(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + cls.SaleOrder = cls.env["sale.order"] + cls.product_uom_id = cls.env.ref("uom.product_uom_unit") + cls.product = cls.env["product.product"].create( + { + "name": "test", + "type": "product", + "uom_id": cls.product_uom_id.id, + "standard_price": 10.0, + "list_price": 20.00, + "tracking": "none", + } + ) + cls.src_location = cls.env.ref("stock.stock_location_stock") + cls.pricelist = cls.env["product.pricelist"].create( + { + "name": "Test pricelist", + "item_ids": [ + Command.create( + { + "applied_on": "3_global", + "compute_price": "formula", + "base": "list_price", + }, + ) + ], + } + ) + cls.partner = cls.env["res.partner"].create( + {"name": "partner test", "property_product_pricelist": cls.pricelist.id} + ) + cls.quant = cls.env["stock.quant"].create( + { + "product_id": cls.product.id, + "location_id": cls.src_location.id, + "quantity": 100.0, + } + ) + + def _new_sale_order(self, product=None, product_qty=6.0): + """Create a new Order with desired product and quantity""" + if not product: + product = self.product + sale_order_form = Form(self.SaleOrder) + sale_order_form.partner_id = self.partner + with sale_order_form.order_line.new() as order_line_form: + order_line_form.product_id = product + order_line_form.product_uom_qty = product_qty + return sale_order_form.save() + + def get_return_picking_wizard(self, picking): + """Returns the wizard to create a return picking""" + stock_return_picking_form = Form( + self.env["stock.return.picking"].with_context( + active_ids=picking.ids, + active_id=picking.ids[0], + active_model="stock.picking", + ) + ) + return stock_return_picking_form.save() + + def test_sale_margin_ordered(self): + """Non confirmed Order""" + sale_order = self._new_sale_order() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 0.0) + self.assertEqual(order_line.margin_delivered_percent, 0.0) + self.assertEqual(order_line.purchase_price_delivery, 0.0) + + def test_sale_margin_delivered(self): + """Delivered less quantities than ordered""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_ids.quantity = 3.0 + picking.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 30.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_excess(self): + """Delivered quantities exceed ordered quantities""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.quantity = 12.0 + picking.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 120.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_zero(self): + """Zero quantities Order""" + sale_order = self._new_sale_order() + order_line = sale_order.order_line[:1] + order_line.product_uom_qty = 0.0 + self.assertEqual(order_line.margin_delivered, 0.0) + self.assertEqual(order_line.margin_delivered_percent, 0) + + def _create_return(self, picking, qty_refund=3.0, to_refund=False): + """Creates a return picking""" + return_wiz = self.get_return_picking_wizard(picking) + return_wiz.product_return_moves.write( + {"quantity": qty_refund, "to_refund": to_refund} + ) + res = return_wiz.create_returns() + return self.env["stock.picking"].browse(res["res_id"]) + + def _validate_so_picking(self, sale_order, qty_done=6.0): + """Validate picking""" + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.quantity = qty_done + picking.with_context(skip_backorder=True).button_validate() + return picking + + def test_sale_margin_delivered_return_to_refund(self): + """Delivered same quantities than ordered and return half on a refund""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = self._validate_so_picking(sale_order, qty_done=6.0) + picking_return = self._create_return(picking, qty_refund=3.0, to_refund=True) + picking_return.move_line_ids.quantity = 3.0 + picking_return.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 30.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_return_to_refund_excess(self): + """Delivered quantities exceed ordered quantities and return with refund""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = self._validate_so_picking(sale_order, qty_done=12.0) + picking_return = self._create_return(picking, qty_refund=3.0, to_refund=True) + picking_return.move_line_ids.quantity = 3.0 + picking_return.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 90.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_return_no_refund(self): + """Delivered same quantities than ordered and return without refund""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = self._validate_so_picking(sale_order, qty_done=6.0) + picking_return = self._create_return(picking, qty_refund=3.0, to_refund=False) + picking_return.move_line_ids.quantity = 3.0 + picking_return._action_done() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 60.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_return_no_refund_excess(self): + """Delivered quantities exceed ordered quantities and return without refund""" + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = self._validate_so_picking(sale_order, qty_done=12.0) + picking_return = self._create_return(picking, qty_refund=3.0, to_refund=False) + picking_return.move_line_ids.quantity = 3.0 + picking_return._action_done() + order_line = sale_order.order_line[:1] + self.assertEqual(order_line.margin_delivered, 120.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_precision(self): + self.product.standard_price = 10.30 + self.product.list_price = 20.17 + sale_order = self._new_sale_order() + sale_order.order_line[:1].discount = 17.0 + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.quantity = 6.0 + picking.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + # price_subtotal is rounded + self.assertEqual(order_line.price_subtotal, 100.45) + # the unit reduce price will be computed as 100.45 / 6 = 16.741666666666667 + # it should not be rounded to 16.74 + # margin_delivered: + # round(6 * ((100.45 /6) - 10.30)) != round(6 * (16.74 - 10.30)) + self.assertEqual(order_line.margin_delivered, 38.65) + self.assertAlmostEqual(order_line.margin_delivered_percent, 0.38476854156296) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_no_cost(self): + self.product.standard_price = 0.0 + self.product.list_price = 20 + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.quantity = 6.0 + picking.with_context(skip_backorder=True).button_validate() + order_line = sale_order.order_line[:1] + # price_subtotal is rounded + self.assertEqual(order_line.margin_delivered, 120) + self.assertAlmostEqual(order_line.margin_delivered_percent, 1) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) diff --git a/sale_margin_delivered/views/sale_margin_delivered_view.xml b/sale_margin_delivered/views/sale_margin_delivered_view.xml new file mode 100644 index 000000000..b07662acb --- /dev/null +++ b/sale_margin_delivered/views/sale_margin_delivered_view.xml @@ -0,0 +1,26 @@ + + + + + sale.order + + + + + + + + + +