[MOV] mrp_account
The module is moved from the enterprise repository as it was *without* the bom cost report. task 1831382
This commit is contained in:
committed by
Simon Lejeune
parent
7146a4b23b
commit
1019ceee6e
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import test_mrp_account
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user