355 lines
17 KiB
Python
355 lines
17 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import Command
|
||
|
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||
|
from odoo.addons.stock_account.tests.test_account_move import TestAccountMoveStockCommon
|
||
|
from odoo.tests import Form, tagged
|
||
|
|
||
|
|
||
|
class TestMrpAccount(TestMrpCommon):
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
super(TestMrpAccount, cls).setUpClass()
|
||
|
cls.source_location_id = cls.stock_location_14.id
|
||
|
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||
|
# setting up alternative workcenters
|
||
|
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
|
||
|
'name': 'Nuclear Workcenter bis',
|
||
|
'default_capacity': 3,
|
||
|
'time_start': 9,
|
||
|
'time_stop': 5,
|
||
|
'time_efficiency': 80,
|
||
|
})
|
||
|
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
|
||
|
'name': 'Nuclear Workcenter ter',
|
||
|
'default_capacity': 1,
|
||
|
'time_start': 10,
|
||
|
'time_stop': 5,
|
||
|
'time_efficiency': 85,
|
||
|
})
|
||
|
cls.product_4.uom_id = cls.uom_unit
|
||
|
cls.planning_bom = cls.env['mrp.bom'].create({
|
||
|
'product_id': cls.product_4.id,
|
||
|
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||
|
'product_uom_id': cls.uom_unit.id,
|
||
|
'product_qty': 4.0,
|
||
|
'consumption': 'flexible',
|
||
|
'operation_ids': [
|
||
|
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||
|
],
|
||
|
'type': 'normal',
|
||
|
'bom_line_ids': [
|
||
|
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||
|
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||
|
]})
|
||
|
cls.dining_table = cls.env['product.product'].create({
|
||
|
'name': 'Table (MTO)',
|
||
|
'type': 'product',
|
||
|
'tracking': 'serial',
|
||
|
})
|
||
|
cls.product_table_sheet = cls.env['product.product'].create({
|
||
|
'name': 'Table Top',
|
||
|
'type': 'product',
|
||
|
'tracking': 'serial',
|
||
|
})
|
||
|
cls.product_table_leg = cls.env['product.product'].create({
|
||
|
'name': 'Table Leg',
|
||
|
'type': 'product',
|
||
|
'tracking': 'lot',
|
||
|
})
|
||
|
cls.product_bolt = cls.env['product.product'].create({
|
||
|
'name': 'Bolt',
|
||
|
'type': 'product',
|
||
|
})
|
||
|
cls.product_screw = cls.env['product.product'].create({
|
||
|
'name': 'Screw',
|
||
|
'type': 'product',
|
||
|
})
|
||
|
|
||
|
cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
|
||
|
'name': 'Assembly Line 1',
|
||
|
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||
|
})
|
||
|
cls.mrp_bom_desk = cls.env['mrp.bom'].create({
|
||
|
'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
|
||
|
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||
|
'sequence': 3,
|
||
|
'consumption': 'flexible',
|
||
|
'operation_ids': [
|
||
|
(0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
|
||
|
],
|
||
|
})
|
||
|
cls.mrp_bom_desk.write({
|
||
|
'bom_line_ids': [
|
||
|
(0, 0, {
|
||
|
'product_id': cls.product_table_sheet.id,
|
||
|
'product_qty': 1,
|
||
|
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||
|
'sequence': 1,
|
||
|
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||
|
(0, 0, {
|
||
|
'product_id': cls.product_table_leg.id,
|
||
|
'product_qty': 4,
|
||
|
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||
|
'sequence': 2,
|
||
|
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||
|
(0, 0, {
|
||
|
'product_id': cls.product_bolt.id,
|
||
|
'product_qty': 4,
|
||
|
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||
|
'sequence': 3,
|
||
|
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||
|
(0, 0, {
|
||
|
'product_id': cls.product_screw.id,
|
||
|
'product_qty': 10,
|
||
|
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||
|
'sequence': 4,
|
||
|
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||
|
]
|
||
|
})
|
||
|
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
|
||
|
'name': 'Drill Station 1',
|
||
|
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||
|
})
|
||
|
cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
|
||
|
'name': 'Assembly Line 1',
|
||
|
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||
|
})
|
||
|
cls.categ_standard = cls.env['product.category'].create({
|
||
|
'name': 'STANDARD',
|
||
|
'property_cost_method': 'standard'
|
||
|
})
|
||
|
cls.categ_real = cls.env['product.category'].create({
|
||
|
'name': 'REAL',
|
||
|
'property_cost_method': 'fifo'
|
||
|
})
|
||
|
cls.categ_average = cls.env['product.category'].create({
|
||
|
'name': 'AVERAGE',
|
||
|
'property_cost_method': 'average'
|
||
|
})
|
||
|
cls.dining_table.categ_id = cls.categ_real.id
|
||
|
cls.product_table_sheet.categ_id = cls.categ_real.id
|
||
|
cls.product_table_leg.categ_id = cls.categ_average.id
|
||
|
cls.product_bolt.categ_id = cls.categ_standard.id
|
||
|
cls.product_screw.categ_id = cls.categ_standard.id
|
||
|
cls.env['stock.move'].search([('product_id', 'in', [cls.product_bolt.id, cls.product_screw.id])])._do_unreserve()
|
||
|
(cls.product_bolt + cls.product_screw).write({'type': 'product'})
|
||
|
cls.dining_table.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_screw.standard_price = 0.1
|
||
|
self.product_table_leg.tracking = 'none'
|
||
|
self.product_table_sheet.tracking = 'none'
|
||
|
# Inventory Product Table
|
||
|
quants = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||
|
'product_id': self.product_table_sheet.id, # tracking serial
|
||
|
'inventory_quantity': 20,
|
||
|
'location_id': self.source_location_id,
|
||
|
})
|
||
|
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||
|
'product_id': self.product_table_leg.id, # tracking lot
|
||
|
'inventory_quantity': 20,
|
||
|
'location_id': self.source_location_id,
|
||
|
})
|
||
|
quants |= self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||
|
'product_id': self.product_bolt.id,
|
||
|
'inventory_quantity': 20,
|
||
|
'location_id': self.source_location_id,
|
||
|
})
|
||
|
quants |= self.env['stock.quant'].create({
|
||
|
'product_id': self.product_screw.id,
|
||
|
'inventory_quantity': 200000,
|
||
|
'location_id': self.source_location_id,
|
||
|
})
|
||
|
quants.action_apply_inventory()
|
||
|
|
||
|
bom = self.mrp_bom_desk.copy()
|
||
|
bom.bom_line_ids.manual_consumption = False
|
||
|
bom.operation_ids = False
|
||
|
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 = 1
|
||
|
production_table = production_table_form.save()
|
||
|
|
||
|
production_table.extra_cost = 20
|
||
|
production_table.action_confirm()
|
||
|
|
||
|
mo_form = Form(production_table)
|
||
|
mo_form.qty_producing = 1
|
||
|
production_table = mo_form.save()
|
||
|
production_table._post_inventory()
|
||
|
move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").stock_valuation_layer_ids.value
|
||
|
|
||
|
# 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + 1*20 (extra cost)
|
||
|
self.assertEqual(move_value, 141, 'Thing should have the correct price')
|
||
|
|
||
|
|
||
|
@tagged("post_install", "-at_install")
|
||
|
class TestMrpAccountMove(TestAccountMoveStockCommon):
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
super().setUpClass()
|
||
|
cls.product_B = cls.env["product.product"].create(
|
||
|
{
|
||
|
"name": "Product B",
|
||
|
"type": "product",
|
||
|
"default_code": "prda",
|
||
|
"categ_id": cls.auto_categ.id,
|
||
|
"taxes_id": [(5, 0, 0)],
|
||
|
"supplier_taxes_id": [(5, 0, 0)],
|
||
|
"lst_price": 100.0,
|
||
|
"standard_price": 10.0,
|
||
|
"property_account_income_id": cls.company_data["default_account_revenue"].id,
|
||
|
"property_account_expense_id": cls.company_data["default_account_expense"].id,
|
||
|
}
|
||
|
)
|
||
|
cls.bom = cls.env['mrp.bom'].create({
|
||
|
'product_id': cls.product_A.id,
|
||
|
'product_tmpl_id': cls.product_A.product_tmpl_id.id,
|
||
|
'product_qty': 1.0,
|
||
|
'bom_line_ids': [
|
||
|
(0, 0, {'product_id': cls.product_B.id, 'product_qty': 1}),
|
||
|
]})
|
||
|
|
||
|
def test_unbuild_account_00(self):
|
||
|
"""Test when after unbuild, the journal entries are the reversal of the
|
||
|
journal entries created when produce the product.
|
||
|
"""
|
||
|
# build
|
||
|
production_form = Form(self.env['mrp.production'])
|
||
|
production_form.product_id = self.product_A
|
||
|
production_form.bom_id = self.bom
|
||
|
production_form.product_qty = 1
|
||
|
production = production_form.save()
|
||
|
production.action_confirm()
|
||
|
mo_form = Form(production)
|
||
|
mo_form.qty_producing = 1
|
||
|
production = mo_form.save()
|
||
|
production._post_inventory()
|
||
|
production.button_mark_done()
|
||
|
|
||
|
# finished product move
|
||
|
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('credit', '=', 0)])
|
||
|
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productA_debit_line.account_id, self.stock_valuation_account)
|
||
|
self.assertEqual(productA_credit_line.account_id, self.stock_input_account)
|
||
|
# component move
|
||
|
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('credit', '=', 0)])
|
||
|
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productB_debit_line.account_id, self.stock_output_account)
|
||
|
self.assertEqual(productB_credit_line.account_id, self.stock_valuation_account)
|
||
|
|
||
|
# unbuild
|
||
|
res_dict = production.button_unbuild()
|
||
|
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||
|
wizard.action_validate()
|
||
|
|
||
|
# finished product move
|
||
|
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('credit', '=', 0)])
|
||
|
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productA_debit_line.account_id, self.stock_input_account)
|
||
|
self.assertEqual(productA_credit_line.account_id, self.stock_valuation_account)
|
||
|
# component move
|
||
|
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('credit', '=', 0)])
|
||
|
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productB_debit_line.account_id, self.stock_valuation_account)
|
||
|
self.assertEqual(productB_credit_line.account_id, self.stock_output_account)
|
||
|
|
||
|
def test_unbuild_account_01(self):
|
||
|
"""Test when production location has its valuation accounts. After unbuild,
|
||
|
the journal entries are the reversal of the journal entries created when
|
||
|
produce the product.
|
||
|
"""
|
||
|
# set accounts for production location
|
||
|
production_location = self.product_A.property_stock_production
|
||
|
wip_incoming_account = self.env['account.account'].create({
|
||
|
'name': 'wip incoming',
|
||
|
'code': '000001',
|
||
|
'account_type': 'asset_current',
|
||
|
})
|
||
|
wip_outgoing_account = self.env['account.account'].create({
|
||
|
'name': 'wip outgoing',
|
||
|
'code': '000002',
|
||
|
'account_type': 'asset_current',
|
||
|
})
|
||
|
production_location.write({
|
||
|
'valuation_in_account_id': wip_incoming_account.id,
|
||
|
'valuation_out_account_id': wip_outgoing_account.id,
|
||
|
})
|
||
|
|
||
|
# build
|
||
|
production_form = Form(self.env['mrp.production'])
|
||
|
production_form.product_id = self.product_A
|
||
|
production_form.bom_id = self.bom
|
||
|
production_form.product_qty = 1
|
||
|
production = production_form.save()
|
||
|
production.action_confirm()
|
||
|
mo_form = Form(production)
|
||
|
mo_form.qty_producing = 1
|
||
|
production = mo_form.save()
|
||
|
production._post_inventory()
|
||
|
production.button_mark_done()
|
||
|
|
||
|
# finished product move
|
||
|
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('credit', '=', 0)])
|
||
|
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product A'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productA_debit_line.account_id, self.stock_valuation_account)
|
||
|
self.assertEqual(productA_credit_line.account_id, wip_outgoing_account)
|
||
|
# component move
|
||
|
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('credit', '=', 0)])
|
||
|
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'MO%Product B'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productB_debit_line.account_id, wip_incoming_account)
|
||
|
self.assertEqual(productB_credit_line.account_id, self.stock_valuation_account)
|
||
|
|
||
|
# unbuild
|
||
|
res_dict = production.button_unbuild()
|
||
|
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||
|
wizard.action_validate()
|
||
|
|
||
|
productA_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('credit', '=', 0)])
|
||
|
productA_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product A'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productA_debit_line.account_id, wip_outgoing_account)
|
||
|
self.assertEqual(productA_credit_line.account_id, self.stock_valuation_account)
|
||
|
# component move
|
||
|
productB_debit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('credit', '=', 0)])
|
||
|
productB_credit_line = self.env['account.move.line'].search([('ref', 'ilike', 'UB%Product B'), ('debit', '=', 0)])
|
||
|
self.assertEqual(productB_debit_line.account_id, self.stock_valuation_account)
|
||
|
self.assertEqual(productB_credit_line.account_id, wip_incoming_account)
|
||
|
|
||
|
|
||
|
@tagged("post_install", "-at_install")
|
||
|
class TestMrpAnalyticAccount(TestMrpCommon):
|
||
|
def test_mo_analytic_account(self):
|
||
|
"""
|
||
|
Check that an mrp user without accounting rights is able to mark as done
|
||
|
an MO linked to an analytic account.
|
||
|
"""
|
||
|
if not (self.env.ref('mrp_account_enterprise.account_assembly_hours', raise_if_not_found=False) and self.env.ref('hr_timesheet.group_hr_timesheet_user', raise_if_not_found=False)):
|
||
|
self.skipTest("This test requires the installation of hr_timesheet")
|
||
|
mrp_user = self.user_mrp_user
|
||
|
mrp_user.groups_id = [Command.set([self.ref('mrp.group_mrp_user'), self.ref('hr_timesheet.group_hr_timesheet_user')])]
|
||
|
analytic_account = self.env.ref('mrp_account_enterprise.account_assembly_hours')
|
||
|
bom = self.bom_4
|
||
|
product = bom.product_id
|
||
|
bom.bom_line_ids.product_id.standard_price = 1.0
|
||
|
bom.analytic_account_id = analytic_account
|
||
|
mo = self.env['mrp.production'].with_user(mrp_user.id).create({
|
||
|
'product_id': product.id,
|
||
|
'product_uom_id': product.uom_id.id,
|
||
|
'bom_id': bom.id
|
||
|
})
|
||
|
mo.with_user(mrp_user.id).action_confirm()
|
||
|
action = mo.with_user(mrp_user.id).button_mark_done()
|
||
|
wizard = Form(self.env[action['res_model']].with_context(action['context']).with_user(mrp_user.id)).save()
|
||
|
wizard.with_user(mrp_user.id).process()
|
||
|
self.assertTrue(mo.move_raw_ids.move_line_ids)
|
||
|
self.assertEqual(mo.move_raw_ids.quantity_done, bom.bom_line_ids.product_qty)
|
||
|
self.assertEqual(mo.move_raw_ids.state, 'done')
|