diff --git a/addons/project_purchase/i18n/project_purchase.pot b/addons/project_purchase/i18n/project_purchase.pot index f4348f389da..ea4870985aa 100644 --- a/addons/project_purchase/i18n/project_purchase.pot +++ b/addons/project_purchase/i18n/project_purchase.pot @@ -20,6 +20,13 @@ msgstr "" msgid "# Purchase Orders" msgstr "" +#. module: project_purchase +#: code:addons/project_purchase/models/project.py:0 +#: code:addons/project_purchase/models/project.py:0 +#, python-format +msgid "Other Costs" +msgstr "" + #. module: project_purchase #: model:ir.model,name:project_purchase.model_project_project msgid "Project" diff --git a/addons/project_purchase/models/project.py b/addons/project_purchase/models/project.py index e18461da662..7e90ba0c515 100644 --- a/addons/project_purchase/models/project.py +++ b/addons/project_purchase/models/project.py @@ -109,11 +109,13 @@ class Project(models.Model): def _get_profitability_labels(self): labels = super()._get_profitability_labels() labels['purchase_order'] = _lt('Purchase Orders') + labels['other_purchase_costs'] = _lt('Other Costs') return labels def _get_profitability_sequence_per_invoice_type(self): sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type() sequence_per_invoice_type['purchase_order'] = 9 + sequence_per_invoice_type['other_purchase_costs'] = 10 return sequence_per_invoice_type def _get_profitability_items(self, with_action=True): @@ -128,11 +130,16 @@ class Project(models.Model): query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit') self._cr.execute(query_string, query_param) - purchase_order_line_read = [pol for pol in self._cr.dictfetchall()] + purchase_order_line_read = [{ + **pol, + 'invoice_lines': self.env['purchase.order.line'].browse(pol['id']).invoice_lines, # One2Many cannot be queried, they are not columns + } for pol in self._cr.dictfetchall()] + purchase_order_line_invoice_line_ids = [] if purchase_order_line_read: amount_invoiced = amount_to_invoice = 0.0 purchase_order_line_ids = [] for pol_read in purchase_order_line_read: + purchase_order_line_invoice_line_ids.extend(pol_read['invoice_lines'].ids) price_unit = pol_read['price_unit'] amount_invoiced -= price_unit * pol_read['qty_invoiced'] if pol_read['qty_invoiced'] > 0 else 0.0 if pol_read['qty_to_invoice'] > 0: @@ -152,4 +159,44 @@ class Project(models.Model): costs['data'].append(purchase_order_costs) costs['total']['billed'] += amount_invoiced costs['total']['to_bill'] += amount_to_invoice + # calculate the cost of bills without a purchase order + query = self.env['account.move.line'].sudo()._search([ + ('move_id.move_type', 'in', ['in_invoice', 'in_refund']), + ('parent_state', 'in', ['draft', 'posted']), + ('price_subtotal', '>', 0), + ('id', 'not in', purchase_order_line_invoice_line_ids), + ]) + query.add_where('account_move_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + # account_move_line__move_id is the alias of the joined table account_move in the query + # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join + # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line + query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line__move_id.move_type') + self._cr.execute(query_string, query_param) + bills_move_line_read = self._cr.dictfetchall() + if bills_move_line_read: + amount_invoiced = amount_to_invoice = 0.0 + for moves_read in bills_move_line_read: + if moves_read['parent_state'] == 'draft': + if moves_read['move_type'] == 'in_invoice': + amount_to_invoice -= moves_read['price_subtotal'] + else: # moves_read['move_type'] == 'in_refund' + amount_to_invoice += moves_read['price_subtotal'] + else: # moves_read['parent_state'] == 'posted' + if moves_read['move_type'] == 'in_invoice': + amount_invoiced -= moves_read['price_subtotal'] + else: # moves_read['move_type'] == 'in_refund' + amount_invoiced += moves_read['price_subtotal'] + # don't display the section if the final values are both 0 (bill -> vendor credit) + if amount_invoiced != 0 or amount_to_invoice != 0: + costs = profitability_items['costs'] + section_id = 'other_purchase_costs' + bills_costs = { + 'id': section_id, + 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], + 'billed': amount_invoiced, + 'to_bill': amount_to_invoice, + } + costs['data'].append(bills_costs) + costs['total']['billed'] += amount_invoiced + costs['total']['to_bill'] += amount_to_invoice return profitability_items diff --git a/addons/project_purchase/tests/__init__.py b/addons/project_purchase/tests/__init__.py new file mode 100644 index 00000000000..4fa67e8be11 --- /dev/null +++ b/addons/project_purchase/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_project_profitability diff --git a/addons/project_purchase/tests/test_project_profitability.py b/addons/project_purchase/tests/test_project_profitability.py new file mode 100644 index 00000000000..b232492e850 --- /dev/null +++ b/addons/project_purchase/tests/test_project_profitability.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime + +from odoo import Command +from odoo.tests import tagged + +from odoo.addons.project.tests.test_project_profitability import TestProjectProfitabilityCommon +from odoo.addons.purchase.tests.test_purchase_invoice import TestPurchaseToInvoiceCommon + + +@tagged('-at_install', 'post_install') +class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurchaseToInvoiceCommon): + + def test_bills_without_purchase_order_are_accounted_in_profitability(self): + """ + A bill that has an AAL on one of its line should be taken into account + for the profitability of the project. + """ + # create a bill_1 with the AAL + bill_1 = self.env['account.move'].create({ + "name": "Bill_1 name", + "move_type": "in_invoice", + "state": "draft", + "partner_id": self.partner.id, + "invoice_date": datetime.today(), + "invoice_line_ids": [Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_a.id, + "quantity": 1, + "product_uom_id": self.product_a.uom_id.id, + "price_unit": self.product_a.standard_price, + })], + }) + # the bill_1 is in draft, therefor it should have the cost "to_bill" same as the -product_price (untaxed) + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': -self.product_a.standard_price, + 'billed': 0.0, + }], + 'total': {'to_bill': -self.product_a.standard_price, 'billed': 0.0}, + }, + ) + # post bill_1 + bill_1.action_post() + # we posted the bill_1, therefore the cost "billed" should be -product_price, to_bill should be back to 0 + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': 0.0, + 'billed': -self.product_a.standard_price, + }], + 'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price}, + }, + ) + # create another bill, with 2 lines, 2 diff products, the second line has 2 as quantity + bill_2 = self.env['account.move'].create({ + "name": "I have 2 lines", + "move_type": "in_invoice", + "state": "draft", + "partner_id": self.partner.id, + "invoice_date": datetime.today(), + "invoice_line_ids": [Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_a.id, + "quantity": 1, + "product_uom_id": self.product_a.uom_id.id, + "price_unit": self.product_a.standard_price, + }), Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_b.id, + "quantity": 2, + "product_uom_id": self.product_b.uom_id.id, + "price_unit": self.product_b.standard_price, + })], + }) + # bill_2 is not posted, therefor its cost should be "to_billed" = - sum of all product_price * qty for each line + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price), + 'billed': -self.product_a.standard_price, + }], + 'total': { + 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price), + 'billed': -self.product_a.standard_price, + }, + }, + ) + # post bill_2 + bill_2.action_post() + # bill_2 is posted, therefor its cost should be counting in "billed", with the cost of bill_1 + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': 0.0, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + }], + 'total': { + 'to_bill': 0.0, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + }, + }, + ) + # create a new purchase order + purchase_order = self.env['purchase.order'].create({ + "name": "A purchase order", + "partner_id": self.partner_a.id, + "order_line": [Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_order.id, + "product_qty": 1, + "price_unit": self.product_order.standard_price, + })], + }) + purchase_order.button_confirm() + # we should have a new section "purchase_order", the total should be updated, + # but the "other_purchase_costs" shouldn't change, as we don't takes into + # account bills from purchase orders, as those are already taken into calculations + # from the purchase orders (in "purchase_order" section) + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'purchase_order', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], + 'to_bill': -self.product_order.standard_price, + 'billed': 0.0, + }, { + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': 0.0, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + }], + 'total': { + 'to_bill': -self.product_order.standard_price, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + }, + }, + ) + purchase_order.action_create_invoice() + purchase_bill = purchase_order.invoice_ids # get the bill from the purchase + purchase_bill.invoice_date = datetime.today() + purchase_bill.action_post() + # now the bill has been posted, its costs should be accounted in the "billed" part + # of the purchase_order section, but should touch in the other_purchase_costs + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'purchase_order', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], + 'to_bill': 0.0, + 'billed': -self.product_order.standard_price, + }, { + 'id': 'other_purchase_costs', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], + 'to_bill': 0.0, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + }], + 'total': { + 'to_bill': 0.0, + 'billed': -(2 * self.product_a.standard_price + + 2 * self.product_b.standard_price + + self.product_order.standard_price), + }, + }, + ) diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py index ef44ac68e13..99832f507ca 100644 --- a/addons/purchase/tests/test_purchase_invoice.py +++ b/addons/purchase/tests/test_purchase_invoice.py @@ -6,12 +6,11 @@ from odoo.tests.common import Form from odoo import Command, fields -@tagged('post_install', '-at_install') -class TestPurchaseToInvoice(AccountTestInvoicingCommon): +class TestPurchaseToInvoiceCommon(AccountTestInvoicingCommon): @classmethod def setUpClass(cls): - super(TestPurchaseToInvoice, cls).setUpClass() + super(TestPurchaseToInvoiceCommon, cls).setUpClass() uom_unit = cls.env.ref('uom.product_uom_unit') uom_hour = cls.env.ref('uom.product_uom_hour') cls.product_order = cls.env['product.product'].create({ @@ -59,6 +58,10 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon): 'taxes_id': False, }) + +@tagged('post_install', '-at_install') +class TestPurchaseToInvoice(TestPurchaseToInvoiceCommon): + def test_vendor_bill_delivered(self): """Test if a order of product invoiced by delivered quantity can be correctly invoiced.""" diff --git a/addons/sale_project/i18n/sale_project.pot b/addons/sale_project/i18n/sale_project.pot index 471fa8da10e..ab9695689fa 100644 --- a/addons/sale_project/i18n/sale_project.pot +++ b/addons/sale_project/i18n/sale_project.pot @@ -333,6 +333,13 @@ msgid "" " 'In sale order's project': Will use the sale order's configured project if defined or fallback to creating a new project based on the selected template." msgstr "" +#. module: sale_project +#: code:addons/sale_project/models/project.py:0 +#: code:addons/sale_project/models/project.py:0 +#, python-format +msgid "Other Revenues" +msgstr "" + #. module: sale_project #: code:addons/sale_project/models/project.py:0 #, python-format diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index bbe5a7349a5..90dc93cc8e0 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -305,6 +305,7 @@ class Project(models.Model): **super()._get_profitability_labels(), 'service_revenues': _lt('Other Services'), 'other_revenues': _lt('Materials'), + 'other_invoice_revenues': _lt('Other Revenues'), } def _get_profitability_sequence_per_invoice_type(self): @@ -312,6 +313,7 @@ class Project(models.Model): **super()._get_profitability_sequence_per_invoice_type(), 'service_revenues': 6, 'other_revenues': 7, + 'other_invoice_revenues': 8, } def _get_service_policy_to_invoice_type(self): @@ -401,16 +403,88 @@ class Project(models.Model): 'total': {'to_invoice': total_to_invoice, 'invoiced': total_invoiced}, } + def _get_revenues_items_from_invoices(self, excluded_move_line_ids=None): + """ + Get all revenues items from invoices, and put them into their own + "other_invoice_revenues" section. + If the final total is 0 for either to_invoice or invoiced (ex: invoice -> credit note), + we don't output a new section + + :param excluded_move_line_ids a list of 'account.move.line' to ignore + when fetching the move lines, for example a list of invoices that were + generated from a sales order + """ + if excluded_move_line_ids is None: + excluded_move_line_ids = [] + query = self.env['account.move.line'].sudo()._search([ + ('move_id.move_type', 'in', ['out_invoice', 'out_refund']), + ('parent_state', 'in', ['draft', 'posted']), + ('price_subtotal', '>', 0), + ('id', 'not in', excluded_move_line_ids), + ]) + query.add_where('account_move_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + # account_move_line__move_id is the alias of the joined table account_move in the query + # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join + # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line + query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line__move_id.move_type') + self._cr.execute(query_string, query_param) + invoices_move_line_read = self._cr.dictfetchall() + if invoices_move_line_read: + amount_invoiced = amount_to_invoice = 0.0 + for moves_read in invoices_move_line_read: + if moves_read['parent_state'] == 'draft': + if moves_read['move_type'] == 'out_invoice': + amount_to_invoice += moves_read['price_subtotal'] + else: # moves_read['move_type'] == 'out_refund' + amount_to_invoice -= moves_read['price_subtotal'] + else: # moves_read['parent_state'] == 'posted' + if moves_read['move_type'] == 'out_invoice': + amount_invoiced += moves_read['price_subtotal'] + else: # moves_read['move_type'] == 'out_refund' + amount_invoiced -= moves_read['price_subtotal'] + # don't display the section if the final values are both 0 (invoice -> credit note) + if amount_invoiced != 0 or amount_to_invoice != 0: + section_id = 'other_invoice_revenues' + invoices_revenues = { + 'id': section_id, + 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], + 'invoiced': amount_invoiced, + 'to_invoice': amount_to_invoice, + } + return { + 'data': [invoices_revenues], + 'total': { + 'invoiced': amount_invoiced, + 'to_invoice': amount_to_invoice, + }, + } + return {'data': [], 'total': {'invoiced': 0.0, 'to_invoice': 0.0}} + def _get_profitability_items(self, with_action=True): profitability_items = super()._get_profitability_items(with_action) + domain = [('order_id', 'in', self.sudo()._get_sale_orders().ids)] revenue_items_from_sol = self._get_revenues_items_from_sol( - [('order_id', 'in', self.sudo()._get_sale_orders().ids)], + domain, with_action, ) revenues = profitability_items['revenues'] revenues['data'] += revenue_items_from_sol['data'] revenues['total']['to_invoice'] += revenue_items_from_sol['total']['to_invoice'] revenues['total']['invoiced'] += revenue_items_from_sol['total']['invoiced'] + + sale_line_read_group = self.env['sale.order.line'].sudo()._read_group( + self._get_profitability_sale_order_items_domain(domain), + ['ids:array_agg(id)'], + ['product_id'], + ) + revenue_items_from_invoices = self._get_revenues_items_from_invoices( + excluded_move_line_ids=self.env['sale.order.line'].browse( + [sol_id for sol_read in sale_line_read_group for sol_id in sol_read['ids']] + ).invoice_lines.ids + ) + revenues['data'] += revenue_items_from_invoices['data'] + revenues['total']['to_invoice'] += revenue_items_from_invoices['total']['to_invoice'] + revenues['total']['invoiced'] += revenue_items_from_invoices['total']['invoiced'] return profitability_items def _get_stat_buttons(self): diff --git a/addons/sale_project/tests/test_project_profitability.py b/addons/sale_project/tests/test_project_profitability.py index 2eec2858baf..c68a6c333be 100644 --- a/addons/sale_project/tests/test_project_profitability.py +++ b/addons/sale_project/tests/test_project_profitability.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime + +from odoo import Command from odoo.tests import tagged from odoo.addons.sale.tests.common import TestSaleCommon @@ -237,3 +240,108 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo self.project._get_profitability_items(False), self.project_profitability_items_empty, ) + + def test_invoices_without_sale_order_are_accounted_in_profitability(self): + """ + An invoice that has an AAL on one of its line should be taken into account + for the profitability of the project. + """ + self.project.allow_billable = True + # create a invoice_1 with the AAL + invoice_1 = self.env['account.move'].create({ + "name": "Invoice_1", + "move_type": "out_invoice", + "state": "draft", + "partner_id": self.partner.id, + "invoice_date": datetime.today(), + "invoice_line_ids": [Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_a.id, + "quantity": 1, + "product_uom_id": self.product_a.uom_id.id, + "price_unit": self.product_a.standard_price, + })], + }) + # the bill_1 is in draft, therefor it should have the cost "to_invoice" same as the -product_price (untaxed) + self.assertDictEqual( + self.project._get_profitability_items(False)['revenues'], + { + 'data': [{ + 'id': 'other_invoice_revenues', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], + 'to_invoice': self.product_a.standard_price, + 'invoiced': 0.0, + }], + 'total': {'to_invoice': self.product_a.standard_price, 'invoiced': 0.0}, + }, + ) + # post invoice_1 + invoice_1.action_post() + # we posted the invoice_1, therefore the revenue "invoiced" should be -product_price, to_invoice should be back to 0 + self.assertDictEqual( + self.project._get_profitability_items(False)['revenues'], + { + 'data': [{ + 'id': 'other_invoice_revenues', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], + 'to_invoice': 0.0, + 'invoiced': self.product_a.standard_price, + }], + 'total': {'to_invoice': 0.0, 'invoiced': self.product_a.standard_price}, + }, + ) + # create another invoice, with 2 lines, 2 diff products, the second line has 2 as quantity + invoice_2 = self.env['account.move'].create({ + "name": "I have 2 lines", + "move_type": "out_invoice", + "state": "draft", + "partner_id": self.partner.id, + "invoice_date": datetime.today(), + "invoice_line_ids": [Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_a.id, + "quantity": 1, + "product_uom_id": self.product_a.uom_id.id, + "price_unit": self.product_a.standard_price, + }), Command.create({ + "analytic_distribution": {self.analytic_account.id: 100}, + "product_id": self.product_b.id, + "quantity": 2, + "product_uom_id": self.product_b.uom_id.id, + "price_unit": self.product_b.standard_price, + })], + }) + # invoice_2 is not posted, therefor its cost should be "to_invoice" = - sum of all product_price * qty for each line + self.assertDictEqual( + self.project._get_profitability_items(False)['revenues'], + { + 'data': [{ + 'id': 'other_invoice_revenues', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], + 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price), + 'invoiced': self.product_a.standard_price, + }], + 'total': { + 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price), + 'invoiced': self.product_a.standard_price, + }, + }, + ) + # post invoice_2 + invoice_2.action_post() + # invoice_2 is posted, therefor its revenue should be counting in "invoiced", with the revenues from invoice_1 + self.assertDictEqual( + self.project._get_profitability_items(False)['revenues'], + { + 'data': [{ + 'id': 'other_invoice_revenues', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], + 'to_invoice': 0.0, + 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price), + }], + 'total': { + 'to_invoice': 0.0, + 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price), + }, + }, + ) diff --git a/addons/sale_timesheet/i18n/sale_timesheet.pot b/addons/sale_timesheet/i18n/sale_timesheet.pot index 8a5ce00be85..3d057b57c90 100644 --- a/addons/sale_timesheet/i18n/sale_timesheet.pot +++ b/addons/sale_timesheet/i18n/sale_timesheet.pot @@ -825,13 +825,13 @@ msgstr "" #. module: sale_timesheet #: model:ir.model.fields.selection,name:sale_timesheet.selection__account_analytic_line__timesheet_invoice_type__other_costs #: model:ir.model.fields.selection,name:sale_timesheet.selection__timesheets_analysis_report__timesheet_invoice_type__other_costs -msgid "Other Costs" +msgid "Materials" msgstr "" #. module: sale_timesheet #: model:ir.model.fields.selection,name:sale_timesheet.selection__account_analytic_line__timesheet_invoice_type__other_revenues #: model:ir.model.fields.selection,name:sale_timesheet.selection__timesheets_analysis_report__timesheet_invoice_type__other_revenues -msgid "Other Revenues" +msgid "Materials" msgstr "" #. module: sale_timesheet diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 8fa2be75f66..7a89aa97eba 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -14,8 +14,8 @@ TIMESHEET_INVOICE_TYPES = [ ('non_billable', 'Non Billable Tasks'), ('timesheet_revenues', 'Timesheet Revenues'), ('service_revenues', 'Service Revenues'), - ('other_revenues', 'Other Revenues'), - ('other_costs', 'Other Costs'), + ('other_revenues', 'Materials'), + ('other_costs', 'Materials'), ] class AccountAnalyticLine(models.Model):