Odoo18-Base/addons/mrp_account/tests/test_analytic_account.py
2025-03-10 10:52:11 +07:00

499 lines
22 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
from odoo.tests import Form
class TestMrpAnalyticAccount(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# The group 'mrp.group_mrp_routings' is required to make the field
# 'workorder_ids' visible in the view of 'mrp.production'. The subviews
# of `workorder_ids` must be present in many tests to create records.
cls.env.user.groups_id += (
cls.env.ref('analytic.group_analytic_accounting')
+ cls.env.ref('mrp.group_mrp_routings')
)
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan',
})
cls.applicability = cls.env['account.analytic.applicability'].create({
'business_domain': 'general',
'analytic_plan_id': cls.analytic_plan.id,
'applicability': 'mandatory',
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'test_analytic_account',
'plan_id': cls.analytic_plan.id,
})
cls.workcenter = cls.env['mrp.workcenter'].create({
'name': 'Workcenter',
'default_capacity': 1,
'time_efficiency': 100,
'costs_hour': 10,
})
cls.product = cls.env['product.product'].create({
'name': 'Product',
'type': 'product',
'standard_price': 233.0,
})
cls.component = cls.env['product.product'].create({
'name': 'Component',
'type': 'product',
'standard_price': 10.0,
})
cls.bom = cls.env['mrp.bom'].create({
'product_id': cls.product.id,
'product_tmpl_id': cls.product.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': cls.component.id, 'product_qty': 1.0}),
],
'operation_ids': [
(0, 0, {'name': 'work work', 'workcenter_id': cls.workcenter.id, 'time_cycle': 15, 'sequence': 1}),
]})
class TestAnalyticAccount(TestMrpAnalyticAccount):
def test_mo_analytic(self):
"""Test the amount on analytic line will change when consumed qty of the
component changed.
"""
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_distribution = {str(self.analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 0)
# increase qty_producing to 5.0
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.save()
self.assertEqual(mo.state, 'progress')
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.amount, -50.0)
# increase qty_producing to 10.0
mo_form = Form(mo)
mo_form.qty_producing = 10.0
mo_form.save()
mo.workorder_ids.button_finish()
self.assertEqual(mo.state, 'to_close')
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.amount, -100.0)
# mark as done
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.amount, -100.0)
def test_mo_analytic_backorder(self):
"""Test the analytic lines are correctly posted when backorder.
"""
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_distribution = {str(self.analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 0)
# increase qty_producing to 5.0
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.save()
self.assertEqual(mo.state, 'progress')
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.amount, -50.0)
backorder_wizard_dict = mo.button_mark_done()
Form(self.env[(backorder_wizard_dict.get('res_model'))].with_context(backorder_wizard_dict['context'])).save().action_backorder()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.amount, -50.0)
def test_workcenter_different_analytic_account(self):
"""Test when workcenter and MO are using the same analytic account, no
duplicated lines will be post.
"""
# Required for `workorder_ids` to be visible in the view
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
# set wc analytic account to be different from the one on the bom
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test'})
wc_analytic_account = self.env['account.analytic.account'].create({'name': 'wc_analytic_account', 'plan_id': analytic_plan.id})
self.workcenter.analytic_distribution = {str(wc_analytic_account.id): 100.0}
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_distribution = {str(self.analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_ids), 0)
# change duration to 60
mo_form = Form(mo)
with mo_form.workorder_ids.edit(0) as line_edit:
line_edit.duration = 60.0
mo_form.save()
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -10.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids[self.analytic_plan._column_name()], self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids.amount, -10.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids[analytic_plan._column_name()], wc_analytic_account)
# change duration to 120
with mo_form.workorder_ids.edit(0) as line_edit:
line_edit.duration = 120.0
mo_form.save()
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids[self.analytic_plan._column_name()], self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids[analytic_plan._column_name()], wc_analytic_account)
# mark as done
mo_form.qty_producing = 10.0
mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids[self.analytic_plan._column_name()], self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_ids[analytic_plan._column_name()], wc_analytic_account)
def test_changing_mo_analytic_account(self):
""" Check if the MO account analytic lines are correctly updated
after the change of the MO account analytic.
"""
# Required for `workorder_ids` to be visible in the view
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 1
mo_form.analytic_distribution = {str(self.analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 0)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_ids), 0)
# Change duration to 60
mo_form = Form(mo)
with mo_form.workorder_ids.edit(0) as line_edit:
line_edit.duration = 60.0
mo_form.save()
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids[self.analytic_plan._column_name()], self.analytic_account)
# Mark as done
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 1)
# Create a new analytic account
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test'})
new_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account_2', 'plan_id': analytic_plan.id})
# Change the MO analytic account
mo.analytic_distribution = {str(new_analytic_account.id): 100.0}
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids[analytic_plan._column_name()], new_analytic_account)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids[analytic_plan._column_name()], new_analytic_account)
#Get the MO analytic account lines
mo_analytic_account_raw_lines = mo.move_raw_ids.analytic_account_line_ids
mo_analytic_account_wc_lines = mo.move_raw_ids.analytic_account_line_ids
mo.analytic_distribution = {}
# Check that the MO analytic account lines are deleted
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 0)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_ids), 0)
self.assertFalse(mo_analytic_account_raw_lines.exists())
self.assertFalse(mo_analytic_account_wc_lines.exists())
# Check that the AA lines are recreated correctly if we delete the AA, save the MO, and assign a new one
mo.analytic_distribution = {str(self.analytic_account.id): 100.0}
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 1)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_ids), 1)
def test_add_remove_wo_analytic_no_company(self):
"""Test the addition and removal of work orders to a MO linked to
an analytic account that has no company associated
"""
# Create an analytic account and remove the company
analytic_account_no_company = self.env['account.analytic.account'].create({
'name': 'test_analytic_account_no_company',
'plan_id': self.analytic_plan.id,
}).with_context(analytic_plan_id=self.analytic_plan.id)
analytic_account_no_company.company_id = False
# Create a mo linked to an analytic account with no associated company
mo_no_company = self.env['mrp.production'].create({
'product_id': self.product.id,
'analytic_distribution': {str(analytic_account_no_company.id): 100.0},
'product_uom_id': self.bom.product_uom_id.id,
})
mo_no_c_form = Form(mo_no_company)
wo = self.env['mrp.workorder'].create({
'name': 'Work_order',
'workcenter_id': self.workcenter.id,
'product_uom_id': self.bom.product_uom_id.id,
'production_id': mo_no_c_form.id,
'duration': 60,
})
mo_no_c_form.save()
self.assertTrue(mo_no_company.workorder_ids)
self.assertEqual(wo.production_id.analytic_account_ids, analytic_account_no_company)
self.assertEqual(len(analytic_account_no_company.line_ids), 1)
mo_no_company.workorder_ids.unlink()
self.assertEqual(len(analytic_account_no_company.line_ids), 0)
def test_update_components_qty_to_0(self):
""" Test that the analytic lines are deleted when the quantity of the component is set to 0.
Create a Mo with analytic account and a component, confirm and validate it,
set the quantity of the component to 0, the analytic lines should be deleted.
"""
component = self.env['product.product'].create({
'name': 'Component',
'type': 'product',
'standard_price': 100,
})
product = self.env['product.product'].create({
'name': 'Product',
'type': 'product',
})
bom = self.env['mrp.bom'].create({
'product_tmpl_id': product.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': product.uom_id.id,
'type': 'normal',
'bom_line_ids': [(0, 0, {
'product_id': component.id,
'product_qty': 1,
'product_uom_id': component.uom_id.id,
})],
})
analytic_account = self.env['account.analytic.account'].create({
'name': "Test Account",
'plan_id': self.analytic_plan.id,
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product
mo_form.bom_id = bom
mo_form.product_qty = 1.0
mo_form.analytic_distribution = {str(analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
self.assertEqual(mo.state, 'to_close')
mo.button_mark_done()
self.assertEqual(mo.state, 'done')
self.assertEqual(analytic_account.debit, 100)
mo.move_raw_ids[0].quantity = 0
self.assertEqual(analytic_account.debit, 0)
self.assertFalse(analytic_account.line_ids)
def test_cross_analytics(self):
""" Test analytic distributions (AD) with cross analytics on an MO."""
ap1 = self.env['account.analytic.plan'].create({
'name': 'Plan 1',
})
ap2 = self.env['account.analytic.plan'].create({
'name': 'Plan 2',
})
ac1A = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 1 account A',
'plan_id': ap1.id,
})
ac1B = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 1 account B',
'plan_id': ap1.id,
})
ac1C = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 1 account C',
'plan_id': ap1.id,
})
ac2A = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 2 account A',
'plan_id': ap2.id,
})
ac2B = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 2 account B',
'plan_id': ap2.id,
})
ac2C = self.env['account.analytic.account'].create({
'name': 'test_cross_analytics plan 2 account C',
'plan_id': ap2.id,
})
analytic_distribution = {
f"{ac1A.id},{ac2A.id}": 33.33,
f"{ac1B.id},{ac2B.id}": 33.33,
f"{ac1C.id},{ac2C.id}": 33.33,
}
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_distribution = analytic_distribution
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_ids), 0)
# increase qty_producing to 5.0
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.save()
self.assertEqual(mo.state, 'progress')
aals = mo.move_raw_ids.analytic_account_line_ids
self.assertEqual(len(aals), 3)
self.assertEqual(sum(aals.mapped('amount')), -49.99)
# increase qty_producing to 10.0
mo_form = Form(mo)
mo_form.qty_producing = 10.0
mo_form.save()
mo.workorder_ids.button_finish()
aals = mo.move_raw_ids.analytic_account_line_ids
self.assertEqual(mo.state, 'to_close')
self.assertEqual(len(aals), 3)
self.assertEqual(sum(aals.mapped('amount')), -99.99)
# mark as done
mo.button_mark_done()
aals = mo.move_raw_ids.analytic_account_line_ids
self.assertEqual(mo.state, 'done')
self.assertEqual(len(aals), 3)
self.assertEqual(sum(aals.mapped('amount')), -99.99)
# assert the right accounts are on the right analytic lines
ap1_column = ap1._column_name()
ap2_column = ap2._column_name()
line_with_A_accounts = aals.filtered_domain([(ap1_column, '=', ac1A.id)])
line_with_B_accounts = aals.filtered_domain([(ap1_column, '=', ac1B.id)])
line_with_C_accounts = aals.filtered_domain([(ap1_column, '=', ac1C.id)])
self.assertEqual(line_with_A_accounts[ap2_column], ac2A)
self.assertEqual(line_with_B_accounts[ap2_column], ac2B)
self.assertEqual(line_with_C_accounts[ap2_column], ac2C)
# ensure the right amounts are on the right lines
# the distribution calculation corrects for rounding error accumulation to ensure
# the total sum is correct, so individual lines can have minor deviations
self.assertAlmostEqual(line_with_A_accounts.amount, -100/3, delta=0.02)
self.assertAlmostEqual(line_with_B_accounts.amount, -100/3, delta=0.02)
self.assertAlmostEqual(line_with_B_accounts.amount, -100/3, delta=0.02)
def test_mo_qty_analytics(self):
"""
This test tests multiple behaviours and edge cases. First off, when
an analytic distribution with an MO, the analytic entries should only
be generated when the MO's components are consumed/reserved (i.e.
picked). Second, when changing the produced quantity in a confirmed MO,
it should appropriately adjust the amount of picked components. Third,
analytic entries should at all times reflect the current situation, and
thus must be regenerated every time there's a change in components'
picked status.
"""
# refill components
location = self.env.ref('stock.stock_location_stock')
self.env['stock.quant']._update_available_quantity(self.component, location, 10)
# create a mo
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_distribution = {str(self.analytic_account.id): 100.0}
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.state, 'confirmed')
self.assertEqual(self.analytic_account.balance, 0.0)
# increase qty_producing to 5.0
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.save()
self.assertEqual(mo.state, 'progress')
self.assertEqual(self.analytic_account.balance, -50.0)
# decrease qty_producing to 0.0
mo_form = Form(mo)
mo_form.qty_producing = 0.0
mo_form.save()
self.assertEqual(mo.state, 'progress')
self.assertEqual(self.analytic_account.balance, 0.0)
def test_mandatory_analytic_plan_production(self):
"""
Tests that the distribution validation is correctly evaluated
The Production (Manufacturing Order) creation should be constrained by an analytic applicability rule if any relevant one.
"""
manufacturing_order = self.env['mrp.production'].create({
'product_id': self.product.id,
})
self.assertTrue(manufacturing_order)
self.applicability.business_domain = 'manufacturing_order'
with self.assertRaises(ValidationError):
self.env['mrp.production'].create({
'product_id': self.product.id,
})
mo_analytic = self.env['mrp.production'].create({
'product_id': self.product.id,
'analytic_distribution': {str(self.analytic_account.id): 100.0},
})
self.assertTrue(mo_analytic)
def test_mandatory_analytic_plan_bom(self):
"""
Tests that the distribution validation is correctly evaluated
The BOM creation should not be constrained by any analytic applicability rule.
"""
bom = self.env['mrp.bom'].create({
'product_tmpl_id': self.product.product_tmpl_id.id,
})
self.assertTrue(bom)
self.applicability.business_domain = 'manufacturing_order'
bom_2 = self.env['mrp.bom'].create({
'product_tmpl_id': self.product.product_tmpl_id.id,
})
self.assertTrue(bom_2)
def test_mandatory_analytic_plan_workcenter(self):
"""
Tests that the distribution validation is correctly evaluated
The Workcenter creation should not be constrained by any analytic applicability rule.
"""
workcenter = self.env['mrp.workcenter'].create({
'name': "Great Workcenter",
'analytic_distribution': False,
})
self.assertTrue(workcenter)
self.applicability.business_domain = 'manufacturing_order'
workcenter_2 = self.env['mrp.workcenter'].create({
'name': "Great Workcenter",
'analytic_distribution': False,
})
self.assertTrue(workcenter_2)