[IMP] mrp: New analytic for MRP

The (legacy) analytic account field on MOs, BOMs and workcenters has
been replaced by the new "analytic distribution" field.

The analytic line items are now created based on this distribution for
the raw materials, workcenter costs and employee costs.

A new analytic applicability (=domain) has been added for manufacturing
orders as well.

Community PR: https://github.com/odoo/odoo/pull/116477
Enterprise PR: https://github.com/odoo/enterprise/pull/38690

closes odoo/odoo#116477

Task: 3196563
Related: odoo/upgrade#4704
Related: odoo/enterprise#38690
Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
Pieter Claeys (clpi)
2023-06-23 17:59:57 +02:00
parent f120977751
commit f13d537a42
17 changed files with 282 additions and 225 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ class AnalyticMixin(models.AbstractModel):
FROM information_schema.tables
WHERE table_name=%s '''
self.env.cr.execute(query, [self._table])
if self.env.cr.dictfetchone():
if self.env.cr.dictfetchone() and self._fields['analytic_distribution'].store:
query = f"""
CREATE INDEX IF NOT EXISTS {self._table}_analytic_distribution_gin_index
ON {self._table} USING gin(analytic_distribution);
+14 -3
View File
@@ -8,11 +8,11 @@ class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
_description = 'Analytic Account'
production_ids = fields.One2many('mrp.production', 'analytic_account_id', string='Manufacturing Orders')
production_ids = fields.Many2many('mrp.production')
production_count = fields.Integer("Manufacturing Orders Count", compute='_compute_production_count')
bom_ids = fields.One2many('mrp.bom', 'analytic_account_id', string='Bills of Materials')
bom_ids = fields.Many2many('mrp.bom')
bom_count = fields.Integer("BoM Count", compute='_compute_bom_count')
workcenter_ids = fields.One2many('mrp.workcenter', 'costs_hour_account_id', string='Workcenters')
workcenter_ids = fields.Many2many('mrp.workcenter')
workorder_count = fields.Integer("Work Order Count", compute='_compute_workorder_count')
@api.depends('production_ids')
@@ -77,3 +77,14 @@ class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
category = fields.Selection(selection_add=[('manufacturing_order', 'Manufacturing Order')])
class AccountAnalyticApplicability(models.Model):
_inherit = 'account.analytic.applicability'
_description = "Analytic Plan's Applicabilities"
business_domain = fields.Selection(
selection_add=[
('manufacturing_order', 'Manufacturing Order'),
],
ondelete={'manufacturing_order': 'cascade'},
)
+42 -4
View File
@@ -1,10 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo import models, api, fields
from odoo import fields, models
class MrpBom(models.Model):
_inherit = 'mrp.bom'
_name = 'mrp.bom'
_inherit = ['mrp.bom', 'analytic.mixin']
analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', company_dependent=True, copy=True,
help="Analytic account in which cost and revenue entries will take place for financial management of the manufacturing order.")
# Company dependent JSON fields are not yet supported
analytic_distribution_text = fields.Text(company_dependent=True)
analytic_distribution = fields.Json(inverse="_inverse_analytic_distribution", store=False, precompute=False)
analytic_account_ids = fields.Many2many('account.analytic.account', compute="_compute_analytic_account_ids", copy=True)
@api.depends_context('company')
@api.depends('analytic_distribution_text')
def _compute_analytic_distribution(self):
for record in self:
record.analytic_distribution = json.loads(record.analytic_distribution_text or '{}')
def _inverse_analytic_distribution(self):
for record in self:
record.analytic_distribution_text = json.dumps(record.analytic_distribution)
@api.depends('analytic_distribution')
def _compute_analytic_account_ids(self):
for record in self:
record.analytic_account_ids = list(map(int, record.analytic_distribution.keys())) if record.analytic_distribution else []
@api.onchange('product_id')
def _onchange_analytic_distribution(self):
for record in self:
if record.product_id:
record.analytic_distribution = record.env['account.analytic.distribution.model']._get_distribution({
"product_id": record.product_id.id,
"product_categ_id": record.product_id.categ_id.id,
"company_id": record.company_id.id,
})
@api.constrains('analytic_distribution')
def _check_analytic(self):
for record in self:
record.with_context({'validate_analytic': True})._validate_distribution(**{
'product': record.product_id.id,
'company_id': record.company_id.id,
})
+39 -33
View File
@@ -1,53 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import api, fields, models, _
from odoo.tools import float_is_zero, float_round
from odoo.tools import float_round
class MrpProduction(models.Model):
_inherit = 'mrp.production'
_name = 'mrp.production'
_inherit = ['mrp.production', 'analytic.mixin']
extra_cost = fields.Float(copy=False, string='Extra Unit Cost')
show_valuation = fields.Boolean(compute='_compute_show_valuation')
analytic_account_id = fields.Many2one(
'account.analytic.account', 'Analytic Account', copy=True,
help="Analytic account in which cost and revenue entries will take\
place for financial management of the manufacturing order.",
compute='_compute_analytic_account_id', store=True, readonly=False)
analytic_account_ids = fields.Many2many('account.analytic.account', compute='_compute_analytic_account_ids', store=True)
def _compute_show_valuation(self):
for order in self:
order.show_valuation = any(m.state == 'done' for m in order.move_finished_ids)
@api.depends('bom_id')
def _compute_analytic_account_id(self):
if self.bom_id.analytic_account_id:
self.analytic_account_id = self.bom_id.analytic_account_id
@api.depends('bom_id', 'product_id')
def _compute_analytic_distribution(self):
for record in self:
if record.bom_id.analytic_distribution:
record.analytic_distribution = record.bom_id.analytic_distribution
else:
record.analytic_distribution = record.env['account.analytic.distribution.model']._get_distribution({
"product_id": record.product_id.id,
"product_categ_id": record.product_id.categ_id.id,
"company_id": record.company_id.id,
})
@api.depends('analytic_distribution')
def _compute_analytic_account_ids(self):
for record in self:
record.analytic_account_ids = list(
map(int, record.analytic_distribution.keys())) if record.analytic_distribution else []
@api.constrains('analytic_distribution')
def _check_analytic(self):
for record in self:
params = {'business_domain': 'manufacturing_order', 'company_id': record.company_id.id}
if record.product_id:
params['product'] = record.product_id.id
record.with_context({'validate_analytic': True})._validate_distribution(**params)
def write(self, vals):
origin_analytic_account = {production: production.analytic_account_id for production in self}
res = super().write(vals)
for production in self:
if vals.get('name'):
production.move_raw_ids.analytic_account_line_id.ref = production.display_name
production.move_raw_ids.analytic_account_line_ids.ref = production.display_name
for workorder in production.workorder_ids:
workorder.mo_analytic_account_line_id.ref = production.display_name
workorder.mo_analytic_account_line_id.name = _("[WC] %s", workorder.display_name)
if 'analytic_account_id' in vals and production.state != 'draft':
if vals['analytic_account_id'] and origin_analytic_account[production]:
# Link the account analytic lines to the new AA
production.move_raw_ids.analytic_account_line_id.write({'account_id': vals['analytic_account_id']})
production.workorder_ids.mo_analytic_account_line_id.write({'account_id': vals['analytic_account_id']})
elif vals['analytic_account_id'] and not origin_analytic_account[production]:
# Create the account analytic lines if no AA is set in the MO
production.move_raw_ids._account_analytic_entry_move()
production.workorder_ids._create_or_update_analytic_entry()
else:
production.move_raw_ids.analytic_account_line_id.unlink()
production.workorder_ids.mo_analytic_account_line_id.unlink()
workorder.mo_analytic_account_line_ids.ref = production.display_name
workorder.mo_analytic_account_line_ids.name = _("[WC] %s", workorder.display_name)
if 'analytic_distribution' in vals and production.state != 'draft':
production.move_raw_ids._account_analytic_entry_move()
production.workorder_ids._create_or_update_analytic_entry()
return res
def action_view_stock_valuation_layers(self):
@@ -60,15 +67,14 @@ class MrpProduction(models.Model):
context['search_default_group_by_product_id'] = False
return dict(action, domain=domain, context=context)
def action_view_analytic_account(self):
def action_view_analytic_accounts(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "account.analytic.account",
'res_id': self.analytic_account_id.id,
"context": {"create": False},
"name": _("Analytic Account"),
'view_mode': 'form',
'domain': [('id', 'in', self.analytic_account_ids.ids)],
"name": _("Analytic Accounts"),
'view_mode': 'tree,form',
}
def _cal_price(self, consumed_moves):
+16 -5
View File
@@ -1,12 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, models, fields
class MrpWorkcenter(models.Model):
_inherit = 'mrp.workcenter'
_name = 'mrp.workcenter'
_inherit = ['mrp.workcenter', 'analytic.mixin']
costs_hour_account_id = fields.Many2one(
'account.analytic.account', string='Analytic Account',
help="Posts analytical accounting entries in real time for both component and operational costs.")
costs_hour_account_ids = fields.Many2many('account.analytic.account', compute="_compute_costs_hour_account_ids", store=True)
@api.depends('analytic_distribution')
def _compute_costs_hour_account_ids(self):
for record in self:
record.costs_hour_account_ids = list(map(int, record.analytic_distribution.keys())) if record.analytic_distribution else []
@api.constrains('analytic_distribution')
def _check_analytic(self):
for record in self:
record.with_context({'validate_analytic': True})._validate_distribution(**{
'company_id': record.company_id.id,
})
+15 -41
View File
@@ -2,14 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.tools import float_is_zero
class MrpWorkorder(models.Model):
_inherit = 'mrp.workorder'
mo_analytic_account_line_id = fields.Many2one('account.analytic.line', copy=False)
wc_analytic_account_line_id = fields.Many2one('account.analytic.line', copy=False)
mo_analytic_account_line_ids = fields.Many2many('account.analytic.line', 'mrp_workorder_mo_analytic_rel', copy=False)
wc_analytic_account_line_ids = fields.Many2many('account.analytic.line', 'mrp_workorder_wc_analytic_rel', copy=False)
def _compute_duration(self):
res = super()._compute_duration()
@@ -22,15 +21,15 @@ class MrpWorkorder(models.Model):
return res
def action_cancel(self):
(self.mo_analytic_account_line_id | self.wc_analytic_account_line_id).unlink()
(self.mo_analytic_account_line_ids | self.wc_analytic_account_line_ids).unlink()
return super().action_cancel()
def _prepare_analytic_line_values(self, account, unit_amount, amount):
def _prepare_analytic_line_values(self, account, amount, unit_amount):
self.ensure_one()
return {
'name': _("[WC] %s", self.display_name),
'amount': amount,
'account_id': account.id,
'account_id': account,
'unit_amount': unit_amount,
'product_id': self.product_id.id,
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
@@ -40,43 +39,18 @@ class MrpWorkorder(models.Model):
}
def _create_or_update_analytic_entry(self):
wo_to_link_mo_analytic_line = self.env['mrp.workorder']
wo_to_link_wc_analytic_line = self.env['mrp.workorder']
mo_analytic_line_vals_list = []
wc_analytic_line_vals_list = []
for wo in self.filtered(lambda wo: wo.production_id.analytic_account_id or wo.workcenter_id.costs_hour_account_id):
for wo in self.filtered(lambda wo: wo.id and (wo.production_id.analytic_distribution or wo.workcenter_id.analytic_distribution or wo.wc_analytic_account_line_ids or wo.mo_analytic_account_line_ids)):
hours = wo.duration / 60.0
value = -hours * wo.workcenter_id.costs_hour
mo_account = wo.production_id.analytic_account_id
wc_account = wo.workcenter_id.costs_hour_account_id
if mo_account:
mo_currency = mo_account.currency_id or wo.company_id.currency_id
is_zero = float_is_zero(value, precision_rounding=mo_currency.rounding)
if wo.mo_analytic_account_line_id:
wo.mo_analytic_account_line_id.write({
'unit_amount': hours,
'amount': value if not is_zero else 0,
})
elif not is_zero:
wo_to_link_mo_analytic_line += wo
mo_analytic_line_vals_list.append(wo._prepare_analytic_line_values(mo_account, hours, value))
if wc_account and wc_account != mo_account:
is_zero = float_is_zero(value, precision_rounding=wc_account.currency_id.rounding)
if wo.wc_analytic_account_line_id:
wo.wc_analytic_account_line_id.write({
'unit_amount': hours,
'amount': value if not is_zero else 0,
})
elif not is_zero:
wo_to_link_wc_analytic_line += wo
wc_analytic_line_vals_list.append(wo._prepare_analytic_line_values(wc_account, hours, value))
analytic_lines = self.env['account.analytic.line'].sudo().create(mo_analytic_line_vals_list + wc_analytic_line_vals_list)
mo_analytic_lines, wc_analytic_lines = analytic_lines[:len(wo_to_link_mo_analytic_line)], analytic_lines[len(wo_to_link_mo_analytic_line):]
for wo, analytic_line in zip(wo_to_link_mo_analytic_line, mo_analytic_lines):
wo.mo_analytic_account_line_id = analytic_line
for wo, analytic_line in zip(wo_to_link_wc_analytic_line, wc_analytic_lines):
wo.wc_analytic_account_line_id = analytic_line
mo_analytic_line_vals = self.env['account.analytic.account']._perform_analytic_distribution(wo.production_id.analytic_distribution, value, hours, wo.mo_analytic_account_line_ids, wo)
if mo_analytic_line_vals:
wo.mo_analytic_account_line_ids += self.env['account.analytic.line'].sudo().create(mo_analytic_line_vals)
wc_analytic_line_vals = self.env['account.analytic.account']._perform_analytic_distribution(wo.workcenter_id.analytic_distribution, value, hours, wo.wc_analytic_account_line_ids, wo)
if wc_analytic_line_vals:
wo.wc_analytic_account_line_ids += self.env['account.analytic.line'].sudo().create(wc_analytic_line_vals)
def unlink(self):
(self.mo_analytic_account_line_id | self.wc_analytic_account_line_id).unlink()
(self.mo_analytic_account_line_ids | self.wc_analytic_account_line_ids).unlink()
return super().unlink()
+8 -8
View File
@@ -12,19 +12,19 @@ class StockMove(models.Model):
res += self.filtered(lambda m: m.bom_line_id.bom_id.product_tmpl_id.id == product.product_tmpl_id.id)
return res
def _generate_analytic_lines_data(self, unit_amount, amount):
vals = super()._generate_analytic_lines_data(unit_amount, amount)
if self.raw_material_production_id.analytic_account_id:
def _generate_analytic_lines_data(self, unit_amount, amount, distribution, account, distribution_on_each_plan):
vals = super()._generate_analytic_lines_data(unit_amount, amount, distribution, account, distribution_on_each_plan)
if self._get_analytic_distribution():
vals['name'] = _('[Raw] %s', self.product_id.display_name)
vals['ref'] = self.raw_material_production_id.display_name
vals['category'] = 'manufacturing_order'
return vals
def _get_analytic_account(self):
account = self.raw_material_production_id.analytic_account_id
if account:
return account
return super()._get_analytic_account()
def _get_analytic_distribution(self):
distribution = self.raw_material_production_id.analytic_distribution
if distribution:
return distribution
return super()._get_analytic_distribution()
def _ignore_automatic_valuation(self):
return bool(self.raw_material_production_id)
+2 -2
View File
@@ -9,6 +9,6 @@ class StockRule(models.Model):
def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values, bom):
res = super()._prepare_mo_vals(product_id, product_qty, product_uom, location_id, name, origin, company_id, values, bom)
if not bom.analytic_account_id and values.get('analytic_account_id'):
res['analytic_account_id'] = values.get('analytic_account_id').id
if not bom.analytic_distribution and values.get('analytic_distribution'):
res['analytic_distribution'] = values.get('analytic_distribution')
return res
@@ -64,32 +64,30 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_account_id = self.analytic_account
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_id), 0)
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_id.amount, -50.0)
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()
# Hack to bypass test doing strange things
mo._set_qty_producing()
mo.workorder_ids.button_finish()
self.assertEqual(mo.state, 'to_close')
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.amount, -100.0)
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_id.amount, -100.0)
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.
@@ -99,65 +97,23 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 10.0
mo_form.analytic_account_id = self.analytic_account
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_id), 0)
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_id.amount, -50.0)
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_id.amount, -50.0)
def test_workcenter_same_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 the same of the one on the bom
self.workcenter.costs_hour_account_id = self.analytic_account
# 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_account_id = self.analytic_account
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 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_id.amount, -10.0)
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
# 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_id.amount, -20.0)
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
# 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_id.amount, -20.0)
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
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
@@ -168,46 +124,46 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
# set wc analytic account to be different from the one on the bom
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
wc_analytic_account = self.env['account.analytic.account'].create({'name': 'wc_analytic_account', 'plan_id': analytic_plan.id})
self.workcenter.costs_hour_account_id = wc_analytic_account
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_account_id = self.analytic_account
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_id), 0)
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_id.amount, -10.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -10.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -10.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.account_id, 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.account_id, 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_id.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -20.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.account_id, 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.account_id, 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_id.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id, self.analytic_account)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.amount, -20.0)
self.assertEqual(mo.workorder_ids.wc_analytic_account_line_id.account_id, wc_analytic_account)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.amount, -20.0)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.account_id, 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.account_id, wc_analytic_account)
def test_changing_mo_analytic_account(self):
""" Check if the MO account analytic lines are correctly updated
@@ -220,46 +176,46 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
mo_form.product_id = self.product
mo_form.bom_id = self.bom
mo_form.product_qty = 1
mo_form.analytic_account_id = self.analytic_account
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_id), 0)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 0)
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_id.account_id, self.analytic_account)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.account_id, 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_id), 1)
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', 'company_id': False})
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_account_id = new_analytic_account
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.account_id.id, new_analytic_account.id)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_id.account_id.id, new_analytic_account.id)
mo.analytic_distribution = {str(new_analytic_account.id): 100.0}
self.assertEqual(mo.move_raw_ids.analytic_account_line_ids.account_id.id, new_analytic_account.id)
self.assertEqual(mo.workorder_ids.mo_analytic_account_line_ids.account_id.id, new_analytic_account.id)
#Get the MO analytic account lines
mo_analytic_account_raw_lines = mo.move_raw_ids.analytic_account_line_id
mo_analytic_account_wc_lines = mo.workorder_ids.mo_analytic_account_line_id
mo.analytic_account_id = False
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_id), 0)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 0)
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_account_id = self.analytic_account
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 1)
self.assertEqual(len(mo.workorder_ids.mo_analytic_account_line_id), 1)
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
@@ -275,7 +231,7 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
# 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_account_id': analytic_account_no_company.id,
'analytic_distribution': {str(analytic_account_no_company.id): 100.0},
'product_uom_id': self.bom.product_uom_id.id,
})
@@ -289,7 +245,7 @@ class TestAnalyticAccount(TestMrpAnalyticAccount):
})
mo_no_c_form.save()
self.assertTrue(mo_no_company.workorder_ids)
self.assertEqual(wo.production_id.analytic_account_id, analytic_account_no_company)
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)
+3 -1
View File
@@ -6,7 +6,9 @@
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='picking_type_id']" position="after">
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id'}"/>
</xpath>
</field>
</record>
@@ -5,7 +5,10 @@
<field name="inherit_id" ref="mrp.mrp_production_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" optional="hide"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id', 'business_domain': 'manufacturing_order'}"
optional="hide" />
</field>
</field>
</record>
@@ -29,9 +32,9 @@
</div>
</button>
<button class="oe_stat_button" type="object"
name="action_view_analytic_account"
name="action_view_analytic_accounts"
icon="fa-bar-chart-o"
attrs="{'invisible': ['|', ('analytic_account_id', '=', False), ('state', 'in', ['draft', 'cancel'])]}"
attrs="{'invisible': ['|', ('analytic_distribution', '=', False), ('state', 'in', ['draft', 'cancel'])]}"
groups="analytic.group_analytic_accounting">
<div class="o_stat_info">
<span class="o_stat_text">Analytic Account</span>
@@ -41,7 +44,9 @@
</xpath>
<xpath expr="//page[@name='miscellaneous']//field[@name='date_deadline']" position="after">
<t groups="stock.group_stock_manager">
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id', 'business_domain': 'manufacturing_order'}"/>
</t>
</xpath>
</field>
-3
View File
@@ -7,9 +7,6 @@
'summary': "Monitor MRP using project",
'category': 'Services/Project',
'depends': ['mrp_account', 'project'],
'demo': [
'data/project_mrp_demo.xml',
],
'auto_install': True,
'license': 'LGPL-3',
}
@@ -1,7 +0,0 @@
<odoo>
<data noupdate="1">
<record id="mrp.mrp_production_3" model="mrp.production">
<field name="analytic_account_id" ref="project.analytic_office_design"/>
</record>
</data>
</odoo>
+1 -1
View File
@@ -15,7 +15,7 @@ class Project(models.Model):
def action_view_mrp_production(self):
self.ensure_one()
action = self.env['ir.actions.actions']._for_xml_id('mrp.mrp_production_action')
action['domain'] = [('analytic_account_id', '=', self.analytic_account_id.id)]
action['domain'] = [('id', 'in', self.analytic_account_id.production_ids.ids)]
action['context'] = {'default_analytic_account_id': self.analytic_account_id.id}
if self.production_count == 1:
action['view_mode'] = 'form'
+1
View File
@@ -3,6 +3,7 @@
from . import account_chart_template
from . import account_move
from . import analytic_account
from . import product
from . import stock_move
from . import stock_location
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.tools import float_compare, float_is_zero
class AccountAnalyticPlan(models.Model):
_inherit = 'account.analytic.plan'
def _calculate_distribution_amount(self, amount, percentage, distribution_on_each_plan):
existing_amount = distribution_on_each_plan.get(self, 0)
distribution_plan = existing_amount + percentage
decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
if float_compare(distribution_plan, 100, precision_digits=decimal_precision) == 0:
distributed_amount = amount * (100 - existing_amount) / 100.0
else:
distributed_amount = amount * percentage / 100.0
distribution_on_each_plan[self] = distribution_plan
return distributed_amount
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
def _perform_analytic_distribution(self, distribution, amount, unit_amount, lines, obj, additive=False):
"""
Redistributes the analytic lines to match the given distribution:
- For account_ids where lines already exist, the amount and unit_amount of these lines get updated,
lines where the updated amount becomes zero get unlinked.
- For account_ids where lines don't exist yet, the line values to create them are returned,
lines where the amount becomes zero are not included.
:param distribution: the desired distribution to match the analytic lines to
:param amount: the total amount to distribute over the analytic lines
:param unit_amount: the total unit amount (will not be distributed)
:param lines: the (current) analytic account lines that need to be matched to the new distribution
:param obj: the object on which _prepare_analytic_line_values(account_id, amount, unit_amount) will be
called to get the template for the values of new analytic line objects
:param additive: if True, the unit_amount and (distributed) amount get added to the existing lines
:returns: a list of dicts containing the values for new analytic lines that need to be created
:rtype: dict
"""
distribution_on_each_plan = {}
processed_distributions = []
for existing_aal in lines:
if distribution and existing_aal.account_id.id in [int(i) for i in distribution]:
# Update the existing AAL for this account
percentage = distribution[str(existing_aal.account_id.id)]
new_amount = existing_aal.account_id.root_plan_id._calculate_distribution_amount(amount, percentage, distribution_on_each_plan)
new_unit_amount = amount
if additive:
new_amount += existing_aal.amount
new_unit_amount += existing_aal.unit_amount
currency = existing_aal.account_id.currency_id or obj.company_id.currency_id
if float_is_zero(new_amount, precision_rounding=currency.rounding):
existing_aal.unlink()
else:
existing_aal.amount = new_amount
existing_aal.unit_amount = new_unit_amount
processed_distributions.append(existing_aal.account_id.id)
else:
# Delete the existing AAL if it is no longer present in the new distribution
existing_aal.unlink()
lines_to_link = []
if not distribution:
return []
for account_id, percentage in distribution.items():
# Only create a new AAL if an existing one was not already modified before
account_id = int(account_id)
if account_id not in processed_distributions:
account = self.browse(account_id)
new_amount = account.root_plan_id._calculate_distribution_amount(amount, percentage, distribution_on_each_plan)
currency = account.currency_id or obj.company_id.currency_id
if not float_is_zero(new_amount, precision_rounding=currency.rounding):
lines_to_link.append(obj._prepare_analytic_line_values(account.id, new_amount, unit_amount))
return lines_to_link
+12 -26
View File
@@ -5,7 +5,7 @@ from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero, float_round, OrderedSet
from odoo.tools import float_is_zero, float_round, float_compare, OrderedSet
import logging
_logger = logging.getLogger(__name__)
@@ -18,8 +18,7 @@ class StockMove(models.Model):
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
account_move_ids = fields.One2many('account.move', 'stock_move_id')
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_move_id')
analytic_account_line_id = fields.Many2one(
'account.analytic.line', copy=False)
analytic_account_line_ids = fields.Many2many('account.analytic.line', copy=False)
def _filter_anglo_saxon_moves(self, product):
return self.filtered(lambda m: m.product_id.id == product.id)
@@ -31,7 +30,7 @@ class StockMove(models.Model):
return action_data
def _action_cancel(self):
self.analytic_account_line_id.unlink()
self.analytic_account_line_ids.unlink()
return super()._action_cancel()
def _should_force_price_unit(self):
@@ -407,7 +406,7 @@ class StockMove(models.Model):
def _prepare_analytic_lines(self):
self.ensure_one()
if not self._get_analytic_account():
if not self._get_analytic_distribution() and not self.analytic_account_line_ids:
return False
if self.state in ['cancel', 'draft']:
@@ -429,24 +428,19 @@ class StockMove(models.Model):
elif sum(self.stock_valuation_layer_ids.mapped('quantity')):
amount = sum(self.stock_valuation_layer_ids.mapped('value'))
unit_amount = - sum(self.stock_valuation_layer_ids.mapped('quantity'))
if self.analytic_account_line_id:
self.analytic_account_line_id.unit_amount = unit_amount
self.analytic_account_line_id.amount = amount
return False
elif amount:
return self._generate_analytic_lines_data(
unit_amount, amount)
return self.env['account.analytic.account']._perform_analytic_distribution(
self._get_analytic_distribution(), amount, unit_amount, self.analytic_account_line_ids, self)
def _ignore_automatic_valuation(self):
return False
def _generate_analytic_lines_data(self, unit_amount, amount):
def _prepare_analytic_line_values(self, account, amount, unit_amount):
self.ensure_one()
account_id = self._get_analytic_account()
return {
'name': self.name,
'amount': amount,
'account_id': account_id.id,
'account_id': account,
'unit_amount': unit_amount,
'product_id': self.product_id.id,
'product_uom_id': self.product_id.uom_id.id,
@@ -532,18 +526,10 @@ class StockMove(models.Model):
}
def _account_analytic_entry_move(self):
analytic_lines_vals = []
moves_to_link = []
for move in self:
analytic_line_vals = move._prepare_analytic_lines()
if not analytic_line_vals:
continue
moves_to_link.append(move.id)
analytic_lines_vals.append(analytic_line_vals)
analytic_lines = self.env['account.analytic.line'].sudo().create(analytic_lines_vals)
for move_id, analytic_line in zip(moves_to_link, analytic_lines):
self.env['stock.move'].browse(
move_id).analytic_account_line_id = analytic_line
if analytic_line_vals:
move.analytic_account_line_ids += self.env['account.analytic.line'].sudo().create(analytic_line_vals)
def _account_entry_move(self, qty, description, svl_id, cost):
""" Accounting Valuation Entries """
@@ -593,7 +579,7 @@ class StockMove(models.Model):
return am_vals
def _get_analytic_account(self):
def _get_analytic_distribution(self):
return False
def _get_related_invoices(self): # To be overridden in purchase and sale_stock