499 lines
22 KiB
Python
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)
|