Compare commits

...

68 Commits
18.0 ... 16.0

Author SHA1 Message Date
Alexis de Lattre
864340850f [MIG] base_mail_sender_bcc: make base_mail_sender_bcc work again ! 2025-12-12 15:14:58 +01:00
Alexis de Lattre
1b80dd5957 [IMP] base_usability: improve comments 2025-12-01 18:30:13 +01:00
Alexis de Lattre
6897acd3df [FIX] base_usability: fix code for smtp_session in inherit of send_email() 2025-12-01 18:27:31 +01:00
Alexis de Lattre
74b2917875 [FIX] base_usability: ir_mail_server avoid breaking Bcc
Disable module base_mail_sender_bcc that just doesn't work in v16 ; as it's a design problem, I don't plan to fix it.
2025-12-01 17:05:17 +00:00
Alexis de Lattre
93ca6631e0 [IMP] commission_simple: allow removal of commission lines 2025-11-03 12:27:26 +01:00
Alexis de Lattre
b73a34e3d7 [IMP] commission_simple: allow to restart commission computation on the same period without deleting all commission results 2025-11-03 12:27:09 +01:00
Alexis de Lattre
ce3f10b8ca [IMP] base_usability: add industry_id in partner tree view
Show industry_id on form view of partner even when is_company=False ; hide for contacts
2025-11-03 12:14:16 +01:00
Alexis de Lattre
8f4f4dafdf [IMP] commission_simple: add index=True on the M2O of account.move.line
This speeds-up the opening of commission results
2025-11-03 12:05:57 +01:00
Alexis de Lattre
7033f8650f [IMP] commission_simple: add commission_amount to account.invoice.report 2025-11-03 12:03:30 +01:00
Alexis de Lattre
a9a205a08f [IMP] commission_simple: fine-tune access rights 2025-11-03 12:02:46 +01:00
Alexis de Lattre
1843e19e5f [IMP] commission_simple: periodicity moved from company to profile
Update commission_simple_agent_purchase to adapt config page form view
accordingly
2025-11-03 12:01:44 +01:00
Alexis de Lattre
c18791fb60 [IMP] l10n_fr_account_profile_akretion: add account_dashboard_banner 2025-10-22 10:05:19 +02:00
Alexis de Lattre
cf694a5f85 l10n_fr_account_profile_akretion: comment intrastat_product 2025-10-22 10:03:29 +02:00
Alexis de Lattre
e25785baf4 [IMP] base_usability: restore monkey-patch for formatLang() 2025-10-18 08:05:14 +02:00
Alexis de Lattre
7b0b738510 [IMP] commission_simple: validation from list view 2025-10-10 14:21:31 +00:00
Alexis de Lattre
9769393759 [ADD] hr_timesheet_sheet_usability_akretion 2025-09-05 17:48:13 +00:00
Alexis de Lattre
dc00366d80 [IMP] commission_simple: add total base amount in view and XLSX report 2025-09-05 09:29:00 +00:00
Alexis de Lattre
ffb031de12 [IMP] commission_simple: update fr translation 2025-09-02 13:30:36 +02:00
Alexis de Lattre
f600057b1b [IMP] commission_simple: allow use of lambda in inherit of commission result lines 2025-09-02 10:50:08 +00:00
Alexis de Lattre
13756ec6c3 [IMP] commission_simple: XSLX report sorted by date (inheritable) 2025-09-02 10:33:00 +00:00
Alexis de Lattre
69baec2c43 [IMP] commission_simple*: add XLSX report + improve PO generation
Add message un chatter of PO
2025-09-01 21:43:54 +00:00
Alexis de Lattre
e864e383ec [IMP] purchase_usability: show supplier taxes to accounting manager 2025-09-01 13:09:44 +00:00
Alexis de Lattre
dbd79c0ed0 [IMP] account_usability_akretion: order analytic accounts by code 2025-07-22 17:41:57 +02:00
Alexis de Lattre
934d1b8b02 [IMP] commission_simple: improve views and add description for rule list views 2025-06-23 16:13:38 +00:00
Alexis de Lattre
e4504fae0e [IMP] base_profile_akretion: add mail_debrand 2025-06-23 14:36:41 +00:00
Alexis de Lattre
e05102d807 [IMP] stock_usability: improve quants: allow groupby product category and show available qté in tree view of Inventory > Analysis > Locations 2025-06-23 09:53:09 +00:00
Alexis de Lattre
6567d6ad29 [IMP] stock_quant_package_move_wizard: more accurate source location on picking when all quants have the same location 2025-06-23 09:29:39 +00:00
Alexis de Lattre
0c97c7e1b2 Add module product_print_zpl_barcode_cups 2025-06-17 11:16:20 +00:00
Alexis de Lattre
c82efba0af l10n_fr_account_profile_akretion: add dep on account_lock_date_update 2025-06-06 11:02:13 +02:00
Florent THOMAS
47b029c2d2 FIX: The field has been removed by 0aa31956e0
This Commit adapt the post install script consequently
2025-05-27 00:33:38 +02:00
Alexis de Lattre
70647387d1 Add patch pos-product_analytic.diff 2025-05-22 21:44:26 +02:00
Alexis de Lattre
03d3f30df6 [MIG] product_category_tax from v14 to v16 2025-05-22 16:12:31 +02:00
Alexis de Lattre
bd58dcf351 [UDP] account_usability_akretion: Remove account-oca_localization.diff which is now merged in OCB 2025-05-22 16:11:40 +02:00
Alexis de Lattre
dd915b906b [MIG] purchase_stock_partner_default_picking_type from v14 to v16 2025-05-20 15:37:20 +02:00
Alexis de Lattre
8cc20fa84f [IMP] product_usability: forward-port seller_id now a computed field with search method
stock_usability: Add seller_id on orderpoints.
2025-05-20 10:09:43 +02:00
Alexis de Lattre
1b469946c0 [IMP] l10n_fr_account_profile_akretion: Add dep on date_range_account 2025-04-24 00:21:26 +02:00
Alexis de Lattre
89b27a4cab [FIX] base_profile_akretion: double entry in manifest 2025-04-15 20:37:27 +00:00
Alexis de Lattre
b9230b2cf5 Add modules base_profile_akretion and l10n_fr_account_profile_akretion 2025-04-15 17:52:51 +02:00
Alexis de Lattre
af01ae8ff3 [UDP] pos_check_deposit: adapt patch to changes in native point_of_sale module 2025-04-08 09:23:27 +00:00
Alexis de Lattre
cb632c1fc5 [IMP] stock_usability: add direct search on picking_type_id from stock.move.line 2025-04-07 09:19:40 +00:00
Alexis de Lattre
f6071b8324 [IMP] base_usability: tree view of res.partner: hide translated_display_name and show display_name 2025-03-17 14:14:19 +00:00
Alexis de Lattre
5a9bdcfd84 [IMP] account_usability_akretion: improve account.invoice.report search view
In account.invoice.report search view, direct access to group by commercial_partner_id instead of partner_id
2025-03-17 13:39:21 +00:00
Alexis de Lattre
4503d3f89d [IMP] agent: headers 2025-03-15 07:26:38 +01:00
Alexis de Lattre
287a2ab0fd [IMP] commission_simple: code cleanup and minor improvements 2025-03-15 07:25:53 +01:00
Alexis de Lattre
90fc4c3562 sale_stock_usability: don't raise in report method when partner is empty on picking 2025-03-10 16:36:41 +00:00
Alexis de Lattre
61a2205539 [IMP] account_usability_akretion: add filter on invoice/refund in invoice search view and invoice report search view 2025-03-03 11:49:27 +01:00
Alexis de Lattre
195a0203ab [IMP] stock_valuation_xlsx: move field in wizard 2025-02-26 12:11:34 +01:00
Alexis de Lattre
26abf1c8d6 [IMP] stock_usability: origin field is editable on done pickings
origin field is tracked in picking
2025-02-20 11:12:38 +00:00
Alexis de Lattre
66f617e797 account_usability_akretion: change behavior of running balance in dashboard
Use accounting balance like in v14 and not bank statement start/end balance
2025-02-19 10:43:02 +00:00
Alexis de Lattre
0aa31956e0 [IMP] base_partner_ref: remove field invalidate_display_name, because we can now call _compute_display_name()
Add script to recompute display name on all partners
2025-01-31 20:49:59 +00:00
Alexis de Lattre
ebd6003f08 [IMP] sale_usability: remove dead code that odoo bug #179392 is fixed 2025-01-31 16:19:31 +00:00
Alexis de Lattre
8200eb2dea [IMP] stock_usability: hide user_id by default in quant tree view 2025-01-29 23:01:36 +01:00
Alexis de Lattre
e35ce49ee2 [IMP] stock_usability: add in_date in quant views 2025-01-29 22:38:47 +01:00
Alexis de Lattre
9f4392b6bd [IMP] pos_usability: search on products from pos.order search view 2025-01-29 19:22:53 +01:00
Alexis de Lattre
d84b4bc8ac stock_usability: fix small native error on domain of a menu entry 2025-01-22 14:08:06 +00:00
Alexis de Lattre
2ca1279cb5 [MIG] sale_margin_no_onchange from v14 to v16 2025-01-16 19:19:19 +01:00
Alexis de Lattre
699ebd5893 [MIG] sale_margin_no_onchange from v12 to v14 2025-01-16 17:32:46 +01:00
Alexis de Lattre
d973ca6740 [IMP] account_invoice_margin: update string and translate to fr 2025-01-16 17:26:56 +01:00
Alexis de Lattre
e009106e12 [MIG] account_invoice_margin from v14 to v16
The is not more accidental invalidation in the Odoo ORM, so I switch to
computed field for standard price company currency on invoice line. No
more inherit of create() and write()
2025-01-16 17:08:37 +01:00
Alexis de Lattre
9ff6e15b45 [MIG] account_invoice_margin: from v12 to v14 2025-01-16 16:07:25 +01:00
Alexis de Lattre
33da5cd370 [FIX] base_partner_ref: don't display [REF] when there is show_address in context 2025-01-15 21:26:15 +01:00
Alexis de Lattre
a7b8401cd7 [ADD] hr_expense_usability_akretion
Copy attachments from expense sheet to invoice
2025-01-14 17:34:31 +00:00
Alexis de Lattre
96b4c9b094 [IMP] base_usability: add siren/siret on display method for partners 2025-01-14 14:33:38 +00:00
Alexis de Lattre
2f6491be4a Remove patch that was for older versions 2025-01-14 14:32:05 +00:00
Alexis de Lattre
e8caa77d88 [IMP] sale_usability: show complete invoice address on sale order form view 2025-01-10 10:09:34 +00:00
Alexis de Lattre
95b92d4027 [FIX] sale_stock_usability: fix order of lines in report method 2025-01-06 11:22:48 +00:00
Alexis de Lattre
bec65a009f [FIX] sale_usability: double field inherit 2024-12-31 09:58:22 +01:00
Alexis de Lattre
3757b12f39 [IMP] stock_usability: improve warehouse form view 2024-12-30 12:56:40 +01:00
128 changed files with 2065 additions and 938 deletions

View File

@@ -1,2 +1 @@
from . import account_invoice from . import models
from . import account_invoice_report

View File

@@ -1,24 +1,26 @@
# Copyright 2015-2019 Akretion France (http://www.akretion.com) # Copyright 2015-2025 Akretion France (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Account Invoice Margin', 'name': 'Account Invoice Margin',
'version': '12.0.1.0.0', 'version': '16.0.1.0.0',
'category': 'Invoicing Management', 'category': 'Invoicing Management',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': 'Copy standard price on invoice line and compute margins', 'summary': 'Copy standard price on invoice line and compute margins',
'description': """ 'description': """
This module copies the field *standard_price* of the product on the invoice line when the invoice line is created. The allows the computation of the margin of the invoice. This module copies the field *standard_price* of the product on the invoice line when the invoice line is created. The allows the computation of the margin of the invoice.
A new measure *Margin* is available in the Invoice Analysis.
This module has been written by Alexis de Lattre from Akretion This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>. <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['account'], 'depends': ['account'],
'data': [ 'data': [
'account_invoice_view.xml', 'views/account_move.xml',
], ],
'installable': False, 'installable': True,
} }

View File

@@ -1,152 +0,0 @@
# Copyright 2015-2019 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 api, fields, models
import odoo.addons.decimal_precision as dp
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
standard_price_company_currency = fields.Float(
string='Cost Price in Company Currency', readonly=True,
digits=dp.get_precision('Product Price'),
help="Cost price in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
string='Cost Price in Invoice Currency', readonly=True,
compute='_compute_margin', store=True,
digits=dp.get_precision('Product Price'),
help="Cost price in invoice currency in the unit of measure "
"of the invoice line")
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'standard_price_company_currency', 'invoice_id.currency_id',
'invoice_id.type', 'invoice_id.company_id',
'invoice_id.date_invoice', 'quantity', 'price_subtotal')
def _compute_margin(self):
for il in self:
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
inv = il.invoice_id
if inv and inv.type in ('out_invoice', 'out_refund'):
# it works in _get_current_rate
# even if we set date = False in context
# standard_price_inv_cur is in the UoM of the invoice line
date = inv._get_currency_rate_date() or\
fields.Date.context_today(self)
company = inv.company_id
company_currency = company.currency_id
standard_price_inv_cur =\
company_currency._convert(
il.standard_price_company_currency,
inv.currency_id, company, date)
margin_inv_cur =\
il.price_subtotal - il.quantity * standard_price_inv_cur
margin_comp_cur = inv.currency_id._convert(
margin_inv_cur, company_currency, company, date)
if il.price_subtotal:
margin_rate = 100 * margin_inv_cur / il.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if inv.type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
il.standard_price_invoice_currency = standard_price_inv_cur
il.margin_invoice_currency = margin_inv_cur
il.margin_company_currency = margin_comp_cur
il.margin_rate = margin_rate
# We want to copy standard_price on invoice line for customer
# invoice/refunds. We can't do that via on_change of product_id,
# because it is not always played when invoice is created from code
# => we inherit write/create
# We write standard_price_company_currency even on supplier invoice/refunds
# because we don't have access to the 'type' of the invoice
@api.model
def create(self, vals):
if vals.get('product_id'):
pp = self.env['product.product'].browse(vals['product_id'])
std_price = pp.standard_price
inv_uom_id = vals.get('uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super(AccountInvoiceLine, self).create(vals)
def write(self, vals):
if not vals:
vals = {}
if 'product_id' in vals or 'uom_id' in vals:
for il in self:
if 'product_id' in vals:
if vals.get('product_id'):
pp = self.env['product.product'].browse(
vals['product_id'])
else:
pp = False
else:
pp = il.product_id or False
# uom_id is NOT a required field
if 'uom_id' in vals:
if vals.get('uom_id'):
inv_uom = self.env['uom.uom'].browse(
vals['uom_id'])
else:
inv_uom = False
else:
inv_uom = il.uom_id or False
std_price = 0.0
if pp:
std_price = pp.standard_price
if inv_uom and inv_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
il.write({'standard_price_company_currency': std_price})
return super(AccountInvoiceLine, self).write(vals)
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='company_currency_id')
@api.depends(
'type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
res = self.env['account.invoice.line'].read_group(
[('invoice_id', 'in', self.ids)],
['invoice_id', 'margin_invoice_currency',
'margin_company_currency'],
['invoice_id'])
for re in res:
if re['invoice_id']:
inv = self.browse(re['invoice_id'][0])
if inv.type in ('out_invoice', 'out_refund'):
inv.margin_invoice_currency = re['margin_invoice_currency']
inv.margin_company_currency = re['margin_company_currency']

View File

@@ -1,60 +0,0 @@
# Copyright 2018-2019 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 api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# why digits=0 ??? Why is it like that in the native "account" module
user_currency_margin = fields.Float(
string="Margin", compute='_compute_user_currency_margin', digits=0)
_depends = {
'account.invoice': [
'account_id', 'amount_total_company_signed',
'commercial_partner_id', 'company_id',
'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
'journal_id', 'number', 'partner_bank_id', 'partner_id',
'payment_term_id', 'residual', 'state', 'type', 'user_id',
],
'account.invoice.line': [
'account_id', 'invoice_id', 'price_subtotal', 'product_id',
'quantity', 'uom_id', 'account_analytic_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.depends('currency_id', 'date', 'margin')
def _compute_user_currency_margin(self):
user_currency = self.env.user.company_id.currency_id
currency_rate = self.env['res.currency.rate'].search([
('rate', '=', 1),
'|',
('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False)], limit=1)
base_currency = currency_rate.currency_id
for record in self:
date = record.date or fields.Date.today()
company = record.company_id
record.user_currency_margin = base_currency._convert(
record.margin, user_currency, company, date)
# TODO check for refunds
def _sub_select(self):
select_str = super(AccountInvoiceReport, self)._sub_select()
select_str += ", SUM(ail.margin_company_currency) AS margin"
return select_str
def _select(self):
select_str = super(AccountInvoiceReport, self)._select()
select_str += ", sub.margin AS margin"
return select_str

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015-2017 Akretion (http://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="view_invoice_line_form" model="ir.ui.view">
<field name="name">margin.account.invoice.line.form</field>
<field name="model">account.invoice.line</field>
<field name="inherit_id" ref="account.view_invoice_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='analytic_tag_ids']/.." position="inside">
<field name="standard_price_company_currency"
string="Cost Price in Comp. Cur."
groups="base.group_no_one"/>
<field name="standard_price_invoice_currency"
string="Cost Price in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_invoice_currency"
string="Margin in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur."
groups="base.group_no_one"/>
<label for="margin_rate" groups="base.group_no_one"/>
<div name="margin_rate" groups="base.group_no_one">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="invoice_form" model="ir.ui.view">
<field name="name">margin.account.invoice.form</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<field name="move_id" position="after">
<field name="margin_invoice_currency"
string="Margin in Inv. Cur." groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur." groups="base.group_no_one"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,98 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_invoice_margin
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-16 16:24+0000\n"
"PO-Revision-Date: 2025-01-16 16:24+0000\n"
"Last-Translator: Alexis de Lattre <alexis.delattre@akretion.com>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_invoice_margin
#: model:ir.model,name:account_invoice_margin.model_account_invoice_report
msgid "Invoices Statistics"
msgstr "Statistiques des factures"
#. module: account_invoice_margin
#: model:ir.model,name:account_invoice_margin.model_account_move
msgid "Journal Entry"
msgstr "Pièce comptable"
#. module: account_invoice_margin
#: model:ir.model,name:account_invoice_margin.model_account_move_line
msgid "Journal Item"
msgstr "Écriture comptable"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_invoice_report__margin
#: model_terms:ir.ui.view,arch_db:account_invoice_margin.view_invoice_tree
#: model_terms:ir.ui.view,arch_db:account_invoice_margin.view_move_form
msgid "Margin"
msgstr "Marge"
#. module: account_invoice_margin
#: model_terms:ir.ui.view,arch_db:account_invoice_margin.view_move_form
msgid "Margin %"
msgstr "Marge %"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move_line__margin_rate
msgid "Margin Rate"
msgstr "Taux de marge"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_bank_statement_line__margin_company_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move__margin_company_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move_line__margin_company_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_payment__margin_company_currency
msgid "Margin in Company Currency"
msgstr "Marge dans la devise de la société"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_bank_statement_line__margin_invoice_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move__margin_invoice_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move_line__margin_invoice_currency
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_payment__margin_invoice_currency
msgid "Margin in Invoice Currency"
msgstr "Marge dans la devise de la facture"
#. module: account_invoice_margin
#: model:ir.model.fields,help:account_invoice_margin.field_account_move_line__margin_rate
msgid "Margin rate in percentage of the sale price"
msgstr "Taux de marge en pourcentage du prix de vente"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move_line__standard_price_company_currency
msgid "Unit Cost in Company Currency"
msgstr "Coût unitaire dans la devise de la société"
#. module: account_invoice_margin
#: model:ir.model.fields,field_description:account_invoice_margin.field_account_move_line__standard_price_invoice_currency
msgid "Unit Cost in Invoice Currency"
msgstr "Coût unitaire dans la devise de la facture"
#. module: account_invoice_margin
#: model:ir.model.fields,help:account_invoice_margin.field_account_move_line__standard_price_company_currency
msgid ""
"Unit Cost in company currency in the unit of measure of the invoice line "
"(which may be different from the unit of measure of the product)."
msgstr ""
"Coût unitaire dans la devise de la société dans l'unité de mesure de la "
"ligne de facture (qui peut être différente de l'unité de mesure du produit)."
#. module: account_invoice_margin
#: model:ir.model.fields,help:account_invoice_margin.field_account_move_line__standard_price_invoice_currency
msgid ""
"Unit Cost in invoice currency in the unit of measure of the invoice line "
"(which may be different from the unit of measure of the product)."
msgstr ""
"Coût unitaire dans la devise de la facture dans l'unité de mesure "
"de la ligne de la facture (qui peut être différente de l'unité de mesure du produit)."

View File

@@ -0,0 +1,2 @@
from . import account_move
from . import account_invoice_report

View File

@@ -0,0 +1,17 @@
# Copyright 2018-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).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
@api.model
def _select(self):
select_str = super()._select()
select_str += ", line.margin_company_currency * currency_table.rate AS margin"
return select_str

View File

@@ -0,0 +1,109 @@
# Copyright 2015-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).
from odoo import api, fields, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
standard_price_company_currency = fields.Float(
compute='_compute_margin', store=True, digits='Product Price',
string='Unit Cost in Company Currency',
help="Unit Cost in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
compute='_compute_margin', store=True, digits='Product Price',
string='Unit Cost in Invoice Currency',
help="Unit Cost in invoice currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
margin_invoice_currency = fields.Monetary(
compute='_compute_margin', store=True,
string='Margin in Invoice Currency', currency_field='currency_id')
margin_company_currency = fields.Monetary(
compute='_compute_margin', store=True,
string='Margin in Company Currency',
currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'product_id', 'product_uom_id', 'display_type', 'quantity', 'price_subtotal',
'move_id.currency_id', 'move_id.move_type', 'move_id.company_id', 'move_id.date')
def _compute_margin(self):
for ml in self:
standard_price_comp_cur = 0.0
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
if (
ml.display_type == 'product' and
ml.product_id and
ml.move_type in ('out_invoice', 'out_refund')):
move = ml.move_id
date = move.date
company = move.company_id
company_currency = company.currency_id
move_currency = move.currency_id
standard_price_comp_cur = ml.product_id.with_company(company.id).standard_price
if ml.product_uom_id and ml.product_uom_id != ml.product_id.uom_id:
standard_price_comp_cur = ml.product_id.uom_id._compute_price(
standard_price_comp_cur, ml.product_uom_id)
standard_price_inv_cur = company_currency._convert(
standard_price_comp_cur, move_currency, company, date)
margin_inv_cur =\
ml.price_subtotal - (ml.quantity * standard_price_inv_cur)
margin_comp_cur = move_currency._convert(
margin_inv_cur, company_currency, company, date)
if ml.price_subtotal:
margin_rate = 100 * margin_inv_cur / ml.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if ml.move_type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
ml.standard_price_company_currency = standard_price_comp_cur
ml.standard_price_invoice_currency = standard_price_inv_cur
ml.margin_invoice_currency = margin_inv_cur
ml.margin_company_currency = margin_comp_cur
ml.margin_rate = margin_rate
class AccountMove(models.Model):
_inherit = 'account.move'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True,
currency_field='company_currency_id')
@api.depends(
'move_type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
rg_res = self.env['account.move.line'].read_group(
[
('move_id', 'in', self.ids),
('display_type', '=', 'product'),
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
],
['move_id', 'margin_invoice_currency:sum', 'margin_company_currency:sum'],
['move_id'])
mapped_data = dict([(x['move_id'][0], {
'margin_invoice_currency': x['margin_invoice_currency'],
'margin_company_currency': x['margin_company_currency'],
}) for x in rg_res])
for move in self:
move.margin_invoice_currency = mapped_data.get(move.id, {}).get('margin_invoice_currency')
move.margin_company_currency = mapped_data.get(move.id, {}).get('margin_company_currency')

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015-2025 Akretion (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="view_move_form" model="ir.ui.view">
<field name="name">margin.account.move.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<group name="sale_info_group" position="inside">
<field name="margin_invoice_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</group>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='price_total']" position="after">
<field name="standard_price_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}" string="Margin"/>
<field name="margin_rate" optional="hide" string="Margin %" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</xpath>
<xpath expr="//field[@name='invoice_line_ids']/form//field[@name='price_total']/.." position="inside">
<field name="standard_price_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="standard_price_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<label for="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<div name="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="view_invoice_tree" model="ir.ui.view">
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="arch" type="xml">
<field name="amount_residual_signed" position="after">
<field name="margin_company_currency" optional="hide" sum="1" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund')" string="Margin"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,15 +0,0 @@
diff --git a/addons/account/__init__.py b/addons/account/__init__.py
index 138004b0849..07e6475f760 100644
--- a/addons/account/__init__.py
+++ b/addons/account/__init__.py
@@ -45,7 +45,9 @@ def _auto_install_l10n(env):
module_list.append('l10n_de_skr03')
module_list.append('l10n_de_skr04')
else:
- if env['ir.module.module'].search([('name', '=', 'l10n_' + country_code.lower())]):
+ if env['ir.module.module'].search([('name', '=', 'l10n_%s_oca' % country_code.lower())]):
+ module_list.append('l10n_%s_oca' % country_code.lower())
+ elif env['ir.module.module'].search([('name', '=', 'l10n_' + country_code.lower())]):
module_list.append('l10n_' + country_code.lower())
else:
module_list.append('l10n_generic_coa')

View File

@@ -1,55 +0,0 @@
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 2dd1f9cef83..62275fca65e 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -262,6 +262,7 @@ class AccountPayment(models.Model):
'credit': write_off_balance > 0.0 and write_off_balance or 0.0,
'partner_id': self.partner_id.id,
'account_id': write_off_line_vals.get('account_id'),
+ 'analytic_account_id': write_off_line_vals.get('analytic_account_id'),
})
return line_vals_list
@@ -699,6 +700,7 @@ class AccountPayment(models.Model):
'name': writeoff_lines[0].name,
'amount': writeoff_amount * sign,
'account_id': writeoff_lines[0].account_id.id,
+ 'analytic_account_id': writeoff_lines[0].analytic_account_id.id,
}
else:
write_off_line_vals = {}
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py
index 3fc91f716ad..35636774c7e 100644
--- a/addons/account/wizard/account_payment_register.py
+++ b/addons/account/wizard/account_payment_register.py
@@ -93,6 +93,7 @@ class AccountPaymentRegister(models.TransientModel):
], default='open', string="Payment Difference Handling")
writeoff_account_id = fields.Many2one('account.account', string="Difference Account", copy=False,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]")
+ writeoff_analytic_account_id = fields.Many2one('account.analytic.account', string="Difference Analytic Account", copy=False, domain="[('company_id', '=', company_id)]")
writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
help='Change label of the counterpart that will hold the payment difference')
@@ -422,6 +423,7 @@ class AccountPaymentRegister(models.TransientModel):
'name': self.writeoff_label,
'amount': self.payment_difference,
'account_id': self.writeoff_account_id.id,
+ 'analytic_account_id': self.writeoff_analytic_account_id.id or False,
}
return payment_vals
diff --git a/addons/account/wizard/account_payment_register_views.xml b/addons/account/wizard/account_payment_register_views.xml
index 16eec30e265..b9386567baa 100644
--- a/addons/account/wizard/account_payment_register_views.xml
+++ b/addons/account/wizard/account_payment_register_views.xml
@@ -65,6 +65,10 @@
string="Post Difference In"
options="{'no_create': True}"
attrs="{'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
+ <label for="writeoff_analytic_account_id" class="oe_edit_only" string="Analytic Account" groups="analytic.group_analytic_accounting"/>
+ <field name="writeoff_analytic_account_id"
+ groups="analytic.group_analytic_accounting"
+ options="{'no_create': True}" />
<label for="writeoff_label" class="oe_edit_only" string="Label"/>
<field name="writeoff_label" attrs="{'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
</div>

View File

@@ -7,6 +7,8 @@ from odoo import models
class AccountAnalyticAccount(models.Model): class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account' _inherit = 'account.analytic.account'
# native: _order = 'plan_id, name asc'
_order = 'plan_id, code, name'
def name_get(self): def name_get(self):
if self._context.get('analytic_account_show_code_only'): if self._context.get('analytic_account_show_code_only'):

View File

@@ -9,7 +9,7 @@ class AccountJournal(models.Model):
_inherit = 'account.journal' _inherit = 'account.journal'
hide_bank_statement_balance = fields.Boolean( hide_bank_statement_balance = fields.Boolean(
string='Hide and Disable Bank Statement Balance', string='Hide and Disable Bank Statement Balance', default=True,
help="When this option is enabled, the start and end balance is " help="When this option is enabled, the start and end balance is "
"not displayed on the bank statement form view, and the check of " "not displayed on the bank statement form view, and the check of "
"the end balance vs the real end balance is disabled. When you enable " "the end balance vs the real end balance is disabled. When you enable "
@@ -55,3 +55,12 @@ class AccountJournal(models.Model):
'search_default_posted': True, 'search_default_posted': True,
} }
return action return action
# field used in journal dashboard for bank journals
# compute account_balance like in v14, using accounting and
# NOT bank statement balance_start/balance_end
def _compute_current_statement_balance(self):
query_result = self._get_journal_dashboard_bank_running_balance()
for journal in self:
journal.has_statement_lines = query_result.get(journal.id)[0]
journal.current_statement_balance = journal._get_journal_bank_account_balance()[0]

View File

@@ -34,13 +34,20 @@
</field> </field>
<filter name="category_product" position="after"> <filter name="category_product" position="after">
<filter string="Product" name="product_groupby" context="{'group_by': 'product_id', 'residual_invisible':True}"/> <filter string="Product" name="product_groupby" context="{'group_by': 'product_id', 'residual_invisible':True}"/>
<filter name="invoice_refund_groupby" string="Invoice/Refund" context="{'group_by': 'move_type'}"/>
</filter>
<!-- group by on commercial_partner_id by default -->
<filter name="partner_id" position="attributes">
<attribute name="invisible">1</attribute>
</filter> </filter>
<filter name="partner_id" position="after"> <filter name="partner_id" position="after">
<filter name="commercial_partner_groupby" string="Commercial Partner" context="{'group_by': 'commercial_partner_id'}"/>
<filter name="industry_groupby" string="Partner Industry" context="{'group_by': 'industry_id'}"/> <filter name="industry_groupby" string="Partner Industry" context="{'group_by': 'industry_id'}"/>
</filter> </filter>
</field> </field>
</record> </record>
<!-- Switch order: pivot in 1st position -->
<record id="account.action_account_invoice_report_all_supp" model="ir.actions.act_window"> <record id="account.action_account_invoice_report_all_supp" model="ir.actions.act_window">
<field name="view_mode">pivot,graph</field> <field name="view_mode">pivot,graph</field>
</record> </record>

View File

@@ -36,7 +36,6 @@
</field> </field>
</record> </record>
<!-- TODO
<record id="account_journal_dashboard_kanban_view" model="ir.ui.view"> <record id="account_journal_dashboard_kanban_view" model="ir.ui.view">
<field name="name">usability.account.journal.dashboard</field> <field name="name">usability.account.journal.dashboard</field>
<field name="model">account.journal</field> <field name="model">account.journal</field>
@@ -48,12 +47,12 @@
<xpath expr="//div[@name='latest_statement']/.." position="attributes"> <xpath expr="//div[@name='latest_statement']/.." position="attributes">
<attribute name="t-if">dashboard.has_at_least_one_statement and dashboard.account_balance != dashboard.last_balance and !record.hide_bank_statement_balance.raw_value</attribute> <attribute name="t-if">dashboard.has_at_least_one_statement and dashboard.account_balance != dashboard.last_balance and !record.hide_bank_statement_balance.raw_value</attribute>
</xpath> </xpath>
<!--
<t t-esc="dashboard.outstanding_pay_account_balance" position="replace"> <t t-esc="dashboard.outstanding_pay_account_balance" position="replace">
<a name="open_outstanding_payments" type="object" title="Outstanding Payments/Receipts"><t t-esc="dashboard.outstanding_pay_account_balance"/></a> <a name="open_outstanding_payments" type="object" title="Outstanding Payments/Receipts"><t t-esc="dashboard.outstanding_pay_account_balance"/></a>
</t> </t> -->
</field> </field>
</record> </record>
-->
<record id="view_account_journal_search" model="ir.ui.view"> <record id="view_account_journal_search" model="ir.ui.view">
<field name="name">usability.account.journal.search</field> <field name="name">usability.account.journal.search</field>

View File

@@ -81,6 +81,11 @@
<field name="model">account.move</field> <field name="model">account.move</field>
<field name="inherit_id" ref="account.view_account_invoice_filter"/> <field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<filter name="invoice_date" position="before">
<filter name="invoice" string="Invoices" domain="[('move_type', 'in', ('out_invoice', 'in_invoice'))]"/>
<filter name="refund" string="Refunds" domain="[('move_type', 'in', ('out_refund', 'in_refund'))]"/>
<separator/>
</filter>
<filter name="due_date" position="after"> <filter name="due_date" position="after">
<separator/> <separator/>
<filter name="to_send" string="To Send" domain="[('is_move_sent', '=', False), ('state', '=', 'posted'), ('move_type', 'in', ('out_invoice', 'out_refund'))]"/> <filter name="to_send" string="To Send" domain="[('is_move_sent', '=', False), ('state', '=', 'posted'), ('move_type', 'in', ('out_invoice', 'out_refund'))]"/>

View File

@@ -2,7 +2,7 @@
# @author: Alexis de Lattre <alexis.delattre@akretion.com> # @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models from odoo import models, tools
class IrMailServer(models.Model): class IrMailServer(models.Model):
@@ -25,3 +25,13 @@ class IrMailServer(models.Model):
message_id=message_id, references=references, object_id=object_id, message_id=message_id, references=references, object_id=object_id,
subtype=subtype, headers=headers, subtype=subtype, headers=headers,
body_alternative=body_alternative, subtype_alternative=subtype_alternative) body_alternative=body_alternative, subtype_alternative=subtype_alternative)
def _prepare_email_message(self, message, smtp_session):
validated_to = self.env.context.get('send_validated_to') or []
if message['Bcc']:
email_bcc_normalized = tools.email_normalize_all(message['Bcc'])
for email in email_bcc_normalized:
if email not in validated_to:
validated_to.append(email)
return super(IrMailServer, self.with_context(send_validated_to=validated_to))._prepare_email_message(
message, smtp_session)

View File

@@ -4,13 +4,14 @@
from odoo import api, fields, models from odoo import api, fields, models
import re import re
import logging
logger = logging.getLogger(__name__)
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
ref = fields.Char(copy=False) # To avoid blocking duplicate ref = fields.Char(copy=False) # To avoid blocking duplicate
invalidate_display_name = fields.Boolean()
_sql_constraints = [( _sql_constraints = [(
'ref_unique', 'ref_unique',
@@ -19,7 +20,7 @@ class ResPartner(models.Model):
)] )]
# add 'ref' in depends # add 'ref' in depends
@api.depends('ref', 'invalidate_display_name') @api.depends('ref')
def _compute_display_name(self): def _compute_display_name(self):
super()._compute_display_name() super()._compute_display_name()
@@ -28,7 +29,7 @@ class ResPartner(models.Model):
name = partner.name or '' name = partner.name or ''
# START modif of native method # START modif of native method
if partner.ref: if not self._context.get('show_address') and partner.ref:
name = "[%s] %s" % (partner.ref, name) name = "[%s] %s" % (partner.ref, name)
# END modif of native method # END modif of native method
if partner.company_name or partner.parent_id: if partner.company_name or partner.parent_id:
@@ -39,7 +40,7 @@ class ResPartner(models.Model):
# START modif of native name_get() method # START modif of native name_get() method
company_name = partner.commercial_company_name or\ company_name = partner.commercial_company_name or\
partner.sudo().parent_id.name partner.sudo().parent_id.name
if partner.parent_id.ref: if not self._context.get('show_address') and partner.parent_id.ref:
company_name = "[%s] %s" % (partner.parent_id.ref, company_name) company_name = "[%s] %s" % (partner.parent_id.ref, company_name)
name = "%s, %s" % (company_name, name) name = "%s, %s" % (company_name, name)
# END modif of native name_get() method # END modif of native name_get() method
@@ -71,3 +72,12 @@ class ResPartner(models.Model):
rec_childs = self.search([('id', 'child_of', recs.ids)]) rec_childs = self.search([('id', 'child_of', recs.ids)])
return rec_childs.name_get() return rec_childs.name_get()
return super().name_search(name=name, args=args, operator=operator, limit=limit) return super().name_search(name=name, args=args, operator=operator, limit=limit)
@api.model
def _script_invalidate_display_name(self):
"""Script designed to regenerate the display_name"""
logger.info('Start script to invalidate display_name')
partners = self.with_context(active_test=False).search([])
logger.info('Calling _compute_display_name on %d partners', len(partners))
partners._compute_display_name()
logger.info('End of the script to invalidate display_name')

View File

@@ -9,4 +9,4 @@ def update_partner_display_name(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
partners = env['res.partner'].with_context(active_test=False).search( partners = env['res.partner'].with_context(active_test=False).search(
[('ref', '!=', False)]) [('ref', '!=', False)])
partners.write({'invalidate_display_name': True}) partners._compute_display_name()

View File

View File

@@ -0,0 +1,43 @@
# 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).
{
'name': 'Base Profile by Akretion',
'version': '16.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Base module set selected by Alexis de Lattre',
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': [
# PARTNER
'partner_firstname', # OCA/partner-contact
'partner_email_duplicate_warn', # OCA/partner-contact
'partner_mobile_duplicate_warn', # OCA/partner-contact
'contacts', # official addons
# AUTH
'auth_admin_passkey', # OCA/server-auth
# REMOVE or FIX BAD NATIVE STUFF
'disable_odoo_online', # OCA/server-brand
'remove_odoo_enterprise', # OCA/server-brand
'mail_debrand', # OCA/social
'partner_disable_gravatar', # OCA/partner-contact
'base_technical_features', # OCA/server-ux
### WEB
'web_responsive', # OCA/web
'web_environment_ribbon', # OCA/web
'web_no_bubble', # OCA/web
'web_dialog_size', # OCA/web
'web_chatter_position', # OCA/web
### MISC
'base_usability', # akretion/odoo-usability
'mail_usability', # akretion/odoo-usability
'eradicate_quick_create', # akretion/odoo-usability
'base_company_extension', # akretion/odoo-usability
# password_security will be enabled when the move to ir.config_parameter
# will be backported
#'password_security', # OCA/server-auth
],
'installable': True,
}

View File

@@ -5,3 +5,4 @@ from . import res_company
from . import ir_mail_server from . import ir_mail_server
from . import ir_model from . import ir_model
from . import ir_model_fields from . import ir_model_fields
from . import misc

View File

@@ -18,14 +18,24 @@ class IrMailServer(models.Model):
smtp_ssl_certificate=None, smtp_ssl_private_key=None, smtp_ssl_certificate=None, smtp_ssl_private_key=None,
smtp_debug=False, smtp_session=None): smtp_debug=False, smtp_session=None):
# Start copy from native method # Start copy from native method
if not smtp_session:
smtp_session = self.connect(
smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption,
smtp_from=message['From'], ssl_certificate=smtp_ssl_certificate,
ssl_private_key=smtp_ssl_private_key,
smtp_debug=smtp_debug, mail_server_id=mail_server_id)
# _prepare_email_message() will remove the Bcc field in message
# that's why we need to save it and re-inject it in message
email_bcc = message['Bcc']
smtp_from, smtp_to_list, message = self._prepare_email_message( smtp_from, smtp_to_list, message = self._prepare_email_message(
message, smtp_session) message, smtp_session)
message['Bcc'] = email_bcc
# End copy from native method # End copy from native method
logger.info( logger.info(
"Sending email from '%s' to '%s' Cc '%s' Bcc '%s' " "Sending email from '%s' to '%s' Cc '%s' Bcc '%s' "
"with subject '%s'", "with subject '%s'. smtp_to_list=%s",
smtp_from, message.get('To'), message.get('Cc'), smtp_from, message.get('To'), message.get('Cc'),
message.get('Bcc'), message.get('Subject')) message.get('Bcc'), message.get('Subject'), smtp_to_list)
return super().send_email( return super().send_email(
message, mail_server_id=mail_server_id, message, mail_server_id=mail_server_id,
smtp_server=smtp_server, smtp_port=smtp_port, smtp_server=smtp_server, smtp_port=smtp_port,

View File

@@ -0,0 +1,36 @@
# Copyright 2016-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).
from odoo import models
from odoo.tools import misc, float_compare
class BaseUsabilityInstalled(models.AbstractModel):
_name = "base.usability.installed"
_description = "Base Usability Installed"
formatLang_original = misc.formatLang
def formatLang(
env, value, digits=None, grouping=True,
monetary=False, dp=False, currency_obj=False, int_no_digits=True):
if (
'base.usability.installed' in env and
int_no_digits and
not monetary and
isinstance(value, float) and
dp):
prec = env['decimal.precision'].precision_get(dp)
if not float_compare(value, int(value), precision_digits=prec):
digits = 0
dp = False
res = formatLang_original(
env, value, digits=digits, grouping=grouping,
monetary=monetary, dp=dp, currency_obj=currency_obj)
return res
misc.formatLang = formatLang

View File

@@ -125,6 +125,20 @@ class ResPartner(models.Model):
'label': _('Supplier Number:'), 'label': _('Supplier Number:'),
}, },
} }
if hasattr(self, 'siren'):
options['siren'] = {
'value': self.siren,
'label': _("SIREN:"),
}
if hasattr(self, 'siret'):
if hasattr(self, 'siren'): # l10n_fr_siret is installed
siret = self.siren and self.nic and self.siret or False
else:
siret = self.siret
options['siret'] = {
'value': siret,
'label': _("SIRET:"),
}
res = [] res = []
for detail in details: for detail in details:
if options.get(detail) and options[detail]['value']: if options.get(detail) and options[detail]['value']:

View File

@@ -20,6 +20,9 @@
<div attrs="{'invisible': [('same_vat_partner_id', '=', False)]}" position="attributes"> <div attrs="{'invisible': [('same_vat_partner_id', '=', False)]}" position="attributes">
<attribute name="class">alert alert-warning</attribute> <attribute name="class">alert alert-warning</attribute>
</div> </div>
<field name="industry_id" position="attributes">
<attribute name="attrs">{'invisible': [('parent_id', '!=', False)]}</attribute>
</field>
</field> </field>
</record> </record>
@@ -39,6 +42,16 @@
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/> <field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- By default, the tree view shows the field translated_display_name which is NOT stored
so the user cannot order by this column. I prefer to show display_name which is
a stored field. If the experience shows that some people prefer the native behavior
we'll go back to display translated_display_name -->
<field name="translated_display_name" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="display_name" position="attributes">
<attribute name="invisible">0</attribute>
</field>
<field name="display_name" position="after"> <field name="display_name" position="after">
<field name="ref" optional="hide"/> <field name="ref" optional="hide"/>
</field> </field>
@@ -50,6 +63,9 @@
<field name="street2" optional="hide"/> <field name="street2" optional="hide"/>
<field name="zip" optional="hide"/> <field name="zip" optional="hide"/>
</field> </field>
<field name="category_id" position="after">
<field name="industry_id" optional="hide"/>
</field>
</field> </field>
</record> </record>

View File

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

View File

@@ -30,18 +30,17 @@ This module has been written by Alexis de Lattre from Akretion
'depends': [ 'depends': [
'account', 'account',
'date_range', 'date_range',
# this uses some related fields on account.move.line 'report_xlsx',
# 'account_usability_akretion',
], ],
'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',
'views/commission_result.xml', 'views/commission_result.xml',
'views/account_move_line.xml', 'views/account_move_line.xml',
'views/res_config_settings.xml',
'wizards/commission_compute_view.xml', 'wizards/commission_compute_view.xml',
], ],
'installable': True, 'installable': True,

View File

@@ -6,15 +6,25 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 16.0\n" "Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-29 23:38+0000\n" "POT-Creation-Date: 2025-09-02 11:22+0000\n"
"PO-Revision-Date: 2024-11-29 23:38+0000\n" "PO-Revision-Date: 2025-09-02 11:23+0000\n"
"Last-Translator: \n" "Last-Translator: Alexis de Lattre <alexis.delattre@akretion.com>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n" "Content-Transfer-Encoding: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#. module: commission_simple
#: model:ir.actions.report,print_report_name:commission_simple.commission_result_xlsx_report
msgid ""
"'commission-%s-%s' % (object.date_range_id.name.replace(' ', '_'), object."
"partner_id.name.replace(' ', '_'))"
msgstr ""
"'commission-%s-%s' % (object.date_range_id.name.replace(' ', '_'), object."
"partner_id.name.replace(' ', '_'))"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.constraint,message:commission_simple.constraint_commission_result_salesman_period_company_unique #: model:ir.model.constraint,message:commission_simple.constraint_commission_result_salesman_period_company_unique
msgid "" msgid ""
@@ -33,6 +43,13 @@ msgstr ""
"Un vendeur doit être sélectionné lorsque le type d'affectation est " "Un vendeur doit être sélectionné lorsque le type d'affectation est "
"\"Vendeur\"." "\"Vendeur\"."
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/models/commission_rule.py:0
#, python-format
msgid "AND"
msgstr "ET"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_needaction #: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_needaction
msgid "Action Needed" msgid "Action Needed"
@@ -108,13 +125,24 @@ msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
#. module: commission_simple #. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.view_move_line_form
msgid "Commission"
msgstr "Commission"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_amount #: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_amount
#, python-format
msgid "Commission Amount" msgid "Commission Amount"
msgstr "Montant de la commission" msgstr "Montant de la commission"
#. module: commission_simple #. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_base #: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_base
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__base #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__base
#, python-format
msgid "Commission Base" msgid "Commission Base"
msgstr "Base de la commission" msgstr "Base de la commission"
@@ -122,14 +150,14 @@ msgstr "Base de la commission"
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__line_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_result__line_ids
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form #: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form
msgid "Commission Lines" msgid "Commission Lines"
msgstr "Lignes commission" msgstr "Lignes de commission"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_compute__date_range_type_id #: model:ir.model.fields,field_description:commission_simple.field_commission_compute__date_range_type_id
#: model:ir.model.fields,field_description:commission_simple.field_res_company__commission_date_range_type_id #: model:ir.model.fields,field_description:commission_simple.field_res_company__commission_date_range_type_id
#: model:ir.model.fields,field_description:commission_simple.field_res_config_settings__commission_date_range_type_id #: model:ir.model.fields,field_description:commission_simple.field_res_config_settings__commission_date_range_type_id
msgid "Commission Periodicity" msgid "Commission Periodicity"
msgstr "Périodicité de commission" msgstr "Périodicité des commissions"
#. module: commission_simple #. module: commission_simple
#: model:ir.model,name:commission_simple.model_commission_profile #: model:ir.model,name:commission_simple.model_commission_profile
@@ -140,7 +168,7 @@ msgstr "Profil de commission"
#. module: commission_simple #. module: commission_simple
#: model:ir.model,name:commission_simple.model_commission_profile_assignment #: model:ir.model,name:commission_simple.model_commission_profile_assignment
msgid "Commission Profile Assignment" msgid "Commission Profile Assignment"
msgstr "Affectation du profil de commission" msgstr "Affectation des profils de commission"
#. module: commission_simple #. module: commission_simple
#: model:ir.actions.act_window,name:commission_simple.commission_profile_action #: model:ir.actions.act_window,name:commission_simple.commission_profile_action
@@ -149,8 +177,11 @@ msgid "Commission Profiles"
msgstr "Profils de commission" msgstr "Profils de commission"
#. module: commission_simple #. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_rate #: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_rate
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__rate #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__rate
#, python-format
msgid "Commission Rate" msgid "Commission Rate"
msgstr "Taux de commission" msgstr "Taux de commission"
@@ -160,6 +191,11 @@ msgstr "Taux de commission"
msgid "Commission Result" msgid "Commission Result"
msgstr "État des commissions" msgstr "État des commissions"
#. module: commission_simple
#: model:ir.model,name:commission_simple.model_report_commission_simple_report_xlsx
msgid "Commission Result XLSX"
msgstr "État des commission XLSX"
#. module: commission_simple #. module: commission_simple
#: model:ir.model,name:commission_simple.model_commission_rule #: model:ir.model,name:commission_simple.model_commission_rule
msgid "Commission Rule" msgid "Commission Rule"
@@ -184,7 +220,7 @@ msgstr "Total des commissions"
#: model:ir.ui.menu,name:commission_simple.commission_root #: model:ir.ui.menu,name:commission_simple.commission_root
#: model_terms:ir.ui.view,arch_db:commission_simple.res_config_settings_view_form #: model_terms:ir.ui.view,arch_db:commission_simple.res_config_settings_view_form
msgid "Commissions" msgid "Commissions"
msgstr "" msgstr "Commissions"
#. module: commission_simple #. module: commission_simple
#. odoo-python #. odoo-python
@@ -194,6 +230,13 @@ msgid "Commissions already exist for %(period)s in company %(company)s."
msgstr "" msgstr ""
"Des commissions existent déjà pour %(period)s dans la société %(company)s." "Des commissions existent déjà pour %(period)s dans la société %(company)s."
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Commissions of %(partner)s for period %(period)s"
msgstr "Commissions de %(partner)s pour la période %(period)s"
#. module: commission_simple #. module: commission_simple
#: model:ir.model,name:commission_simple.model_res_company #: model:ir.model,name:commission_simple.model_res_company
msgid "Companies" msgid "Companies"
@@ -254,11 +297,37 @@ msgstr "Créé par"
msgid "Created on" msgid "Created on"
msgstr "Créé le" msgstr "Créé le"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Currency"
msgstr "Devise"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Customer"
msgstr "Client"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__partner_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__partner_ids
msgid "Customers" msgid "Customers"
msgstr "Clients" msgstr "Clients"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/models/commission_rule.py:0
#, python-format
msgid "Customers:"
msgstr "Clients :"
#. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form
msgid "Disc.%"
msgstr "Rem.%"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_compute__display_name #: model:ir.model.fields,field_description:commission_simple.field_commission_compute__display_name
#: model:ir.model.fields,field_description:commission_simple.field_commission_profile__display_name #: model:ir.model.fields,field_description:commission_simple.field_commission_profile__display_name
@@ -272,7 +341,7 @@ msgstr "Nom affiché"
#: model:ir.model.fields.selection,name:commission_simple.selection__commission_result__state__done #: model:ir.model.fields.selection,name:commission_simple.selection__commission_result__state__done
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_search #: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_search
msgid "Done" msgid "Done"
msgstr "Validé" msgstr "Terminé"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields.selection,name:commission_simple.selection__commission_result__state__draft #: model:ir.model.fields.selection,name:commission_simple.selection__commission_result__state__draft
@@ -281,7 +350,15 @@ msgid "Draft"
msgstr "Brouillon" msgstr "Brouillon"
#. module: commission_simple #. module: commission_simple
#: model:ir.actions.report,name:commission_simple.commission_result_xlsx_report
msgid "Détails Excel"
msgstr "Détails Excel"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__date_end #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__date_end
#, python-format
msgid "End Date" msgid "End Date"
msgstr "Date de fin" msgstr "Date de fin"
@@ -291,6 +368,11 @@ msgstr "Date de fin"
msgid "End date" msgid "End date"
msgstr "Date de fin" msgstr "Date de fin"
#. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form
msgid "Excel Export"
msgstr "Export Excel"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_follower_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_follower_ids
msgid "Followers" msgid "Followers"
@@ -307,9 +389,19 @@ msgid "Font awesome icon e.g. fa-tasks"
msgstr "Îcone font-awesome, par exemple fa-task" msgstr "Îcone font-awesome, par exemple fa-task"
#. module: commission_simple #. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Generated from Odoo on %s by %s"
msgstr "Généré à partir d'Odoo le %s par %s"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/models/commission_rule.py:0
#: model:ir.model.fields.selection,name:commission_simple.selection__commission_rule__applied_on__4_global #: model:ir.model.fields.selection,name:commission_simple.selection__commission_rule__applied_on__4_global
#, python-format
msgid "Global" msgid "Global"
msgstr "" msgstr "Global"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__has_message #: model:ir.model.fields,field_description:commission_simple.field_commission_result__has_message
@@ -323,7 +415,7 @@ msgstr "A un message"
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__id #: model:ir.model.fields,field_description:commission_simple.field_commission_result__id
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__id #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__id
msgid "ID" msgid "ID"
msgstr "" msgstr "ID"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__activity_exception_icon #: model:ir.model.fields,field_description:commission_simple.field_commission_result__activity_exception_icon
@@ -350,6 +442,20 @@ msgstr "Si activé, des messages ont une erreur d'envoi."
msgid "In Payment and Paid" msgid "In Payment and Paid"
msgstr "En paiement et payé" msgstr "En paiement et payé"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Invoice"
msgstr "Facture"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Invoice Date"
msgstr "Date de facture"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields.selection,name:commission_simple.selection__commission_profile__trigger_type__invoice #: model:ir.model.fields.selection,name:commission_simple.selection__commission_profile__trigger_type__invoice
msgid "Invoiced" msgid "Invoiced"
@@ -411,7 +517,12 @@ msgstr "Marge"
#. module: commission_simple #. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_rule_form #: model_terms:ir.ui.view,arch_db:commission_simple.commission_rule_form
msgid "Match" msgid "Match"
msgstr "" msgstr "Correspondance"
#. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__apply_description
msgid "Match Criteria"
msgstr "Critères de correspondance"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_rule_id #: model:ir.model.fields,field_description:commission_simple.field_account_move_line__commission_rule_id
@@ -426,7 +537,7 @@ msgstr "Erreur d'envoi du message"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_result__message_ids
msgid "Messages" msgid "Messages"
msgstr "" msgstr "Messages"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__my_activity_date_deadline #: model:ir.model.fields,field_description:commission_simple.field_commission_result__my_activity_date_deadline
@@ -497,6 +608,18 @@ msgstr "Payé"
msgid "Period" msgid "Period"
msgstr "Période" msgstr "Période"
#. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form
msgid "Price"
msgstr "Prix"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Product"
msgstr "Produit"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__product_categ_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__product_categ_ids
#: model:ir.model.fields.selection,name:commission_simple.selection__commission_rule__applied_on__3_product_category #: model:ir.model.fields.selection,name:commission_simple.selection__commission_rule__applied_on__3_product_category
@@ -508,6 +631,13 @@ msgstr "Catégories de produits"
msgid "Product Categories and Customers" msgid "Product Categories and Customers"
msgstr "Catégories de produits et clients" msgstr "Catégories de produits et clients"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/models/commission_rule.py:0
#, python-format
msgid "Product Categories:"
msgstr "Catégories de produits :"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_account_move_line__product_categ_id #: model:ir.model.fields,field_description:commission_simple.field_account_move_line__product_categ_id
msgid "Product Category" msgid "Product Category"
@@ -524,6 +654,13 @@ msgstr "Produits"
msgid "Products and Customers" msgid "Products and Customers"
msgstr "Produits et clients" msgstr "Produits et clients"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/models/commission_rule.py:0
#, python-format
msgid "Products:"
msgstr "Produits :"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_profile_assignment__profile_id #: model:ir.model.fields,field_description:commission_simple.field_commission_profile_assignment__profile_id
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__profile_id #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__profile_id
@@ -531,6 +668,13 @@ msgstr "Produits et clients"
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Quantity"
msgstr "Quantité"
#. module: commission_simple #. module: commission_simple
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form #: model_terms:ir.ui.view,arch_db:commission_simple.commission_result_form
#: model_terms:ir.ui.view,arch_db:commission_simple.commission_rule_tree #: model_terms:ir.ui.view,arch_db:commission_simple.commission_rule_tree
@@ -572,7 +716,10 @@ msgid "Sequence"
msgstr "Séquence" msgstr "Séquence"
#. module: commission_simple #. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#: model:ir.model.fields,field_description:commission_simple.field_commission_rule__date_start #: model:ir.model.fields,field_description:commission_simple.field_commission_rule__date_start
#, python-format
msgid "Start Date" msgid "Start Date"
msgstr "Date de début" msgstr "Date de début"
@@ -605,6 +752,13 @@ msgstr ""
msgid "This salesman already has an assignment in this company." msgid "This salesman already has an assignment in this company."
msgstr "Ce vendeur a déjà une assignation dans cette société." msgstr "Ce vendeur a déjà une assignation dans cette société."
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Total Amount"
msgstr "Montant total"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_profile__trigger_type #: model:ir.model.fields,field_description:commission_simple.field_commission_profile__trigger_type
msgid "Trigger" msgid "Trigger"
@@ -613,13 +767,20 @@ msgstr "Déclencheur"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_profile_assignment__assign_type #: model:ir.model.fields,field_description:commission_simple.field_commission_profile_assignment__assign_type
msgid "Type" msgid "Type"
msgstr "" msgstr "Type"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,help:commission_simple.field_commission_result__activity_exception_decoration #: model:ir.model.fields,help:commission_simple.field_commission_result__activity_exception_decoration
msgid "Type of the exception activity on record." msgid "Type of the exception activity on record."
msgstr "Type de l'activité-alerte sur l'enregistrement." msgstr "Type de l'activité-alerte sur l'enregistrement."
#. module: commission_simple
#. odoo-python
#: code:addons/commission_simple/reports/commission_result_xlsx.py:0
#, python-format
msgid "Unit"
msgstr "Unité"
#. module: commission_simple #. module: commission_simple
#: model:ir.model.fields,field_description:commission_simple.field_commission_result__website_message_ids #: model:ir.model.fields,field_description:commission_simple.field_commission_result__website_message_ids
msgid "Website Messages" msgid "Website Messages"
@@ -636,5 +797,5 @@ msgstr "Historique des échanges sur le site Web"
#, python-format #, python-format
msgid "You cannot delete commission result %s because it is in done state." msgid "You cannot delete commission result %s because it is in done state."
msgstr "" msgstr ""
"Vous ne pouvez pas supprimer l'état de commission %s parce qu'il est à " "Vous ne pouvez pas supprimer l'état de commission %s parce qu'il est "
"l'état \"validé\"." "à l'état \"terminé\"."

View File

@@ -1,5 +1,5 @@
from . import commission_profile from . import commission_profile
from . import commission_rule from . import commission_rule
from . import commission_result from . import commission_result
from . import res_company
from . import account_move_line from . import account_move_line
from . import account_invoice_report

View File

@@ -0,0 +1,17 @@
# Copyright 2018-2019 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 api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
commission_amount = fields.Float(readonly=True)
@api.model
def _select(self):
select_str = super()._select()
select_str += ", line.commission_amount * currency_table.rate AS commission_amount"
return select_str

View File

@@ -11,7 +11,7 @@ class AccountMoveLine(models.Model):
_inherit = 'account.move.line' _inherit = 'account.move.line'
commission_result_id = fields.Many2one( commission_result_id = fields.Many2one(
'commission.result', string='Commission Result', check_company=True) 'commission.result', string='Commission Result', check_company=True, index=True)
commission_rule_id = fields.Many2one( commission_rule_id = fields.Many2one(
'commission.rule', 'Matched Commission Rule', ondelete='restrict', check_company=True) 'commission.rule', 'Matched Commission Rule', ondelete='restrict', check_company=True)
commission_base = fields.Monetary('Commission Base', currency_field='company_currency_id') commission_base = fields.Monetary('Commission Base', currency_field='company_currency_id')
@@ -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

@@ -27,6 +27,9 @@ class CommissionProfile(models.Model):
('paid', 'Paid'), ('paid', 'Paid'),
('in_payment', 'In Payment and Paid'), ('in_payment', 'In Payment and Paid'),
], default='paid', string='Trigger', required=True) ], default='paid', string='Trigger', required=True)
date_range_type_id = fields.Many2one(
'date.range.type', string='Commission Periodicity', ondelete='restrict',
domain="[('company_id', 'in', (False, company_id))]")
class CommissionProfileAssignment(models.Model): class CommissionProfileAssignment(models.Model):
@@ -100,6 +103,7 @@ class CommissionProfileAssignment(models.Model):
'profile_id': self.profile_id.id, 'profile_id': self.profile_id.id,
'date_range_id': date_range.id, 'date_range_id': date_range.id,
'assign_type': self.assign_type, 'assign_type': self.assign_type,
'assignment_id': self.id,
'company_id': self.company_id.id, 'company_id': self.company_id.id,
} }
return vals return vals

View File

@@ -17,6 +17,8 @@ class CommissionResult(models.Model):
readonly=True, tracking=True) readonly=True, tracking=True)
profile_id = fields.Many2one( profile_id = fields.Many2one(
'commission.profile', string='Commission Profile', readonly=True, tracking=True) 'commission.profile', string='Commission Profile', readonly=True, tracking=True)
assignment_id = fields.Many2one(
'commission.profile.assignment', string="Commission Profile Assignment", readonly=True)
assign_type = fields.Selection('_assign_type_selection', readonly=True, tracking=True) assign_type = fields.Selection('_assign_type_selection', readonly=True, tracking=True)
company_id = fields.Many2one( company_id = fields.Many2one(
'res.company', string='Company', ondelete='cascade', 'res.company', string='Company', ondelete='cascade',
@@ -32,7 +34,10 @@ class CommissionResult(models.Model):
states={'done': [('readonly', True)]}) states={'done': [('readonly', True)]})
amount_total = fields.Monetary( amount_total = fields.Monetary(
string='Commission Total', currency_field='company_currency_id', string='Commission Total', currency_field='company_currency_id',
compute='_compute_amount_total', store=True, tracking=True) compute='_compute_totals', store=True, tracking=True)
base_total = fields.Monetary(
string="Commission Base Total", currency_field='company_currency_id',
compute='_compute_totals', store=True, tracking=True)
state = fields.Selection([ state = fields.Selection([
('draft', 'Draft'), ('draft', 'Draft'),
('done', 'Done'), ('done', 'Done'),
@@ -44,12 +49,13 @@ class CommissionResult(models.Model):
def _assign_type_selection(self): def _assign_type_selection(self):
return self.env['commission.profile.assignment']._assign_type_selection() return self.env['commission.profile.assignment']._assign_type_selection()
@api.depends('line_ids.commission_amount') @api.depends('line_ids.commission_amount', 'line_ids.commission_base')
def _compute_amount_total(self): def _compute_totals(self):
rg_res = self.env['account.move.line'].read_group([('commission_result_id', 'in', self.ids)], ['commission_result_id', 'commission_amount:sum'], ['commission_result_id']) rg_res = self.env['account.move.line'].read_group([('commission_result_id', 'in', self.ids)], ['commission_result_id', 'commission_amount:sum', 'commission_base:sum'], ['commission_result_id'])
mapped_data = dict([(x['commission_result_id'][0], x['commission_amount']) for x in rg_res]) mapped_data = dict([(x['commission_result_id'][0], {'amount': x['commission_amount'], 'base': x['commission_base']}) for x in rg_res])
for rec in self: for rec in self:
rec.amount_total = mapped_data.get(rec.id, 0) rec.amount_total = mapped_data.get(rec.id, {}).get('amount')
rec.base_total = mapped_data.get(rec.id, {}).get('base')
def unlink(self): def unlink(self):
for result in self: for result in self:
@@ -59,7 +65,7 @@ class CommissionResult(models.Model):
return super().unlink() return super().unlink()
def draft2done(self): def draft2done(self):
self.write({'state': 'done'}) self.filtered(lambda x: x.state == 'draft').write({'state': 'done'})
def backtodraft(self): def backtodraft(self):
self.write({'state': 'draft'}) self.write({'state': 'draft'})
@@ -75,3 +81,7 @@ class CommissionResult(models.Model):
'salesman_period_company_unique', 'salesman_period_company_unique',
'unique(company_id, partner_id, date_range_id)', 'unique(company_id, partner_id, date_range_id)',
'A commission result already exists for this salesman/agent for the same period.')] 'A commission result already exists for this salesman/agent for the same period.')]
def _prepare_xlsx_lines(self):
self.ensure_one()
return self.line_ids.sorted(key=lambda x: x.move_id.invoice_date)

View File

@@ -3,13 +3,13 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models, api from odoo import fields, models, api, _
class CommissionRule(models.Model): class CommissionRule(models.Model):
_name = 'commission.rule' _name = 'commission.rule'
_description = 'Commission Rule' _description = 'Commission Rule'
_order = 'profile_id, applied_on' _order = 'profile_id, applied_on, rate desc'
partner_ids = fields.Many2many( partner_ids = fields.Many2many(
'res.partner', string='Customers', domain=[('parent_id', '=', False)]) 'res.partner', string='Customers', domain=[('parent_id', '=', False)])
@@ -34,6 +34,31 @@ class CommissionRule(models.Model):
('4_global', 'Global')], ('4_global', 'Global')],
string='Apply On', default='4_global', required=True) string='Apply On', default='4_global', required=True)
active = fields.Boolean(string='Active', default=True) active = fields.Boolean(string='Active', default=True)
apply_description = fields.Html(compute='_compute_apply_description', string="Match Criteria")
_sql_constraints = [(
'rate_positive',
'CHECK(rate >= 0)',
'Rate must be positive !')]
def _compute_apply_description(self):
customer_label = "<strong>" + _("Customers:") + "</strong>"
product_label = "<strong>" + _("Products:") + "</strong>"
product_categ_label = "<strong>" + _("Product Categories:") + "</strong>"
and_label = "<strong>" + _('AND') + "</strong>"
for rule in self:
desc = False
if rule.applied_on == '0_customer_product':
desc = f"{customer_label} {', '.join([part.ref or part.name for part in rule.partner_ids])} {and_label} {product_label} {', '.join([pp.default_code or pp.name for pp in rule.product_ids])}"
elif rule.applied_on == '1_customer_product_category':
desc = f"{customer_label} {', '.join([part.ref or part.name for part in rule.partner_ids])} {and_label} {product_categ_label} {', '.join([categ.display_name for categ in rule.product_categ_ids])}"
elif rule.applied_on == '2_product':
desc = f"{product_label} {', '.join([pp.default_code or pp.name for pp in rule.product_ids])}"
elif rule.applied_on == '3_product_category':
desc = f"{product_categ_label} {', '.join([categ.display_name for categ in rule.product_categ_ids])}"
elif rule.applied_on == '4_global':
desc = _('Global')
rule.apply_description = desc
@api.model @api.model
def load_all_rules(self): def load_all_rules(self):
@@ -45,8 +70,3 @@ class CommissionRule(models.Model):
else: else:
res[rule['profile_id'][0]].append(rule) res[rule['profile_id'][0]].append(rule)
return res return res
_sql_constraints = [(
'rate_positive',
'CHECK(rate >= 0)',
'Rate must be positive !')]

View File

@@ -1,13 +0,0 @@
# Copyright 2019-2024 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).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
commission_date_range_type_id = fields.Many2one(
'date.range.type', string='Commission Periodicity', ondelete='restrict')

View File

@@ -1,20 +0,0 @@
# Copyright 2019-2024 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).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = 'res.users'
# TODO mon idée : déplacer ça dans une table dédiée
# company_id oblig
# partner_id (filtré... sur lien vers user ou agent petit difficulté)
# profile_id
# type agent ou user => ça donne le champ de recherche
commission_profile_id = fields.Many2one(
'commission.profile', string='Commission Profile',
company_dependent=True)

View File

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

View File

@@ -0,0 +1,112 @@
# 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, _('Base Total'), styles['subtitle'])
sheet.write(i, 1, result.base_total, styles['subtitle_amount'])
i += 1
sheet.write(i, 0, _('Amount Total'), 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._prepare_xlsx_lines():
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

@@ -8,4 +8,4 @@ access_commission_rule_audit,Read access on commission.rule for viewer group,mod
access_commission_result_full,Full access on commission.result to accountant,model_commission_result,account.group_account_user,1,1,1,1 access_commission_result_full,Full access on commission.result to accountant,model_commission_result,account.group_account_user,1,1,1,1
access_commission_result_read,Read access on commission.result to invoicing grp,model_commission_result,account.group_account_invoice,1,0,0,0 access_commission_result_read,Read access on commission.result to invoicing grp,model_commission_result,account.group_account_invoice,1,0,0,0
access_commission_result_audit,Read access on commission.result to viewer grp,model_commission_result,account.group_account_readonly,1,0,0,0 access_commission_result_audit,Read access on commission.result to viewer grp,model_commission_result,account.group_account_readonly,1,0,0,0
access_commission_compute_full,Full access to wizard commission.compute,model_commission_compute,account.group_account_manager,1,1,1,1 access_commission_compute_full,Full access to wizard commission.compute to Accountant,model_commission_compute,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_commission_result_full Full access on commission.result to accountant model_commission_result account.group_account_user 1 1 1 1
9 access_commission_result_read Read access on commission.result to invoicing grp model_commission_result account.group_account_invoice 1 0 0 0
10 access_commission_result_audit Read access on commission.result to viewer grp model_commission_result account.group_account_readonly 1 0 0 0
11 access_commission_compute_full Full access to wizard commission.compute Full access to wizard commission.compute to Accountant model_commission_compute account.group_account_manager account.group_account_user 1 1 1 1

View File

@@ -22,6 +22,7 @@
<field name="name"/> <field name="name"/>
<field name="active" invisible="1"/> <field name="active" invisible="1"/>
<field name="trigger_type" widget="radio"/> <field name="trigger_type" widget="radio"/>
<field name="date_range_type_id"/>
</group> </group>
<group name="main-right"> <group name="main-right">
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
@@ -55,12 +56,31 @@
<tree> <tree>
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="name" decoration-bf="1"/> <field name="name" decoration-bf="1"/>
<field name="trigger_type" optional="show"/> <field name="trigger_type" optional="show" widget="badge" decoration-info="trigger_type == 'invoice'" decoration-success="trigger_type == 'paid'" decoration-warning="trigger_type == 'in_payment'"/>
<field name="date_range_type_id" optional="show"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
</tree> </tree>
</field> </field>
</record> </record>
<record id="commission_profile_search" model="ir.ui.view">
<field name="model">commission.profile</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<filter string="Invoiced" name="invoice" domain="[('trigger_type', '=', 'invoice')]"/>
<filter string="Paid" name="paid" domain="[('trigger_type', '=', 'paid')]"/>
<filter string="In Payment and Paid" name="in_payment" domain="[('trigger_type', '=', 'in_payment')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<separator/>
<group name="groupby">
<filter name="date_range_type_groupby" string="Commission Periodicity" context="{'group_by': 'date_range_type_id'}"/>
</group>
</search>
</field>
</record>
<record id="commission_profile_action" model="ir.actions.act_window"> <record id="commission_profile_action" model="ir.actions.act_window">
<field name="name">Commission Profiles</field> <field name="name">Commission Profiles</field>
<field name="res_model">commission.profile</field> <field name="res_model">commission.profile</field>

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">
@@ -23,6 +24,7 @@
<field name="date_range_id"/> <field name="date_range_id"/>
<field name="date_start"/> <field name="date_start"/>
<field name="date_end"/> <field name="date_end"/>
<field name="base_total"/>
<field name="amount_total"/> <field name="amount_total"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
@@ -34,7 +36,7 @@
</group> </group>
</group> </group>
<group name="lines" string="Commission Lines"> <group name="lines" string="Commission Lines">
<field nolabel="1" name="line_ids" colspan="2"> <field nolabel="1" name="line_ids" colspan="2" widget="many2many">
<tree> <tree>
<field name="move_id"/> <field name="move_id"/>
<field name="date" optional="hide"/> <field name="date" optional="hide"/>
@@ -42,8 +44,12 @@
<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" sum="1"/>
<field name="commission_rate" string="Rate (%)"/> <field name="commission_rate" string="Rate (%)"/>
<field name="commission_amount" sum="1"/> <field name="commission_amount" sum="1"/>
<field name="commission_rule_id" optional="hide"/> <field name="commission_rule_id" optional="hide"/>
@@ -65,7 +71,14 @@
<field name="name">commission.result.tree</field> <field name="name">commission.result.tree</field>
<field name="model">commission.result</field> <field name="model">commission.result</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree decoration-info="state == 'draft'">
<header>
<button
name="draft2done"
type="object"
string="Validate"
/>
</header>
<field name="date_range_id" optional="show"/> <field name="date_range_id" optional="show"/>
<field name="date_start" optional="hide"/> <field name="date_start" optional="hide"/>
<field name="date_end" optional="hide"/> <field name="date_end" optional="hide"/>
@@ -74,6 +87,7 @@
<field name="assign_type" optional="hide" widget="badge" decoration-warning="assign_type == 'user'"/> <field name="assign_type" optional="hide" widget="badge" decoration-warning="assign_type == 'user'"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="base_total" sum="1" optional="hide"/>
<field name="amount_total" sum="1" optional="show"/> <field name="amount_total" sum="1" optional="show"/>
<field name="state" decoration-info="state == 'draft'" decoration-success="state == 'done'" widget="badge"/> <field name="state" decoration-info="state == 'draft'" decoration-success="state == 'done'" widget="badge"/>
</tree> </tree>

View File

@@ -43,11 +43,12 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="profile_id" invisible="not context.get('commission_rule_main_view')"/> <field name="profile_id" invisible="not context.get('commission_rule_main_view')"/>
<field name="applied_on"/> <field name="applied_on" widget="badge" decoration-danger="applied_on == '0_customer_product'" decoration-warning="applied_on == '1_customer_product_category'" decoration-info="applied_on == '2_product'" decoration-success="applied_on == '3_product_category'"/>
<field name="date_start"/> <field name="apply_description"/>
<field name="date_end"/> <field name="date_start" optional="show"/>
<field name="date_end" optional="show"/>
<field name="rate" string="Rate (%)"/> <field name="rate" string="Rate (%)"/>
<field name="base"/> <field name="base" widget="badge" decoration-success="base == 'invoiced'" decoration-warning="base == 'margin'"/>
</tree> </tree>
</field> </field>
</record> </record>

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019-2024 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="res_config_settings_view_form" model="ir.ui.view">
<field name="name">commission.res.config.settings.form</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='storno']" position="after">
<h2>Commissions</h2>
<div class="row mt16 o_settings_container" id="commission_simple">
<div class="col-12 col-lg-12 o_setting_box" id="commission_simple-settings">
<div class="o_setting_left_pane" />
<div class="o_setting_right_pane">
<div class="row" id="commission_date_range_type_id">
<label
for="commission_date_range_type_id"
class="col-md-5"
/>
<field name="commission_date_range_type_id" />
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019-2024 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="view_users_form" model="ir.ui.view">
<field name="name">commission.res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<group name="preferences" position="after">
<group name="commission" string="Commission" groups="account.group_account_manager">
<field name="commission_profile_id"/>
</group>
</group>
</field>
</record>
<record id="view_users_tree" model="ir.ui.view">
<field name="name">commission.res.users.tree</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_tree"/>
<field name="arch" type="xml">
<field name="login_date" position="after">
<field name="commission_profile_id" optional="hide" groups="account.group_account_manager"/>
</field>
</field>
</record>
</odoo>

View File

@@ -1,2 +1 @@
from . import commission_compute from . import commission_compute
from . import res_config_settings

View File

@@ -3,8 +3,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools.misc import format_date
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,47 +14,44 @@ class CommissionCompute(models.TransientModel):
_name = 'commission.compute' _name = 'commission.compute'
_description = 'Compute Commissions' _description = 'Compute Commissions'
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) company_id = fields.Many2one('res.company', required=True)
date_range_type_id = fields.Many2one(related='company_id.commission_date_range_type_id') date_start = fields.Date(string="Period Start Date", required=True)
date_range_id = fields.Many2one(
'date.range', required=True, string='Period',
compute='_compute_date_range_id', store=True, precompute=True, readonly=False,
domain="[('type_id', '=', date_range_type_id)]")
date_start = fields.Date(related='date_range_id.date_start')
date_end = fields.Date(related='date_range_id.date_end')
@api.depends('company_id') @api.model
def _compute_date_range_id(self): def default_get(self, fields_list):
for wiz in self: res = super().default_get(fields_list)
date_range_id = False company = self.env.company
company = wiz.company_id last_commission_result = self.env['commission.result'].search([
if company and company.commission_date_range_type_id: ('company_id', '=', company.id),
type_id = company.commission_date_range_type_id.id ], order='date_start desc', limit=1)
last_commission_result = self.env['commission.result'].search([ if last_commission_result:
('company_id', '=', company.id), last_start_date = last_commission_result.date_start
], order='date_end desc', limit=1) commissions_last_start_date = self.env['commission.result'].search([
limit_date = last_commission_result and last_commission_result.date_end or (fields.Date.context_today(self) + relativedelta(months=-2, day=31)) ('date_start', '=', last_start_date),
date_range = self.env['date.range'].search([ ('company_id', '=', company.id),
('company_id', 'in', (company.id, False)), ], order="date_end asc", limit=1)
('type_id', '=', type_id), min_end_date = commissions_last_start_date.date_end
('date_start', '>', limit_date) date_start = min_end_date + timedelta(1)
], order='date_start', limit=1) else:
date_range_id = date_range and date_range.id or False today = fields.Date.context_today(self)
wiz.date_range_id = date_range_id date_start = datetime(today.year, today.month, 1)
res.update({
'company_id': company.id,
'date_start': date_start,
})
return res
def run(self): def run(self):
self.ensure_one() self.ensure_one()
if not self.date_start:
raise UserError(_("Missing Period Start Date."))
creso = self.env['commission.result'] creso = self.env['commission.result']
date_range = self.date_range_id existing_commissions = creso.search_read([
existing_commissions = creso.search([ ('date_start', '=', self.date_start),
('date_range_id', '=', date_range.id),
('company_id', '=', self.company_id.id), ('company_id', '=', self.company_id.id),
]) ], ['assignment_id'])
if existing_commissions: exclude_assignment_ids = [x['assignment_id'][0] for x in existing_commissions if x['assignment_id']]
raise UserError(_( com_result_ids = self._core_compute(exclude_assignment_ids)
'Commissions already exist for %(period)s in company %(company)s.',
period=date_range.display_name, company=self.company_id.display_name))
com_result_ids = self._core_compute()
if not com_result_ids: if not com_result_ids:
raise UserError(_('No commissions generated.')) raise UserError(_('No commissions generated.'))
action = self.env['ir.actions.actions']._for_xml_id( action = self.env['ir.actions.actions']._for_xml_id(
@@ -64,12 +62,32 @@ class CommissionCompute(models.TransientModel):
}) })
return action return action
def _core_compute(self): def _core_compute(self, exclude_assignment_ids):
rules = self.env['commission.rule'].load_all_rules() rules = self.env['commission.rule'].load_all_rules()
com_result_ids = [] com_result_ids = []
assignments = self.env['commission.profile.assignment'].search([('company_id', '=', self.company_id.id)]) assignments = self.env['commission.profile.assignment'].search(
[('company_id', '=', self.company_id.id), ('id', 'not in', exclude_assignment_ids)])
date_range_type2date_range = {}
for assignment in assignments: for assignment in assignments:
com_result = assignment._generate_commission_result(self.date_range_id, rules) profile = assignment.profile_id
date_range_type = profile.date_range_type_id
if not date_range_type:
raise UserError(_("Missing commission periodicity on commission profile '%s'.") % profile.display_name)
if date_range_type not in date_range_type2date_range:
domain = [
('date_start', '=', self.date_start),
('type_id', '=', date_range_type.id),
]
date_range = self.env['date.range'].search(
domain + [('company_id', '=', self.company_id.id)], limit=1)
if not date_range:
date_range = self.env['date.range'].search(
domain + [('company_id', '=', False)], limit=1)
if not date_range:
logger.info('There is no date range with type %s starting on %s. Skipping commission generation for assignment ID %s', date_range_type.name, self.date_start, assignment.id)
continue
date_range_type2date_range[date_range_type] = date_range
com_result = assignment._generate_commission_result(date_range_type2date_range[date_range_type], rules)
if com_result: if com_result:
com_result_ids.append(com_result.id) com_result_ids.append(com_result.id)
else: else:

View File

@@ -15,10 +15,7 @@
<group name="main"> <group name="main">
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="date_range_type_id" invisible="1"/>
<field name="date_range_id"/>
<field name="date_start"/> <field name="date_start"/>
<field name="date_end"/>
</group> </group>
<footer> <footer>
<button name="run" type="object" string="Compute" <button name="run" type="object" string="Compute"
@@ -36,6 +33,6 @@
<field name="target">new</field> <field name="target">new</field>
</record> </record>
<menuitem id="commission_compute_menu" action="commission_compute_action" parent="commission_root" sequence="15" groups="account.group_account_user"/> <menuitem id="commission_compute_menu" action="commission_compute_action" parent="commission_root" sequence="15"/>
</odoo> </odoo>

View File

@@ -1,12 +0,0 @@
# Copyright 2019-2024 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).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
commission_date_range_type_id = fields.Many2one(
related='company_id.commission_date_range_type_id', readonly=False)

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2024 Akretion France (http://www.akretion.com) Copyright 2024 Akretion France (https://www.akretion.com)
@author Alexis de Lattre <alexis.delattre@akretion.com> @author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
--> -->
@@ -12,7 +12,7 @@
<field name="inherit_id" ref="commission_simple.commission_result_tree"/> <field name="inherit_id" ref="commission_simple.commission_result_tree"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="assign_type" position="attributes"> <field name="assign_type" position="attributes">
<attribute name="decoration-danger">assign_type == 'agent'</attribute> <attribute name="decoration-danger">assign_type == 'agent'</attribute>
</field> </field>
</field> </field>
</record> </record>

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': True, 'installable': True,

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):
@@ -14,26 +15,34 @@ class CommissionResult(models.Model):
def draft2done(self): def draft2done(self):
for result in self: for result in self:
if result.assign_type == 'agent': if result.state == "draft" and result.assign_type == 'agent':
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

@@ -11,12 +11,24 @@
<record id="res_config_settings_view_form" model="ir.ui.view"> <record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">commission.res.config.settings.form</field> <field name="name">commission.res.config.settings.form</field>
<field name="model">res.config.settings</field> <field name="model">res.config.settings</field>
<field name="inherit_id" ref="commission_simple.res_config_settings_view_form" /> <field name="inherit_id" ref="account.res_config_settings_view_form" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//div[@id='commission_simple-settings']/div[hasclass('o_setting_right_pane')]" position="inside"> <xpath expr="//div[@id='analytic']" position="after">
<div class="row" id="commission_product_id"> <h2>Commissions</h2>
<label for="commission_product_id" class="col-md-5" /> <div class="row mt16 o_settings_container" id="commission_simple">
<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 class="col-12 col-lg-12 o_setting_box" id="commission_simple-settings">
<div class="o_setting_left_pane" />
<div class="o_setting_right_pane">
<div class="row" id="commission_product_id">
<label for="commission_product_id" class="col-md-5" />
<field name="commission_product_id" context="{'default_type': 'service', 'default_purchase_ok': True, 'default_sale_ok': False, 'default_available_in_pos': False, 'default_purchase_method': 'purchase'}"/>
</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>
</div>
</div>
</div> </div>
</xpath> </xpath>
</field> </field>

View File

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

View File

@@ -0,0 +1,23 @@
# Copyright 2025 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).
{
'name': 'HR Expense Usability by Akretion',
'version': '16.0.1.0.0',
'category': 'Human Resources/Expenses',
'license': 'AGPL-3',
'summary': 'Usability improvements on expenses',
'description': """
This module includes several small usability improvements to expenses.
This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'maintainers': ['alexis-via'],
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['hr_expense'],
'data': [],
'installable': True,
}

View File

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

View File

@@ -0,0 +1,16 @@
# 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 models, Command
class HrExpenseSheet(models.Model):
_inherit = "hr.expense.sheet"
def _prepare_move_vals(self):
"""Copy attachments from hr.expense.sheet to supplier invoice"""
vals = super()._prepare_move_vals()
if self.attachment_ids:
vals['attachment_ids'] = [Command.create({'res_model': 'account.move', 'name': attach.name, 'datas': attach.datas}) for attach in self.attachment_ids]
return vals

View File

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

View File

@@ -0,0 +1,18 @@
# Copyright 2025 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).
{
'name': 'HR Timesheet Sheet Usability Akretion',
'version': '16.0.1.0.0',
'category': 'Timesheet',
'license': 'AGPL-3',
'summary': 'Better usability in hr_timesheet_sheet module',
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['hr_timesheet_sheet'],
'data': [
'views/hr_timesheet_sheet.xml',
],
'installable': True,
}

View File

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

View File

@@ -0,0 +1,28 @@
# 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 fields, models, _
from odoo.exceptions import UserError
class HrTimesheetSheet(models.Model):
_inherit = 'hr_timesheet.sheet'
def show_lines_fullscreen(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"hr_timesheet.timesheet_action_all")
action['domain'] = [('sheet_id', '=', self.id)]
return action
# Inherit native method. We don't want tons of followers by default. We just want the manager.
def _get_subscribers(self):
self.ensure_one()
subscribers = self._get_informables()
return subscribers
def _check_can_review(self):
if self.employee_id.user_id == self.env.user and self.employee_id.parent_id:
raise UserError(_("You cannot approve your own timesheet!"))
return super()._check_can_review()

View File

@@ -0,0 +1,23 @@
<?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="hr_timesheet_sheet_form" model="ir.ui.view">
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/field[@name='name']" position="before">
<div class="oe_button_box" name="button_box">
<button name="show_lines_fullscreen" type="object"
class="oe_stat_button" icon="fa-bars" string="Détails plein écran"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,102 @@
# 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).
{
'name': 'Account Profile for France',
'version': '16.0.1.0.0',
'category': 'Accounting & Finance',
'license': 'AGPL-3',
'summary': 'Module set for accounting for a French company',
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': [
### MISC
'date_range_account', # OCA/server-ux
'account_usability_akretion', # akretion/odoo-usability
'account_usability', # OCA/account-financial-tools
'l10n_fr_siret_lookup', # OCA/l10n-france
'account_payment_partner', # OCA/bank-payment
'account_lock', # addons officiels
'account_lock_date_update', # OCA/account-financial-tools
'account_move_name_sequence', # OCA/account-financial-tools
'account_move_csv_import', # akretion/account-move-import
'account_move_line_reconcile_manual', # OCA/account-reconcile
'account_dashboard_banner', # OCA/account-financial-tools
### INVOICING
'account_invoice_fiscal_position_update', # OCA/account-invoicing
'account_fiscal_position_vat_check', # OCA/account-financial-tools
'account_invoice_facturx', # OCA/edi
### FINANCIAL REPORTS
'account_financial_report', # OCA/account-financial-reporting
'account_balance_ebp_csv_export', # OCA/l10n-france
'l10n_fr_mis_reports', # OCA/l10n-france
'l10n_fr_fec_oca', # OCA/l10n-france
### BANK STATEMENTS
'account_statement_completion_label_simple', # akretion/bank-statement-reconcile-simple
#'account_statement_completion_label_simple_sale', # akretion/bank-statement-reconcile-simple
'account_statement_import_file_reconcile_oca', # OCA/bank-statement-import
'account_statement_import_ofx', # OCA/bank-statement-import
'account_statement_import_fr_cfonb', # OCA/l10n-france
'account_reconcile_oca', # OCA/account-reconcile
### CURRENCY RATES
'currency_rate_update', # OCA/currency
'currency_old_rate_notify', # OCA/currency
### INVOICE IMPORT
'account_invoice_import_simple_pdf', # OCA/edi
'account_invoice_import_facturx', # OCA/edi
'l10n_fr_account_invoice_import_facturx', # OCA/l10n-france
### OVERDUE
'account_invoice_overdue_warn', # OCA/credit-control
#'account_invoice_overdue_warn_sale', # OCA/credit-control
'account_invoice_overdue_reminder', # OCA/credit-control
### FRENCH DECLARATIONS
'l10n_fr_account_vat_return_teledec', # OCA/l10n-france
'l10n_fr_account_vat_return_einvoice_generate', # OCA/l10n-france
# 'intrastat_product' depends on 'sale_stock' and 'purchase_stock', so it is
# commented in this list of dependencies
# 'l10n_fr_intrastat_product', # OCA/l10n-france
# 'product_net_weight', # OCA/product-attribute
'l10n_fr_intrastat_service', # OCA/l10n-france
'l10n_fr_das2', # OCA/l10n-france
# ANALYTIC
#'account_analytic_distribution_manual', # OCA/account-analytic
### INVOICE IMPORT
#'account_invoice_download_ovh', # OCA/edi
#'account_invoice_download_scaleway', # OCA/edi
### PAYMENT ORDERS and DEBIT ORDERS
# It is recommended to use the code of the following PRs that work together:
# https://github.com/OCA/bank-payment/pull/1174 (all modules of OCA/bank-payment)
# https://github.com/OCA/l10n-france/pull/560 (account_banking_fr_lcr)
# https://github.com/OCA/l10n-france/pull/490 (l10n_fr_account_banking_pain_base)
#'partner_bank_acc_type_constraint', # OCA/partner-contact
#'account_banking_sepa_credit_transfer', # OCA/bank-payment
#'account_banking_sepa_direct_debit', # OCA/bank-payment
#'account_banking_fr_lcr', # OCA/l10n-france
#'l10n_fr_account_banking_pain_base', # OCA/l10n-france
### PY3O
#'account_invoice_facturx_py3o', # OCA/edi
### CHORUS
#'l10n_fr_chorus_account', # OCA/l10n-france
#'l10n_fr_chorus_sale', # OCA/l10n-france
#'l10n_fr_chorus_facturx', # OCA/l10n-france
### CUTOFF
#'account_cutoff_start_end_dates', # OCA/account-closing
#'account_cutoff_picking', # OCA/account-closing
#'account_cutoff_accrual_subscription', # OCA/account-closing
### MISC
#'account_check_deposit', # OCA/account-financial-tools
#'account_cash_deposit', # OCA/account-financial-tools
#'account_invoice_pricelist', # OCA/account-invoicing
#'account_asset_management', # OCA/account-financial-tools
### MOONCARD
#'mooncard_payment_card', # akretion/odoo-mooncard-connector
#'l10n_fr_base_newgen_payment_card', # akretion/odoo-mooncard-connector
#'base_newgen_payment_card_start_end_dates', # akretion/odoo-mooncard-connector
],
'excludes': [
'l10n_fr_fec',
'account_edi_ubl_cii',
],
'installable': True,
}

View File

@@ -1,8 +1,8 @@
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 31e39105612..fe74620369a 100644 index f0656aa85c9..9a0838030f1 100644
--- a/addons/point_of_sale/models/pos_session.py --- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py
@@ -928,8 +928,13 @@ class PosSession(models.Model): @@ -955,8 +955,13 @@ class PosSession(models.Model):
if not payment_method.journal_id: if not payment_method.journal_id:
return self.env['account.move.line'] return self.env['account.move.line']
outstanding_account = payment_method.outstanding_account_id or self.company_id.account_journal_payment_debit_account_id outstanding_account = payment_method.outstanding_account_id or self.company_id.account_journal_payment_debit_account_id
@@ -18,7 +18,7 @@ index 31e39105612..fe74620369a 100644
if float_compare(amounts['amount'], 0, precision_rounding=self.currency_id.rounding) < 0: if float_compare(amounts['amount'], 0, precision_rounding=self.currency_id.rounding) < 0:
# revert the accounts because account.payment doesn't accept negative amount. # revert the accounts because account.payment doesn't accept negative amount.
@@ -937,7 +942,7 @@ class PosSession(models.Model): @@ -964,7 +969,7 @@ class PosSession(models.Model):
account_payment = self.env['account.payment'].create({ account_payment = self.env['account.payment'].create({
'amount': abs(amounts['amount']), 'amount': abs(amounts['amount']),
@@ -27,18 +27,18 @@ index 31e39105612..fe74620369a 100644
'journal_id': payment_method.journal_id.id, 'journal_id': payment_method.journal_id.id,
'force_outstanding_account_id': outstanding_account.id, 'force_outstanding_account_id': outstanding_account.id,
'destination_account_id': destination_account.id, 'destination_account_id': destination_account.id,
@@ -1097,8 +1102,8 @@ class PosSession(models.Model): @@ -1124,8 +1129,8 @@ class PosSession(models.Model):
lines.filtered(lambda line: not line.reconciled).reconcile() lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile()
for payment, lines in payment_to_receivable_lines.items(): for payment, lines in payment_to_receivable_lines.items():
- if payment.partner_id.property_account_receivable_id.reconcile: - if payment.partner_id.property_account_receivable_id.reconcile:
- lines.filtered(lambda line: not line.reconciled).reconcile() - lines.filtered(lambda line: not line.reconciled).with_context(no_cash_basis=True).reconcile()
+ # HACK for pos_check_deposit + # HACK for pos_check_deposit
+ lines.filtered(lambda line: line.account_id.reconcile and not line.reconciled).reconcile() + lines.filtered(lambda line: line.account_id.reconcile and not line.reconciled).with_context(no_cash_basis=True).reconcile()
# Reconcile invoice payments' receivable lines. But we only do when the account is reconcilable. # Reconcile invoice payments' receivable lines. But we only do when the account is reconcilable.
# Though `account_default_pos_receivable_account_id` should be of type receivable, there is currently # Though `account_default_pos_receivable_account_id` should be of type receivable, there is currently
@@ -1176,15 +1181,17 @@ class PosSession(models.Model): @@ -1198,15 +1203,17 @@ class PosSession(models.Model):
return self._credit_amounts(partial_args, amount, amount_converted) return self._credit_amounts(partial_args, amount, amount_converted)
def _get_split_receivable_vals(self, payment, amount, amount_converted): def _get_split_receivable_vals(self, payment, amount, amount_converted):
@@ -64,10 +64,10 @@ index 31e39105612..fe74620369a 100644
} }
return self._debit_amounts(partial_vals, amount, amount_converted) return self._debit_amounts(partial_vals, amount, amount_converted)
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index b9a237eb34c..62ad67e9517 100644 index 47ae691680c..30a3cd25628 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -288,7 +288,8 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { @@ -371,7 +371,8 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
return false; return false;
} }

View File

@@ -14,6 +14,9 @@
<field name="split_transactions" position="after"> <field name="split_transactions" position="after">
<field name="identify_customer" attrs="{'invisible': [('split_transactions', '=', False)]}"/> <field name="identify_customer" attrs="{'invisible': [('split_transactions', '=', False)]}"/>
</field> </field>
<field name="receivable_account_id" position="attributes">
<attribute name="attrs">{'invisible': [('identify_customer', '=', True)]}</attribute>
</field>
</field> </field>
</record> </record>
@@ -24,6 +27,9 @@
<field name="split_transactions" position="after"> <field name="split_transactions" position="after">
<field name="identify_customer" attrs="{'invisible': [('split_transactions', '=', False)]}" optional="hide"/> <field name="identify_customer" attrs="{'invisible': [('split_transactions', '=', False)]}" optional="hide"/>
</field> </field>
<field name="receivable_account_id" position="attributes">
<attribute name="attrs">{'invisible': [('identify_customer', '=', True)]}</attribute>
</field>
</field> </field>
</record> </record>

View File

@@ -11,6 +11,8 @@ class PosOrder(models.Model):
# field displayed in pos.order list view # field displayed in pos.order list view
payments_char = fields.Char( payments_char = fields.Char(
string="Payment Methods", compute="_compute_payments_char", store=True) string="Payment Methods", compute="_compute_payments_char", store=True)
# Used to search on products in pos.order search view
product_id = fields.Many2one(related='lines.product_id')
@api.depends('payment_ids') @api.depends('payment_ids')
def _compute_payments_char(self): def _compute_payments_char(self):

View File

@@ -0,0 +1,44 @@
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index f0656aa85c9..393962e061e 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -753,6 +753,7 @@ class PosSession(models.Model):
sale_key = (
# account
line['income_account_id'],
+ line['income_analytic_account_id'],
# sign
-1 if line['amount'] < 0 else 1,
# for taxes
@@ -1175,9 +1176,14 @@ class PosSession(models.Model):
tax_data = tax_ids.compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund, fixed_multiplicator=sign, include_caba_tags=True)
date_order = order_line.order_id.date_order
taxes = [{'date_order': date_order, **tax} for tax in tax_data['taxes']]
+ # _get_product_analytic_accounts() is a method of the OCA module product_analytic
+ # from https://github.com/OCA/account-analytic
+ income_analytic_account = order_line.product_id.product_tmpl_id.with_company(
+ order_line.company_id)._get_product_analytic_accounts()['income']
return {
'date_order': order_line.order_id.date_order,
'income_account_id': get_income_account(order_line).id,
+ 'income_analytic_account_id': income_analytic_account and income_analytic_account.id or False,
'amount': order_line.price_subtotal,
'taxes': taxes,
'base_tags': tuple(tax_data['base_tags']),
@@ -1228,7 +1234,7 @@ class PosSession(models.Model):
return self._credit_amounts(partial_vals, amount, amount_converted)
def _get_sale_vals(self, key, amount, amount_converted):
- account_id, sign, tax_keys, base_tag_ids = key
+ account_id, analytic_account_id, sign, tax_keys, base_tag_ids = key
tax_ids = set(tax[0] for tax in tax_keys)
applied_taxes = self.env['account.tax'].browse(tax_ids)
title = _('Sales') if sign == 1 else _('Refund')
@@ -1238,6 +1244,7 @@ class PosSession(models.Model):
partial_vals = {
'name': name,
'account_id': account_id,
+ 'analytic_distribution': {analytic_account_id: 100},
'move_id': self.move_id.id,
'tax_ids': [(6, 0, tax_ids)],
'tax_tag_ids': [(6, 0, base_tag_ids)],

View File

@@ -23,9 +23,19 @@
<field name="model">pos.order</field> <field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/> <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="amount_total" position="after"> <field name="amount_total" position="after">
<field name="payments_char" optional="show"/> <field name="payments_char" optional="show"/>
</field> </field>
</field>
</record>
<record id="view_pos_order_filter" model="ir.ui.view">
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_order_filter"/>
<field name="arch" type="xml">
<field name="date_order" position="after">
<field name="product_id"/>
</field>
</field> </field>
</record> </record>

View File

@@ -1,17 +1,17 @@
# Copyright 2015-2020 Akretion (http://www.akretion.com) # Copyright 2015-2025 Akretion France (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Product Category Tax', 'name': 'Product Category Tax',
'version': '14.0.1.0.0', 'version': '16.0.1.0.0',
'category': 'Accounting & Finance', 'category': 'Accounting & Finance',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': 'Adds sale and purchase taxes on product category', 'summary': 'Adds sale and purchase taxes on product category',
'description': "", 'description': "",
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['account'], 'depends': ['account'],
'data': ['product_view.xml'], 'data': ['product_view.xml'],
'installable': False, 'installable': True,
} }

View File

@@ -1,8 +1,8 @@
# Copyright 2015-2020 Akretion (http://www.akretion.com) # Copyright 2015-2025 Akretion France (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _ from odoo import api, fields, models, Command, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
@@ -24,8 +24,8 @@ class ProductCategTaxMixin(models.AbstractModel):
# of replacing the taxes... and I want to REPLACE the taxes # of replacing the taxes... and I want to REPLACE the taxes
# So I have to use the awful syntax (6, 0, [IDs]) # So I have to use the awful syntax (6, 0, [IDs])
# values are sent to ('taxes_id' and 'supplier_taxes_id') # values are sent to ('taxes_id' and 'supplier_taxes_id')
return ([(6, 0, categ.sale_tax_ids.ids)], return ([Command.set(categ.sale_tax_ids.ids)],
[(6, 0, categ.purchase_tax_ids.ids)]) [Command.set(categ.purchase_tax_ids.ids)])
@api.model @api.model
def write_or_create(self, vals): def write_or_create(self, vals):
@@ -34,10 +34,11 @@ class ProductCategTaxMixin(models.AbstractModel):
vals['taxes_id'], vals['supplier_taxes_id'] =\ vals['taxes_id'], vals['supplier_taxes_id'] =\
self.apply_tax_from_category(categ) self.apply_tax_from_category(categ)
@api.model @api.model_create_multi
def create(self, vals): def create(self, vals_list):
self.write_or_create(vals) for vals in vals_list:
return super().create(vals) self.write_or_create(vals)
return super().create(vals_list)
def write(self, vals): def write(self, vals):
self.write_or_create(vals) self.write_or_create(vals)
@@ -48,12 +49,10 @@ class ProductTemplate(models.Model):
_inherit = ['product.template', 'product.categ.tax.mixin'] _inherit = ['product.template', 'product.categ.tax.mixin']
_name = 'product.template' _name = 'product.template'
@api.constrains('taxes_id', 'supplier_taxes_id') @api.constrains('taxes_id', 'supplier_taxes_id', 'categ_id')
def _check_tax_categ(self): def _check_tax_categ(self):
# self.name != 'Pay Debt' is a stupid hack to avoid blocking the
# installation of the module 'pos_debt_notebook'
for pt in self: for pt in self:
if pt.categ_id: # and self.name != 'Pay Debt': if pt.categ_id:
if pt.categ_id.sale_tax_ids.ids != pt.taxes_id.ids: if pt.categ_id.sale_tax_ids.ids != pt.taxes_id.ids:
raise ValidationError(_( raise ValidationError(_(
"The sale taxes configured on the product '%s' " "The sale taxes configured on the product '%s' "

View File

@@ -0,0 +1,6 @@
Product Print ZPL Barcode via CUPS
==================================
This is a glue module between product_print_zpl_barcode (same repository) and base_report_to_printer (from `OCA/report-print-send <https://github.com/OCA/report-print-send>`). It is useful when you have an USB ZPL printer that you can only reach via CUPS.
This module has been written by Alexis de Lattre from Akretion France <alexis.delattre@akretion.com>.

View File

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

View File

@@ -0,0 +1,21 @@
# Copyright 2016-2020 Akretion (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Product Print ZPL Barcode via CUPS',
'version': '16.0.1.0.0',
'category': 'Extra Tools',
'license': 'AGPL-3',
'summary': 'Glue module between product_print_zpl_barcode and base_report_to_printer',
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': [
'product_print_zpl_barcode',
'base_report_to_printer',
],
'data': [
'wizards/product_print_zpl_barcode_view.xml',
],
'installable': True,
}

View File

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

View File

@@ -0,0 +1,26 @@
# Copyright 2025 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 api, fields, models
import base64
class ProductPrintZplBarcode(models.TransientModel):
_inherit = 'product.print.zpl.barcode'
zpl_printer_id = fields.Many2one('printing.printer', string='ZPL Printer')
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
printer = self.env['printing.printer'].get_default()
res['zpl_printer_id'] = printer and printer.id or False
return res
def print_zpl(self):
if self.zpl_printer_id:
self.zpl_printer_id.print_document(
self.zpl_filename, base64.decodebytes(self.zpl_file), format='raw')
else:
return super().print_zpl()

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2025 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).
-->
<odoo>
<record id="product_print_zpl_barcode_form" model="ir.ui.view">
<field name="name">product_print_zpl_barcode.CUPS.form</field>
<field name="model">product.print.zpl.barcode</field>
<field name="inherit_id" ref="product_print_zpl_barcode.product_print_zpl_barcode_form"/>
<field name="arch" type="xml">
<field name="zpl_printer_ip" position="attributes">
<attribute name="attrs">{'invisible': 1}</attribute>
</field>
<field name="zpl_printer_ip" position="after">
<field name="zpl_printer_id" attrs="{'required': [('state', '=', 'step2')]}"/>
</field>
</field>
</record>
</odoo>

View File

@@ -9,12 +9,12 @@ from odoo import api, models, fields
class ProductTemplate(models.Model): class ProductTemplate(models.Model):
_inherit = 'product.template' _inherit = 'product.template'
# restore v8 native field # seller_id cannot be stored, because its value may be different
# https://github.com/odoo/odoo/blob/8.0/addons/product/product.py#L592 # from one company to another
# in v10, that field was defined in procurement_suggest, but we will
# probably not port procurement_suggest because it is native in v14
seller_id = fields.Many2one( seller_id = fields.Many2one(
'res.partner', related='seller_ids.partner_id', store=True, 'res.partner',
compute="_compute_seller_id",
search="_search_seller_id",
string='Main Supplier') string='Main Supplier')
# in v14, I noticed that the tracking of the fields of product.template # in v14, I noticed that the tracking of the fields of product.template
@@ -35,6 +35,18 @@ class ProductTemplate(models.Model):
company_id = fields.Many2one(tracking=110) company_id = fields.Many2one(tracking=110)
barcode_type = fields.Char(compute='_compute_template_barcode_type') barcode_type = fields.Char(compute='_compute_template_barcode_type')
def _search_seller_id(self, operator, value):
# searching on the first line of a o2m is not that easy
# So we search all potential matching products
# Then we filter on the seller_id
records = self.search([("seller_ids.partner_id", operator, value)])
records = records.filtered_domain([("seller_id", operator, value)])
return [("id", "in", records.ids)]
def _compute_seller_id(self):
for template in self:
template.seller_id = fields.first(template.seller_ids).partner_id
# precompute=True doesn't work on product.template # precompute=True doesn't work on product.template
# (works fine on product.product), probably because we don't depend # (works fine on product.product), probably because we don't depend
# on 'barcode' # on 'barcode'

View File

@@ -32,5 +32,15 @@
</field> </field>
</record> </record>
<record id="product_product_tree_view" model="ir.ui.view">
<field name="name">usability.product.product.tree</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_product_tree_view"/>
<field name="arch" type="xml">
<field name="company_id" position="before">
<field name="seller_id" optional="hide"/>
</field>
</field>
</record>
</odoo> </odoo>

View File

@@ -14,7 +14,7 @@
<field name="inherit_id" ref="product.product_template_search_view" /> <field name="inherit_id" ref="product.product_template_search_view" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="categ_id" position="after"> <field name="categ_id" position="after">
<field name="seller_ids" string="Supplier" filter_domain="[('seller_ids.partner_id', 'ilike', self)]"/> <field name="seller_id" domain="[('parent_id', '=', False)]"/>
</field> </field>
<filter name="type" position="attributes"> <filter name="type" position="attributes">
<attribute name="context">{'group_by': 'detailed_type'}</attribute> <attribute name="context">{'group_by': 'detailed_type'}</attribute>

View File

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

View File

@@ -0,0 +1,24 @@
# Copyright (C) 2025 Akretion (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Purchase Stock Default Picking Type on Partner',
'version': '16.0.1.0.0',
'category': 'Purchases',
'license': 'AGPL-3',
'summary': 'Configure the default picking type for purchase orders on partners',
'description': """
Purchase Stock Default Picking Type on Partner
==============================================
Allow to configure on partners the default picking type for purchase orders.
Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for any help or question about this module.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['purchase_stock'],
'data': ['views/res_partner.xml'],
'installable': True,
}

View File

@@ -0,0 +1,2 @@
from . import res_partner
from . import purchase_order

View File

@@ -0,0 +1,23 @@
# 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
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
# If I set picking_type_id as computed field with store=True and readonly=False
# it doesn't work when creating a PO from the smartbutton of the partner form view
# in v14 and v16... and I don't understand why :-(
# So, until I find the cause of this, I use a good old onchange !
@api.onchange("partner_id", "company_id")
def onchange_partner_id(self):
super().onchange_partner_id()
if self.partner_id and self.company_id:
partner = self.partner_id.commercial_partner_id.with_company(self.company_id.id)
if partner.purchase_picking_type_id:
self.picking_type_id = partner.purchase_picking_type_id
else:
self.picking_type_id = self._get_picking_type(self.company_id.id)

View File

@@ -0,0 +1,15 @@
# 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 fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
# Used only for manual POs
purchase_picking_type_id = fields.Many2one(
'stock.picking.type', string="Purchase Picking Type",
company_dependent=True,
domain="[('code', '=', 'incoming'), ('company_id', '=', current_company_id)]")

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,21 @@
<?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="view_partner_property_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="purchase.view_partner_property_form"/>
<field name="arch" type="xml">
<field name="property_purchase_currency_id" position="before">
<field name="purchase_picking_type_id" attrs="{'invisible': [('parent_id', '!=', False)]}"/>
</field>
</field>
</record>
</odoo>

View File

@@ -15,6 +15,7 @@
'views/purchase_order.xml', 'views/purchase_order.xml',
'views/purchase_report.xml', 'views/purchase_report.xml',
'views/account_move.xml', 'views/account_move.xml',
'views/product_template.xml',
], ],
'installable': True, 'installable': True,
} }

View File

@@ -0,0 +1,23 @@
<?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="view_product_supplier_inherit" model="ir.ui.view">
<field name="model">product.template</field>
<field name="inherit_id" ref="purchase.view_product_supplier_inherit"/>
<field name="arch" type="xml">
<group name="bill" position="attributes">
<!-- native in purchase module : purchase.group_purchase_manager -->
<attribute name="groups">purchase.group_purchase_manager,account.group_account_manager</attribute>
</group>
</field>
</record>
</odoo>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2020 Akretion France (http://www.akretion.com/) Copyright 2024-2025 Akretion France (https://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com> @author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->

View File

@@ -1,2 +1 @@
from . import sale from . import models
from . import sale_report

View File

@@ -1,25 +1,25 @@
# Copyright (C) 2015-2019 Akretion (http://www.akretion.com) # Copyright 2015-2025 Akretion France (https://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Sale Margin No Onchange', "name": "Sale Margin No Onchange",
'version': '12.0.1.0.0', "version": "16.0.1.0.0",
'category': 'Sales', "category": "Sales",
'license': 'AGPL-3', "license": "AGPL-3",
'summary': 'Copy standard price on sale order line and compute margins', "summary": "Copy standard price on sale order line and compute margins",
'description': """ "description": """
This module copies the field *standard_price* of the product on the sale order line when the sale order line is created and then computes the margin of the sale order and the sale order line (in the currency of the quotation, in the currency of the company and the margin rate). This module copies the cost of the product on the sale order line when the sale order line is created and it computes the margin amount (in the currency of the order and in the currency of the company) and the margin rate. It also computes the total margin of the sale order (in the currency of the order and in the currency of the company).
I decided to develop this module as an alternative to the OCA sale margin modules because I wanted a small and simple module. The module *account_invoice_margin*, available in the same Github repository, do the same thing on customer invoices. I decided to develop this module as an alternative to the OCA sale margin modules because I wanted a small and simple module. The module *account_invoice_margin*, available in the same Github repository, does the same thing on customer invoices.
This module has been written by Alexis de Lattre from Akretion This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>. <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', "author": "Akretion",
'website': 'http://www.akretion.com', "website": "https://github.com/akretion/odoo-usability",
'depends': ['sale'], "depends": ["sale"],
'data': ['sale_view.xml'], "data": ["views/sale_order.xml", "views/sale_report.xml"],
'installable': False, "installable": True,
} }

View File

@@ -0,0 +1,102 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_margin_no_onchange
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-16 18:17+0000\n"
"PO-Revision-Date: 2025-01-16 18:17+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: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order__company_currency_id
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__company_currency_id
msgid "Company Currency"
msgstr "Devise de la société"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_report__margin
#: model_terms:ir.ui.view,arch_db:sale_margin_no_onchange.view_order_form
msgid "Margin"
msgstr "Marge"
#. module: sale_margin_no_onchange
#: model_terms:ir.ui.view,arch_db:sale_margin_no_onchange.view_order_form
#: model_terms:ir.ui.view,arch_db:sale_margin_no_onchange.view_order_line_tree
msgid "Margin %"
msgstr "Marge %"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__margin_rate
msgid "Margin Rate"
msgstr "Taux de marge"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order__margin_company_currency
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__margin_company_currency
msgid "Margin in Company Currency"
msgstr "Marge dans la devise de la société"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order__margin_sale_currency
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__margin_sale_currency
msgid "Margin in Sale Currency"
msgstr "Marge dans la devise de vente"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,help:sale_margin_no_onchange.field_sale_order_line__margin_rate
msgid "Margin rate in percentage of the sale price"
msgstr "Taux de marge en pourcentage du prix de vente"
#. module: sale_margin_no_onchange
#: model:ir.model,name:sale_margin_no_onchange.model_sale_report
msgid "Sales Analysis Report"
msgstr "Rapport d'analyse des ventes"
#. module: sale_margin_no_onchange
#: model:ir.model,name:sale_margin_no_onchange.model_sale_order
msgid "Sales Order"
msgstr "Bon de commande"
#. module: sale_margin_no_onchange
#: model:ir.model,name:sale_margin_no_onchange.model_sale_order_line
msgid "Sales Order Line"
msgstr "Ligne de commande"
#. module: sale_margin_no_onchange
#: model_terms:ir.ui.view,arch_db:sale_margin_no_onchange.view_order_form
msgid "Unit Cost"
msgstr "Coût unitaire"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__standard_price_company_currency
msgid "Unit Cost in Company Currency"
msgstr "Coût unitaire dans la devise de la société"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,field_description:sale_margin_no_onchange.field_sale_order_line__standard_price_sale_currency
msgid "Unit Cost in Sale Currency"
msgstr "Coût unitaire dans la devise de vente"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,help:sale_margin_no_onchange.field_sale_order_line__standard_price_company_currency
msgid ""
"Unit cost in company currency in the unit of measure of the sale order line"
msgstr ""
"Coût unitaire dans la devise de la société dans l'unité de mesure de la "
"ligne de commande client"
#. module: sale_margin_no_onchange
#: model:ir.model.fields,help:sale_margin_no_onchange.field_sale_order_line__standard_price_sale_currency
msgid ""
"Unit cost in sale currency in the unit of measure of the sale order line"
msgstr ""
"Coût unitaire dans la devise de vente dans l'unité de mesure de la ligne de "
"commande client"

Some files were not shown because too many files have changed in this diff Show More