diff --git a/account_invoice_margin/__init__.py b/account_invoice_margin/__init__.py new file mode 100644 index 0000000..ab922af --- /dev/null +++ b/account_invoice_margin/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import account_invoice diff --git a/account_invoice_margin/__openerp__.py b/account_invoice_margin/__openerp__.py new file mode 100644 index 0000000..64e2d6f --- /dev/null +++ b/account_invoice_margin/__openerp__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Account Invoice Margin module for Odoo +# Copyright (C) 2015 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Account Invoice Margin', + 'version': '0.1', + 'category': 'Accounting & Finance', + 'license': 'AGPL-3', + 'summary': 'Copy standard price on invoice line and compute margins', + '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 has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['account'], + 'data': [ + 'account_invoice_view.xml', + ], + 'installable': True, +} diff --git a/account_invoice_margin/account_invoice.py b/account_invoice_margin/account_invoice.py new file mode 100644 index 0000000..ab8a919 --- /dev/null +++ b/account_invoice_margin/account_invoice.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Account Invoice Margin module for Odoo +# Copyright (C) 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm, fields +import openerp.addons.decimal_precision as dp + + +class AccountInvoiceLine(orm.Model): + _inherit = 'account.invoice.line' + + def _compute_margin(self, cr, uid, ids, name, arg, context=None): + if context is None: + context = {} + res = {} + # we need an up-to-date price_subtotal + # We cannot read il.price_subtotal, because it may be out-of-date + # (I saw that with invoices in foreign currency) + price_subtotal = self._amount_line(cr, uid, ids, name, arg, context) + for il in self.browse(cr, uid, ids, context=context): + standard_price_inv_cur = 0.0 + margin_inv_cur = 0.0 + margin_comp_cur = 0.0 + margin_rate = 0.0 + if ( + il.invoice_id and + il.invoice_id.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 + + rco = self.pool['res.currency'] + ctx_currency = context.copy() + if il.invoice_id.date_invoice: + ctx_currency['date'] = il.invoice_id.date_invoice + standard_price_inv_cur =\ + rco.compute( + cr, uid, il.invoice_id.company_id.currency_id.id, + il.invoice_id.currency_id.id, + il.standard_price_company_currency, + context=ctx_currency) + margin_inv_cur =\ + price_subtotal[il.id] - il.quantity * standard_price_inv_cur + margin_comp_cur = rco.compute( + cr, uid, il.invoice_id.currency_id.id, + il.invoice_id.company_id.currency_id.id, + margin_inv_cur, context=ctx_currency) + if price_subtotal[il.id]: + margin_rate = 100 * margin_inv_cur / price_subtotal[il.id] + # for a refund, margin should be negative + # but margin rate should stay positive + if il.invoice_id.type == 'out_refund': + margin_inv_cur *= -1 + margin_comp_cur *= -1 + res[il.id] = { + 'standard_price_invoice_currency': standard_price_inv_cur, + 'margin_invoice_currency': margin_inv_cur, + 'margin_company_currency': margin_comp_cur, + 'margin_rate': margin_rate, + } + return res + + def _get_lines_from_invoices(self, cr, uid, ids, context=None): + return self.pool['account.invoice.line'].search( + cr, uid, [('invoice_id', 'in', ids)], context=context) + + _columns = { + 'standard_price_company_currency': fields.float( + 'Cost Price per Unit in Company Currency', readonly=True, + digits_compute=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.function( + _compute_margin, string='Cost Price per Unit in Invoice Currency', + type='float', readonly=True, multi='il-margin', store={ + 'account.invoice.line': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'standard_price_company_currency', + 'quantity', + 'price_unit', + 'discount', + 'invoice_line_tax_id'], 10), + 'account.invoice': ( + _get_lines_from_invoices, [ + 'type', + 'currency_id', + 'date_invoice', + 'company_id'], 20), + }, + digits_compute=dp.get_precision('Product Price'), + help="Cost price in invoice currency in the unit of measure " + "of the invoice line"), + 'margin_invoice_currency': fields.function( + _compute_margin, string='Margin in Invoice Currency', + type='float', readonly=True, multi='il-margin', store={ + 'account.invoice.line': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'standard_price_company_currency', + 'quantity', + 'price_unit', + 'discount', + 'invoice_line_tax_id'], 10), + 'account.invoice': ( + _get_lines_from_invoices, [ + 'type', + 'currency_id', + 'date_invoice', + 'company_id'], 20), + }, + digits_compute=dp.get_precision('Account')), + 'margin_company_currency': fields.function( + _compute_margin, type='float', + string='Margin in Company Currency', readonly=True, + multi='il-margin', store={ + 'account.invoice.line': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'standard_price_company_currency', + 'quantity', + 'price_unit', + 'discount', + 'invoice_line_tax_id'], 10), + 'account.invoice': ( + _get_lines_from_invoices, [ + 'type', + 'currency_id', + 'date_invoice', + 'company_id'], 20), + }, + digits_compute=dp.get_precision('Account')), + 'margin_rate': fields.function( + _compute_margin, type='float', + string="Margin Rate", readonly=True, multi='il-margin', store={ + 'account.invoice.line': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'standard_price_company_currency', + 'quantity', + 'price_unit', + 'discount', + 'invoice_line_tax_id'], 10), + 'account.invoice': ( + _get_lines_from_invoices, [ + 'type', + 'currency_id', + 'date_invoice', + 'company_id'], 20), + }, + digits=(16, 2), + help="Margin rate in percentage of the sale price"), + } + + # 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 + def create(self, cr, uid, vals, context=None): + if vals.get('product_id'): + pp = self.pool['product.product'].browse( + cr, uid, vals['product_id'], context=context) + std_price = pp.standard_price + inv_uom_id = vals.get('uos_id') + if inv_uom_id and inv_uom_id != pp.uom_id.id: + std_price = self.pool['product.uom']._compute_price( + cr, uid, pp.uom_id.id, std_price, inv_uom_id) + vals['standard_price_company_currency'] = std_price + return super(AccountInvoiceLine, self).create( + cr, uid, vals, context=context) + + def write(self, cr, uid, ids, vals, context=None): + """This code can seem a bit strange with the super() inside + the loop, but it is required if we want to support all scenarios, + including very strange scenarios where you write a uos_id on + several lines with different products, etc... + OK, these scenarios will probably never happen, but I prefer + to make sure that the code is right and works in all + scenarios + """ + if not vals: + vals = {} + if isinstance(ids, (int, long)): + ids = [ids] + if 'product_id' in vals or 'uos_id' in vals: + for il in self.browse(cr, uid, ids, context=context): + if 'product_id' in vals: + if vals.get('product_id'): + pp = self.pool['product.product'].browse( + cr, uid, vals['product_id'], context=context) + else: + pp = False + else: + pp = il.product_id or False + # uos_id is NOT a required field + if 'uos_id' in vals: + if vals.get('uos_id'): + inv_uom = self.pool['product.uom'].browse( + cr, uid, vals['uos_id'], context=context) + else: + inv_uom = False + else: + inv_uom = il.uos_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 = self.pool['product.uom']._compute_price( + cr, uid, pp.uom_id.id, std_price, inv_uom.id) + vals_line = vals.copy() + vals_line['standard_price_company_currency'] = std_price + super(AccountInvoiceLine, self).write( + cr, uid, [il.id], vals_line, context=context) + return True + else: + return super(AccountInvoiceLine, self).write( + cr, uid, ids, vals, context=context) + + +class AccountInvoice(orm.Model): + _inherit = 'account.invoice' + + def _compute_margin(self, cr, uid, ids, name, arg, context=None): + res = {} + for inv in self.browse(cr, uid, ids, context=context): + margin_inv_cur = 0.0 + margin_comp_cur = 0.0 + if inv.type in ('out_invoice', 'out_refund'): + for il in inv.invoice_line: + margin_inv_cur += il.margin_invoice_currency + margin_comp_cur += il.margin_company_currency + res[inv.id] = { + 'margin_invoice_currency': margin_inv_cur, + 'margin_company_currency': margin_comp_cur, + } + return res + + def _get_invoices_from_inv_lines(self, cr, uid, ids, context=None): + return self.pool['account.invoice'].search( + cr, uid, [('invoice_line', 'in', ids)], context=context) + + _columns = { + 'margin_invoice_currency': fields.function( + _compute_margin, type='float', + string='Margin in Invoice Currency', + readonly=True, multi='inv-margin', + digits_compute=dp.get_precision('Account'), store={ + 'account.invoice': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'type', 'currency_id'], 10), + 'account.invoice.line': ( + _get_invoices_from_inv_lines, [ + 'margin_company_currency'], 20), + }), + 'margin_company_currency': fields.function( + _compute_margin, type='float', + string='Margin in Company Currency', + readonly=True, multi='inv-margin', + digits_compute=dp.get_precision('Account'), store={ + 'account.invoice': ( + lambda self, cr, uid, ids, c={}: ids, [ + 'type', 'currency_id'], 10), + 'account.invoice.line': ( + _get_invoices_from_inv_lines, [ + 'margin_company_currency'], 20), + }), + } diff --git a/account_invoice_margin/account_invoice_view.xml b/account_invoice_margin/account_invoice_view.xml new file mode 100644 index 0000000..269d7b2 --- /dev/null +++ b/account_invoice_margin/account_invoice_view.xml @@ -0,0 +1,59 @@ + + + + + + + + margin.account.invoice.line.form + account.invoice.line + + + + + + + + + + + + + margin.account.invoice.form + account.invoice + + + + + + + + + + + + + + + + + + +