diff --git a/sale_account_line_ipnr_split/README.rst b/sale_account_line_ipnr_split/README.rst new file mode 100644 index 00000000..a4bdaf04 --- /dev/null +++ b/sale_account_line_ipnr_split/README.rst @@ -0,0 +1,38 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg + :target: https://opensource.org/licenses/AGPL-3.0 + :alt: License: AGPL-3 + +============================ +Sale Account Line IPNR Split +============================ + +Splits the IPNR amount embedded in the sale price into a separate line +for each product containing non-recyclable plastic. + +Features: +- IPNR split on sale orders +- IPNR split on invoices +- Split reversal (order and invoice), restoring the original gross price +- Preserves analytic accounting, taxes and line order + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Ana Juaristi +* Lucía Echeverría + +Do not contact contributors directly about support or help with technical issues. + + + diff --git a/sale_account_line_ipnr_split/__init__.py b/sale_account_line_ipnr_split/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sale_account_line_ipnr_split/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_account_line_ipnr_split/__manifest__.py b/sale_account_line_ipnr_split/__manifest__.py new file mode 100644 index 00000000..b89d24b1 --- /dev/null +++ b/sale_account_line_ipnr_split/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +{ + "name": "Sale Account Line IPNR Split", + "summary": "Breakdown of IPNR into separate lines on invoices and sales orders", + "version": "16.0.1.0.0", + "category": "Accounting/Invoicing", + "license": "AGPL-3", + "author": "AvanzOSC", + "website": "https://github.com/avanzosc/sale-addons", + "depends": [ + "sale", + "account", + "l10n_es_aeat_mod592", + ], + "data": [ + "views/account_move_views.xml", + "views/sale_order_views.xml", + "views/res_config_settings_views.xml", + ], + "installable": True, +} diff --git a/sale_account_line_ipnr_split/i18n/ca_ES.po b/sale_account_line_ipnr_split/i18n/ca_ES.po new file mode 100644 index 00000000..aa0a75f5 --- /dev/null +++ b/sale_account_line_ipnr_split/i18n/ca_ES.po @@ -0,0 +1,168 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_account_line_ipnr_split +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-03-09 13:54+0000\n" +"PO-Revision-Date: 2026-03-09 13:54+0000\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_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Bill line to which this IPNR line belongs." +msgstr "Línia de factura a la qual pertany aquesta línia d'IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustos de configuració" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Decimal places used when rounding IPNR prices." +msgstr "Nombre de decimals utilitzats en arrodonir els preus de l'IPNR." + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR Product" +msgstr "Producte IPNR" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "IPNR product not configured. Please set it in Sales Settings → IPNR." +msgstr "Producte IPNR no configurat. Si us plau, configureu-lo a Configuració de Vendes → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_split +msgid "IPNR – Line Breakdown" +msgstr "IPNR – Desglossament de línies" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_reverse +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_reverse +msgid "IPNR – Line Breakdown REVERT" +msgstr "IPNR – Desglossament de línies REVERTIR" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move +msgid "Journal Entry" +msgstr "Apunt comptable" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move_line +msgid "Journal Item" +msgstr "Apunt comptable" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "No record found in l10n.es.aeat.mod592.report. Cannot read amount_plastic_tax." +msgstr "No s'ha trobat cap registre a l10n.es.aeat.mod592.report. No es pot llegir amount_plastic_tax." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +msgid "" +"Number of decimal places used when rounding IPNR unit prices (default: 6)." +msgstr "" +"Nombre de decimals utilitzats en arrodonir els preus unitaris de l'IPNR (per" +" defecte: 6)." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "Original unit price (with IPNR)" +msgstr "Preu unitari original (amb IPNR)" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown and to transfer the gross price to the invoice." +msgstr "" +"Preu unitari original abans del desglossament de l'IPNR (IPNR inclòs). " +"S'utilitza per revertir el desglossament i transferir el preu brut a la " +"factura." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown." +msgstr "" +"Preu unitari original abans del desglossament de l'IPNR (IPNR inclòs). " +"S'utilitza per revertir el desglossament." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Parent invoice line ID" +msgstr "ID de línia de factura mare" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Parent order line ID" +msgstr "ID de línia de comanda mare" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Product used for IPNR breakdown lines." +msgstr "Producte utilitzat per a les línies de desglossament de l'IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +msgid "Product used to represent the IPNR line in sales orders and invoices." +msgstr "" +"Producte utilitzat per representar la línia d'IPNR en comandes de venda i " +"factures." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Rounding decimals" +msgstr "Decimals d'arrodoniment" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order +msgid "Sales Order" +msgstr "Comanda de venda" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línia comanda de venda" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Sales order line to which this IPNR line belongs." +msgstr "Línia de comanda de venda a la qual pertany aquesta línia d'IPNR." + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "The configured IPNR product no longer exists. Please update it in Sales Settings → IPNR." +msgstr "El producte IPNR configurat ja no existeix. Si us plau, actualitzeu-lo a Configuració de Vendes → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "Unit price including IPNR" +msgstr "Preu unitari amb IPNR inclòs" diff --git a/sale_account_line_ipnr_split/i18n/en_US.po b/sale_account_line_ipnr_split/i18n/en_US.po new file mode 100644 index 00000000..933202c4 --- /dev/null +++ b/sale_account_line_ipnr_split/i18n/en_US.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_account_line_ipnr_split +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-03-09 13:54+0000\n" +"PO-Revision-Date: 2026-03-09 13:54+0000\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_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Bill line to which this IPNR line belongs." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Decimal places used when rounding IPNR prices." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR Product" +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "IPNR product not configured. Please set it in Sales Settings → IPNR." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_split +msgid "IPNR – Line Breakdown" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_reverse +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_reverse +msgid "IPNR – Line Breakdown REVERT" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "" +"No record found in l10n.es.aeat.mod592.report. Cannot read " +"amount_plastic_tax." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +msgid "" +"Number of decimal places used when rounding IPNR unit prices (default: 6)." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "Original unit price (with IPNR)" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown and to transfer the gross price to the invoice." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Parent invoice line ID" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Parent order line ID" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Product used for IPNR breakdown lines." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +msgid "Product used to represent the IPNR line in sales orders and invoices." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Rounding decimals" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Sales order line to which this IPNR line belongs." +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "" +"The configured IPNR product no longer exists. Please update it in Sales " +"Settings → IPNR." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "Unit price including IPNR" +msgstr "" diff --git a/sale_account_line_ipnr_split/i18n/es.po b/sale_account_line_ipnr_split/i18n/es.po new file mode 100644 index 00000000..99fadc9c --- /dev/null +++ b/sale_account_line_ipnr_split/i18n/es.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_account_line_ipnr_split +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-03-09 13:54+0000\n" +"PO-Revision-Date: 2026-03-09 13:54+0000\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_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Bill line to which this IPNR line belongs." +msgstr "Línea de factura a la que pertenece esta línea de IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Decimal places used when rounding IPNR prices." +msgstr "Número de decimales usado al redondear los precios del IPNR." + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR Product" +msgstr "Producto IPNR" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "IPNR product not configured. Please set it in Sales Settings → IPNR." +msgstr "Producto IPNR no configurado. Por favor, configúrelo en Ajustes de Ventas → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_split +msgid "IPNR – Line Breakdown" +msgstr "IPNR – Desglose de líneas" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_reverse +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_reverse +msgid "IPNR – Line Breakdown REVERT" +msgstr "IPNR – Desglose de líneas REVERTIR" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move +msgid "Journal Entry" +msgstr "Asiento contable" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move_line +msgid "Journal Item" +msgstr "Apunte contable" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "No record found in l10n.es.aeat.mod592.report. Cannot read amount_plastic_tax." +msgstr "No se ha encontrado ningún registro en l10n.es.aeat.mod592.report. No se puede leer amount_plastic_tax." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +msgid "" +"Number of decimal places used when rounding IPNR unit prices (default: 6)." +msgstr "" +"Número de decimales usado al redondear los precios unitarios del IPNR (por " +"defecto: 6)." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "Original unit price (with IPNR)" +msgstr "Precio unitario original (con IPNR)" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown and to transfer the gross price to the invoice." +msgstr "Precio unitario original (con IPNR)" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown." +msgstr "Precio unitario con IPRN incluido" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Parent invoice line ID" +msgstr "id linea de factura madre" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Parent order line ID" +msgstr "id linea de venta madre" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Product used for IPNR breakdown lines." +msgstr "Producto utilizado para las líneas de desglose del IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +msgid "Product used to represent the IPNR line in sales orders and invoices." +msgstr "" +"Producto utilizado para representar la línea de IPNR en pedidos de venta y " +"facturas." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Rounding decimals" +msgstr "Decimales de redondeo" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order +msgid "Sales Order" +msgstr "Pedido de venta" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Sales order line to which this IPNR line belongs." +msgstr "Línea de pedido de venta a la que pertenece esta línea de IPNR." + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "The configured IPNR product no longer exists. Please update it in Sales Settings → IPNR." +msgstr "El producto IPNR configurado ya no existe. Por favor, actualícelo en Ajustes de Ventas → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "Unit price including IPNR" +msgstr "Precio unitario con IPRN incluido" diff --git a/sale_account_line_ipnr_split/i18n/fr.po b/sale_account_line_ipnr_split/i18n/fr.po new file mode 100644 index 00000000..8db8109c --- /dev/null +++ b/sale_account_line_ipnr_split/i18n/fr.po @@ -0,0 +1,167 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_account_line_ipnr_split +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-03-09 13:54+0000\n" +"PO-Revision-Date: 2026-03-09 13:54+0000\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_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Bill line to which this IPNR line belongs." +msgstr "Ligne de facture à laquelle appartient cette ligne IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de configuration" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Decimal places used when rounding IPNR prices." +msgstr "Nombre de décimales utilisées pour arrondir les prix IPNR." + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR Product" +msgstr "Produit IPNR" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "IPNR product not configured. Please set it in Sales Settings → IPNR." +msgstr "Produit IPNR non configuré. Veuillez le définir dans Paramètres des ventes → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_split +msgid "IPNR – Line Breakdown" +msgstr "IPNR – Ventilation des lignes" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_reverse +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_reverse +msgid "IPNR – Line Breakdown REVERT" +msgstr "IPNR – Ventilation des lignes ANNULER" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move_line +msgid "Journal Item" +msgstr "Écriture comptable" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "No record found in l10n.es.aeat.mod592.report. Cannot read amount_plastic_tax." +msgstr "Aucun enregistrement trouvé dans l10n.es.aeat.mod592.report. Impossible de lire amount_plastic_tax." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +msgid "" +"Number of decimal places used when rounding IPNR unit prices (default: 6)." +msgstr "" +"Nombre de décimales utilisées pour arrondir les prix unitaires IPNR (par " +"défaut : 6)." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "Original unit price (with IPNR)" +msgstr "Prix unitaire d'origine (avec IPNR)" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown and to transfer the gross price to the invoice." +msgstr "" +"Prix unitaire d'origine avant ventilation de l'IPNR (IPNR inclus). Utilisé " +"pour annuler la ventilation et transférer le prix brut à la facture." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown." +msgstr "" +"Prix unitaire d'origine avant ventilation de l'IPNR (IPNR inclus). Utilisé " +"pour annuler la ventilation." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Parent invoice line ID" +msgstr "ID de la ligne de facture parente" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Parent order line ID" +msgstr "ID de la ligne de commande parente" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Product used for IPNR breakdown lines." +msgstr "Produit utilisé pour les lignes de ventilation IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +msgid "Product used to represent the IPNR line in sales orders and invoices." +msgstr "" +"Produit utilisé pour représenter la ligne IPNR dans les commandes client et " +"les factures." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Rounding decimals" +msgstr "Décimales d'arrondi" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order +msgid "Sales Order" +msgstr "Commande client" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order_line +msgid "Sales Order Line" +msgstr "Ligne de commande client" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Sales order line to which this IPNR line belongs." +msgstr "Ligne de commande client à laquelle appartient cette ligne IPNR." + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "The configured IPNR product no longer exists. Please update it in Sales Settings → IPNR." +msgstr "Le produit IPNR configuré n'existe plus. Veuillez le mettre à jour dans Paramètres des ventes → IPNR." + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "Unit price including IPNR" +msgstr "Prix unitaire IPNR inclus" diff --git a/sale_account_line_ipnr_split/i18n/sale_account_line_ipnr_split.pot b/sale_account_line_ipnr_split/i18n/sale_account_line_ipnr_split.pot new file mode 100644 index 00000000..e5e5aa15 --- /dev/null +++ b/sale_account_line_ipnr_split/i18n/sale_account_line_ipnr_split.pot @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_account_line_ipnr_split +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-03-09 13:53+0000\n" +"PO-Revision-Date: 2026-03-09 13:53+0000\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_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Bill line to which this IPNR line belongs." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Decimal places used when rounding IPNR prices." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "IPNR Product" +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "IPNR product not configured. Please set it in Sales Settings → IPNR." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_split +msgid "IPNR – Line Breakdown" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_account_move_ipnr_reverse +#: model:ir.actions.server,name:sale_account_line_ipnr_split.action_sale_order_ipnr_reverse +msgid "IPNR – Line Breakdown REVERT" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "" +"No record found in l10n.es.aeat.mod592.report. Cannot read " +"amount_plastic_tax." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +msgid "" +"Number of decimal places used when rounding IPNR unit prices (default: 6)." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "Original unit price (with IPNR)" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown and to transfer the gross price to the invoice." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "" +"Original unit price before IPNR breakdown (IPNR included). It is used to " +"reverse the breakdown." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_parent_line_id +msgid "Parent invoice line ID" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Parent order line ID" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Product used for IPNR breakdown lines." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_product_id +msgid "Product used to represent the IPNR line in sales orders and invoices." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_res_config_settings__x_ipnr_rounding_digits +#: model_terms:ir.ui.view,arch_db:sale_account_line_ipnr_split.res_config_settings_view_form +msgid "Rounding decimals" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model,name:sale_account_line_ipnr_split.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,help:sale_account_line_ipnr_split.field_sale_order_line__x_ipnr_parent_line_id +msgid "Sales order line to which this IPNR line belongs." +msgstr "" + +#. module: sale_account_line_ipnr_split +#. odoo-python +#: code:addons/sale_account_line_ipnr_split/models/account_move.py:0 +#: code:addons/sale_account_line_ipnr_split/models/sale_order.py:0 +#, python-format +msgid "" +"The configured IPNR product no longer exists. Please update it in Sales " +"Settings → IPNR." +msgstr "" + +#. module: sale_account_line_ipnr_split +#: model:ir.model.fields,field_description:sale_account_line_ipnr_split.field_account_move_line__x_ipnr_gross_price_unit +msgid "Unit price including IPNR" +msgstr "" diff --git a/sale_account_line_ipnr_split/models/__init__.py b/sale_account_line_ipnr_split/models/__init__.py new file mode 100644 index 00000000..f1acba0d --- /dev/null +++ b/sale_account_line_ipnr_split/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_move_line +from . import account_move +from . import sale_order_line +from . import sale_order +from . import res_config_settings diff --git a/sale_account_line_ipnr_split/models/account_move.py b/sale_account_line_ipnr_split/models/account_move.py new file mode 100644 index 00000000..74a6e814 --- /dev/null +++ b/sale_account_line_ipnr_split/models/account_move.py @@ -0,0 +1,234 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import _, models +from odoo.exceptions import UserError + +INVOICE_TYPES = ("out_invoice", "out_refund", "in_invoice", "in_refund") + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _get_ipnr_rounding(self): + digits = int( + self.env["ir.config_parameter"].sudo().get_param("ipnr.rounding_digits") + ) + return digits if digits else 0 + + def _get_ipnr_product(self): + product_id = self.env["ir.config_parameter"].sudo().get_param("ipnr.product_id") + if not product_id: + raise UserError( + _( + "IPNR product not configured. Please set it in Sales Settings → IPNR." + ) + ) + product = self.env["product.product"].browse(int(product_id)) + if not product.exists(): + raise UserError( + _( + "The configured IPNR product no longer exists." + " Please update it in Sales Settings → IPNR." + ) + ) + return product + + def _get_ipnr_rate(self): + report = self.env["l10n.es.aeat.mod592.report"].search([], limit=1) + if not report: + raise UserError( + _( + "No record found in l10n.es.aeat.mod592.report. " + "Cannot read amount_plastic_tax." + ) + ) + rate = report.amount_plastic_tax or 0.0 + if rate <= 0: + raise UserError( + f"Invalid amount_plastic_tax (must be > 0) on " + f"l10n.es.aeat.mod592.report. Current: {rate}" + ) + return rate + + def _assert_invoice_type(self): + if self.move_type not in INVOICE_TYPES: + raise UserError( + f"This action is only allowed for invoices/refunds " + f"({', '.join(INVOICE_TYPES)}). Current type: {self.move_type}" + ) + + def _assert_draft_state(self, action="perform this action"): + if self.state != "draft": + raise UserError( + f"Move must be in draft to {action}. Current state: {self.state}" + ) + + def _is_regular_line(self, line): + return ( + line.display_type not in ("line_section", "line_note") and line.product_id + ) + + def _remove_ipnr_lines(self, ipnr_product): + self.invoice_line_ids.filtered( + lambda line: self._is_regular_line(line) and line.product_id == ipnr_product + ).unlink() + + def _source_lines(self, ipnr_product): + return self.invoice_line_ids.filtered( + lambda line: self._is_regular_line(line) and line.product_id != ipnr_product + ).sorted(key=lambda line: (line.sequence, line.id)) + + def _find_sale_parent_line(self, invoice_line, ipnr_product): + for sale_line in invoice_line.sale_line_ids: + if sale_line.product_id and sale_line.product_id != ipnr_product: + return sale_line + return None + + def _sync_gross_from_so(self, source_lines, ipnr_product, rounding): + for line in source_lines: + if line.x_ipnr_gross_price_unit: + continue + sale_parent = self._find_sale_parent_line(line, ipnr_product) + if sale_parent and sale_parent.x_ipnr_gross_price_unit: + line.write( + { + "x_ipnr_gross_price_unit": round( + sale_parent.x_ipnr_gross_price_unit, rounding + ) + } + ) + + def _resequence_lines(self, source_lines): + for seq, line in enumerate(source_lines, start=1): + new_seq = seq * 10 + if line.sequence != new_seq: + line.write({"sequence": new_seq}) + + def action_ipnr_split(self): + self.ensure_one() + + ipnr_product = self._get_ipnr_product() + rounding = self._get_ipnr_rounding() + + rate = self._get_ipnr_rate() + self._assert_invoice_type() + self._assert_draft_state("split IPNR") + self._remove_ipnr_lines(ipnr_product) + + source_lines = self._source_lines(ipnr_product) + self._sync_gross_from_so(source_lines, ipnr_product, rounding) + self._resequence_lines(source_lines) + + new_ipnr_lines = [] + + for line in source_lines: + qty = line.quantity or 0.0 + if qty <= 0: + continue + + non_recyclable_kg = line.product_id.plastic_weight_non_recyclable or 0.0 + ipnr_unit_price = round(rate * non_recyclable_kg, rounding) + + if ipnr_unit_price <= 0: + if line.x_ipnr_gross_price_unit: + line.write( + { + "price_unit": round(line.x_ipnr_gross_price_unit, rounding), + "x_ipnr_gross_price_unit": 0.0, + } + ) + continue + + gross_visible = round( + line.x_ipnr_gross_price_unit or line.price_unit, rounding + ) + + discount = line.discount or 0.0 + discount_factor = 1.0 - (discount / 100.0) + if discount_factor <= 0.0: + raise UserError( + f"Line '{line.product_id.display_name}' has 100% discount; " + f"cannot split embedded IPNR safely." + ) + + price_after_discount = round(gross_visible * discount_factor, rounding) + net_after_discount = round(price_after_discount - ipnr_unit_price, rounding) + + if net_after_discount < 0: + raise UserError( + f"Line '{line.product_id.display_name}' results in a negative net " + f"(after discount): {price_after_discount:.{rounding}f} - " + f"{ipnr_unit_price:.{rounding}f} = {net_after_discount:.{rounding}f}" + ) + + net_visible = round(net_after_discount / discount_factor, rounding) + + line.write( + { + "x_ipnr_gross_price_unit": gross_visible, + "price_unit": net_visible, + } + ) + + ipnr_vals = { + "sequence": (line.sequence or 0) + 1, + "product_id": ipnr_product.id, + "name": f"IPNR ({rate:.2f} €/kg) - {line.name}", + "quantity": qty, + "product_uom_id": line.product_uom_id.id, + "price_unit": ipnr_unit_price, + "discount": 0.0, + "tax_ids": [(6, 0, line.tax_ids.ids)], + "x_ipnr_parent_line_id": line.id, + } + if line.account_id: + ipnr_vals["account_id"] = line.account_id.id + if line.analytic_distribution: + ipnr_vals["analytic_distribution"] = line.analytic_distribution + + new_ipnr_lines.append((0, 0, ipnr_vals)) + + if new_ipnr_lines: + self.write({"invoice_line_ids": new_ipnr_lines}) + + def action_ipnr_reverse(self): + self.ensure_one() + + ipnr_product = self._get_ipnr_product() + rounding = self._get_ipnr_rounding() + + self._assert_invoice_type() + self._assert_draft_state("revert IPNR split") + self._remove_ipnr_lines(ipnr_product) + + restored = 0 + copied_from_so = 0 + + for line in self._source_lines(ipnr_product): + gross_saved = line.x_ipnr_gross_price_unit + + if not gross_saved: + sale_parent = self._find_sale_parent_line(line, ipnr_product) + if sale_parent and sale_parent.x_ipnr_gross_price_unit: + gross_saved = sale_parent.x_ipnr_gross_price_unit + line.write( + {"x_ipnr_gross_price_unit": round(gross_saved, rounding)} + ) + copied_from_so += 1 + + if gross_saved: + line.write({"price_unit": round(gross_saved, rounding)}) + restored += 1 + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": "IPNR", + "message": ( + f"Revert done. Restored {restored} line(s). " + f"Copied gross from SO for {copied_from_so} line(s)." + ), + "sticky": False, + }, + } diff --git a/sale_account_line_ipnr_split/models/account_move_line.py b/sale_account_line_ipnr_split/models/account_move_line.py new file mode 100644 index 00000000..cbe807d2 --- /dev/null +++ b/sale_account_line_ipnr_split/models/account_move_line.py @@ -0,0 +1,21 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + x_ipnr_gross_price_unit = fields.Float( + string="Unit price including IPNR", + store=True, + copy=True, + help="Original unit price before IPNR breakdown (IPNR included). " + "It is used to reverse the breakdown.", + ) + x_ipnr_parent_line_id = fields.Many2one( + comodel_name="account.move.line", + string="Parent invoice line ID", + store=True, + help="Bill line to which this IPNR line belongs.", + ) diff --git a/sale_account_line_ipnr_split/models/res_config_settings.py b/sale_account_line_ipnr_split/models/res_config_settings.py new file mode 100644 index 00000000..4e681268 --- /dev/null +++ b/sale_account_line_ipnr_split/models/res_config_settings.py @@ -0,0 +1,21 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + x_ipnr_product_id = fields.Many2one( + comodel_name="product.product", + string="IPNR Product", + config_parameter="ipnr.product_id", + domain="[('default_code', '!=', False)]", + help="Product used to represent the IPNR line in sales orders and invoices.", + ) + x_ipnr_rounding_digits = fields.Integer( + string="Rounding decimals", + config_parameter="ipnr.rounding_digits", + default=6, + help="Number of decimal places used when rounding IPNR unit prices (default: 6).", + ) diff --git a/sale_account_line_ipnr_split/models/sale_order.py b/sale_account_line_ipnr_split/models/sale_order.py new file mode 100644 index 00000000..9a44ed7f --- /dev/null +++ b/sale_account_line_ipnr_split/models/sale_order.py @@ -0,0 +1,182 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import _, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _get_ipnr_rounding(self): + digits = int( + self.env["ir.config_parameter"].sudo().get_param("ipnr.rounding_digits") + ) + return digits if digits else 0 + + def _get_ipnr_product(self): + product_id = self.env["ir.config_parameter"].sudo().get_param("ipnr.product_id") + if not product_id: + raise UserError( + _( + "IPNR product not configured. Please set it in Sales Settings → IPNR." + ) + ) + product = self.env["product.product"].browse(int(product_id)) + if not product.exists(): + raise UserError( + _( + "The configured IPNR product no longer exists." + " Please update it in Sales Settings → IPNR." + ) + ) + return product + + def _get_ipnr_rate(self): + report = self.env["l10n.es.aeat.mod592.report"].search([], limit=1) + if not report: + raise UserError( + _( + "No record found in l10n.es.aeat.mod592.report. " + "Cannot read amount_plastic_tax." + ) + ) + rate = report.amount_plastic_tax or 0.0 + if rate <= 0: + raise UserError( + f"Invalid amount_plastic_tax (must be > 0) on " + f"l10n.es.aeat.mod592.report. Current: {rate}" + ) + return rate + + def _assert_quotation_state(self): + if self.state not in ("draft", "sent"): + raise UserError( + f"Order {self.name} is not in quotation/sent state. " + f"Current state: {self.state}" + ) + + def _is_regular_line(self, line): + return ( + line.display_type not in ("line_section", "line_note") and line.product_id + ) + + def _remove_ipnr_lines(self, ipnr_product): + self.order_line.filtered( + lambda line: self._is_regular_line(line) and line.product_id == ipnr_product + ).unlink() + + def _source_lines(self, ipnr_product): + return self.order_line.filtered( + lambda line: self._is_regular_line(line) and line.product_id != ipnr_product + ).sorted(key=lambda line: (line.sequence, line.id)) + + def action_ipnr_split(self): + self.ensure_one() + + ipnr_product = self._get_ipnr_product() + rounding = self._get_ipnr_rounding() + + rate = self._get_ipnr_rate() + self._assert_quotation_state() + self._remove_ipnr_lines(ipnr_product) + + new_ipnr_lines = [] + + for line in self._source_lines(ipnr_product): + qty = line.product_uom_qty or 0.0 + if qty <= 0: + continue + + non_recyclable_kg = line.product_id.plastic_weight_non_recyclable or 0.0 + ipnr_unit_price = round(rate * non_recyclable_kg, rounding) + + if ipnr_unit_price <= 0: + if line.x_ipnr_gross_price_unit: + line.write( + { + "price_unit": round(line.x_ipnr_gross_price_unit, rounding), + "x_ipnr_gross_price_unit": 0.0, + } + ) + continue + + discount = line.discount or 0.0 + discount_factor = 1.0 - (discount / 100.0) + if discount_factor <= 0.0: + raise UserError( + f"Line '{line.product_id.display_name}' has 100% discount; " + f"cannot split embedded IPNR safely." + ) + + gross_visible = round( + line.x_ipnr_gross_price_unit or line.price_unit, rounding + ) + price_after_discount = round(gross_visible * discount_factor, rounding) + net_after_discount = round(price_after_discount - ipnr_unit_price, rounding) + + if net_after_discount < 0: + raise UserError( + f"Line '{line.product_id.display_name}' results in a negative net " + f"(after discount): {price_after_discount:.{rounding}f} - " + f"{ipnr_unit_price:.{rounding}f} = {net_after_discount:.{rounding}f}" + ) + + new_price_unit = round(net_after_discount / discount_factor, rounding) + line.write( + { + "x_ipnr_gross_price_unit": gross_visible, + "price_unit": new_price_unit, + } + ) + + ipnr_vals = { + "sequence": line.sequence or 0, + "product_id": ipnr_product.id, + "name": f"IPNR ({rate:.2f} €/kg) - {line.name}", + "product_uom_qty": qty, + "product_uom": line.product_uom.id, + "price_unit": ipnr_unit_price, + "discount": 0.0, + "x_ipnr_parent_line_id": line.id, + } + if "tax_id" in line._fields: + ipnr_vals["tax_id"] = [(6, 0, line.tax_id.ids)] + + new_ipnr_lines.append((0, 0, ipnr_vals)) + + if new_ipnr_lines: + self.write({"order_line": new_ipnr_lines}) + + self._resequence_lines(ipnr_product) + + def action_ipnr_reverse(self): + self.ensure_one() + + ipnr_product = self._get_ipnr_product() + rounding = self._get_ipnr_rounding() + + self._assert_quotation_state() + self._remove_ipnr_lines(ipnr_product) + + for line in self._source_lines(ipnr_product): + if not line.x_ipnr_gross_price_unit: + continue + line.write({"price_unit": round(line.x_ipnr_gross_price_unit, rounding)}) + + def _resequence_lines(self, ipnr_product): + lines = self.order_line.sorted(key=lambda line: (line.sequence, line.id)) + ipnr_by_parent = {} + for line in lines: + if line.product_id == ipnr_product: + parent = line.x_ipnr_parent_line_id + if parent: + ipnr_by_parent.setdefault(parent.id, []).append(line) + base_lines = [line for line in lines if line.product_id != ipnr_product] + ordered = [] + for base in base_lines: + ordered.append(base) + ordered.extend(sorted(ipnr_by_parent.get(base.id, []), key=lambda x: x.id)) + for seq, line in enumerate(ordered, start=1): + new_seq = seq * 10 + if line.sequence != new_seq: + line.write({"sequence": new_seq}) diff --git a/sale_account_line_ipnr_split/models/sale_order_line.py b/sale_account_line_ipnr_split/models/sale_order_line.py new file mode 100644 index 00000000..0a75aa66 --- /dev/null +++ b/sale_account_line_ipnr_split/models/sale_order_line.py @@ -0,0 +1,22 @@ +# Copyright 2026 Lucía Echeverría - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + x_ipnr_gross_price_unit = fields.Float( + string="Original unit price (with IPNR)", + store=True, + copy=True, + help="Original unit price before IPNR breakdown (IPNR included). " + "It is used to reverse the breakdown and to transfer the gross price to the invoice.", + ) + x_ipnr_parent_line_id = fields.Many2one( + comodel_name="sale.order.line", + ondelete="set null", + string="Parent order line ID", + store=True, + help="Sales order line to which this IPNR line belongs.", + ) diff --git a/sale_account_line_ipnr_split/static/description/icon.png b/sale_account_line_ipnr_split/static/description/icon.png new file mode 100644 index 00000000..5434cfa0 Binary files /dev/null and b/sale_account_line_ipnr_split/static/description/icon.png differ diff --git a/sale_account_line_ipnr_split/views/account_move_views.xml b/sale_account_line_ipnr_split/views/account_move_views.xml new file mode 100644 index 00000000..5d0a4a57 --- /dev/null +++ b/sale_account_line_ipnr_split/views/account_move_views.xml @@ -0,0 +1,18 @@ + + + IPNR – Line Breakdown + + + list,form + code + records.action_ipnr_split() + + + IPNR – Line Breakdown REVERT + + + list,form + code + records.action_ipnr_reverse() + + diff --git a/sale_account_line_ipnr_split/views/res_config_settings_views.xml b/sale_account_line_ipnr_split/views/res_config_settings_views.xml new file mode 100644 index 00000000..5ea613df --- /dev/null +++ b/sale_account_line_ipnr_split/views/res_config_settings_views.xml @@ -0,0 +1,40 @@ + + + res.config.settings + + +
+

IPNR

+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/sale_account_line_ipnr_split/views/sale_order_views.xml b/sale_account_line_ipnr_split/views/sale_order_views.xml new file mode 100644 index 00000000..60221b86 --- /dev/null +++ b/sale_account_line_ipnr_split/views/sale_order_views.xml @@ -0,0 +1,18 @@ + + + IPNR – Line Breakdown + + + list,form + code + records.action_ipnr_split() + + + IPNR – Line Breakdown REVERT + + + list,form + code + records.action_ipnr_reverse() + + diff --git a/setup/sale_account_line_ipnr_split/odoo/addons/sale_account_line_ipnr_split b/setup/sale_account_line_ipnr_split/odoo/addons/sale_account_line_ipnr_split new file mode 120000 index 00000000..a06b1101 --- /dev/null +++ b/setup/sale_account_line_ipnr_split/odoo/addons/sale_account_line_ipnr_split @@ -0,0 +1 @@ +../../../../sale_account_line_ipnr_split \ No newline at end of file diff --git a/setup/sale_account_line_ipnr_split/setup.py b/setup/sale_account_line_ipnr_split/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/sale_account_line_ipnr_split/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)