diff --git a/addons/sale_expense_margin/__init__.py b/addons/sale_expense_margin/__init__.py new file mode 100644 index 00000000000..90b17558ffc --- /dev/null +++ b/addons/sale_expense_margin/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import tests diff --git a/addons/sale_expense_margin/__manifest__.py b/addons/sale_expense_margin/__manifest__.py new file mode 100644 index 00000000000..c8d5abc4860 --- /dev/null +++ b/addons/sale_expense_margin/__manifest__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Sales Expense Margin', + 'version': '1.0', + 'category': 'Sales/Sales', + 'description': 'When re-invoicing the expense on the SO, set the cost to the total untaxed amount of the expense.', + 'depends': ['sale_expense', 'sale_margin'], + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3', +} diff --git a/addons/sale_expense_margin/models/__init__.py b/addons/sale_expense_margin/models/__init__.py new file mode 100644 index 00000000000..f41f60b3a11 --- /dev/null +++ b/addons/sale_expense_margin/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_move +from . import sale_order_line diff --git a/addons/sale_expense_margin/models/account_move.py b/addons/sale_expense_margin/models/account_move.py new file mode 100644 index 00000000000..830c890035d --- /dev/null +++ b/addons/sale_expense_margin/models/account_move.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + def _sale_prepare_sale_line_values(self, order, price): + res = super()._sale_prepare_sale_line_values(order, price) + if self.expense_id: + if self.expense_id: + res['expense_id'] = self.expense_id.id + return res diff --git a/addons/sale_expense_margin/models/sale_order_line.py b/addons/sale_expense_margin/models/sale_order_line.py new file mode 100644 index 00000000000..47c1272b22b --- /dev/null +++ b/addons/sale_expense_margin/models/sale_order_line.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + expense_id = fields.Many2one('hr.expense', string='Expense') + + @api.depends('is_expense') + def _compute_purchase_price(self): + date_today = fields.Date.context_today(self) + expense_lines = self.filtered('expense_id') + for line in expense_lines: + if line.expense_id.product_has_cost: + product_cost = line.expense_id.untaxed_amount / line.expense_id.quantity + else: + product_cost = line.expense_id.untaxed_amount + + from_currency = line.expense_id.currency_id + to_currency = line.currency_id or line.order_id.currency_id + + if to_currency and product_cost and from_currency != to_currency: + line.purchase_price = from_currency._convert( + from_amount=product_cost, + to_currency=to_currency, + company=line.company_id or self.env.company, + date=line.order_id.date_order or date_today, + round=False) + else: + line.purchase_price = product_cost + return super(SaleOrderLine, self - expense_lines)._compute_purchase_price() diff --git a/addons/sale_expense_margin/tests/__init__.py b/addons/sale_expense_margin/tests/__init__.py new file mode 100644 index 00000000000..ee553dddab1 --- /dev/null +++ b/addons/sale_expense_margin/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_so_expense_purchase_price diff --git a/addons/sale_expense_margin/tests/test_so_expense_purchase_price.py b/addons/sale_expense_margin/tests/test_so_expense_purchase_price.py new file mode 100644 index 00000000000..fd1845fd143 --- /dev/null +++ b/addons/sale_expense_margin/tests/test_so_expense_purchase_price.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.hr_expense.tests.common import TestExpenseCommon +from odoo.tests import tagged + +@tagged('-at_install', 'post_install') +class TestExpenseMargin(TestExpenseCommon): + + def test_expense_reinvoice_purchase_price(self): + # re-invoiceable products + product_with_cost = self.product_a + product_with_cost.write({'standard_price': 1000, 'expense_policy': 'sales_price'}) + product_with_no_cost = self.product_b + product_with_no_cost.write({'standard_price': 0, 'expense_policy': 'sales_price'}) + + # create SO line and confirm SO (with only one line) + sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + 'order_line': [(0, 0, { + 'name': product_with_cost.name, + 'product_id': product_with_cost.id, + 'product_uom_qty': 2.0, + 'price_unit': 13.0, + })], + }) + + sale_order.action_confirm() + + expense_sheet = self.env['hr.expense.sheet'].create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2020-10-12', + 'expense_line_ids': [ + # expense with zero cost product, with 15% tax + (0, 0, { + 'name': 'expense_1', + 'date': '2020-10-07', + 'product_id': product_with_no_cost.id, + 'unit_amount': product_with_no_cost.standard_price, + 'total_amount': 100, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + # expense with zero cost product, with no tax + (0, 0, { + 'name': 'expense_2', + 'date': '2020-10-07', + 'product_id': product_with_no_cost.id, + 'unit_amount': product_with_no_cost.standard_price, + 'total_amount': 100, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id + }), + # expense with product with cost (1000), with 15% tax + (0, 0, { + 'name': 'expense_3', + 'date': '2020-10-07', + 'product_id': product_with_cost.id, + 'quantity': 3, + 'unit_amount': product_with_cost.standard_price, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id + }), + # expense with product with cost (1000), with no tax + (0, 0, { + 'name': 'expense_4', + 'date': '2020-10-07', + 'product_id': product_with_cost.id, + 'quantity': 5, + 'unit_amount': product_with_cost.standard_price, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id + }), + ], + }) + + expense_sheet.approve_expense_sheets() + expense_sheet.action_sheet_move_create() + + self.assertRecordValues(sale_order.order_line[1:], [ + # Expense lines: + { + 'purchase_price': 86.96, + 'is_expense': True, + }, + { + 'purchase_price': 100.0, + 'is_expense': True, + }, + { + 'purchase_price': 869.57, + 'is_expense': True, + }, + { + 'purchase_price': 1000.0, + 'is_expense': True, + }, + ])