From a754df3914019005808ecb43756629ea804c6ec9 Mon Sep 17 00:00:00 2001 From: luis Date: Tue, 23 Jun 2026 14:28:38 +0200 Subject: [PATCH] Ecocentral/ecocentral#172: migrate purchase_rappel to 18.0 --- purchase_rappel/README.md | 40 +++ purchase_rappel/__init__.py | 4 + purchase_rappel/__manifest__.py | 22 ++ purchase_rappel/i18n/ca.po | 167 +++++++++ purchase_rappel/i18n/es.po | 167 +++++++++ purchase_rappel/i18n/gl.po | 167 +++++++++ purchase_rappel/i18n/purchase_rappel.pot | 166 +++++++++ purchase_rappel/models/__init__.py | 6 + purchase_rappel/models/account_move.py | 32 ++ purchase_rappel/models/res_company.py | 19 + purchase_rappel/models/res_config_settings.py | 23 ++ purchase_rappel/models/res_partner.py | 19 + purchase_rappel/readme/CONFIGURE.md | 13 + purchase_rappel/readme/DESCRIPTION.md | 7 + purchase_rappel/readme/USAGE.md | 15 + purchase_rappel/security/ir.model.access.csv | 2 + purchase_rappel/tests/__init__.py | 4 + purchase_rappel/tests/test_purchase_rappel.py | 333 ++++++++++++++++++ purchase_rappel/views/account_move_views.xml | 51 +++ .../views/res_config_settings_views.xml | 26 ++ purchase_rappel/views/res_partner_views.xml | 20 ++ purchase_rappel/wizard/__init__.py | 3 + .../wizard/account_move_rappel_wizard.py | 220 ++++++++++++ .../account_move_rappel_wizard_views.xml | 62 ++++ 24 files changed, 1588 insertions(+) create mode 100644 purchase_rappel/README.md create mode 100644 purchase_rappel/__init__.py create mode 100644 purchase_rappel/__manifest__.py create mode 100644 purchase_rappel/i18n/ca.po create mode 100644 purchase_rappel/i18n/es.po create mode 100644 purchase_rappel/i18n/gl.po create mode 100644 purchase_rappel/i18n/purchase_rappel.pot create mode 100644 purchase_rappel/models/__init__.py create mode 100644 purchase_rappel/models/account_move.py create mode 100644 purchase_rappel/models/res_company.py create mode 100644 purchase_rappel/models/res_config_settings.py create mode 100644 purchase_rappel/models/res_partner.py create mode 100644 purchase_rappel/readme/CONFIGURE.md create mode 100644 purchase_rappel/readme/DESCRIPTION.md create mode 100644 purchase_rappel/readme/USAGE.md create mode 100644 purchase_rappel/security/ir.model.access.csv create mode 100644 purchase_rappel/tests/__init__.py create mode 100644 purchase_rappel/tests/test_purchase_rappel.py create mode 100644 purchase_rappel/views/account_move_views.xml create mode 100644 purchase_rappel/views/res_config_settings_views.xml create mode 100644 purchase_rappel/views/res_partner_views.xml create mode 100644 purchase_rappel/wizard/__init__.py create mode 100644 purchase_rappel/wizard/account_move_rappel_wizard.py create mode 100644 purchase_rappel/wizard/account_move_rappel_wizard_views.xml diff --git a/purchase_rappel/README.md b/purchase_rappel/README.md new file mode 100644 index 0000000..dda3969 --- /dev/null +++ b/purchase_rappel/README.md @@ -0,0 +1,40 @@ +# Purchase Rappel Commission + +This module allows managing **rappel commission invoices** for vendors. + +## Features + +- Configure a rappel commission rate per vendor. +- Automatically mark vendor bills as "pending commission". +- Generate customer (sale) invoices from selected vendor bills via a wizard. +- Group invoices by vendor and compute commission amounts based on the taxable base. + +## Configuration + +Go to **Purchase > Configuration > Settings > Invoicing** and set: + +- **Rappel Journal**: Sales journal where rappel commission invoices will be posted. +- **Commission Product**: Product used as invoice line in rappel commission invoices (should have an income account and taxes configured). + +Open a vendor contact and go to the **Sales & Purchases** tab: + +- Enable **Rappel Commission**. +- Set the **Commission Rate (%)** (e.g., 5.00). + +## Usage + +When a vendor bill is created for a partner with rappel enabled, the bill is automatically marked as **Pending Commission**. This flag is visible in the vendor bill list and form views. + +To generate rappel invoices: + +1. Go to **Invoicing > Vendors > Bills**. +2. Select one or more posted vendor bills with pending commission. +3. Click **Action > Create Rappel Commission Invoice**. +4. In the wizard, confirm the **Invoice Date**, **Journal**, and **Product**. +5. Click **Create Rappel Invoice(s)**. + +The system will create one sale invoice per vendor, with one line per original bill. The commission amount is computed as: + + taxable_base × commission_rate / 100 + +The original vendor bills are automatically unmarked as pending. diff --git a/purchase_rappel/__init__.py b/purchase_rappel/__init__.py new file mode 100644 index 0000000..08ff432 --- /dev/null +++ b/purchase_rappel/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models +from . import wizard diff --git a/purchase_rappel/__manifest__.py b/purchase_rappel/__manifest__.py new file mode 100644 index 0000000..5a5850e --- /dev/null +++ b/purchase_rappel/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Purchase Rappel Commission", + "version": "18.0.1.0.0", + "summary": "Manage rappel commission invoices for vendors", + "category": "Purchase", + "author": "Criptomart", + "website": "https://criptomart.net", + "license": "AGPL-3", + "depends": ["purchase", "account"], + "data": [ + "security/ir.model.access.csv", + "wizard/account_move_rappel_wizard_views.xml", + "views/res_partner_views.xml", + "views/res_config_settings_views.xml", + "views/account_move_views.xml", + ], + "installable": True, + "application": False, +} diff --git a/purchase_rappel/i18n/ca.po b/purchase_rappel/i18n/ca.po new file mode 100644 index 0000000..b396106 --- /dev/null +++ b/purchase_rappel/i18n/ca.po @@ -0,0 +1,167 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_rappel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-12 10:00+0000\n" +"PO-Revision-Date: 2024-06-12 10:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ca\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" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Rappel Journal" +msgstr "Diari de Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Sales journal used to post rappel commission invoices." +msgstr "Diari de vendes utilitzat per registrar les factures de comissió per rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__product_id +msgid "Commission Product" +msgstr "Producte de Comissió" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_product_id +msgid "Product used as invoice line in rappel commission invoices." +msgstr "Producte utilitzat com a línia de factura en les factures de comissió per rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_partner_form_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Rappel Commission" +msgstr "Comissió per Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission +msgid "If enabled, vendor bills from this partner will be eligible for rappel commission invoicing." +msgstr "Si està activat, les factures de proveïdor d'aquest contacte seran elegibles per a la facturació de comissió per rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Commission Rate (%)" +msgstr "Taxa de Comissió (%)" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Percentage applied on the taxable base of vendor bills to calculate the rappel commission amount." +msgstr "Percentatge aplicat sobre la base imposable de les factures de proveïdor per calcular l'import de la comissió per rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move__rappel_pending +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Pending Commission" +msgstr "Pendent de Comissió" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_account_move__rappel_pending +msgid "Indicates that this vendor bill has not yet been included in a rappel commission invoice. Automatically set based on the vendor's configuration." +msgstr "Indica que aquesta factura de proveïdor encara no ha estat inclosa en una factura de comissió per rappel. S'estableix automàticament segons la configuració del proveïdor." + +#. module: purchase_rappel +#: model:ir.model,name:purchase_rappel.model_account_move_rappel_wizard +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Commission Invoice" +msgstr "Crear Factura de Comissió per Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__move_ids +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Vendor Bills" +msgstr "Factures de Proveïdor" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__invoice_date +msgid "Invoice Date" +msgstr "Data de Factura" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__journal_id +msgid "Journal" +msgstr "Diari" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "No posted vendor bills found in the selection. Only posted (validated) vendor bills can be used to create rappel commission invoices." +msgstr "No s'han trobat factures de proveïdor comptabilitzades en la selecció. Només les factures de proveïdor comptabilitzades (validades) es poden fer servir per crear factures de comissió per rappel." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills are pending commission. They may have already been commissioned." +msgstr "Cap de les factures de proveïdor seleccionades està pendent de comissió. Pot ser que ja hagin estat comissionades." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendors have the rappel commission enabled. Please configure the commission rate on the vendor contact first." +msgstr "Cap dels proveïdors seleccionats té la comissió per rappel activada. Configureu primer la taxa de comissió en el contacte del proveïdor." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a journal for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccioneu un diari per a les factures de comissió per rappel, o configureu-ne un per defecte en els Ajustos de Compres." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a product for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccioneu un producte per a les factures de comissió per rappel, o configureu-ne un per defecte en els Ajustos de Compres." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills belong to vendors with a rappel commission rate configured." +msgstr "Cap de les factures de proveïdor seleccionades pertany a proveïdors amb una taxa de comissió per rappel configurada." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission %(rate)s%% — Bill %(bill)s (taxable base: %(base)s %(currency)s)" +msgstr "Comissió per rappel %(rate)s%% — Factura %(bill)s (base imposable: %(base)s %(currency)s)" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission invoice generated from %d vendor bill(s)." +msgstr "Factura de comissió per rappel generada a partir de %d factura(es) de proveïdor." + +#. module: purchase_rappel +#: model:ir.actions.act_window,name:purchase_rappel.action_open_rappel_wizard +msgid "Rappel Commission Invoices" +msgstr "Factures de Comissió per Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Configure the journal and product for rappel commission invoices" +msgstr "Configureu el diari i el producte per a les factures de comissió per rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Vendor bills not yet included in a rappel commission invoice" +msgstr "Factures de proveïdor que encara no estan incloses en una factura de comissió per rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Taxable Base" +msgstr "Base Imposable" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Invoice(s)" +msgstr "Crear Factura(es) de Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Cancel" +msgstr "Cancel·lar" diff --git a/purchase_rappel/i18n/es.po b/purchase_rappel/i18n/es.po new file mode 100644 index 0000000..77ab323 --- /dev/null +++ b/purchase_rappel/i18n/es.po @@ -0,0 +1,167 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_rappel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-12 10:00+0000\n" +"PO-Revision-Date: 2024-06-12 10:00+0000\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: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Rappel Journal" +msgstr "Diario de Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Sales journal used to post rappel commission invoices." +msgstr "Diario de ventas utilizado para registrar las facturas de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__product_id +msgid "Commission Product" +msgstr "Producto de Comisión" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_product_id +msgid "Product used as invoice line in rappel commission invoices." +msgstr "Producto utilizado como línea de factura en las facturas de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_partner_form_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Rappel Commission" +msgstr "Comisión por Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission +msgid "If enabled, vendor bills from this partner will be eligible for rappel commission invoicing." +msgstr "Si está activado, las facturas de proveedor de este contacto serán elegibles para la facturación de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Commission Rate (%)" +msgstr "Tasa de Comisión (%)" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Percentage applied on the taxable base of vendor bills to calculate the rappel commission amount." +msgstr "Porcentaje aplicado sobre la base imponible de las facturas de proveedor para calcular el importe de la comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move__rappel_pending +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Pending Commission" +msgstr "Pendiente de Comisión" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_account_move__rappel_pending +msgid "Indicates that this vendor bill has not yet been included in a rappel commission invoice. Automatically set based on the vendor's configuration." +msgstr "Indica que esta factura de proveedor aún no ha sido incluida en una factura de comisión por rappel. Se establece automáticamente según la configuración del proveedor." + +#. module: purchase_rappel +#: model:ir.model,name:purchase_rappel.model_account_move_rappel_wizard +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Commission Invoice" +msgstr "Crear Factura de Comisión por Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__move_ids +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Vendor Bills" +msgstr "Facturas de Proveedor" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__invoice_date +msgid "Invoice Date" +msgstr "Fecha de Factura" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__journal_id +msgid "Journal" +msgstr "Diario" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "No posted vendor bills found in the selection. Only posted (validated) vendor bills can be used to create rappel commission invoices." +msgstr "No se encontraron facturas de proveedor contabilizadas en la selección. Solo las facturas de proveedor contabilizadas (validadas) pueden usarse para crear facturas de comisión por rappel." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills are pending commission. They may have already been commissioned." +msgstr "Ninguna de las facturas de proveedor seleccionadas está pendiente de comisión. Puede que ya hayan sido comisionadas." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendors have the rappel commission enabled. Please configure the commission rate on the vendor contact first." +msgstr "Ninguno de los proveedores seleccionados tiene la comisión por rappel activada. Configure primero la tasa de comisión en el contacto del proveedor." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a journal for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccione un diario para las facturas de comisión por rappel, o configure uno por defecto en los Ajustes de Compras." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a product for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccione un producto para las facturas de comisión por rappel, o configure uno por defecto en los Ajustes de Compras." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills belong to vendors with a rappel commission rate configured." +msgstr "Ninguna de las facturas de proveedor seleccionadas pertenece a proveedores con una tasa de comisión por rappel configurada." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission %(rate)s%% — Bill %(bill)s (taxable base: %(base)s %(currency)s)" +msgstr "Comisión por rappel %(rate)s%% — Factura %(bill)s (base imponible: %(base)s %(currency)s)" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission invoice generated from %d vendor bill(s)." +msgstr "Factura de comisión por rappel generada a partir de %d factura(s) de proveedor." + +#. module: purchase_rappel +#: model:ir.actions.act_window,name:purchase_rappel.action_open_rappel_wizard +msgid "Rappel Commission Invoices" +msgstr "Facturas de Comisión por Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Configure the journal and product for rappel commission invoices" +msgstr "Configure el diario y el producto para las facturas de comisión por rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Vendor bills not yet included in a rappel commission invoice" +msgstr "Facturas de proveedor que aún no están incluidas en una factura de comisión por rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Taxable Base" +msgstr "Base Imponible" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Invoice(s)" +msgstr "Crear Factura(s) de Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Cancel" +msgstr "Cancelar" diff --git a/purchase_rappel/i18n/gl.po b/purchase_rappel/i18n/gl.po new file mode 100644 index 0000000..6731153 --- /dev/null +++ b/purchase_rappel/i18n/gl.po @@ -0,0 +1,167 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_rappel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-12 10:00+0000\n" +"PO-Revision-Date: 2024-06-12 10:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: gl\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" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Rappel Journal" +msgstr "Diario de Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Sales journal used to post rappel commission invoices." +msgstr "Diario de vendas utilizado para rexistrar as facturas de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__product_id +msgid "Commission Product" +msgstr "Produto de Comisión" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_product_id +msgid "Product used as invoice line in rappel commission invoices." +msgstr "Produto utilizado como liña de factura nas facturas de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_partner_form_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Rappel Commission" +msgstr "Comisión por Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission +msgid "If enabled, vendor bills from this partner will be eligible for rappel commission invoicing." +msgstr "Se está activado, as facturas de provedor deste contacto serán elixibles para a facturación de comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Commission Rate (%)" +msgstr "Taxa de Comisión (%)" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Percentage applied on the taxable base of vendor bills to calculate the rappel commission amount." +msgstr "Porcentaxe aplicado sobre a base imponible das facturas de provedor para calcular o importe da comisión por rappel." + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move__rappel_pending +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Pending Commission" +msgstr "Pendente de Comisión" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_account_move__rappel_pending +msgid "Indicates that this vendor bill has not yet been included in a rappel commission invoice. Automatically set based on the vendor's configuration." +msgstr "Indica que esta factura de provedor aínda non foi incluída nunha factura de comisión por rappel. Establécese automaticamente segundo a configuración do provedor." + +#. module: purchase_rappel +#: model:ir.model,name:purchase_rappel.model_account_move_rappel_wizard +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Commission Invoice" +msgstr "Crear Factura de Comisión por Rappel" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__move_ids +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Vendor Bills" +msgstr "Facturas de Provedor" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__invoice_date +msgid "Invoice Date" +msgstr "Data de Factura" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__journal_id +msgid "Journal" +msgstr "Diario" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "No posted vendor bills found in the selection. Only posted (validated) vendor bills can be used to create rappel commission invoices." +msgstr "Non se atoparon facturas de provedor contabilizadas na selección. Só as facturas de provedor contabilizadas (validadas) poden usarse para crear facturas de comisión por rappel." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills are pending commission. They may have already been commissioned." +msgstr "Ningunha das facturas de provedor seleccionadas está pendente de comisión. Pode que xa fosen comisionadas." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendors have the rappel commission enabled. Please configure the commission rate on the vendor contact first." +msgstr "Ningún dos provedores seleccionados ten a comisión por rappel activada. Configure primeiro a taxa de comisión no contacto do provedor." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a journal for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccione un diario para as facturas de comisión por rappel, ou configure un por defecto nos Axustes de Compras." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a product for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "Seleccione un produto para as facturas de comisión por rappel, ou configure un por defecto nos Axustes de Compras." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills belong to vendors with a rappel commission rate configured." +msgstr "Ningunha das facturas de provedor seleccionadas pertence a provedores cunha taxa de comisión por rappel configurada." + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission %(rate)s%% — Bill %(bill)s (taxable base: %(base)s %(currency)s)" +msgstr "Comisión por rappel %(rate)s%% — Factura %(bill)s (base imponible: %(base)s %(currency)s)" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission invoice generated from %d vendor bill(s)." +msgstr "Factura de comisión por rappel xerada a partir de %d factura(s) de provedor." + +#. module: purchase_rappel +#: model:ir.actions.act_window,name:purchase_rappel.action_open_rappel_wizard +msgid "Rappel Commission Invoices" +msgstr "Facturas de Comisión por Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Configure the journal and product for rappel commission invoices" +msgstr "Configure o diario e o produto para as facturas de comisión por rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Vendor bills not yet included in a rappel commission invoice" +msgstr "Facturas de provedor que aínda non están incluídas nunha factura de comisión por rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Taxable Base" +msgstr "Base Imponible" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Invoice(s)" +msgstr "Crear Factura(s) de Rappel" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Cancel" +msgstr "Cancelar" diff --git a/purchase_rappel/i18n/purchase_rappel.pot b/purchase_rappel/i18n/purchase_rappel.pot new file mode 100644 index 0000000..27b0e4a --- /dev/null +++ b/purchase_rappel/i18n/purchase_rappel.pot @@ -0,0 +1,166 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_rappel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-12 10:00+0000\n" +"PO-Revision-Date: 2024-06-12 10:00+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: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Rappel Journal" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_journal_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_journal_id +msgid "Sales journal used to post rappel commission invoices." +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_res_config_settings__rappel_product_id +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__product_id +msgid "Commission Product" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_company__rappel_product_id +#: model:ir.model.fields,help:purchase_rappel.field_res_config_settings__rappel_product_id +msgid "Product used as invoice line in rappel commission invoices." +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_partner_form_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Rappel Commission" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission +msgid "If enabled, vendor bills from this partner will be eligible for rappel commission invoicing." +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Commission Rate (%)" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_res_partner__rappel_commission_rate +msgid "Percentage applied on the taxable base of vendor bills to calculate the rappel commission amount." +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move__rappel_pending +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Pending Commission" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,help:purchase_rappel.field_account_move__rappel_pending +msgid "Indicates that this vendor bill has not yet been included in a rappel commission invoice. Automatically set based on the vendor's configuration." +msgstr "" + +#. module: purchase_rappel +#: model:ir.model,name:purchase_rappel.model_account_move_rappel_wizard +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Commission Invoice" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__move_ids +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Vendor Bills" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__invoice_date +msgid "Invoice Date" +msgstr "" + +#. module: purchase_rappel +#: model:ir.model.fields,field_description:purchase_rappel.field_account_move_rappel_wizard__journal_id +msgid "Journal" +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "No posted vendor bills found in the selection. Only posted (validated) vendor bills can be used to create rappel commission invoices." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills are pending commission. They may have already been commissioned." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendors have the rappel commission enabled. Please configure the commission rate on the vendor contact first." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a journal for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Please select a product for the rappel commission invoices, or configure a default one in Purchase Settings." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "None of the selected vendor bills belong to vendors with a rappel commission rate configured." +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission %(rate)s%% — Bill %(bill)s (taxable base: %(base)s %(currency)s)" +msgstr "" + +#. module: purchase_rappel +#: code:addons/purchase_rappel/wizard/account_move_rappel_wizard.py:0 +msgid "Rappel commission invoice generated from %d vendor bill(s)." +msgstr "" + +#. module: purchase_rappel +#: model:ir.actions.act_window,name:purchase_rappel.action_open_rappel_wizard +msgid "Rappel Commission Invoices" +msgstr "" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.res_config_settings_view_form_rappel +msgid "Configure the journal and product for rappel commission invoices" +msgstr "" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_invoice_filter_rappel +msgid "Vendor bills not yet included in a rappel commission invoice" +msgstr "" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Taxable Base" +msgstr "" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Create Rappel Invoice(s)" +msgstr "" + +#. module: purchase_rappel +#: model_terms:ir.ui.view,arch_db:purchase_rappel.view_account_move_rappel_wizard_form +msgid "Cancel" +msgstr "" diff --git a/purchase_rappel/models/__init__.py b/purchase_rappel/models/__init__.py new file mode 100644 index 0000000..1ced8dd --- /dev/null +++ b/purchase_rappel/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import res_company +from . import res_partner +from . import res_config_settings +from . import account_move diff --git a/purchase_rappel/models/account_move.py b/purchase_rappel/models/account_move.py new file mode 100644 index 0000000..75549c2 --- /dev/null +++ b/purchase_rappel/models/account_move.py @@ -0,0 +1,32 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class AccountMove(models.Model): + _inherit = "account.move" + + rappel_pending = fields.Boolean( + string="Pending Commission", + copy=False, + help="Indicates that this vendor bill has not yet been included in a rappel " + "commission invoice. Automatically set based on the vendor's configuration.", + ) + + @api.model_create_multi + def create(self, vals_list): + moves = super().create(vals_list) + for move in moves: + if ( + move.move_type in ("in_invoice", "in_refund") + and move.partner_id.rappel_commission + ): + move.rappel_pending = True + return moves + + @api.onchange("partner_id") + def _onchange_partner_id_rappel(self): + if self.move_type in ("in_invoice", "in_refund"): + self.rappel_pending = bool( + self.partner_id and self.partner_id.rappel_commission + ) diff --git a/purchase_rappel/models/res_company.py b/purchase_rappel/models/res_company.py new file mode 100644 index 0000000..358faf0 --- /dev/null +++ b/purchase_rappel/models/res_company.py @@ -0,0 +1,19 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + rappel_journal_id = fields.Many2one( + comodel_name="account.journal", + string="Rappel Journal", + domain="[('type', '=', 'sale'), ('company_id', '=', id)]", + help="Sales journal used to post rappel commission invoices.", + ) + rappel_product_id = fields.Many2one( + comodel_name="product.product", + string="Commission Product", + help="Product used as invoice line in rappel commission invoices.", + ) diff --git a/purchase_rappel/models/res_config_settings.py b/purchase_rappel/models/res_config_settings.py new file mode 100644 index 0000000..afe0f54 --- /dev/null +++ b/purchase_rappel/models/res_config_settings.py @@ -0,0 +1,23 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + rappel_journal_id = fields.Many2one( + comodel_name="account.journal", + string="Rappel Journal", + related="company_id.rappel_journal_id", + domain=[("type", "=", "sale")], + readonly=False, + help="Sales journal used to post rappel commission invoices.", + ) + rappel_product_id = fields.Many2one( + comodel_name="product.product", + string="Commission Product", + related="company_id.rappel_product_id", + readonly=False, + help="Product used as invoice line in rappel commission invoices.", + ) diff --git a/purchase_rappel/models/res_partner.py b/purchase_rappel/models/res_partner.py new file mode 100644 index 0000000..7698d9c --- /dev/null +++ b/purchase_rappel/models/res_partner.py @@ -0,0 +1,19 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + rappel_commission = fields.Boolean( + string="Rappel Commission", + help="If enabled, vendor bills from this partner will be eligible for rappel " + "commission invoicing.", + ) + rappel_commission_rate = fields.Float( + string="Commission Rate (%)", + digits=(5, 4), + help="Percentage applied on the taxable base of vendor bills to calculate the " + "rappel commission amount.", + ) diff --git a/purchase_rappel/readme/CONFIGURE.md b/purchase_rappel/readme/CONFIGURE.md new file mode 100644 index 0000000..072c482 --- /dev/null +++ b/purchase_rappel/readme/CONFIGURE.md @@ -0,0 +1,13 @@ +## Company Settings + +Go to **Purchase > Configuration > Settings > Invoicing** and set: + +- **Rappel Journal**: Sales journal where rappel commission invoices will be posted. +- **Commission Product**: Product used as invoice line in rappel commission invoices (should have an income account and taxes configured). + +## Vendor Configuration + +Open a vendor contact and go to the **Sales & Purchases** tab: + +- Enable **Rappel Commission**. +- Set the **Commission Rate (%)** (e.g., 5.00). diff --git a/purchase_rappel/readme/DESCRIPTION.md b/purchase_rappel/readme/DESCRIPTION.md new file mode 100644 index 0000000..f960608 --- /dev/null +++ b/purchase_rappel/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +This module allows managing **rappel commission invoices** for vendors. + +Features: +- Configure a rappel commission rate per vendor. +- Automatically mark vendor bills as "pending commission". +- Generate customer (sale) invoices from selected vendor bills via a wizard. +- Group invoices by vendor and compute commission amounts based on the taxable base. diff --git a/purchase_rappel/readme/USAGE.md b/purchase_rappel/readme/USAGE.md new file mode 100644 index 0000000..16eaee7 --- /dev/null +++ b/purchase_rappel/readme/USAGE.md @@ -0,0 +1,15 @@ +When a vendor bill is created for a partner with rappel enabled, the bill is automatically marked as **Pending Commission**. This flag is visible in the vendor bill list and form views. + +To generate rappel invoices: + +1. Go to **Invoicing > Vendors > Bills**. +2. Select one or more posted vendor bills with pending commission. +3. Click **Action > Create Rappel Commission Invoice**. +4. In the wizard, confirm the **Invoice Date**, **Journal**, and **Product**. +5. Click **Create Rappel Invoice(s)**. + +The system will create one sale invoice per vendor, with one line per original bill. The commission amount is computed as: + + taxable_base × commission_rate / 100 + +The original vendor bills are automatically unmarked as pending. diff --git a/purchase_rappel/security/ir.model.access.csv b/purchase_rappel/security/ir.model.access.csv new file mode 100644 index 0000000..1b8dade --- /dev/null +++ b/purchase_rappel/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_move_rappel_wizard_user,account.move.rappel.wizard user,model_account_move_rappel_wizard,account.group_account_invoice,1,1,1,1 diff --git a/purchase_rappel/tests/__init__.py b/purchase_rappel/tests/__init__.py new file mode 100644 index 0000000..eabb36b --- /dev/null +++ b/purchase_rappel/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_purchase_rappel diff --git a/purchase_rappel/tests/test_purchase_rappel.py b/purchase_rappel/tests/test_purchase_rappel.py new file mode 100644 index 0000000..2bea079 --- /dev/null +++ b/purchase_rappel/tests/test_purchase_rappel.py @@ -0,0 +1,333 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestPurchaseRappel(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.vendor_rappel = cls.env["res.partner"].create( + { + "name": "Vendor with Rappel", + "rappel_commission": True, + "rappel_commission_rate": 5.0, + "property_account_receivable_id": cls.company_data[ + "default_account_receivable" + ].id, + "property_account_payable_id": cls.company_data[ + "default_account_payable" + ].id, + } + ) + cls.vendor_no_rappel = cls.env["res.partner"].create( + { + "name": "Vendor without Rappel", + "rappel_commission": False, + "property_account_receivable_id": cls.company_data[ + "default_account_receivable" + ].id, + "property_account_payable_id": cls.company_data[ + "default_account_payable" + ].id, + } + ) + cls.vendor_zero_rate = cls.env["res.partner"].create( + { + "name": "Vendor Zero Rate", + "rappel_commission": True, + "rappel_commission_rate": 0.0, + "property_account_receivable_id": cls.company_data[ + "default_account_receivable" + ].id, + "property_account_payable_id": cls.company_data[ + "default_account_payable" + ].id, + } + ) + cls.rappel_product = cls.env["product.product"].create( + { + "name": "Rappel Commission Product", + "type": "service", + "list_price": 100.0, + "property_account_income_id": cls.company_data[ + "default_account_revenue" + ].id, + "taxes_id": [(6, 0, [cls.tax_sale_a.id])], + } + ) + cls.rappel_journal = cls.company_data["default_journal_sale"] + cls.company_data["company"].write( + { + "rappel_journal_id": cls.rappel_journal.id, + "rappel_product_id": cls.rappel_product.id, + } + ) + + def _create_vendor_bill(self, partner, amount=1000.0, post=True, ref=None): + move = self.init_invoice( + "in_invoice", + partner=partner, + products=self.product_a, + post=False, + ) + move.invoice_line_ids[0].price_unit = amount + if ref is not None: + move.ref = ref + if post: + move.action_post() + return move + + def _create_vendor_refund(self, partner, amount=200.0, post=True, ref=None): + move = self.init_invoice( + "in_refund", + partner=partner, + products=self.product_a, + post=False, + ) + move.invoice_line_ids[0].price_unit = amount + if ref is not None: + move.ref = ref + if post: + move.action_post() + return move + + def test_rappel_pending_on_create(self): + bill_rappel = self._create_vendor_bill(self.vendor_rappel) + self.assertTrue(bill_rappel.rappel_pending) + + bill_no_rappel = self._create_vendor_bill(self.vendor_no_rappel) + self.assertFalse(bill_no_rappel.rappel_pending) + + def test_rappel_pending_onchange(self): + move = self.env["account.move"].new( + { + "move_type": "in_invoice", + "partner_id": self.vendor_no_rappel.id, + "invoice_date": fields.Date.from_string("2019-01-01"), + } + ) + move._onchange_partner_id_rappel() + self.assertFalse(move.rappel_pending) + + move.partner_id = self.vendor_rappel + move._onchange_partner_id_rappel() + self.assertTrue(move.rappel_pending) + + def test_wizard_default_get_valid(self): + bill = self._create_vendor_bill(self.vendor_rappel) + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill.ids) + .create({}) + ) + self.assertIn(bill, wizard.move_ids) + self.assertEqual(wizard.journal_id, self.rappel_journal) + self.assertEqual(wizard.product_id, self.rappel_product) + + def test_wizard_default_get_no_valid(self): + bill = self._create_vendor_bill(self.vendor_rappel, post=False) + with self.assertRaises(UserError): + self.env["account.move.rappel.wizard"].with_context( + active_ids=bill.ids + ).create({}) + + def test_wizard_default_get_no_rappel(self): + bill = self._create_vendor_bill(self.vendor_no_rappel) + with self.assertRaises(UserError): + self.env["account.move.rappel.wizard"].with_context( + active_ids=bill.ids + ).create({}) + + def test_wizard_default_get_not_pending(self): + bill = self._create_vendor_bill(self.vendor_rappel) + bill.rappel_pending = False + with self.assertRaises(UserError): + self.env["account.move.rappel.wizard"].with_context( + active_ids=bill.ids + ).create({}) + + def test_wizard_create_invoice(self): + bill = self._create_vendor_bill(self.vendor_rappel, amount=2000.0) + self.assertTrue(bill.rappel_pending) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill.ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + self.assertEqual(rappel_invoice.move_type, "out_invoice") + self.assertEqual(rappel_invoice.partner_id, self.vendor_rappel) + self.assertEqual(len(rappel_invoice.invoice_line_ids), 1) + + expected_amount = 2000.0 * 5.0 / 100.0 + self.assertEqual(rappel_invoice.invoice_line_ids[0].price_unit, expected_amount) + + self.assertFalse(bill.rappel_pending) + + def test_wizard_grouping(self): + bill1 = self._create_vendor_bill(self.vendor_rappel, amount=1000.0) + vendor_rappel_2 = self.env["res.partner"].create( + { + "name": "Vendor Rappel 2", + "rappel_commission": True, + "rappel_commission_rate": 10.0, + "property_account_receivable_id": self.company_data[ + "default_account_receivable" + ].id, + "property_account_payable_id": self.company_data[ + "default_account_payable" + ].id, + } + ) + bill2 = self._create_vendor_bill(vendor_rappel_2, amount=2000.0) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=(bill1 + bill2).ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + + invoices = self.env["account.move"].search(action.get("domain", [])) + self.assertEqual(len(invoices), 2) + + invoice_vendor_1 = invoices.filtered( + lambda m: m.partner_id == self.vendor_rappel + ) + self.assertEqual( + invoice_vendor_1.invoice_line_ids[0].price_unit, + 1000.0 * 5.0 / 100.0, + ) + + invoice_vendor_2 = invoices.filtered(lambda m: m.partner_id == vendor_rappel_2) + self.assertEqual( + invoice_vendor_2.invoice_line_ids[0].price_unit, + 2000.0 * 10.0 / 100.0, + ) + + self.assertFalse(bill1.rappel_pending) + self.assertFalse(bill2.rappel_pending) + + def test_wizard_no_journal(self): + bill = self._create_vendor_bill(self.vendor_rappel) + wizard = self.env["account.move.rappel.wizard"].new( + { + "move_ids": [(6, 0, bill.ids)], + "journal_id": False, + "product_id": self.rappel_product.id, + "company_id": self.env.company.id, + } + ) + with self.assertRaises(UserError): + wizard.action_create_rappel_invoices() + + def test_wizard_no_product(self): + bill = self._create_vendor_bill(self.vendor_rappel) + wizard = self.env["account.move.rappel.wizard"].new( + { + "move_ids": [(6, 0, bill.ids)], + "journal_id": self.rappel_journal.id, + "product_id": False, + "company_id": self.env.company.id, + } + ) + with self.assertRaises(UserError): + wizard.action_create_rappel_invoices() + + def test_wizard_zero_rate(self): + bill = self._create_vendor_bill(self.vendor_zero_rate, amount=1000.0) + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill.ids) + .create({}) + ) + with self.assertRaises(UserError): + wizard.action_create_rappel_invoices() + + def test_wizard_vendor_ref_in_description(self): + bill_with_ref = self._create_vendor_bill( + self.vendor_rappel, amount=1000.0, ref="VENDOR-REF-001" + ) + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill_with_ref.ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + line_name = rappel_invoice.invoice_line_ids[0].name + self.assertIn("VENDOR-REF-001", line_name) + self.assertNotIn(bill_with_ref.name, line_name) + + bill_without_ref = self._create_vendor_bill( + self.vendor_rappel, amount=1000.0, ref=None + ) + wizard2 = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=bill_without_ref.ids) + .create({}) + ) + action2 = wizard2.action_create_rappel_invoices() + rappel_invoice2 = self.env["account.move"].browse(action2["res_id"]) + line_name2 = rappel_invoice2.invoice_line_ids[0].name + self.assertIn(bill_without_ref.name, line_name2) + + def test_wizard_includes_in_refund(self): + bill = self._create_vendor_bill(self.vendor_rappel, amount=1000.0) + refund = self._create_vendor_refund(self.vendor_rappel, amount=200.0) + self.assertTrue(refund.rappel_pending) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=(bill + refund).ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + + self.assertEqual(len(rappel_invoice.invoice_line_ids), 2) + amounts = sorted(rappel_invoice.invoice_line_ids.mapped("price_unit")) + self.assertEqual(amounts, [-10.0, 50.0]) + self.assertFalse(bill.rappel_pending) + self.assertFalse(refund.rappel_pending) + + def test_wizard_refund_subtracts_commission(self): + bill = self._create_vendor_bill(self.vendor_rappel, amount=1000.0) + refund = self._create_vendor_refund(self.vendor_rappel, amount=200.0) + + wizard = ( + self.env["account.move.rappel.wizard"] + .with_context(active_ids=(bill + refund).ids) + .create({}) + ) + action = wizard.action_create_rappel_invoices() + rappel_invoice = self.env["account.move"].browse(action["res_id"]) + + self.assertEqual(len(rappel_invoice.invoice_line_ids), 2) + line_by_sign = { + line.price_unit > 0: line for line in rappel_invoice.invoice_line_ids + } + self.assertEqual(line_by_sign[True].price_unit, 50.0) + self.assertEqual(line_by_sign[False].price_unit, -10.0) + self.assertEqual(rappel_invoice.amount_untaxed, 40.0) + self.assertIn("Refund", line_by_sign[False].name) + + def test_rappel_pending_set_on_refund_create(self): + refund = self._create_vendor_refund( + self.vendor_rappel, amount=200.0, post=False + ) + self.assertTrue(refund.rappel_pending) + + refund_no_rappel = self._create_vendor_refund( + self.vendor_no_rappel, amount=200.0, post=False + ) + self.assertFalse(refund_no_rappel.rappel_pending) diff --git a/purchase_rappel/views/account_move_views.xml b/purchase_rappel/views/account_move_views.xml new file mode 100644 index 0000000..2ded4a4 --- /dev/null +++ b/purchase_rappel/views/account_move_views.xml @@ -0,0 +1,51 @@ + + + + + + account.move.tree.rappel + account.move + + + + + + + + + + + account.invoice.select.rappel + account.move + + + + + + + + + + + + account.move.form.rappel + account.move + + + + + + + + + diff --git a/purchase_rappel/views/res_config_settings_views.xml b/purchase_rappel/views/res_config_settings_views.xml new file mode 100644 index 0000000..23faa25 --- /dev/null +++ b/purchase_rappel/views/res_config_settings_views.xml @@ -0,0 +1,26 @@ + + + + + res.config.settings.view.form.inherit.purchase.rappel + res.config.settings + + + + + +
+
+
+
+
+
+
+
+ +
diff --git a/purchase_rappel/views/res_partner_views.xml b/purchase_rappel/views/res_partner_views.xml new file mode 100644 index 0000000..a9d3eca --- /dev/null +++ b/purchase_rappel/views/res_partner_views.xml @@ -0,0 +1,20 @@ + + + + + res.partner.form.rappel + res.partner + + + + + + + + + + + + diff --git a/purchase_rappel/wizard/__init__.py b/purchase_rappel/wizard/__init__.py new file mode 100644 index 0000000..7cb416e --- /dev/null +++ b/purchase_rappel/wizard/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import account_move_rappel_wizard diff --git a/purchase_rappel/wizard/account_move_rappel_wizard.py b/purchase_rappel/wizard/account_move_rappel_wizard.py new file mode 100644 index 0000000..fdf9f22 --- /dev/null +++ b/purchase_rappel/wizard/account_move_rappel_wizard.py @@ -0,0 +1,220 @@ +# Copyright 2024 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from collections import defaultdict + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class AccountMoveRappelWizard(models.TransientModel): + """Wizard to generate rappel commission (sale) invoices from selected vendor bills. + + One sale invoice is created per vendor. Each original bill becomes one invoice + line whose amount is computed as: taxable_base × commission_rate / 100. + """ + + _name = "account.move.rappel.wizard" + _description = "Create Rappel Commission Invoice" + + move_ids = fields.Many2many( + comodel_name="account.move", + string="Vendor Bills", + readonly=True, + ) + invoice_date = fields.Date( + string="Invoice Date", + default=fields.Date.context_today, + required=True, + ) + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self.env.company, + required=True, + ) + journal_id = fields.Many2one( + comodel_name="account.journal", + string="Journal", + domain="[('type', '=', 'sale'), ('company_id', '=', company_id)]", + required=True, + ) + product_id = fields.Many2one( + comodel_name="product.product", + string="Commission Product", + required=True, + ) + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + company = self.env.company + active_ids = self.env.context.get("active_ids", []) + moves = self.env["account.move"].browse(active_ids) + + # Filter only posted vendor bills and refunds + valid_moves = moves.filtered( + lambda m: m.move_type in ("in_invoice", "in_refund") + and m.state == "posted" + ) + if not valid_moves: + raise UserError( + _( + "No posted vendor bills or refunds found in the selection. " + "Only posted (validated) vendor bills and refunds can be used to " + "create rappel commission invoices." + ) + ) + + # Filter only pending bills + pending_moves = valid_moves.filtered(lambda m: m.rappel_pending) + if not pending_moves: + raise UserError( + _( + "None of the selected vendor bills are pending commission. " + "They may have already been commissioned." + ) + ) + + # Warn about bills whose partner has no rappel configured + no_rappel = pending_moves.filtered(lambda m: not m.partner_id.rappel_commission) + if no_rappel and len(no_rappel) == len(pending_moves): + raise UserError( + _( + "None of the selected vendors have the rappel commission enabled. " + "Please configure the commission rate on the vendor contact first." + ) + ) + + res.update( + { + "move_ids": [(6, 0, pending_moves.ids)], + "journal_id": company.rappel_journal_id.id or False, + "product_id": company.rappel_product_id.id or False, + } + ) + return res + + def action_create_rappel_invoices(self): + self.ensure_one() + + if not self.journal_id: + raise UserError( + _( + "Please select a journal for the rappel commission invoices, or " + "configure a default one in Purchase Settings." + ) + ) + if not self.product_id: + raise UserError( + _( + "Please select a product for the rappel commission invoices, or " + "configure a default one in Purchase Settings." + ) + ) + + # Group eligible bills by partner (only partners with rappel enabled) + bills_by_partner = defaultdict(lambda: self.env["account.move"]) + for bill in self.move_ids: + if ( + bill.partner_id.rappel_commission + and bill.partner_id.rappel_commission_rate + ): + bills_by_partner[bill.partner_id] |= bill + + if not bills_by_partner: + raise UserError( + _( + "None of the selected vendor bills belong to vendors with a rappel " + "commission rate configured." + ) + ) + + created_invoices = self.env["account.move"] + + for partner, bills in bills_by_partner.items(): + rate = partner.rappel_commission_rate + invoice_lines = [] + + for bill in bills: + taxable_base = bill.amount_untaxed + commission_amount = taxable_base * rate / 100.0 + vendor_ref = bill.ref or bill.name + if bill.move_type == "in_refund": + # Vendor refunds reduce the commission: negate the amount so + # the resulting line subtracts from the rappel invoice total. + # amount_untaxed is always positive in Odoo, so we flip the + # sign here and display a negative base in the description. + commission_amount = -commission_amount + display_base = -taxable_base + name = _( + "Rappel commission %(rate)s%% — Refund %(bill)s " + "(taxable base: %(base)s %(currency)s)", + rate=rate, + bill=vendor_ref, + base=f"{display_base:,.2f}", + currency=bill.currency_id.name, + ) + else: + name = _( + "Rappel commission %(rate)s%% — Bill %(bill)s " + "(taxable base: %(base)s %(currency)s)", + rate=rate, + bill=vendor_ref, + base=f"{taxable_base:,.2f}", + currency=bill.currency_id.name, + ) + invoice_lines.append( + ( + 0, + 0, + { + "product_id": self.product_id.id, + "name": name, + "quantity": 1, + "price_unit": commission_amount, + "tax_ids": [ + ( + 6, + 0, + self.product_id.taxes_id.filtered( + lambda t: t.company_id == self.company_id + ).ids, + ) + ], + }, + ) + ) + + invoice_vals = { + "move_type": "out_invoice", + "partner_id": partner.id, + "invoice_date": self.invoice_date, + "journal_id": self.journal_id.id, + "company_id": self.company_id.id, + "invoice_line_ids": invoice_lines, + "narration": _( + "Rappel commission invoice generated from %d vendor bill(s).", + len(bills), + ), + } + created_invoice = self.env["account.move"].create(invoice_vals) + created_invoices |= created_invoice + + # Mark source bills as commissioned + bills.write({"rappel_pending": False}) + + # Open the created invoices + if len(created_invoices) == 1: + return { + "type": "ir.actions.act_window", + "res_model": "account.move", + "res_id": created_invoices.id, + "view_mode": "form", + "views": [(False, "form")], + } + return { + "type": "ir.actions.act_window", + "res_model": "account.move", + "domain": [("id", "in", created_invoices.ids)], + "view_mode": "list,form", + "name": _("Rappel Commission Invoices"), + } diff --git a/purchase_rappel/wizard/account_move_rappel_wizard_views.xml b/purchase_rappel/wizard/account_move_rappel_wizard_views.xml new file mode 100644 index 0000000..f30326f --- /dev/null +++ b/purchase_rappel/wizard/account_move_rappel_wizard_views.xml @@ -0,0 +1,62 @@ + + + + + + account.move.rappel.wizard.form + account.move.rappel.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + Create Rappel Commission Invoice + account.move.rappel.wizard + form + new + + list + + +