From 232cb24fd9d4a97e3df60186a14056466c3d05fd Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 1 Sep 2025 21:43:54 +0000 Subject: [PATCH] [IMP] commission_simple*: add XLSX report + improve PO generation Add message un chatter of PO --- commission_simple/__init__.py | 1 + commission_simple/__manifest__.py | 2 + commission_simple/models/account_move_line.py | 15 +++ commission_simple/reports/__init__.py | 1 + .../reports/commission_result_xlsx.py | 109 ++++++++++++++++++ commission_simple/reports/report.xml | 20 ++++ commission_simple/views/commission_result.xml | 5 + .../__manifest__.py | 1 + .../models/__init__.py | 1 + .../models/commission_profile.py | 20 ++++ .../models/commission_result.py | 43 ++++++- .../models/res_company.py | 4 + .../views/commission_profile.xml | 22 ++++ .../wizards/res_config_settings.py | 1 + .../wizards/res_config_settings.xml | 4 + 15 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 commission_simple/reports/__init__.py create mode 100644 commission_simple/reports/commission_result_xlsx.py create mode 100644 commission_simple/reports/report.xml create mode 100644 commission_simple_agent_purchase/models/commission_profile.py create mode 100644 commission_simple_agent_purchase/views/commission_profile.xml diff --git a/commission_simple/__init__.py b/commission_simple/__init__.py index aee8895..b83f59e 100644 --- a/commission_simple/__init__.py +++ b/commission_simple/__init__.py @@ -1,2 +1,3 @@ from . import models from . import wizards +from . import reports diff --git a/commission_simple/__manifest__.py b/commission_simple/__manifest__.py index 9c20e92..b1e1662 100644 --- a/commission_simple/__manifest__.py +++ b/commission_simple/__manifest__.py @@ -30,10 +30,12 @@ This module has been written by Alexis de Lattre from Akretion 'depends': [ 'account', 'date_range', + 'report_xlsx', ], 'data': [ 'security/ir.model.access.csv', 'security/rule.xml', + 'reports/report.xml', 'data/decimal_precision.xml', 'views/commission_profile.xml', 'views/commission_rule.xml', diff --git a/commission_simple/models/account_move_line.py b/commission_simple/models/account_move_line.py index 7bf84dd..457b6dc 100644 --- a/commission_simple/models/account_move_line.py +++ b/commission_simple/models/account_move_line.py @@ -88,3 +88,18 @@ class AccountMoveLine(models.Model): if float_is_zero(lvals['commission_rate'], precision_digits=rate_prec) or self.company_currency_id.is_zero(lvals['commission_base']): return False return lvals + + def _prepare_commission_xlsx(self): + self.ensure_one() + vals = { + "inv.name": self.move_id.name, + "inv.date": self.move_id.invoice_date, + "inv.partner": self.move_id.commercial_partner_id.display_name, + "product": self.product_id and self.product_id.display_name or self.name, + "qty": self.quantity, + "uom": self.product_uom_id.name, + "commission_base": self.commission_base, + "commission_rate": self.commission_rate / 100, + "commission_amount": self.commission_amount, + } + return vals diff --git a/commission_simple/reports/__init__.py b/commission_simple/reports/__init__.py new file mode 100644 index 0000000..6240c56 --- /dev/null +++ b/commission_simple/reports/__init__.py @@ -0,0 +1 @@ +from . import commission_result_xlsx diff --git a/commission_simple/reports/commission_result_xlsx.py b/commission_simple/reports/commission_result_xlsx.py new file mode 100644 index 0000000..447e578 --- /dev/null +++ b/commission_simple/reports/commission_result_xlsx.py @@ -0,0 +1,109 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, tools, Command, _ +from odoo.exceptions import UserError +from datetime import datetime +from odoo.tools.misc import format_datetime + + +class CommissionResultXlsx(models.AbstractModel): + _name = "report.commission_simple.report_xlsx" + _inherit = "report.report_xlsx.abstract" + _description = "Commission Result XLSX" + + def generate_xlsx_report(self, workbook, data, objects): + # for some strange reasons, lang is not kept in context + self = self.with_context(lang=self.env.user.lang) + result = objects[0] + sheet = workbook.add_worksheet(result.date_range_id.name) + styles = self._prepare_styles(workbook, result.company_id) + title = _("Commissions of %(partner)s for period %(period)s", partner=result.partner_id.name, period=result.date_range_id.name) + now_str = format_datetime(self.env, datetime.now()) + i = 0 + sheet.write(i, 0, title, styles['title']) + sheet.write(i, 5, _('Generated from Odoo on %s by %s') % (now_str, self.env.user.name), styles['regular_small']) + i += 1 + sheet.write(i, 0, _('Start Date'), styles['subtitle']) + sheet.write(i, 1, result.date_start, styles['subtitle_date']) + i += 1 + sheet.write(i, 0, _('End Date'), styles['subtitle']) + sheet.write(i, 1, result.date_end, styles['subtitle_date']) + i += 1 + sheet.write(i, 0, _('Currency'), styles['subtitle']) + sheet.write(i, 1, result.company_id.currency_id.name, styles['subtitle']) + i += 1 + sheet.write(i, 0, _('Total Amount'), styles['subtitle']) + sheet.write(i, 1, result.amount_total, styles['subtitle_amount']) + i += 3 + cols = self._prepare_xlsx_cols() + coldict = {} + pos = 0 + for key, label, width, style_suffix in cols: + coldict[key] = { + "label": label, + "width": width, + "pos": pos, + "style": style_suffix and f"regular_{style_suffix}" or "regular", + } + pos += 1 + # header + for col_key, col_vals in coldict.items(): + sheet.write(i, col_vals['pos'], col_vals['label'], styles['col_title']) + sheet.set_column(col_vals['pos'], col_vals['pos'], col_vals['width']) + # table content + for line in result.line_ids: + i += 1 + for col_key, value in line._prepare_commission_xlsx().items(): + sheet.write(i, coldict[col_key]["pos"], value, styles[coldict[col_key]["style"]]) + + def _prepare_xlsx_cols(self): + cols = [ # key, label, width, style_suffix + ("inv.name", _("Invoice"), 14, False), + ("inv.date", _("Invoice Date"), 11, "date"), + ("inv.partner", _("Customer"), 50, False), + ("product", _("Product"), 35, False), + ("qty", _("Quantity"), 8, "qty"), + ("uom", _("Unit"), 8, False), + ("commission_base", _("Commission Base"), 14, "amount"), + ("commission_rate", _("Commission Rate"), 10, "rate"), + ("commission_amount", _("Commission Amount"), 14, "amount"), + ] + return cols + + def _prepare_styles(self, workbook, company): + col_title_bg_color = '#eeeeee' + prec_qty = self.env['decimal.precision'].precision_get('Product Unit of Measure') + prec_rate = self.env['decimal.precision'].precision_get('Commission Rate') + prec_price = self.env['decimal.precision'].precision_get('Product Price') + regular_font_size = 10 + date_format = "dd/mm/yyyy" # TODO depend on lang + num_format_amount = f"# ##0.{'0' * company.currency_id.decimal_places}" + num_format_qty = f"# ##0.{'0' * prec_qty}" + num_format_rate = f"""0.{'0' * prec_rate} " "%""" + num_format_price = f"# ##0.{'0' * prec_price}" + styles = { + 'title': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size + 10, + 'font_color': '#003b6f'}), + 'subtitle': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size}), + 'subtitle_date': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size, 'num_format': date_format}), + 'subtitle_amount': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size, 'num_format': num_format_amount}), + 'col_title': workbook.add_format({ + 'bold': True, 'bg_color': col_title_bg_color, + 'text_wrap': True, 'font_size': regular_font_size, + 'align': 'center', + }), + 'regular_date': workbook.add_format({'num_format': date_format}), + 'regular_amount': workbook.add_format({'num_format': num_format_amount}), + 'regular_rate': workbook.add_format({'num_format': num_format_rate}), + 'regular_qty': workbook.add_format({'num_format': num_format_qty}), + 'regular_price': workbook.add_format({'num_format': num_format_price}), + 'regular': workbook.add_format({}), + 'regular_small': workbook.add_format({'font_size': regular_font_size - 2}), + } + return styles diff --git a/commission_simple/reports/report.xml b/commission_simple/reports/report.xml new file mode 100644 index 0000000..4b84f89 --- /dev/null +++ b/commission_simple/reports/report.xml @@ -0,0 +1,20 @@ + + + + + + + Détails Excel + commission.result + xlsx + commission_simple.report_xlsx + commission_simple.report_xlsx + 'commission-%s-%s' % (object.date_range_id.name.replace(' ', '_'), object.partner_id.name.replace(' ', '_')) + + + + diff --git a/commission_simple/views/commission_result.xml b/commission_simple/views/commission_result.xml index a3e8ec7..db2c77b 100644 --- a/commission_simple/views/commission_result.xml +++ b/commission_simple/views/commission_result.xml @@ -15,6 +15,7 @@
@@ -42,6 +43,10 @@ + + + + diff --git a/commission_simple_agent_purchase/__manifest__.py b/commission_simple_agent_purchase/__manifest__.py index 317f9ab..d0ce4f4 100644 --- a/commission_simple_agent_purchase/__manifest__.py +++ b/commission_simple_agent_purchase/__manifest__.py @@ -16,6 +16,7 @@ ], 'data': [ 'views/commission_result.xml', + 'views/commission_profile.xml', 'wizards/res_config_settings.xml', ], 'installable': False, diff --git a/commission_simple_agent_purchase/models/__init__.py b/commission_simple_agent_purchase/models/__init__.py index 8b8e848..a542643 100644 --- a/commission_simple_agent_purchase/models/__init__.py +++ b/commission_simple_agent_purchase/models/__init__.py @@ -1,2 +1,3 @@ from . import commission_result +from . import commission_profile from . import res_company diff --git a/commission_simple_agent_purchase/models/commission_profile.py b/commission_simple_agent_purchase/models/commission_profile.py new file mode 100644 index 0000000..c3bf206 --- /dev/null +++ b/commission_simple_agent_purchase/models/commission_profile.py @@ -0,0 +1,20 @@ +# Copyright Akretion France (http://www.akretion.com/) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError + + +class CommissionProfile(models.Model): + _inherit = 'commission.profile' + + commission_product_id = fields.Many2one( + 'product.product', string='Specific Commission Product', ondelete='restrict', + check_company=True, + domain=[('type', '=', 'service')], + help="If not set, Odoo will use the commission product configured on the accounting " + "configuration page." + ) + diff --git a/commission_simple_agent_purchase/models/commission_result.py b/commission_simple_agent_purchase/models/commission_result.py index 9c3778c..4a76ceb 100644 --- a/commission_simple_agent_purchase/models/commission_result.py +++ b/commission_simple_agent_purchase/models/commission_result.py @@ -5,6 +5,7 @@ from odoo import fields, models, _ from odoo.exceptions import UserError from odoo.tools.misc import format_amount, formatLang +from markupsafe import Markup class CommissionResult(models.Model): @@ -18,22 +19,30 @@ class CommissionResult(models.Model): if not result.purchase_id: vals = result._prepare_purchase_order() po = self.env['purchase.order'].create(vals) + po.message_post(body=Markup(_("Generated from commission %s.") % (result.id, result.display_name))) result.write({'purchase_id': po.id}) else: po = self.purchase_id if po.state in ('draft', 'sent', 'cancel'): po.order_line.unlink() + po.message_post(body=Markup(_("Purchase order lines re-generated from commission %s.") % (result.id, result.display_name))) else: raise UserError(_("Purchase Order %s has already been confirmed. You should cancel it first.") % po.display_name) if po.state == 'cancel': po.button_draft() assert not po.order_line # create lines - if not result.company_id.commission_product_id: - raise UserError(_("Commission product is not set on company %s.") % result.company_id.display_name) line_vals = [] - for move_line in result.line_ids: - line_vals.append(result._prepare_purchase_order_line(move_line, po)) + if not result.company_id.commission_po_config: + raise UserError(_( + "Purchase order configuration for commission is not set on " + "the accounting configuration page of company '%s'.") + % result.company_id.display_name) + if result.company_id.commission_po_config == 'single_line': + line_vals.append(result._prepare_purchase_order_line_single_line(po)) + else: + for move_line in result.line_ids: + line_vals.append(result._prepare_purchase_order_line(move_line, po)) po_lines = self.env['purchase.order.line'].create(line_vals) po_lines._compute_tax_id() return super().draft2done() @@ -57,7 +66,13 @@ class CommissionResult(models.Model): company_currency = move_line.company_id.currency_id lang = self.partner_id.lang or self.env.lang env = self.with_context(lang=lang).env - product = self.company_id.commission_product_id + product = self.profile_id.commission_product_id or self.company_id.commission_product_id + if not product: + raise UserError(_( + "Commission product is not set on profile '%(profile)s' " + "nor on company '%(company)s'.", + profile=self.profile_id.display_name, + company=self.company_id.display_name)) vals = { 'order_id': order.id, 'product_id': product.id, @@ -68,6 +83,24 @@ class CommissionResult(models.Model): } return vals + def _prepare_purchase_order_line_single_line(self, order): + product = self.profile_id.commission_product_id or self.company_id.commission_product_id + if not product: + raise UserError(_( + "Commission product is not set on profile '%(profile)s' " + "nor on company '%(company)s'.", + profile=self.profile_id.display_name, + company=self.company_id.display_name)) + vals = { + 'order_id': order.id, + 'product_id': product.id, + 'name': _("Commissions for period %(period)s", period=self.date_range_id.name), + 'product_qty': 1, + 'product_uom': product.uom_id.id, + 'price_unit': self.amount_total, + } + return vals + def unlink(self): for result in self: if result.purchase_id: diff --git a/commission_simple_agent_purchase/models/res_company.py b/commission_simple_agent_purchase/models/res_company.py index 253223a..a7c7651 100644 --- a/commission_simple_agent_purchase/models/res_company.py +++ b/commission_simple_agent_purchase/models/res_company.py @@ -12,3 +12,7 @@ class ResCompany(models.Model): commission_product_id = fields.Many2one( 'product.product', string='Commission Product', ondelete='restrict', check_company=True, domain=[('type', '=', 'service')]) + commission_po_config = fields.Selection([ + ('single_line', 'Single Line'), + ('details', 'One line per commission line'), + ], default='details', string="Purchase Order Configuration") diff --git a/commission_simple_agent_purchase/views/commission_profile.xml b/commission_simple_agent_purchase/views/commission_profile.xml new file mode 100644 index 0000000..f279bdb --- /dev/null +++ b/commission_simple_agent_purchase/views/commission_profile.xml @@ -0,0 +1,22 @@ + + + + + + + commission.profile + + + + + + + + + + + diff --git a/commission_simple_agent_purchase/wizards/res_config_settings.py b/commission_simple_agent_purchase/wizards/res_config_settings.py index 0a99838..895f3b5 100644 --- a/commission_simple_agent_purchase/wizards/res_config_settings.py +++ b/commission_simple_agent_purchase/wizards/res_config_settings.py @@ -10,3 +10,4 @@ class ResConfigSettings(models.TransientModel): commission_product_id = fields.Many2one( related='company_id.commission_product_id', readonly=False) + commission_po_config = fields.Selection(related="company_id.commission_po_config", readonly=False) diff --git a/commission_simple_agent_purchase/wizards/res_config_settings.xml b/commission_simple_agent_purchase/wizards/res_config_settings.xml index 585ce6f..58b0420 100644 --- a/commission_simple_agent_purchase/wizards/res_config_settings.xml +++ b/commission_simple_agent_purchase/wizards/res_config_settings.xml @@ -18,6 +18,10 @@