diff --git a/addons/hr_expense/models/product_template.py b/addons/hr_expense/models/product_template.py index eb63b64b2da..df01a901534 100644 --- a/addons/hr_expense/models/product_template.py +++ b/addons/hr_expense/models/product_template.py @@ -16,3 +16,8 @@ class ProductTemplate(models.Model): if vals.get('can_be_expensed', False): vals.update({'supplier_taxes_id': False}) return super(ProductTemplate, self).create(vals) + + @api.onchange('type') + def _onchange_type_for_expense(self): + if self.type not in ['consu', 'service']: # stockable can not be expensed. + self.can_be_expensed = False diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py index c7b7726bd4c..71a4d33e757 100644 --- a/addons/hr_expense/tests/common.py +++ b/addons/hr_expense/tests/common.py @@ -51,3 +51,8 @@ class TestExpenseCommon(TestAccountNoChartCommon): 'employee_id': cls.employee.id, 'journal_id': cls.journal.id, }) + cls.expense_sheet2 = cls.env['hr.expense.sheet'].create({ + 'name': 'Second Expense for Johnny Employee', + 'employee_id': cls.employee.id, + 'journal_id': cls.journal.id, + }) diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index b42cdacb93d..b15c6aaf637 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -242,7 +242,7 @@
-
+
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 95d1a4fbffd..6532067bb57 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -48,6 +48,10 @@ + - - - - 0: self.discount = discount - - ########################### - # Analytic Methods - ########################### - - @api.multi - def _analytic_compute_delivered_quantity_domain(self): - """ Return the domain of the analytic lines to use to recompute the delivered quantity - on SO lines. This method is a hook: since analytic line are used for timesheet, - expense, ... each use case should provide its part of the domain. - """ - return [('so_line', 'in', self.ids), ('amount', '<=', 0.0)] - - @api.multi - def _analytic_compute_delivered_quantity(self): - """ Compute and write the delivered quantity of current SO lines, based on their related - analytic lines. - """ - # avoid recomputation if no SO lines concerned - if not self: - return False - - # group anaytic lines by product uom and so line - domain = self._analytic_compute_delivered_quantity_domain() - data = self.env['account.analytic.line'].read_group( - domain, - ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False - ) - # Force recompute for the "unlink last line" case: if remove the last AAL link to the SO, the read_group - # will give no value for the qty of the SOL, so we need to reset it to 0.0 - value_to_write = {} - if self._context.get('sale_analytic_force_recompute'): - value_to_write = dict.fromkeys([sol for sol in self], 0.0) - # convert uom and sum all unit_amount of analytic lines to get the delivered qty of SO lines - for item in data: - if not item['product_uom_id']: - continue - so_line = self.browse(item['so_line'][0]) - value_to_write.setdefault(so_line, 0.0) - uom = self.env['product.uom'].browse(item['product_uom_id'][0]) - if so_line.product_uom.category_id == uom.category_id: - qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom) - else: - qty = item['unit_amount'] - value_to_write[so_line] += qty - - # write the delivered quantity - for so_line, qty in value_to_write.items(): - so_line.write({'qty_delivered': qty}) - - return True diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 3aac8a509ba..8223c972244 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import test_sale_to_invoice from . import test_sale_order from . import test_product_id_change +from . import test_reinvoice diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py new file mode 100644 index 00000000000..6f7080307a4 --- /dev/null +++ b/addons/sale/tests/test_reinvoice.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart + + +class TestReInvoice(TestCommonSaleNoChart): + + @classmethod + def setUpClass(cls): + super(TestReInvoice, cls).setUpClass() + + cls.setUpExpenseProducts() + cls.setUpAdditionalAccounts() + cls.setUpAccountJournal() + + cls.analytic_account = cls.env['account.analytic.account'].create({ + 'name': 'Test AA', + 'code': 'TESTSALE_REINVOICE', + 'company_id': cls.partner_customer_usd.company_id.id, + 'partner_id': cls.partner_customer_usd.id + }) + + cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': cls.partner_customer_usd.id, + 'partner_invoice_id': cls.partner_customer_usd.id, + 'partner_shipping_id': cls.partner_customer_usd.id, + 'analytic_account_id': cls.analytic_account.id, + }) + + Invoice = cls.env['account.invoice'].with_context(mail_notrack=True, mail_create_nolog=True) + cls.invoiceA = Invoice.create({ + 'name': 'Test Invoice', + 'type': 'in_invoice', + 'partner_id': cls.partner_customer_usd.id, + 'account_id': cls.account_payable.id, + 'journal_id': cls.journal_purchase.id, + 'currency_id': cls.env.user.company_id.currency_id.id, + }) + cls.invoiceB = Invoice.create({ + 'name': 'Test Invoice 2', + 'type': 'in_invoice', + 'partner_id': cls.partner_customer_usd.id, + 'account_id': cls.account_payable.id, + 'journal_id': cls.journal_purchase.id, + 'currency_id': cls.env.user.company_id.currency_id.id, + }) + + def test_at_cost(self): + """ Test vendor bill at cost for product based on ordered and delivered quantities. """ + # create SO line and confirm SO (with only one line) + sale_order_line1 = self.env['sale.order.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'product_uom_qty': 2, + 'qty_delivered': 1, + 'product_uom': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line1.product_id_change() + sale_order_line2 = self.env['sale.order.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'product_uom_qty': 4, + 'qty_delivered': 1, + 'product_uom': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line2.product_id_change() + + self.sale_order.onchange_partner_id() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'quantity': 3, + 'uom_id': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + invoice_lineA2 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'quantity': 3, + 'uom_id': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.product_ordered_cost) + sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.product_deliver_cost) + + self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product") + self.assertEquals(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)") + + self.assertEquals(sale_order_line1.qty_delivered, 1, "Exising SO line 1 should not be impacted by reinvoicing product at cost") + self.assertEquals(sale_order_line2.qty_delivered, 1, "Exising SO line 2 should not be impacted by reinvoicing product at cost") + + self.assertEquals((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.product_ordered_cost.standard_price, invoice_lineA1.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + self.assertEquals((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.product_deliver_cost.standard_price, invoice_lineA2.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + + self.assertEquals(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount") + self.assertEquals(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount") + + # create second invoice lines and validate it + invoice_lineB1 = self.env['account.invoice.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'quantity': 2, + 'uom_id': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + invoice_lineB2 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'quantity': 2, + 'uom_id': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + self.invoiceB.action_invoice_open() + + sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.product_ordered_cost) + sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.product_deliver_cost) + + self.assertTrue(sale_order_line5, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line6, "A new sale line should have been created with delivered product") + + self.assertEquals(len(self.sale_order.order_line), 6, "There should be still 4 lines on the SO, no new created") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 4, "There should be still 2 expenses lines on the SO") + + self.assertEquals((sale_order_line5.price_unit, sale_order_line5.qty_delivered, sale_order_line5.product_uom_qty, sale_order_line5.qty_invoiced), (self.product_ordered_cost.standard_price, invoice_lineB1.quantity, 0, 0), 'Sale line 5 is wrong after confirming 2e vendor invoice') + self.assertEquals((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line6.product_uom_qty, sale_order_line6.qty_invoiced), (self.product_deliver_cost.standard_price, invoice_lineB2.quantity, 0, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice') + + def test_sales_price(self): + """ Test invoicing vendor bill at sales price for products based on delivered and ordered quantities. Check no existing SO line is incremented, but when invoicing a + second time, increment only the delivered so line. + """ + # create SO line and confirm SO (with only one line) + sale_order_line1 = self.env['sale.order.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'product_uom_qty': 2, + 'qty_delivered': 1, + 'product_uom': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line1.product_id_change() + sale_order_line2 = self.env['sale.order.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'product_uom_qty': 3, + 'qty_delivered': 1, + 'product_uom': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line2.product_id_change() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'quantity': 3, + 'uom_id': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + invoice_lineA2 = self.env['account.invoice.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'quantity': 3, + 'uom_id': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.product_deliver_sales_price) + sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.product_order_sales_price) + + self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product") + self.assertEquals(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)") + + self.assertEquals(sale_order_line1.qty_delivered, 1, "Exising SO line 1 should not be impacted by reinvoicing product at cost") + self.assertEquals(sale_order_line2.qty_delivered, 1, "Exising SO line 2 should not be impacted by reinvoicing product at cost") + + self.assertEquals((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.product_deliver_sales_price.list_price, invoice_lineA1.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + self.assertEquals((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.product_order_sales_price.list_price, invoice_lineA2.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + + self.assertEquals(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 3 should be computed by analytic amount") + self.assertEquals(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 4 should be computed by analytic amount") + + # create second invoice lines and validate it + invoice_lineB1 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'quantity': 2, + 'uom_id': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + invoice_lineB2 = self.env['account.invoice.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'quantity': 2, + 'uom_id': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + self.invoiceB.action_invoice_open() + + sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.product_deliver_sales_price) + sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.product_order_sales_price) + + self.assertFalse(sale_order_line5, "No new sale line should have been created with delivered product !!") + self.assertTrue(sale_order_line6, "A new sale line should have been created with ordered product") + + self.assertEquals(len(self.sale_order.order_line), 5, "There should be 5 lines on the SO, 1 new created and 1 incremented") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 3, "There should be 3 expenses lines on the SO") + + self.assertEquals((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line6.qty_invoiced), (self.product_order_sales_price.list_price, invoice_lineB2.quantity, 0, 0), 'Sale line is wrong after confirming 2e vendor invoice') + + def test_no_expense(self): + """ Test invoicing vendor bill with no policy. Check nothing happen. """ + # confirm SO + sale_order_line = self.env['sale.order.line'].create({ + 'name': self.product_no_expense.name, + 'product_id': self.product_no_expense.id, + 'product_uom_qty': 2, + 'qty_delivered': 1, + 'product_uom': self.product_no_expense.uom_id.id, + 'price_unit': self.product_no_expense.list_price, + 'order_id': self.sale_order.id, + }) + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_no_expense.name, + 'product_id': self.product_no_expense.id, + 'quantity': 3, + 'uom_id': self.product_no_expense.uom_id.id, + 'price_unit': self.product_no_expense.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill") + self.assertEquals(sale_order_line.qty_delivered, 1, "The delivered quantity of SO line should not have been incremented") + self.assertTrue(invoice_lineA1.invoice_id.move_id.mapped('line_ids.analytic_line_ids'), "Analytic lines should be generated") diff --git a/addons/sale/tests/test_sale_common.py b/addons/sale/tests/test_sale_common.py index 78768f7d514..69bdff636db 100644 --- a/addons/sale/tests/test_sale_common.py +++ b/addons/sale/tests/test_sale_common.py @@ -143,7 +143,7 @@ class TestCommonSaleNoChart(TestAccountNoChartCommon): # Create an expense journal user_type_expense = cls.env.ref('account.data_account_type_expenses') cls.account_expense_for_products = cls.env['account.account'].create({ - 'code': 'NC1113', + 'code': 'EXP_PROD13', 'name': 'Expense - Test Purchase Account', 'user_type_id': user_type_expense.id }) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 87271c0324f..a68c992438b 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import odoo from odoo.exceptions import UserError, AccessError from .test_sale_common import TestCommonSaleNoChart diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index e8f9a70b3cf..2f1dbc16f45 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -247,7 +247,8 @@ - + +