[IMP] stock: stock.move.line flexible reservation
This commit allows the user to choose which quants to reserve from stock and the quantity to reserve from each quant, making reservation much more flexible from the picking form. In addition, the `reserved_uom_qty` of stock.move.line is now editable in views to allow for users to change the qty reserved by existing move lines. When editing the reserved qty, if quantity is not available, it will reserve only what is available.. closes odoo/odoo#106006 Taskid: 3090913 Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
@@ -24,6 +24,7 @@ Also implements the removal strategy First Expiry First Out (FEFO) widely used,
|
||||
'views/stock_move_views.xml',
|
||||
'views/stock_quant_views.xml',
|
||||
'wizard/confirm_expiry_view.xml',
|
||||
'wizard/stock_quant_reserve.xml',
|
||||
'report/report_deliveryslip.xml',
|
||||
'report/report_lot_barcode.xml',
|
||||
'report/report_package_barcode.xml',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import confirm_expiry
|
||||
from . import stock_quant_reserve
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ReserveStockQuantLine(models.TransientModel):
|
||||
_inherit = 'stock.quant.reserve.line'
|
||||
|
||||
expiration_date = fields.Datetime(related='lot_id.expiration_date')
|
||||
|
||||
|
||||
class ReserveStockQuant(models.TransientModel):
|
||||
_inherit = 'stock.quant.reserve'
|
||||
|
||||
show_expiry = fields.Boolean(related='move_id.product_id.use_expiration_date')
|
||||
@@ -0,0 +1,15 @@
|
||||
<odoo>
|
||||
<record id="stock_quant_reserve_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.quant.reserve.inherit</field>
|
||||
<field name="model">stock.quant.reserve</field>
|
||||
<field name="inherit_id" ref="stock.view_stock_quant_reserve_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="move_id" position="after">
|
||||
<field name="show_expiry" invisible="1"/>
|
||||
</field>
|
||||
<field name="lot_id" position='after'>
|
||||
<field name="expiration_date" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_expiry', '=', False)]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -65,6 +65,7 @@
|
||||
'wizard/stock_inventory_warning.xml',
|
||||
'wizard/stock_label_type.xml',
|
||||
'wizard/stock_lot_label_layout.xml',
|
||||
'wizard/stock_quant_reserve.xml',
|
||||
|
||||
'views/res_partner_views.xml',
|
||||
'views/product_strategy_views.xml',
|
||||
|
||||
@@ -321,7 +321,7 @@ class StockMoveLine(models.Model):
|
||||
if ml.state == 'done':
|
||||
if 'qty_done' in vals:
|
||||
ml.move_id.product_uom_qty = ml.move_id.quantity_done
|
||||
if ml.product_id.type == 'product':
|
||||
if ml.product_id.type == 'product' and not self.env.context.get('bypass_reservation_update'):
|
||||
Quant = self.env['stock.quant']
|
||||
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
|
||||
in_date = None
|
||||
@@ -382,7 +382,7 @@ class StockMoveLine(models.Model):
|
||||
# the quants). If the new charateristics are not available on the quants, we chose to
|
||||
# reserve the maximum possible.
|
||||
if updates or 'reserved_uom_qty' in vals:
|
||||
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'):
|
||||
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned', 'confirmed'] and ml.product_id.type == 'product'):
|
||||
|
||||
if 'reserved_uom_qty' in vals:
|
||||
new_reserved_uom_qty = ml.product_uom_id._compute_quantity(
|
||||
@@ -401,17 +401,22 @@ class StockMoveLine(models.Model):
|
||||
if not ml.move_id._should_bypass_reservation(updates.get('location_id', ml.location_id)):
|
||||
reserved_qty = 0
|
||||
try:
|
||||
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), new_reserved_uom_qty, lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
available_qty = Quant._get_available_quantity(ml.product_id, updates.get('location_id', ml.location_id), lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
to_reserve = min(available_qty, new_reserved_uom_qty)
|
||||
q = []
|
||||
if to_reserve:
|
||||
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), to_reserve, lot_id=updates.get('lot_id', ml.lot_id),
|
||||
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||||
reserved_qty = sum([x[1] for x in q])
|
||||
except UserError:
|
||||
pass
|
||||
if reserved_qty != new_reserved_uom_qty:
|
||||
new_reserved_uom_qty = ml.product_id.uom_id._compute_quantity(reserved_qty, ml.product_uom_id, rounding_method='HALF-UP')
|
||||
moves_to_recompute_state |= ml.move_id
|
||||
ml.with_context(bypass_reservation_update=True).reserved_uom_qty = new_reserved_uom_qty
|
||||
# we don't want to override the new reserved quantity
|
||||
vals.pop('reserved_uom_qty', None)
|
||||
moves_to_recompute_state |= ml.move_id
|
||||
|
||||
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
|
||||
if updates or 'qty_done' in vals:
|
||||
@@ -919,3 +924,14 @@ class StockMoveLine(models.Model):
|
||||
'message': _("The inventory adjustments have been reverted."),
|
||||
}
|
||||
}
|
||||
|
||||
def action_open_reserve_stock(self):
|
||||
move_id = self.env['stock.move'].browse(self.env.context.get('default_move_id'))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'stock.quant.reserve',
|
||||
'view_mode': 'form',
|
||||
'context': {'default_move_id': move_id.id},
|
||||
'target': 'new',
|
||||
'name': _('Reserve stock: %(product)s', product=move_id.product_id.name),
|
||||
}
|
||||
|
||||
@@ -91,3 +91,5 @@ access_stock_replenishment_info,stock.replenishment.info,model_stock_replenishme
|
||||
access_stock_picking_label_type_user,picking.label.type.user,model_picking_label_type,stock.group_stock_user,1,1,1,0
|
||||
access_stock_lot_label_layout_user,lot.label.layout.user,model_lot_label_layout,stock.group_stock_user,1,1,1,0
|
||||
access_stock_replenish_option,stock.replenishment.option,model_stock_replenishment_option,stock.group_stock_user,1,1,1,0
|
||||
access_stock_quant_reserve,stock.quant.reserve,model_stock_quant_reserve,stock.group_stock_manager,1,1,1,0
|
||||
access_stock_quant_reserve_line,stock.quant.reserve.line,model_stock_quant_reserve_line,stock.group_stock_manager,1,1,1,0
|
||||
|
||||
|
@@ -0,0 +1,26 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class StockQuantReserve extends Component {
|
||||
|
||||
setup() {
|
||||
this.reserve_line_record = this.props.record;
|
||||
this.reserve_record = this.props.record.model.root;
|
||||
}
|
||||
|
||||
async setToReserve() {
|
||||
const qty_to_reserve = Math.min(this.reserve_record.data.qty_to_reserve, this.reserve_line_record.data.available_quantity);
|
||||
await this.reserve_line_record.update({ qty_to_reserve });
|
||||
}
|
||||
}
|
||||
|
||||
StockQuantReserve.template = "stock.StockQuantReserve";
|
||||
|
||||
const stockQuantReserve = {
|
||||
component: StockQuantReserve,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("stock_quant_reserve", stockQuantReserve);
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="stock.StockQuantReserve" owl="1">
|
||||
<a t-on-click.stop.prevent="setToReserve" class="btn-link"><icon class="fa fa-check" /></a>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -3612,9 +3612,8 @@ class StockMove(TransactionCase):
|
||||
def test_edit_reserved_move_line_9(self):
|
||||
"""
|
||||
When writing on the reserved quantity on the SML, a process tries to
|
||||
reserve the quants with that new quantity. If it fails (for instance
|
||||
because the written quantity is more than actually available), this
|
||||
quantity should be reset to 0.
|
||||
reserve the quants with that new quantity. If the written quantity is
|
||||
more than actually available, this quantity should be set to the available quantity.
|
||||
"""
|
||||
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 1.0)
|
||||
|
||||
@@ -3633,7 +3632,7 @@ class StockMove(TransactionCase):
|
||||
out_move.move_line_ids.reserved_uom_qty = 2
|
||||
|
||||
self.assertTrue(out_move.move_line_ids)
|
||||
self.assertEqual(out_move.move_line_ids.reserved_uom_qty, 0, "The reserved quantity should be cancelled")
|
||||
self.assertEqual(out_move.move_line_ids.reserved_uom_qty, 1, "The reserved quantity should be what is available")
|
||||
|
||||
def test_edit_done_move_line_1(self):
|
||||
""" Test that editing a done stock move line linked to an untracked product correctly and
|
||||
|
||||
@@ -796,3 +796,57 @@ class StockQuant(TransactionCase):
|
||||
# cache to ensure that the value will be the newest
|
||||
quant.invalidate_recordset(['quantity'])
|
||||
self.assertEqual(quant.quantity, 11)
|
||||
|
||||
def test_quant_reserve(self):
|
||||
""" Tests the reserve stock wizard which allows to choose a specific quant to reserve from
|
||||
also checks if editing the reserved_uom_qty on stock.move.line updates the quants
|
||||
"""
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': self.product.id,
|
||||
'quantity': 2,
|
||||
'location_id': self.stock_location.id
|
||||
})
|
||||
move_id = self.env['stock.move'].create({
|
||||
'name': 'move out',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_id._action_confirm()
|
||||
move_id._action_assign()
|
||||
self.assertEqual(move_id.state, 'assigned')
|
||||
self.assertEqual(len(move_id.move_line_ids), 1)
|
||||
self.assertEqual(move_id.move_line_ids.reserved_uom_qty, 2)
|
||||
# available should be 0
|
||||
available_qty = self.env['stock.quant']._get_available_quantity(self.product, self.stock_location)
|
||||
self.assertEqual(available_qty, 0)
|
||||
# unreserve, qty available should be 2 and move state back to confirmed
|
||||
move_id.move_line_ids.reserved_uom_qty = 0
|
||||
available_qty = self.env['stock.quant']._get_available_quantity(self.product, self.stock_location)
|
||||
self.assertEqual(available_qty, 2)
|
||||
self.assertEqual(move_id.state, 'confirmed')
|
||||
# reserve qty again, checks to be able to reserve from confirmed state, and only reserve what's available
|
||||
move_id.move_line_ids.reserved_uom_qty = 4
|
||||
self.assertEqual(move_id.state, 'assigned')
|
||||
self.assertEqual(move_id.move_line_ids.reserved_uom_qty, 2)
|
||||
# unreserve and try to reserve from wizard
|
||||
move_id.move_line_ids.unlink()
|
||||
wiz_action = self.env['stock.move.line'].with_context(default_move_id=move_id.id).action_open_reserve_stock()
|
||||
wiz = self.env[wiz_action['res_model']].with_context(wiz_action['context']).create({})
|
||||
self.assertEqual(wiz.move_id.id, move_id.id)
|
||||
self.assertEqual(wiz.demand_qty, move_id.product_qty)
|
||||
self.assertEqual(len(wiz.quant_line_ids), 1)
|
||||
wiz.quant_line_ids.qty_to_reserve = 4
|
||||
with self.assertRaises(UserError):
|
||||
wiz.reserve_stock()
|
||||
wiz.quant_line_ids.qty_to_reserve = 2
|
||||
wiz.reserve_stock()
|
||||
self.assertEqual(len(move_id.move_line_ids), 1)
|
||||
self.assertEqual(move_id.state, 'assigned')
|
||||
self.assertEqual(move_id.move_line_ids.reserved_uom_qty, 2)
|
||||
move_id._set_quantities_to_reservation()
|
||||
move_id._action_done()
|
||||
self.assertEqual(move_id.state, 'done')
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<group>
|
||||
<label for="reserved_uom_qty" string="Quantity Reserved" attrs="{'invisible': [('state', '=', 'done')]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('state', '=', 'done')]}">
|
||||
<field name="reserved_uom_qty" readonly="1"/>
|
||||
<field name="reserved_uom_qty" attrs="{'readonly': [('id', '=', False)]}" />
|
||||
<field name="product_uom_id" options="{'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
|
||||
</div>
|
||||
<label for="qty_done" string="Quantity Done"/>
|
||||
|
||||
@@ -222,6 +222,10 @@
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom" decoration-muted="state == 'done' and is_locked == True" decoration-success="reserved_uom_qty==qty_done" decoration-danger="qty_done > reserved_uom_qty and state != 'done' and picking_code != 'incoming'">
|
||||
<control>
|
||||
<create name="add_move_line" string="Add a line"/>
|
||||
<button name="action_open_reserve_stock" string="Reserve stock" type="object" class="btn-link" />
|
||||
</control>
|
||||
<field name="company_id" invisible="1" force_save="1"/>
|
||||
<field name="picking_id" invisible="1" force_save="1"/>
|
||||
<field name="move_id" invisible="1" force_save="1" />
|
||||
@@ -250,7 +254,7 @@
|
||||
<field name="package_id" attrs="{'readonly': ['&', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]}" invisible="not context.get('show_package')" groups="stock.group_tracking_lot"/>
|
||||
<field name="result_package_id" attrs="{'readonly': ['&', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]}" groups="stock.group_tracking_lot" context="{'picking_id': picking_id}"/>
|
||||
<field name="owner_id" attrs="{'readonly': ['&', ('package_level_id', '!=', False), ('parent.picking_type_entire_packs', '=', True)]}" invisible="not context.get('show_owner')" groups="stock.group_tracking_owner"/>
|
||||
<field name="reserved_uom_qty" invisible="not context.get('show_reserved_quantity')" readonly="1"/>
|
||||
<field name="reserved_uom_qty" attrs="{'readonly': [('id', '=', False)]}" invisible="not context.get('show_reserved_quantity')"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="is_locked" invisible="1"/>
|
||||
<field name="picking_code" invisible="1"/>
|
||||
@@ -288,7 +292,7 @@
|
||||
<field name="lot_id" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', True)], 'invisible': [('lots_visible', '=', False)]}" context="{'default_product_id': product_id, 'default_company_id': company_id, 'active_picking_id': picking_id}" optional="show"/>
|
||||
<field name="lot_name" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', False)], 'invisible': [('lots_visible', '=', False)]}" context="{'default_product_id': product_id}"/>
|
||||
<field name="is_initial_demand_editable" invisible="1"/>
|
||||
<field name="reserved_uom_qty" readonly="1" attrs="{'column_invisible': ['|',('parent.immediate_transfer', '=', True),('parent.picking_type_code','=','incoming')]}" optional="show"/>
|
||||
<field name="reserved_uom_qty" attrs="{'column_invisible': ['|',('parent.immediate_transfer', '=', True),('parent.picking_type_code','=','incoming')], 'readonly': [('id', '=', False)]}" optional="show"/>
|
||||
<field name="is_locked" invisible="1"/>
|
||||
<field name="qty_done" attrs="{'readonly': [('state', 'in', ('done', 'cancel')), ('is_locked', '=', True)]}" force_save="1"/>
|
||||
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft'), ('id', '!=', False)]}" groups="uom.group_uom"/>
|
||||
|
||||
@@ -22,3 +22,4 @@ from . import stock_package_destination
|
||||
from . import stock_orderpoint_snooze
|
||||
from . import stock_request_count
|
||||
from . import stock_replenishment_info
|
||||
from . import stock_quant_reserve
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from odoo import _, api, Command, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReserveStockQuantLine(models.TransientModel):
|
||||
_name = 'stock.quant.reserve.line'
|
||||
_description = 'Choose quantity to reserve from each stock quant'
|
||||
|
||||
quant_id = fields.Many2one('stock.quant', required=True, readonly=True)
|
||||
location_id = fields.Many2one(related='quant_id.location_id')
|
||||
product_id = fields.Many2one(related='quant_id.product_id')
|
||||
product_uom_id = fields.Many2one(related='product_id.uom_id')
|
||||
lot_id = fields.Many2one(related='quant_id.lot_id')
|
||||
package_id = fields.Many2one(related='quant_id.package_id')
|
||||
available_quantity = fields.Float(related='quant_id.available_quantity', string='Available')
|
||||
qty_to_reserve = fields.Float('To reserve')
|
||||
|
||||
reserve_id = fields.Many2one('stock.quant.reserve')
|
||||
|
||||
|
||||
class ReserveStockQuant(models.TransientModel):
|
||||
_name = 'stock.quant.reserve'
|
||||
_description = 'Manually reserve stock'
|
||||
|
||||
move_id = fields.Many2one('stock.move')
|
||||
product_tracking = fields.Selection(related='move_id.has_tracking')
|
||||
product_uom_id = fields.Many2one(related='move_id.product_id.uom_id', readonly=True)
|
||||
demand_qty = fields.Float(compute='_compute_demand_qty', store=True)
|
||||
qty_to_reserve = fields.Float('Quantity to reserve', compute='_compute_qty_to_reserve')
|
||||
quant_line_ids = fields.One2many('stock.quant.reserve.line', 'reserve_id', compute='_compute_quant_line_ids',
|
||||
readonly=False, store=True)
|
||||
|
||||
@api.depends('quant_line_ids.qty_to_reserve', 'demand_qty')
|
||||
def _compute_qty_to_reserve(self):
|
||||
for wiz in self:
|
||||
wiz.qty_to_reserve = max(wiz.demand_qty - sum(wiz.quant_line_ids.mapped('qty_to_reserve')), 0)
|
||||
|
||||
@api.depends('move_id')
|
||||
def _compute_demand_qty(self):
|
||||
for wiz in self:
|
||||
wiz.demand_qty = wiz.move_id.product_qty - sum(wiz.move_id.move_line_ids.mapped('reserved_qty'))
|
||||
|
||||
@api.depends('move_id')
|
||||
def _compute_quant_line_ids(self):
|
||||
for wiz in self:
|
||||
move_id = wiz.move_id
|
||||
if not move_id:
|
||||
wiz.quant_line_ids = False
|
||||
continue
|
||||
if wiz.quant_line_ids:
|
||||
continue
|
||||
quant_line_cmds = [Command.clear()]
|
||||
quant_ids = self.env['stock.quant'].search([('product_id', '=', move_id.product_id.id),
|
||||
('location_id', 'child_of', move_id.location_id.id)])
|
||||
quant_line_cmds += [Command.create({'quant_id': quant.id}) for quant in quant_ids.filtered(lambda q: q.available_quantity > 0)]
|
||||
wiz.quant_line_ids = quant_line_cmds
|
||||
|
||||
def reserve_stock(self):
|
||||
move_line_vals = []
|
||||
for wiz in self:
|
||||
for line in wiz.quant_line_ids.filtered(lambda l: l.qty_to_reserve > 0):
|
||||
if line.qty_to_reserve > line.available_quantity:
|
||||
raise UserError(_('Cannot reserve more quantity than available!'))
|
||||
line.quant_id.reserved_quantity += line.qty_to_reserve
|
||||
move_line_vals.append(wiz.move_id._prepare_move_line_vals(quantity=line.qty_to_reserve, reserved_quant=line.quant_id))
|
||||
StockMoveLine = self.env['stock.move.line'].with_context(bypass_reservation_update=True)
|
||||
StockMoveLine.create(move_line_vals)
|
||||
self.move_id._recompute_state()
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_stock_quant_reserve_form" model="ir.ui.view">
|
||||
<field name="name">Reserve stock</field>
|
||||
<field name="model">stock.quant.reserve</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="move_id" invisible="1"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<label for="qty_to_reserve"/>
|
||||
<div class="o_row">
|
||||
<span><field name="qty_to_reserve" nolabel="1"/></span>
|
||||
<span><field name="product_uom_id" nolabel="1"/></span>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<div class="oe_grey">
|
||||
<p>Choose from the following available stock and enter the quantity to reserve.</p>
|
||||
</div>
|
||||
<field name="quant_line_ids" force_save="1">
|
||||
<tree editable="top" create="0">
|
||||
<field name="quant_id" invisible="1" force_save="1"/>
|
||||
<field name="product_id"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.product_tracking','=','none')]}"/>
|
||||
<field name="package_id" groups="stock.group_tracking_lot"/>
|
||||
<field name="available_quantity"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
<field name="qty_to_reserve"/>
|
||||
<widget name="stock_quant_reserve"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="reserve_stock" class="btn btn-primary" type="object" string="Confirm" data-hotkey="v"/>
|
||||
<button special="cancel" string="Cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user