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..408a600
--- /dev/null
+++ b/purchase_rappel/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+from . import models
+from . import wizard
diff --git a/purchase_rappel/__manifest__.py b/purchase_rappel/__manifest__.py
new file mode 100644
index 0000000..9d3beaf
--- /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": "17.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..4c4b03c
--- /dev/null
+++ b/purchase_rappel/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+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..a9360a1
--- /dev/null
+++ b/purchase_rappel/models/account_move.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+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_invoice" 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_invoice":
+ 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..96c4e3a
--- /dev/null
+++ b/purchase_rappel/models/res_company.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+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..898c4d7
--- /dev/null
+++ b/purchase_rappel/models/res_config_settings.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+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..91c0316
--- /dev/null
+++ b/purchase_rappel/models/res_partner.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+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..18cdb70
--- /dev/null
+++ b/purchase_rappel/tests/test_purchase_rappel.py
@@ -0,0 +1,238 @@
+# 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, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+ 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):
+ move = self.init_invoice(
+ "in_invoice",
+ partner=partner,
+ products=self.product_a,
+ post=False,
+ )
+ move.invoice_line_ids[0].price_unit = amount
+ 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()
diff --git a/purchase_rappel/views/account_move_views.xml b/purchase_rappel/views/account_move_views.xml
new file mode 100644
index 0000000..9eb9496
--- /dev/null
+++ b/purchase_rappel/views/account_move_views.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ 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..1574bde
--- /dev/null
+++ b/purchase_rappel/wizard/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+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..078f7f1
--- /dev/null
+++ b/purchase_rappel/wizard/account_move_rappel_wizard.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+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
+ valid_moves = moves.filtered(
+ lambda m: m.move_type == "in_invoice" and m.state == "posted"
+ )
+ if not valid_moves:
+ raise UserError(
+ _(
+ "No posted vendor bills found in the selection. "
+ "Only posted (validated) vendor bills 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
+ name = _(
+ "Rappel commission %(rate)s%% — Bill %(bill)s "
+ "(taxable base: %(base)s %(currency)s)",
+ rate=rate,
+ bill=bill.name,
+ 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..3ce46a9
--- /dev/null
+++ b/purchase_rappel/wizard/account_move_rappel_wizard_views.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+ account.move.rappel.wizard.form
+ account.move.rappel.wizard
+
+
+
+
+
+
+
+ Create Rappel Commission Invoice
+ account.move.rappel.wizard
+ form
+ new
+
+ list
+
+
+