[FIX] mrp_subcontracting: fix flow with tracked component
After the odoo/odoo#52949 , the registering of tracked component, in case of subcontracting, was broken. Fix and try to mimic the same flow than before. The produce wizard has been replaced by a clean MO form to register component and use backorder mechanisme of MO to manage tracked finished product + tracked component. task-2278147 closes odoo/odoo#54994 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
@@ -108,7 +108,7 @@ class StockMove(models.Model):
|
||||
store=True,
|
||||
help='Technical Field to order moves')
|
||||
needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots')
|
||||
order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
|
||||
order_finished_lot_ids = fields.Many2many('stock.production.lot', string="Finished Lot/Serial Number", compute='_compute_order_finished_lot_ids')
|
||||
finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
|
||||
should_consume_qty = fields.Float('Quantity To Consume', compute='_compute_should_consume_qty')
|
||||
|
||||
@@ -123,7 +123,8 @@ class StockMove(models.Model):
|
||||
def _compute_order_finished_lot_ids(self):
|
||||
for move in self:
|
||||
if move.raw_material_production_id.move_finished_ids:
|
||||
finished_lots_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids
|
||||
# TODO ryv : to clean in master, finished_lots_exist is never used. order_finished_lot_ids can become one2many instead
|
||||
finished_lots_ids = move.raw_material_production_id.lot_producing_id
|
||||
if finished_lots_ids:
|
||||
move.order_finished_lot_ids = finished_lots_ids
|
||||
move.finished_lots_exist = True
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<field name="bom_id"
|
||||
context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<group name="group_extra_info">
|
||||
<field name="date_deadline"/>
|
||||
<label for="date_planned_start"/>
|
||||
<div class="o_row">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
'views/stock_picking_views.xml',
|
||||
'views/supplier_info_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/mrp_production_views.xml',
|
||||
'wizard/stock_picking_return_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
|
||||
@@ -9,4 +9,4 @@ from . import stock_move_line
|
||||
from . import stock_picking
|
||||
from . import stock_rule
|
||||
from . import stock_warehouse
|
||||
|
||||
from . import mrp_production
|
||||
|
||||
+54
-19
@@ -1,20 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from collections import defaultdict
|
||||
from odoo import fields, models, _, api
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if self.env.context.get('subcontract_move_id'):
|
||||
for production in self:
|
||||
if production.state not in ('cancel', 'done') and \
|
||||
('lot_producing_id' in vals or 'qty_producing' in vals):
|
||||
production._update_finished_move()
|
||||
move_line_raw_ids = fields.One2many(
|
||||
'stock.move.line', string="Detail Component", readonly=False,
|
||||
inverse='_inverse_move_line_raw_ids', compute='_compute_move_line_raw_ids'
|
||||
)
|
||||
|
||||
@api.depends('move_raw_ids.move_line_ids')
|
||||
def _compute_move_line_raw_ids(self):
|
||||
for production in self:
|
||||
production.move_line_raw_ids = production.move_raw_ids.move_line_ids
|
||||
|
||||
def _inverse_move_line_raw_ids(self):
|
||||
for production in self:
|
||||
line_by_product = defaultdict(lambda: self.env['stock.move.line'])
|
||||
for line in production.move_line_raw_ids:
|
||||
line_by_product[line.product_id] |= line
|
||||
for move in production.move_raw_ids:
|
||||
move.move_line_ids = line_by_product.pop(move.product_id, self.env['stock.move.line'])
|
||||
for product_id, lines in line_by_product.items():
|
||||
qty = sum(line.product_uom_id._compute_quantity(line.qty_done, product_id.uom_id) for line in lines)
|
||||
move = production._get_move_raw_values(product_id, qty, product_id.uom_id)
|
||||
move['additional'] = True
|
||||
production.move_raw_ids = [(0, 0, move)]
|
||||
production.move_raw_ids.filtered(lambda m: m.product_id == product_id)[:1].move_line_ids = lines
|
||||
|
||||
def subcontracting_record_component(self):
|
||||
self.ensure_one()
|
||||
assert self.env.context.get('subcontract_move_id')
|
||||
if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
for sml in self.move_raw_ids.move_line_ids:
|
||||
if sml.tracking != 'none' and not sml.lot_id:
|
||||
raise UserError(_('You must enter a serial number for each line of %s') % sml.product_id.name)
|
||||
self._update_finished_move()
|
||||
quantity_issues = self._get_quantity_produced_issues()
|
||||
if quantity_issues:
|
||||
backorder = self._generate_backorder_productions(close_mo=False)
|
||||
# No qty to consume to avoid propagate additional move
|
||||
# TODO avoid : stock move created in backorder with 0 as qty
|
||||
backorder.move_raw_ids.filtered(lambda m: m.additional).product_uom_qty = 0.0
|
||||
|
||||
backorder.qty_producing = backorder.product_qty
|
||||
backorder._set_qty_producing()
|
||||
|
||||
self.product_qty = self.qty_producing
|
||||
subcontract_move_id = self.env['stock.move'].browse(self.env.context.get('subcontract_move_id'))
|
||||
action = subcontract_move_id._action_record_components()
|
||||
action.update({'res_id': backorder.id})
|
||||
return action
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _pre_button_mark_done(self):
|
||||
if self.env.context.get('subcontract_move_id'):
|
||||
@@ -23,8 +67,8 @@ class MrpProduction(models.Model):
|
||||
|
||||
def _update_finished_move(self):
|
||||
""" After producing, set the move line on the subcontract picking. """
|
||||
self.ensure_one()
|
||||
subcontract_move_id = self.env.context.get('subcontract_move_id')
|
||||
res = None
|
||||
if subcontract_move_id:
|
||||
subcontract_move_id = self.env['stock.move'].browse(subcontract_move_id)
|
||||
quantity = self.qty_producing
|
||||
@@ -73,8 +117,7 @@ class MrpProduction(models.Model):
|
||||
'qty_done': quantity,
|
||||
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
|
||||
})
|
||||
|
||||
if not self._get_todo():
|
||||
if not self._get_quantity_to_backorder():
|
||||
ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
|
||||
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
|
||||
not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding))
|
||||
@@ -82,11 +125,3 @@ class MrpProduction(models.Model):
|
||||
for ml in subcontract_move_id.move_line_ids:
|
||||
ml.product_uom_qty = ml.qty_done
|
||||
subcontract_move_id._recompute_state()
|
||||
return res
|
||||
|
||||
def _get_todo(self):
|
||||
""" This method will return remaining todo quantity of production. """
|
||||
main_product_moves = self.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_id.id)
|
||||
todo_quantity = self.product_qty - sum(main_product_moves.mapped('quantity_done'))
|
||||
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
|
||||
return todo_quantity
|
||||
@@ -54,8 +54,7 @@ class StockMove(models.Model):
|
||||
if 'product_uom_qty' in values:
|
||||
if self.env.context.get('cancel_backorder') is False:
|
||||
return super(StockMove, self).write(values)
|
||||
self.filtered(lambda m: m.is_subcontract and
|
||||
m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
|
||||
self.filtered(lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
|
||||
return super(StockMove, self).write(values)
|
||||
|
||||
def action_show_details(self):
|
||||
@@ -65,7 +64,7 @@ class StockMove(models.Model):
|
||||
self.ensure_one()
|
||||
if self.is_subcontract:
|
||||
rounding = self.product_uom.rounding
|
||||
production = self.move_orig_ids.production_id
|
||||
production = self.move_orig_ids.production_id[-1:]
|
||||
if self._has_tracked_subcontract_components() and\
|
||||
float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\
|
||||
float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0:
|
||||
@@ -84,6 +83,7 @@ class StockMove(models.Model):
|
||||
moves = self.move_orig_ids.production_id.move_raw_ids
|
||||
tree_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_tree_view')
|
||||
form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view')
|
||||
ctx = dict(self._context, search_default_by_product=True, subcontract_move_id=self.id)
|
||||
return {
|
||||
'name': _('Raw Materials for %s') % (self.product_id.display_name),
|
||||
'type': 'ir.actions.act_window',
|
||||
@@ -91,6 +91,7 @@ class StockMove(models.Model):
|
||||
'views': [(tree_view.id, 'list'), (form_view.id, 'form')],
|
||||
'target': 'current',
|
||||
'domain': [('id', 'in', moves.ids)],
|
||||
'context': ctx
|
||||
}
|
||||
|
||||
def _action_cancel(self):
|
||||
@@ -127,8 +128,8 @@ class StockMove(models.Model):
|
||||
|
||||
def _action_record_components(self):
|
||||
self.ensure_one()
|
||||
production = self.move_orig_ids.production_id
|
||||
view = self.env.ref('mrp.mrp_production_form_view')
|
||||
production = self.move_orig_ids.production_id[-1:]
|
||||
view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view')
|
||||
return {
|
||||
'name': _('Subcontract'),
|
||||
'type': 'ir.actions.act_window',
|
||||
@@ -141,32 +142,6 @@ class StockMove(models.Model):
|
||||
'context': dict(self.env.context, subcontract_move_id=self.id),
|
||||
}
|
||||
|
||||
|
||||
def _check_overprocessed_subcontract_qty(self):
|
||||
""" If a subcontracted move use tracked components. Do not allow to add
|
||||
quantity without the produce wizard. Instead update the initial demand
|
||||
and use the register component button. Split or correct a lot/sn is
|
||||
possible.
|
||||
"""
|
||||
overprocessed_moves = self.env['stock.move']
|
||||
for move in self:
|
||||
if not move.is_subcontract:
|
||||
continue
|
||||
# Extra quantity is allowed when components do not need to be register
|
||||
if not move._has_tracked_subcontract_components():
|
||||
continue
|
||||
rounding = move.product_uom.rounding
|
||||
# if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0:
|
||||
# overprocessed_moves |= move
|
||||
if overprocessed_moves:
|
||||
raise UserError(_("""
|
||||
You have to use 'Records Components' button in order to register quantity for a
|
||||
subcontracted product(s) with tracked component(s):
|
||||
%s.
|
||||
If you want to process more than initially planned, you
|
||||
can use the edit + unlock buttons in order to adapt the initial demand on the
|
||||
operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name'))))
|
||||
|
||||
def _get_subcontract_bom(self):
|
||||
self.ensure_one()
|
||||
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
|
||||
@@ -192,6 +167,11 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
|
||||
vals['location_id'] = self.location_id.id
|
||||
return vals
|
||||
|
||||
def _should_bypass_set_qty_producing(self):
|
||||
if self.env.context.get('subcontract_move_id'):
|
||||
return False
|
||||
return super()._should_bypass_set_qty_producing()
|
||||
|
||||
def _should_bypass_reservation(self):
|
||||
""" If the move is subcontracted then ignore the reservation. """
|
||||
should_bypass_reservation = super(StockMove, self)._should_bypass_reservation()
|
||||
@@ -199,12 +179,19 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
|
||||
return True
|
||||
return should_bypass_reservation
|
||||
|
||||
def _update_subcontract_order_qty(self, quantity):
|
||||
def _update_subcontract_order_qty(self, new_quantity):
|
||||
for move in self:
|
||||
quantity_change = quantity - move.product_uom_qty
|
||||
production = move.move_orig_ids.production_id
|
||||
if production:
|
||||
self.env['change.production.qty'].with_context(skip_activity=True).create({
|
||||
'mo_id': production.id,
|
||||
'product_qty': production.product_uom_qty + quantity_change
|
||||
}).change_prod_qty()
|
||||
quantity_to_remove = move.product_uom_qty - new_quantity
|
||||
productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[::-1]
|
||||
# Cancel productions until reach new_quantity
|
||||
for production in productions:
|
||||
if quantity_to_remove <= 0.0:
|
||||
break
|
||||
if quantity_to_remove >= production.product_qty:
|
||||
quantity_to_remove -= production.product_qty
|
||||
production.action_cancel()
|
||||
else:
|
||||
self.env['change.production.qty'].with_context(skip_activity=True).create({
|
||||
'mo_id': production.id,
|
||||
'product_qty': production.product_uom_qty - quantity_to_remove
|
||||
}).change_prod_qty()
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = 'stock.move.line'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super(StockMoveLine, self).create(vals_list)
|
||||
records.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
|
||||
return records
|
||||
|
||||
def write(self, values):
|
||||
res = super(StockMoveLine, self).write(values)
|
||||
self.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
|
||||
return res
|
||||
|
||||
def _should_bypass_reservation(self, location):
|
||||
""" If the move line is subcontracted then ignore the reservation. """
|
||||
should_bypass_reservation = super(StockMoveLine, self)._should_bypass_reservation(location)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.tools.float_utils import float_is_zero
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
@@ -24,12 +24,14 @@ class StockPicking(models.Model):
|
||||
continue
|
||||
# Hide if no components are track
|
||||
subcontracted_productions = picking._get_subcontracted_productions()
|
||||
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
|
||||
component_sub_moves = subcontracted_productions.mapped('move_raw_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in component_sub_moves):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
# Hide if the production is to close
|
||||
if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
|
||||
# Hide if all tracked product move line have a lot and all productions are in rigth state
|
||||
tracked_move_line = component_sub_moves.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids
|
||||
if all(sub_mo.state in ('to_close', 'done') for sub_mo in subcontracted_productions)\
|
||||
and all(sub_sml.lot_id for sub_sml in tracked_move_line):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
picking.display_action_record_components = True
|
||||
@@ -39,29 +41,12 @@ class StockPicking(models.Model):
|
||||
# -------------------------------------------------------------------------
|
||||
def _action_done(self):
|
||||
res = super(StockPicking, self)._action_done()
|
||||
productions = self.env['mrp.production']
|
||||
for picking in self:
|
||||
for move in picking.move_lines:
|
||||
if not move.is_subcontract:
|
||||
continue
|
||||
production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
|
||||
if len(production) > 1:
|
||||
production = production[-1]
|
||||
if move._has_tracked_subcontract_components():
|
||||
move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')).move_line_ids.unlink()
|
||||
move_finished_ids = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||||
for ml in move.move_line_ids:
|
||||
ml.copy({
|
||||
'picking_id': False,
|
||||
'production_id': move_finished_ids.production_id.id,
|
||||
'move_id': move_finished_ids.id,
|
||||
'qty_done': ml.qty_done,
|
||||
'result_package_id': False,
|
||||
'location_id': move_finished_ids.location_id.id,
|
||||
'location_dest_id': move_finished_ids.location_dest_id.id,
|
||||
})
|
||||
else:
|
||||
if not move.move_line_ids.lot_id:
|
||||
# Auto set qty_producing if not tracked component
|
||||
for move in picking.move_lines.filtered(lambda move: move.is_subcontract):
|
||||
if not move._has_tracked_subcontract_components():
|
||||
production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
|
||||
if production:
|
||||
qty_done_production_uom = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
|
||||
production.qty_producing += qty_done_production_uom
|
||||
for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
|
||||
@@ -74,12 +59,15 @@ class StockPicking(models.Model):
|
||||
rounding_method='HALF-UP')
|
||||
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
|
||||
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
|
||||
productions |= production
|
||||
for subcontracted_production in productions:
|
||||
|
||||
productions_to_done = picking.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
|
||||
# TODO : to change in master with field on mrp
|
||||
productions_to_done = productions_to_done.filtered(lambda pro: all(line.lot_id for line in pro.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids))
|
||||
for subcontracted_production in productions_to_done:
|
||||
if subcontracted_production.state == 'progress':
|
||||
subcontracted_production._post_inventory()
|
||||
else:
|
||||
subcontracted_production.button_mark_done()
|
||||
subcontracted_production.with_context(subcontract_move_id=True).button_mark_done()
|
||||
# For concistency, set the date on production move before the date
|
||||
# on picking. (Tracability report + Product Moves menu item)
|
||||
minimum_date = min(picking.move_line_ids.mapped('date'))
|
||||
@@ -93,8 +81,8 @@ class StockPicking(models.Model):
|
||||
for move in self.move_lines:
|
||||
if not move._has_tracked_subcontract_components():
|
||||
continue
|
||||
production = move.move_orig_ids.production_id
|
||||
if not production or production.state in ('done', 'to_close'):
|
||||
production = move.move_orig_ids.production_id[-1:]
|
||||
if not production:
|
||||
continue
|
||||
return move._action_record_components()
|
||||
|
||||
@@ -145,3 +133,7 @@ class StockPicking(models.Model):
|
||||
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
|
||||
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
|
||||
mo.action_assign()
|
||||
|
||||
if move._has_tracked_subcontract_components():
|
||||
mo.qty_producing = move.product_uom_qty
|
||||
mo.with_context(subcontract_move_id=True)._set_qty_producing()
|
||||
|
||||
@@ -402,87 +402,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
|
||||
self.assertEqual(sum(move_finished.mapped('quantity_done')), 3.0)
|
||||
|
||||
def test_flow_7(self):
|
||||
""" Process a subcontracting receipt with tracked component and
|
||||
finished product. Simulate the regiter components button.
|
||||
Once the components are registered, try to do a correction on exisiting
|
||||
move lines and check that the subcontracting document is updated.
|
||||
"""
|
||||
# Create a receipt picking from the subcontractor
|
||||
(self.comp1 | self.comp2 | self.finished).write({'tracking': 'lot'})
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 3 # FIXME sle: need to handle the backorder in subcontract
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
mo = picking_receipt.move_lines.move_orig_ids.production_id
|
||||
move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
|
||||
move_comp2 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
|
||||
# move_finished is linked to receipt and not MO finished move.
|
||||
move_finished = picking_receipt.move_lines
|
||||
|
||||
self.assertEqual(move_comp1.quantity_done, 0)
|
||||
self.assertEqual(move_comp2.quantity_done, 0)
|
||||
|
||||
lot_c1 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT C1',
|
||||
'product_id': self.comp1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_c2 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT C2',
|
||||
'product_id': self.comp2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_f1 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT F1',
|
||||
'product_id': self.finished.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
mo_form = Form(picking_receipt._get_subcontracted_productions().with_context(subcontract_move_id=picking_receipt.move_lines.id))
|
||||
mo_form.qty_producing = 3
|
||||
mo_form.lot_producing_id = lot_f1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = lot_c1
|
||||
ml.qty_done = 3
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.lot_id = lot_c2
|
||||
ml.qty_done = 3
|
||||
details_operation_form.save()
|
||||
|
||||
self.assertEqual(move_comp1.quantity_done, 3)
|
||||
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C1')
|
||||
self.assertEqual(move_comp2.quantity_done, 3)
|
||||
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C2')
|
||||
self.assertEqual(move_finished.quantity_done, 3)
|
||||
self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: ml.product_uom_qty).lot_id.name, 'LOT F1')
|
||||
|
||||
corrected_final_lot = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT F2',
|
||||
'product_id': self.finished.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.lot_id = corrected_final_lot
|
||||
details_operation_form.save()
|
||||
move_raw_comp_1 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
|
||||
move_raw_comp_2 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
|
||||
|
||||
picking_receipt.move_lines.move_orig_ids.production_id.lot_producing_id = corrected_final_lot
|
||||
picking_receipt.move_lines.move_orig_ids.production_id.button_mark_done()
|
||||
|
||||
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
|
||||
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
|
||||
|
||||
def test_flow_8(self):
|
||||
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
|
||||
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
|
||||
@@ -596,7 +515,7 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
})
|
||||
|
||||
# 2.2. Finished prodcut has tracking by serial number
|
||||
self.finished_lot = self.env['product.product'].create({
|
||||
self.finished_product = self.env['product.product'].create({
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
@@ -605,7 +524,7 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
bom_form = Form(self.env['mrp.bom'])
|
||||
bom_form.type = 'subcontract'
|
||||
bom_form.subcontractor_ids.add(self.subcontractor_partner1)
|
||||
bom_form.product_tmpl_id = self.finished_lot.product_tmpl_id
|
||||
bom_form.product_tmpl_id = self.finished_product.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.comp1_sn
|
||||
bom_line.product_qty = 1
|
||||
@@ -622,7 +541,7 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished_lot
|
||||
move.product_id = self.finished_product
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
@@ -633,7 +552,6 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEqual(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.picking_ids), 0)
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
@@ -658,7 +576,7 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.finished_lot.id,
|
||||
'product_id': self.finished_product.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
serial_id = self.env['stock.production.lot'].create({
|
||||
@@ -667,15 +585,15 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
mo_form = Form(mo.with_context(subcontract_move_id=picking_receipt.move_lines.id))
|
||||
action = picking_receipt.action_record_components()
|
||||
mo = self.env['mrp.production'].browse(action['res_id'])
|
||||
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
|
||||
mo_form.qty_producing = 1
|
||||
mo_form.lot_producing_id = lot_id
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 1
|
||||
with mo_form.move_line_raw_ids.edit(0) as ml:
|
||||
ml.lot_id = serial_id
|
||||
details_operation_form.save()
|
||||
mo = mo_form.save()
|
||||
mo.subcontracting_record_component()
|
||||
|
||||
# We should not be able to call the 'record_components' button
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
@@ -686,7 +604,84 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_lot, wh.lot_stock_id)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
|
||||
self.assertEqual(avail_qty_comp1, -1)
|
||||
self.assertEqual(avail_qty_comp2, -1)
|
||||
self.assertEqual(avail_qty_finished, 1)
|
||||
|
||||
def test_flow_tracked_backorder(self):
|
||||
""" This test uses tracked (serial and lot) component and tracked (serial) finished product """
|
||||
todo_nb = 4
|
||||
self.comp2.tracking = 'lot'
|
||||
self.finished_product.tracking = 'serial'
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished_product
|
||||
move.product_uom_qty = todo_nb
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# We should be able to call the 'record_components' button
|
||||
self.assertTrue(picking_receipt.display_action_record_components)
|
||||
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEqual(len(mo.picking_ids), 0)
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
lot_comp2 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot_comp2',
|
||||
'product_id': self.comp2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
serials_finished = []
|
||||
serials_comp1 = []
|
||||
for i in range(todo_nb):
|
||||
serials_finished.append(self.env['stock.production.lot'].create({
|
||||
'name': 'serial_fin_%s' % i,
|
||||
'product_id': self.finished_product.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
serials_comp1.append(self.env['stock.production.lot'].create({
|
||||
'name': 'serials_comp1_%s' % i,
|
||||
'product_id': self.comp1_sn.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
|
||||
for i in range(todo_nb):
|
||||
action = picking_receipt.action_record_components()
|
||||
mo = self.env['mrp.production'].browse(action['res_id'])
|
||||
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
|
||||
mo_form.lot_producing_id = serials_finished[i]
|
||||
with mo_form.move_line_raw_ids.edit(0) as ml:
|
||||
self.assertEqual(ml.product_id, self.comp1_sn)
|
||||
ml.lot_id = serials_comp1[i]
|
||||
with mo_form.move_line_raw_ids.edit(1) as ml:
|
||||
self.assertEqual(ml.product_id, self.comp2)
|
||||
ml.lot_id = lot_comp2
|
||||
mo = mo_form.save()
|
||||
mo.subcontracting_record_component()
|
||||
|
||||
# We should not be able to call the 'record_components' button
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
|
||||
picking_receipt.button_validate()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("state"), ['done'] * todo_nb)
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
|
||||
self.assertEqual(mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"), [1] * todo_nb)
|
||||
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
|
||||
self.assertEqual(avail_qty_comp1, -todo_nb)
|
||||
self.assertEqual(avail_qty_comp2, -todo_nb)
|
||||
self.assertEqual(avail_qty_finished, todo_nb)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mrp_production_subcontracting_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.production.subcontracting.form.view</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="replace">
|
||||
<field name="state" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="replace"/>
|
||||
<xpath expr="//group[@name='group_extra_info']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='operations']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='lot_producing_id']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('product_tracking', 'in', ('none', False))], 'required': [('product_tracking', 'not in', ('none', False))]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='miscellaneous']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='name']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='bom_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_generate_serial']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='move_raw_ids']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='move_raw_ids']" position="after">
|
||||
<field name="move_line_raw_ids" force_save="1"
|
||||
context="{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_stock_move_line_tree_view', 'default_company_id': company_id, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id}"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//sheet" position="inside">
|
||||
<footer>
|
||||
<button name="subcontracting_record_component" attrs="{'invisible': ['|', ('state', '=', 'to_close'), ('qty_producing', '<', 0.001)]}" string="Continue" type="object" class="oe_highlight"/>
|
||||
<button name="subcontracting_record_component" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Record Production" type="object" class="oe_highlight"/>
|
||||
<button string="Discard" special="cancel" />
|
||||
</footer>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,41 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="mrp_subcontracting_stock_move_line_tree_view" model="ir.ui.view">
|
||||
<field name="name">mrp.subcontracting.stock.move.line.tree.view</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="owner_id" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="package_id" invisible="1"/>
|
||||
<field name="result_package_id" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="location_dest_id" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<!-- Don't put move_id here to avoid that the framework send falsy move_id -->
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="product_id" required="1"/>
|
||||
<field name="lot_id"
|
||||
attrs="{'invisible': [('tracking', 'not in', ('serial', 'lot'))], 'required': [('tracking', 'in', ('serial', 'lot'))]}"
|
||||
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
|
||||
<field name="product_uom_qty" readonly="1" force_save="1"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mrp_subcontracting_move_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.subcontracting.move.form.view</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="0" delete="0">
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="finished_lots_exist" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="move_line_ids" context="{'default_product_id': product_id}" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
<field name="location_dest_id" invisible="1"/>
|
||||
<field name="has_tracking" invisible="1"/>
|
||||
<field name="product_uom_qty" invisible="1"/>
|
||||
<group>
|
||||
<field name="order_finished_lot_ids" widget="many2many_tags"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
<field name="quantity_done" string="Total Consumed" readonly="1"/>
|
||||
</group>
|
||||
<field name="move_line_ids"
|
||||
attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"
|
||||
context="{'default_product_uom_id': product_uom, 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id}">
|
||||
<tree editable="bottom" decoration-muted="state in ('done', 'cancel')">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="location_dest_id" invisible="1"/>
|
||||
<field name="product_id" readonly="1" force_save="1"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="lot_id" context="{'default_product_id': product_id}"/>
|
||||
<field name="lot_id" attrs="{'column_invisible':[('parent.has_tracking', 'not in', ('serial', 'lot'))], 'required': [('tracking', 'in', ('serial', 'lot'))]}" context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="mrp_subcontracting_move_tree_view" model="ir.ui.view"> -->
|
||||
<!-- <field name="name">mrp.subcontracting.move.tree.view</field> -->
|
||||
<!-- <field name="model">stock.move</field> -->
|
||||
<!-- <field name="priority">1000</field> -->
|
||||
<!-- <field name="mode">primary</field> -->
|
||||
<!-- <field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/> -->
|
||||
<!-- <field name="arch" type="xml"> -->
|
||||
<!-- <xpath expr="//tree" position="attributes"> -->
|
||||
<!-- <attribute name="create">0</attribute> -->
|
||||
<!-- <attribute name="delete">0</attribute> -->
|
||||
<!-- </xpath> -->
|
||||
<!-- </field> -->
|
||||
<!-- </record> -->
|
||||
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
|
||||
<field name="name">mrp.subcontracting.move.tree.view</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree delete="0" create="0" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty > 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty < 0.0001">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="unit_factor" invisible="1"/>
|
||||
<field name="date" invisible="1"/>
|
||||
<field name="date_expected" invisible="1"/>
|
||||
<field name="picking_type_id" invisible="1"/>
|
||||
<field name="has_tracking" invisible="1"/>
|
||||
<field name="operation_id" invisible="1"/>
|
||||
<field name="is_done" invisible="1"/>
|
||||
<field name="bom_line_id" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="warehouse_id" invisible="1"/>
|
||||
<field name="product_uom_qty" invisible="1"/>
|
||||
<field name="location_dest_id" invisible="1"/>
|
||||
<field name="state" invisible="1" force_save="1"/>
|
||||
<field name="raw_material_production_id" invisible="1"/>
|
||||
<field name="product_id" required="1"/>
|
||||
<field name="order_finished_lot_ids" widget="many2many_tags"/>
|
||||
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}" string="Reserved"/>
|
||||
<field name="quantity_done" string="Consumed" readonly="1"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_product_produce
|
||||
from . import stock_picking_return
|
||||
|
||||
@@ -1093,6 +1093,7 @@ class StockMove(models.Model):
|
||||
'location_id': self.location_id.id,
|
||||
'location_dest_id': location_dest_id,
|
||||
'picking_id': self.picking_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
if quantity:
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Move Detail">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
|
||||
Reference in New Issue
Block a user