Skip to content
Permalink
Browse files

[IMP] stock_landed_costs_mrp: allow to choose mrp production in lande…

…d costs

When we create a landed cost, we can now apply it on a
manufacturing order instead of a picking.

task-2123752
  • Loading branch information
ryv-odoo committed Feb 5, 2020
1 parent eb53ead commit 97bd41e3a1a243e49ab5a6064a6f0cf3b7acefb4
@@ -42,6 +42,10 @@ def _default_account_journal_id(self):
date = fields.Date(
'Date', default=fields.Date.context_today,
copy=False, required=True, states={'done': [('readonly', True)]}, tracking=True)
target_model = fields.Selection(
[('picking', 'Transfers')], string="Apply On",
required=True, default='picking',
copy=False, states={'done': [('readonly', True)]})
picking_ids = fields.Many2many(
'stock.picking', string='Transfers',
copy=False, states={'done': [('readonly', True)]})
@@ -92,6 +96,11 @@ def _compute_allowed_picking_ids(self):
for cost in self:
cost.allowed_picking_ids = valued_picking_ids_per_company[cost.company_id.id]

@api.onchange('target_model')
def _onchange_target_model(self):
if self.target_model != 'picking':
self.picking_ids = False

@api.model
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
@@ -114,10 +123,7 @@ def button_cancel(self):
return self.write({'state': 'cancel'})

def button_validate(self):
if any(cost.state != 'draft' for cost in self):
raise UserError(_('Only draft landed costs can be validated'))
if not all(cost.picking_ids for cost in self):
raise UserError(_('Please define the transfers on which those additional costs should apply.'))
self._check_can_validate()
cost_without_adjusment_lines = self.filtered(lambda c: not c.valuation_adjustment_lines)
if cost_without_adjusment_lines:
cost_without_adjusment_lines.compute_landed_cost()
@@ -179,29 +185,13 @@ def button_validate(self):
accounts = product.product_tmpl_id.get_product_accounts()
input_account = accounts['stock_input']
all_amls.filtered(lambda aml: aml.account_id == input_account and not aml.full_reconcile_id).reconcile()
return True

def _check_sum(self):
""" Check if each cost line its valuation lines sum to the correct amount
and if the overall total amount is correct also """
prec_digits = self.env.company.currency_id.decimal_places
for landed_cost in self:
total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits):
return False

val_to_cost_lines = defaultdict(lambda: 0.0)
for val_line in landed_cost.valuation_adjustment_lines:
val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
for cost_line, val_amount in val_to_cost_lines.items()):
return False
return True

def get_valuation_lines(self):
lines = []

for move in self.mapped('picking_ids').mapped('move_lines'):
for move in self._get_targeted_move_ids():
# it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average') or move.state == 'cancel':
continue
@@ -215,7 +205,7 @@ def get_valuation_lines(self):
}
lines.append(vals)

if not lines and self.mapped('picking_ids'):
if not lines:
raise UserError(_("You cannot apply landed costs on the chosen transfer(s). Landed costs can only be applied for products with automated inventory valuation and FIFO or average costing method."))
return lines

@@ -225,7 +215,7 @@ def compute_landed_cost(self):

digits = self.env['decimal.precision'].precision_get('Product Price')
towrite_dict = {}
for cost in self.filtered(lambda cost: cost.picking_ids):
for cost in self.filtered(lambda cost: cost._get_targeted_records()):
total_qty = 0.0
total_cost = 0.0
total_weight = 0.0
@@ -288,6 +278,38 @@ def action_view_stock_valuation_layers(self):
action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0]
return dict(action, domain=domain)

def _get_targeted_move_ids(self):
return self.filtered(lambda c: c.target_model == 'picking').picking_ids.move_lines

def _get_targeted_records(self):
self.ensure_one()
return self.target_model == 'picking' and self.picking_ids or None

def _check_can_validate(self):
if any(cost.state != 'draft' for cost in self):
raise UserError(_('Only draft landed costs can be validated'))
target_model_descriptions = dict(self._fields['target_model']._description_selection(self.env))
for cost in self:
if not cost._get_targeted_records():
raise UserError(_('Please define %s on which those additional costs should apply.') % target_model_descriptions[cost.target_model])

def _check_sum(self):
""" Check if each cost line its valuation lines sum to the correct amount
and if the overall total amount is correct also """
prec_digits = self.env.company.currency_id.decimal_places
for landed_cost in self:
total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits):
return False

val_to_cost_lines = defaultdict(lambda: 0.0)
for val_line in landed_cost.valuation_adjustment_lines:
val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
for cost_line, val_amount in val_to_cost_lines.items()):
return False
return True


class LandedCostLine(models.Model):
_name = 'stock.landed.cost.lines'
@@ -298,7 +320,7 @@ class LandedCostLine(models.Model):
'stock.landed.cost', 'Landed Cost',
required=True, ondelete='cascade')
product_id = fields.Many2one('product.product', 'Product', required=True)
price_unit = fields.Float('Cost', digits='Product Price', required=True)
price_unit = fields.Monetary('Cost', digits='Product Price', required=True)
split_method = fields.Selection(
SPLIT_METHOD,
string='Split Method',
@@ -309,6 +331,7 @@ class LandedCostLine(models.Model):
"By Weight : Cost will be divided depending on its weight.\n"
"By Volume : Cost will be divided depending on its volume.")
account_id = fields.Many2one('account.account', 'Account', domain=[('deprecated', '=', False)])
currency_id = fields.Many2one('res.currency', related='cost_id.currency_id')

@api.onchange('product_id')
def onchange_product_id(self):
@@ -30,7 +30,8 @@
<group>
<field name="date"/>
<field name="allowed_picking_ids" invisible="1"/>
<field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('id', 'in', allowed_picking_ids)]"/>
<field name="target_model" widget="radio" invisible="1"/>
<field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('id', 'in', allowed_picking_ids)]" attrs="{'invisible': [('target_model', '!=', 'picking')]}"/>
</group>
<group>
<label for="account_journal_id" string="Journal"/>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Landed Costs On MO',
'version': '1.0',
'summary': 'Landed Costs on Manufacturing Order',
'description': """
This module allows you to easily add extra costs on manufacturing order
and decide the split of these costs among their stock moves in order to
take them into account in your stock valuation.
""",
'depends': ['stock_landed_costs', 'mrp'],
'category': 'Operations/Inventory',
'demo': [
],
'data': [
'views/stock_landed_cost_views.xml',
],
'installable': True,
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import stock_landed_cost
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models, api


class LandedCost(models.Model):
_inherit = 'stock.landed.cost'

target_model = fields.Selection(selection_add=[('manufacturing', 'Manufacturing Orders')])
mrp_production_ids = fields.Many2many(
'mrp.production', string='Manufacturing order',
copy=False, states={'done': [('readonly', True)]}, groups="mrp.group_mrp_user")
allowed_mrp_production_ids = fields.Many2many('mrp.production', compute='_compute_mrp_production_ids', groups="mrp.group_mrp_user")

@api.depends('company_id')
def _compute_mrp_production_ids(self):
for cost in self:
moves = self.env['stock.move'].sudo().with_company(cost.company_id).search([('stock_valuation_layer_ids', '!=', False), ('production_id', '!=', False)])
self.allowed_mrp_production_ids = moves.production_id

@api.onchange('target_model')
def _onchange_target_model(self):
super()._onchange_target_model()
if self.target_model != 'manufacturing':
self.mrp_production_ids = False

def _get_targeted_move_ids(self):
moves = super()._get_targeted_move_ids()
moves |= self.filtered(lambda c: c.target_model == 'manufacturing').mrp_production_ids.move_finished_ids
return moves

def _get_targeted_records(self):
return super()._get_targeted_records() or self.target_model == 'manufacturing' and self.mrp_production_ids or None
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import test_stock_landed_costs_mrp
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo.tests import Form
from odoo.addons.stock_account.tests.common import StockAccountTestCommon


class TestStockLandedCostsMrp(StockAccountTestCommon):

@classmethod
def setUpClass(cls):
super(TestStockLandedCostsMrp, cls).setUpClass()
# References
cls.supplier_id = cls.env['res.partner'].create({'name': 'My Test Supplier'}).id
cls.customer_id = cls.env['res.partner'].create({'name': 'My Test Customer'}).id
cls.picking_type_in_id = cls.env.ref('stock.picking_type_in')
cls.picking_type_out_id = cls.env.ref('stock.picking_type_out')
cls.supplier_location_id = cls.env.ref('stock.stock_location_suppliers')
cls.stock_location_id = cls.env.ref('stock.stock_location_stock')
cls.customer_location_id = cls.env.ref('stock.stock_location_customers')
cls.categ_all = cls.env.ref('product.product_category_all')
# Create product refrigerator & oven
cls.product_component1 = cls.env['product.product'].create({
'name': 'Component1',
'type': 'product',
'standard_price': 1.0,
'categ_id': cls.categ_all.id
})
cls.product_component2 = cls.env['product.product'].create({
'name': 'Component2',
'type': 'product',
'standard_price': 2.0,
'categ_id': cls.categ_all.id
})
cls.product_refrigerator = cls.env['product.product'].create({
'name': 'Refrigerator',
'type': 'product',
'categ_id': cls.categ_all.id
})
cls.routing_1 = cls.env['mrp.routing'].create({
'name': 'Simple Line',
})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
cls.bom_refri = cls.env['mrp.bom'].create({
'product_id': cls.product_refrigerator.id,
'product_tmpl_id': cls.product_refrigerator.product_tmpl_id.id,
'product_uom_id': cls.uom_unit.id,
'product_qty': 1.0,
'routing_id': cls.routing_1.id,
'type': 'normal',
})
cls.bom_refri_line1 = cls.env['mrp.bom.line'].create({
'bom_id': cls.bom_refri.id,
'product_id': cls.product_component1.id,
'product_qty': 3,
})
cls.bom_refri_line2 = cls.env['mrp.bom.line'].create({
'bom_id': cls.bom_refri.id,
'product_id': cls.product_component2.id,
'product_qty': 1,
})
# Warehouses
cls.warehouse_1 = cls.env['stock.warehouse'].create({
'name': 'Base Warehouse',
'reception_steps': 'one_step',
'delivery_steps': 'ship_only',
'code': 'BWH'})

cls.product_refrigerator.categ_id.property_cost_method = 'fifo'
cls.product_refrigerator.categ_id.property_valuation = 'real_time'
cls.product_refrigerator.categ_id.property_stock_account_input_categ_id = cls.o_expense
cls.product_refrigerator.categ_id.property_stock_account_output_categ_id = cls.o_income

# Create service type product 1.Labour 2.Brokerage 3.Transportation 4.Packaging
cls.landed_cost = cls.env['product.product'].create({
'name': 'Landed Cost',
'type': 'service',
})
cls.allow_user = cls.env['res.users'].with_context({'no_reset_password': True}).create({
'name': "Adviser",
'login': "fm",
'email': "accountmanager@yourcompany.com",
'groups_id': [(6, 0, [cls.env.ref('account.group_account_manager').id, cls.env.ref('mrp.group_mrp_user').id, cls.env.ref('stock.group_stock_manager').id])]
})

def test_landed_cost_on_mrp(self):
inventory = self.env['stock.inventory'].create({
'name': 'Initial inventory',
'line_ids': [(0, 0, {
'product_id': self.product_component1.id,
'product_uom_id': self.product_component1.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
}), (0, 0, {
'product_id': self.product_component2.id,
'product_uom_id': self.product_component2.uom_id.id,
'product_qty': 500,
'location_id': self.warehouse_1.lot_stock_id.id
})]
})
inventory.action_start()
inventory.action_validate()

man_order_form = Form(self.env['mrp.production'].with_user(self.allow_user))
man_order_form.product_id = self.product_refrigerator
man_order_form.bom_id = self.bom_refri
man_order_form.product_qty = 2.0
man_order = man_order_form.save()

self.assertEqual(man_order.state, 'draft', "Production order should be in draft state.")
man_order.action_confirm()
self.assertEqual(man_order.state, 'confirmed', "Production order should be in confirmed state.")

# check production move
production_move = man_order.move_finished_ids
self.assertEqual(production_move.product_id, self.product_refrigerator)

first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_component1)
self.assertEqual(first_move.product_qty, 6.0)
first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_component2)
self.assertEqual(first_move.product_qty, 2.0)

# produce product
produce_form = Form(self.env['mrp.product.produce'].with_user(self.allow_user).with_context({
'active_id': man_order.id,
'active_ids': [man_order.id],
}))
produce_form.qty_producing = 2.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()

man_order.button_mark_done()

landed_cost = Form(self.env['stock.landed.cost'].with_user(self.allow_user)).save()
landed_cost.target_model = 'manufacturing'

self.assertTrue(man_order.id in landed_cost.allowed_mrp_production_ids.ids)
landed_cost.mrp_production_ids = [(6, 0, [man_order.id])]
landed_cost.cost_lines = [(0, 0, {'product_id': self.landed_cost.id, 'price_unit': 5.0, 'split_method': 'equal'})]

landed_cost.button_validate()

self.assertEqual(landed_cost.state, 'done')
self.assertTrue(landed_cost.account_move_id)
# Link to one layer of product_refrigerator
self.assertEqual(len(landed_cost.stock_valuation_layer_ids), 1)
self.assertEqual(landed_cost.stock_valuation_layer_ids.product_id, self.product_refrigerator)
self.assertEqual(landed_cost.stock_valuation_layer_ids.value, 5.0)
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo><data>
<record id='view_stock_landed_cost_mrp_form' model='ir.ui.view'>
<field name="name">stock.landed.cost.mrp.form</field>
<field name="model">stock.landed.cost</field>
<field name="inherit_id" ref="stock_landed_costs.view_stock_landed_cost_form"/>
<field name="arch" type="xml">
<field name="target_model" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="groups">mrp.group_mrp_user</attribute>
</field>
<field name="picking_ids" position="after">
<field name="allowed_mrp_production_ids" invisible="1"/>
<field name="mrp_production_ids"
widget="many2many_tags" options="{'no_create_edit': True}"
attrs="{'invisible': [('target_model', '!=', 'manufacturing')]}"
domain="[('id', 'in', allowed_mrp_production_ids)]"/>
</field>
</field>
</record>
</data></odoo>

0 comments on commit 97bd41e

Please sign in to comment.
You can’t perform that action at this time.