diff --git a/account_invoice_margin/__init__.py b/account_invoice_margin/__init__.py index 65739c5..0650744 100644 --- a/account_invoice_margin/__init__.py +++ b/account_invoice_margin/__init__.py @@ -1,2 +1 @@ -from . import account_invoice -from . import account_invoice_report +from . import models diff --git a/account_invoice_margin/__manifest__.py b/account_invoice_margin/__manifest__.py index 8911105..c4c7563 100644 --- a/account_invoice_margin/__manifest__.py +++ b/account_invoice_margin/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Account Invoice Margin', - 'version': '12.0.1.0.0', + 'version': '14.0.1.0.0', 'category': 'Invoicing Management', 'license': 'AGPL-3', 'summary': 'Copy standard price on invoice line and compute margins', @@ -15,10 +15,10 @@ This module has been written by Alexis de Lattre from Akretion . """, 'author': 'Akretion', - 'website': 'http://www.akretion.com', + 'website': 'https://github.com/akretion/odoo-usability', 'depends': ['account'], 'data': [ - 'account_invoice_view.xml', + 'views/account_move.xml', ], 'installable': False, } diff --git a/account_invoice_margin/account_invoice.py b/account_invoice_margin/account_invoice.py deleted file mode 100644 index e42cac9..0000000 --- a/account_invoice_margin/account_invoice.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 2015-2019 Akretion France (http://www.akretion.com) -# @author Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import 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'] diff --git a/account_invoice_margin/account_invoice_report.py b/account_invoice_margin/account_invoice_report.py deleted file mode 100644 index 5fcf9d8..0000000 --- a/account_invoice_margin/account_invoice_report.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2018-2019 Akretion France (http://www.akretion.com) -# @author Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import 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 diff --git a/account_invoice_margin/account_invoice_view.xml b/account_invoice_margin/account_invoice_view.xml deleted file mode 100644 index d8f9f35..0000000 --- a/account_invoice_margin/account_invoice_view.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - margin.account.invoice.line.form - account.invoice.line - - - - - - - - - - - - - margin.account.invoice.form - account.invoice - - - - - - - - - - - diff --git a/account_invoice_margin/models/__init__.py b/account_invoice_margin/models/__init__.py new file mode 100644 index 0000000..15214dd --- /dev/null +++ b/account_invoice_margin/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_move +from . import account_invoice_report diff --git a/account_invoice_margin/models/account_invoice_report.py b/account_invoice_margin/models/account_invoice_report.py new file mode 100644 index 0000000..de8aa85 --- /dev/null +++ b/account_invoice_margin/models/account_invoice_report.py @@ -0,0 +1,36 @@ +# Copyright 2018-2019 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountInvoiceReport(models.Model): + _inherit = 'account.invoice.report' + + margin = fields.Float(string='Margin', readonly=True) + + # added margin_company_currency on account.move.line + _depends = { + 'account.move': [ + 'name', 'state', 'move_type', 'partner_id', 'invoice_user_id', 'fiscal_position_id', + 'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'partner_bank_id', + ], + 'account.move.line': [ + 'quantity', 'price_subtotal', 'amount_residual', 'balance', 'amount_currency', + 'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id', + 'journal_id', 'company_id', 'currency_id', 'partner_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.model + def _select(self): + select_str = super()._select() + select_str += ", line.margin_company_currency * currency_table.rate AS margin" + return select_str diff --git a/account_invoice_margin/models/account_move.py b/account_invoice_margin/models/account_move.py new file mode 100644 index 0000000..0c35760 --- /dev/null +++ b/account_invoice_margin/models/account_move.py @@ -0,0 +1,155 @@ +# Copyright 2015-2021 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + standard_price_company_currency = fields.Float( + string='Unit Cost Price in Company Currency', readonly=True, + digits='Product Price', + help="Unit 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='Unit Cost Price in Invoice Currency', + compute='_compute_margin', store=True, digits='Product Price', + help="Unit Cost price in invoice currency in the unit of measure " + "of the invoice line.") + margin_invoice_currency = fields.Monetary( + string='Margin in Invoice Currency', store=True, + compute='_compute_margin', currency_field='currency_id') + margin_company_currency = fields.Monetary( + string='Margin in Company Currency', 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', 'move_id.currency_id', + 'move_id.move_type', 'move_id.company_id', + 'move_id.invoice_date', 'quantity', 'price_subtotal') + def _compute_margin(self): + for ml in self: + standard_price_inv_cur = 0.0 + margin_inv_cur = 0.0 + margin_comp_cur = 0.0 + margin_rate = 0.0 + move = ml.move_id + if move.move_type and move.move_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 = move.date or fields.Date.context_today(self) + company = move.company_id + company_currency = company.currency_id + standard_price_inv_cur =\ + company_currency._convert( + ml.standard_price_company_currency, + ml.currency_id, company, date) + margin_inv_cur =\ + ml.price_subtotal - ml.quantity * standard_price_inv_cur + margin_comp_cur = move.currency_id._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 move.move_type == 'out_refund': + margin_inv_cur *= -1 + margin_comp_cur *= -1 + 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 + + # 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_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('product_id') and not vals.get('display_type'): + pp = self.env['product.product'].browse(vals['product_id']) + std_price = pp.standard_price + inv_uom_id = vals.get('product_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().create(vals_list) + + def write(self, vals): + if not vals: + vals = {} + if 'product_id' in vals or 'product_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 'product_uom_id' in vals: + if vals.get('product_uom_id'): + inv_uom = self.env['uom.uom'].browse( + vals['product_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().write(vals) + + +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', '=', False), + ('exclude_from_invoice_tab', '=', False), + ('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') diff --git a/account_invoice_margin/views/account_move.xml b/account_invoice_margin/views/account_move.xml new file mode 100644 index 0000000..b6f7c97 --- /dev/null +++ b/account_invoice_margin/views/account_move.xml @@ -0,0 +1,55 @@ + + + + + + + margin.account.move.form + account.move + + + + + + + + + + + + + + + + + + + + + + account.move + + + + + + + + +