[IMP] commission_simple*: add XLSX report + improve PO generation

Add message un chatter of PO
This commit is contained in:
Alexis de Lattre
2025-09-01 21:43:54 +00:00
committed by Florian da Costa
parent 827119df6e
commit 232cb24fd9
15 changed files with 244 additions and 5 deletions

View File

@@ -1,2 +1,3 @@
from . import models from . import models
from . import wizards from . import wizards
from . import reports

View File

@@ -30,10 +30,12 @@ This module has been written by Alexis de Lattre from Akretion
'depends': [ 'depends': [
'account', 'account',
'date_range', 'date_range',
'report_xlsx',
], ],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/rule.xml', 'security/rule.xml',
'reports/report.xml',
'data/decimal_precision.xml', 'data/decimal_precision.xml',
'views/commission_profile.xml', 'views/commission_profile.xml',
'views/commission_rule.xml', 'views/commission_rule.xml',

View File

@@ -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']): if float_is_zero(lvals['commission_rate'], precision_digits=rate_prec) or self.company_currency_id.is_zero(lvals['commission_base']):
return False return False
return lvals 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

View File

@@ -0,0 +1 @@
from . import commission_result_xlsx

View File

@@ -0,0 +1,109 @@
# Copyright 2025 Akretion France (https://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# 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

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 Akretion France (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
The licence is in the file __manifest__.py
-->
<odoo>
<record id="commission_result_xlsx_report" model="ir.actions.report">
<field name="name">Détails Excel</field>
<field name="model">commission.result</field>
<field name="report_type">xlsx</field>
<field name="report_name">commission_simple.report_xlsx</field>
<field name="report_file">commission_simple.report_xlsx</field>
<field name="print_report_name">'commission-%s-%s' % (object.date_range_id.name.replace(' ', '_'), object.partner_id.name.replace(' ', '_'))</field>
<field name="binding_model_id" ref="model_commission_result" />
</record>
</odoo>

View File

@@ -15,6 +15,7 @@
<header> <header>
<button name="draft2done" type="object" states="draft" string="Confirm" class="btn-primary"/> <button name="draft2done" type="object" states="draft" string="Confirm" class="btn-primary"/>
<button name="backtodraft" type="object" states="done" string="Back to Draft" confirm="Are you sure you want to go back to draft?"/> <button name="backtodraft" type="object" states="done" string="Back to Draft" confirm="Are you sure you want to go back to draft?"/>
<button name="%(commission_simple.commission_result_xlsx_report)d" type="action" string="Excel Export"/>
<field name="state" widget="statusbar"/> <field name="state" widget="statusbar"/>
</header> </header>
<group name="main"> <group name="main">
@@ -42,6 +43,10 @@
<field name="product_id"/> <field name="product_id"/>
<field name="product_categ_id" optional="hide"/> <field name="product_categ_id" optional="hide"/>
<field name="name" optional="hide"/> <field name="name" optional="hide"/>
<field name="quantity" optional="hide"/>
<field name="product_uom_id" optional="hide" groups="uom.group_uom"/>
<field name="price_unit" string="Price" optional="hide"/>
<field name="discount" string="Disc.%" optional="hide"/>
<field name="price_subtotal" optional="hide" string="Invoiced Amount"/> <field name="price_subtotal" optional="hide" string="Invoiced Amount"/>
<field name="commission_base"/> <field name="commission_base"/>
<field name="commission_rate" string="Rate (%)"/> <field name="commission_rate" string="Rate (%)"/>

View File

@@ -16,6 +16,7 @@
], ],
'data': [ 'data': [
'views/commission_result.xml', 'views/commission_result.xml',
'views/commission_profile.xml',
'wizards/res_config_settings.xml', 'wizards/res_config_settings.xml',
], ],
'installable': False, 'installable': False,

View File

@@ -1,2 +1,3 @@
from . import commission_result from . import commission_result
from . import commission_profile
from . import res_company from . import res_company

View File

@@ -0,0 +1,20 @@
# Copyright Akretion France (http://www.akretion.com/)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# 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."
)

View File

@@ -5,6 +5,7 @@
from odoo import fields, models, _ from odoo import fields, models, _
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools.misc import format_amount, formatLang from odoo.tools.misc import format_amount, formatLang
from markupsafe import Markup
class CommissionResult(models.Model): class CommissionResult(models.Model):
@@ -18,22 +19,30 @@ class CommissionResult(models.Model):
if not result.purchase_id: if not result.purchase_id:
vals = result._prepare_purchase_order() vals = result._prepare_purchase_order()
po = self.env['purchase.order'].create(vals) po = self.env['purchase.order'].create(vals)
po.message_post(body=Markup(_("Generated from commission <a href=# data-oe-model=commission.result data-oe-id=%d>%s</a>.") % (result.id, result.display_name)))
result.write({'purchase_id': po.id}) result.write({'purchase_id': po.id})
else: else:
po = self.purchase_id po = self.purchase_id
if po.state in ('draft', 'sent', 'cancel'): if po.state in ('draft', 'sent', 'cancel'):
po.order_line.unlink() po.order_line.unlink()
po.message_post(body=Markup(_("Purchase order lines re-generated from commission <a href=# data-oe-model=commission.result data-oe-id=%d>%s</a>.") % (result.id, result.display_name)))
else: else:
raise UserError(_("Purchase Order %s has already been confirmed. You should cancel it first.") % po.display_name) raise UserError(_("Purchase Order %s has already been confirmed. You should cancel it first.") % po.display_name)
if po.state == 'cancel': if po.state == 'cancel':
po.button_draft() po.button_draft()
assert not po.order_line assert not po.order_line
# create lines # 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 = [] line_vals = []
for move_line in result.line_ids: if not result.company_id.commission_po_config:
line_vals.append(result._prepare_purchase_order_line(move_line, po)) 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 = self.env['purchase.order.line'].create(line_vals)
po_lines._compute_tax_id() po_lines._compute_tax_id()
return super().draft2done() return super().draft2done()
@@ -57,7 +66,13 @@ class CommissionResult(models.Model):
company_currency = move_line.company_id.currency_id company_currency = move_line.company_id.currency_id
lang = self.partner_id.lang or self.env.lang lang = self.partner_id.lang or self.env.lang
env = self.with_context(lang=lang).env 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 = { vals = {
'order_id': order.id, 'order_id': order.id,
'product_id': product.id, 'product_id': product.id,
@@ -68,6 +83,24 @@ class CommissionResult(models.Model):
} }
return vals 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): def unlink(self):
for result in self: for result in self:
if result.purchase_id: if result.purchase_id:

View File

@@ -12,3 +12,7 @@ class ResCompany(models.Model):
commission_product_id = fields.Many2one( commission_product_id = fields.Many2one(
'product.product', string='Commission Product', ondelete='restrict', check_company=True, 'product.product', string='Commission Product', ondelete='restrict', check_company=True,
domain=[('type', '=', 'service')]) domain=[('type', '=', 'service')])
commission_po_config = fields.Selection([
('single_line', 'Single Line'),
('details', 'One line per commission line'),
], default='details', string="Purchase Order Configuration")

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 Akretion France (https://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
-->
<odoo>
<record id="commission_profile_form" model="ir.ui.view">
<field name="model">commission.profile</field>
<field name="inherit_id" ref="commission_simple_agent.commission_profile_form"/>
<field name="arch" type="xml">
<group name="main-right" position="inside">
<field name="commission_product_id"/>
</group>
</field>
</record>
</odoo>

View File

@@ -10,3 +10,4 @@ class ResConfigSettings(models.TransientModel):
commission_product_id = fields.Many2one( commission_product_id = fields.Many2one(
related='company_id.commission_product_id', readonly=False) related='company_id.commission_product_id', readonly=False)
commission_po_config = fields.Selection(related="company_id.commission_po_config", readonly=False)

View File

@@ -18,6 +18,10 @@
<label for="commission_product_id" class="col-md-5" /> <label for="commission_product_id" class="col-md-5" />
<field name="commission_product_id" context="{'default_detailed_type': 'service', 'default_purchase_ok': True, 'default_sale_ok': False, 'default_available_in_pos': False, 'default_purchase_method': 'purchase'}"/> <field name="commission_product_id" context="{'default_detailed_type': 'service', 'default_purchase_ok': True, 'default_sale_ok': False, 'default_available_in_pos': False, 'default_purchase_method': 'purchase'}"/>
</div> </div>
<div class="row" id="commission_po_config">
<label for="commission_po_config" class="col-md-5" string="Purchase Order"/>
<field name="commission_po_config" />
</div>
</xpath> </xpath>
</field> </field>
</record> </record>