# -*- 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 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"), }