From 1019ceee6eb920f70db8ce49efd2dbe0197a515b Mon Sep 17 00:00:00 2001 From: Arnaud Baes Date: Wed, 27 Mar 2019 16:48:13 +0000 Subject: [PATCH] [MOV] mrp_account The module is moved from the enterprise repository as it was *without* the bom cost report. task 1831382 --- addons/mrp_account/__init__.py | 3 + addons/mrp_account/__manifest__.py | 19 ++++ addons/mrp_account/models/__init__.py | 4 + addons/mrp_account/models/mrp_production.py | 68 +++++++++++++ addons/mrp_account/models/mrp_workcenter.py | 11 ++ addons/mrp_account/tests/__init__.py | 3 + addons/mrp_account/tests/test_mrp_account.py | 101 +++++++++++++++++++ 7 files changed, 209 insertions(+) create mode 100644 addons/mrp_account/__init__.py create mode 100644 addons/mrp_account/__manifest__.py create mode 100644 addons/mrp_account/models/__init__.py create mode 100644 addons/mrp_account/models/mrp_production.py create mode 100644 addons/mrp_account/models/mrp_workcenter.py create mode 100644 addons/mrp_account/tests/__init__.py create mode 100644 addons/mrp_account/tests/test_mrp_account.py diff --git a/addons/mrp_account/__init__.py b/addons/mrp_account/__init__.py new file mode 100644 index 00000000000..be9f4fab00a --- /dev/null +++ b/addons/mrp_account/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import models diff --git a/addons/mrp_account/__manifest__.py b/addons/mrp_account/__manifest__.py new file mode 100644 index 00000000000..de72516401a --- /dev/null +++ b/addons/mrp_account/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Accounting - MRP', + 'version': '1.0', + 'category': 'Manufacturing', + 'summary': 'Analytic accounting in Manufacturing', + 'description': """ +Analytic Accounting in MRP +========================== + +* Cost structure report +""", + 'website': 'https://www.odoo.com/page/manufacturing', + 'depends': ['mrp', 'stock_account'], + 'installable': True, + 'auto_install': True, +} diff --git a/addons/mrp_account/models/__init__.py b/addons/mrp_account/models/__init__.py new file mode 100644 index 00000000000..872a43b7fff --- /dev/null +++ b/addons/mrp_account/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import mrp_workcenter +from . import mrp_production diff --git a/addons/mrp_account/models/mrp_production.py b/addons/mrp_account/models/mrp_production.py new file mode 100644 index 00000000000..01d5c496107 --- /dev/null +++ b/addons/mrp_account/models/mrp_production.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.tools import float_is_zero + + +class MrpProductionWorkcenterLineTime(models.Model): + _inherit = 'mrp.workcenter.productivity' + + cost_already_recorded = fields.Boolean('Cost Recorded', help="Technical field automatically checked when a ongoing production posts journal entries for its costs. This way, we can record one production's cost multiple times and only consider new entries in the work centers time lines.") + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + def _cal_price(self, consumed_moves): + """Set a price unit on the finished move according to `consumed_moves`. + """ + super(MrpProduction, self)._cal_price(consumed_moves) + work_center_cost = 0 + finished_move = self.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel') and x.quantity_done > 0) + if finished_move: + finished_move.ensure_one() + for work_order in self.workorder_ids: + time_lines = work_order.time_ids.filtered(lambda x: x.date_end and not x.cost_already_recorded) + duration = sum(time_lines.mapped('duration')) + time_lines.write({'cost_already_recorded': True}) + work_center_cost += (duration / 60.0) * work_order.workcenter_id.costs_hour + if finished_move.product_id.cost_method in ('fifo', 'average'): + qty_done = finished_move.product_uom._compute_quantity(finished_move.quantity_done, finished_move.product_id.uom_id) + finished_move.price_unit = (sum([-m.value for m in consumed_moves]) + work_center_cost) / qty_done + finished_move.value = sum([-m.value for m in consumed_moves]) + work_center_cost + return True + + def _prepare_wc_analytic_line(self, wc_line): + wc = wc_line.workcenter_id + hours = wc_line.duration / 60.0 + value = hours * wc.costs_hour + account = wc.costs_hour_account_id.id + return { + 'name': wc_line.name + ' (H)', + 'amount': -value, + 'account_id': account, + 'ref': wc.code, + 'unit_amount': hours, + } + + def _costs_generate(self): + """ Calculates total costs at the end of the production. + """ + self.ensure_one() + AccountAnalyticLine = self.env['account.analytic.line'].sudo() + for wc_line in self.workorder_ids.filtered('workcenter_id.costs_hour_account_id'): + vals = self._prepare_wc_analytic_line(wc_line) + precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding + if not float_is_zero(vals.get('amount', 0.0), precision_rounding=precision_rounding): + # we use SUPERUSER_ID as we do not guarantee an mrp user + # has access to account analytic lines but still should be + # able to produce orders + AccountAnalyticLine.create(vals) + + @api.multi + def button_mark_done(self): + self.ensure_one() + res = super(MrpProduction, self).button_mark_done() + self._costs_generate() + return res diff --git a/addons/mrp_account/models/mrp_workcenter.py b/addons/mrp_account/models/mrp_workcenter.py new file mode 100644 index 00000000000..20c9fcd08d6 --- /dev/null +++ b/addons/mrp_account/models/mrp_workcenter.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class MrpWorkcenter(models.Model): + _inherit = 'mrp.workcenter' + + costs_hour_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', + help="Fill this only if you want automatic analytic accounting entries on production orders.") diff --git a/addons/mrp_account/tests/__init__.py b/addons/mrp_account/tests/__init__.py new file mode 100644 index 00000000000..5b2f06a99ce --- /dev/null +++ b/addons/mrp_account/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_mrp_account diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py new file mode 100644 index 00000000000..9f3e75e43c2 --- /dev/null +++ b/addons/mrp_account/tests/test_mrp_account.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form + +from odoo.tests import common + + +class TestMrpAccount(common.TransactionCase): + + def setUp(self): + super(TestMrpAccount, self).setUp() + self.categ_standard = self.env['product.category'].create({'name': 'STANDARD', + 'property_cost_method': 'standard',},) + self.categ_real = self.env['product.category'].create({'name': 'REAL', + 'property_cost_method': 'fifo',}) + self.categ_average = self.env['product.category'].create({'name': 'AVERAGE', + 'property_cost_method': 'average'}) + self.dining_table = self.env.ref("mrp.product_product_computer_desk") + self.dining_table.categ_id = self.categ_real.id + self.product_table_sheet = self.env.ref('mrp.product_product_computer_desk_head') + self.product_table_sheet.categ_id = self.categ_real.id + self.product_table_leg = self.env.ref('mrp.product_product_computer_desk_leg') + self.product_table_leg.categ_id = self.categ_average.id + self.product_bolt = self.env.ref('mrp.product_product_computer_desk_bolt') + self.product_bolt.categ_id = self.categ_standard.id + self.source_location_id = self.ref('stock.stock_location_14') + self.product_screw = self.env.ref('mrp.product_product_computer_desk_screw') + self.product_screw.categ_id = self.categ_standard.id + self.env['stock.move'].search([('product_id', 'in', [self.product_bolt.id, self.product_screw.id])])._do_unreserve() + (self.product_bolt + self.product_screw).write({'type': 'product'}) + self.product_desk = self.env.ref('mrp.product_product_computer_desk') + self.product_desk.tracking = 'none' + + def test_00_production_order_with_accounting(self): + self.product_table_sheet.standard_price = 20.0 + self.product_table_leg.standard_price = 15.0 + self.product_bolt.standard_price = 10.0 + self.product_table_leg.tracking = 'none' + self.product_table_sheet.tracking = 'none' + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.product_table_sheet.id, # tracking serial + 'product_uom_id': self.product_table_sheet.uom_id.id, + 'product_qty': 20, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': self.product_table_leg.id, # tracking lot + 'product_uom_id': self.product_table_leg.uom_id.id, + 'product_qty': 20, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': self.product_bolt.id, + 'product_uom_id': self.product_bolt.uom_id.id, + 'product_qty': 20, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': self.product_screw.id, + 'product_uom_id': self.product_screw.uom_id.id, + 'product_qty': 200000, + 'location_id': self.source_location_id + }), + ] + }) + inventory.action_validate + bom = self.env.ref('mrp.mrp_bom_desk').copy() + bom.routing_id = False # TODO: extend the test later with the necessary operations + production_table_form = Form(self.env['mrp.production']) + production_table_form.product_id = self.dining_table + production_table_form.bom_id = bom + production_table_form.product_qty = 5.0 + production_table = production_table_form.save() + + production_table.action_confirm() + + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': production_table.id, + 'active_ids': [production_table.id], + })) + produce_form.qty_producing = 1.0 + produce_wizard = produce_form.save() + produce_wizard.do_produce() + production_table.post_inventory() + move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").value + + # 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + self.assertEqual(move_value, 121, 'Thing should have the correct price') + +# produce_wizard = self.env['mrp.product.produce'].with_context({ +# 'active_id': production_table.id, +# 'active_ids': [production_table.id], +# }).create({ +# 'product_qty': 2.0, +# }) +# produce_wizard.do_produce() +# production_table.post_inventory() +# move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done" and x.product_qty == 2.0).value +# # 2 * Real price of the head (quant: 20) + standard price screw (product: 20*10) + standard price bolt (product: 8*10) +# self.assertEqual(move_value, 280, 'Thing should have the correct price')