[IMP] stock: editable quants
Made some changes in stock.quant model to allow user to modify product
quantity easier.
To allow the edition of quant without opening more permissions on the
model, we switch to sudo in create mode and write if the conditions are
met (special key in context + access rights on the current user + write
on a specific field).
Also, we have two list views for the quant:
- The old list view who still readonly.
- A new list view where user can create new quant or edit quantity
of existing ones
We display the second one if user is a stock manager.
For existing quants, user can modify counted quantity (which will modify
product quantity). User can also create new quant, in this case:
- The quant is really a new one (ex.: new product/location
association) so will simply create a new quant.
- A corresponding quant already exist, so will modify the existing
quant instead of create a new one.
When an user creates a new quant, the model will check each time a
non-quantity field (product, location, SN/LN, package or owner) is
modified to see if corresponding quant exists. In this case, it'll get
the quant ID and will update reserved and on hand quantities.
If user didn't change the inventory quantity, it'll be updated too.
On product form view, the 'Update quantity' button was removed as user
can now simply modify quant with 'On Hand' stat button.
Task #1935921
Co-authored-by: sle-odoo <sle@odoo.com>
This commit is contained in:
@@ -128,22 +128,22 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for waiting state')
|
||||
|
||||
# Update Inventory
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'new_quantity': 2.0,
|
||||
'inventory_quantity': 2.0,
|
||||
'location_id': self.ref('stock.stock_location_14')
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
|
||||
production_2.action_assign()
|
||||
# check sub product availability state is partially available
|
||||
self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for partially available state')
|
||||
|
||||
# Update Inventory
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'new_quantity': 5.0,
|
||||
'inventory_quantity': 5.0,
|
||||
'location_id': self.ref('stock.stock_location_14')
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
|
||||
production_2.action_assign()
|
||||
# check sub product availability state is assigned
|
||||
|
||||
@@ -54,11 +54,11 @@ class TestProcurement(TestMrpCommon):
|
||||
# Product2 48 Unit
|
||||
# ---------------------
|
||||
# Update Inventory
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'new_quantity': 48,
|
||||
'inventory_quantity': 48,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
produce_product_4.action_assign()
|
||||
self.assertEqual(produce_product_4.product_qty, 8, "Wrong quantity of finish product.")
|
||||
self.assertEqual(produce_product_4.product_uom_id, self.uom_dozen, "Wrong quantity of finish product.")
|
||||
@@ -83,11 +83,11 @@ class TestProcurement(TestMrpCommon):
|
||||
# ------------------
|
||||
|
||||
# Update Inventory
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_2.id,
|
||||
'new_quantity': 12,
|
||||
'inventory_quantity': 12,
|
||||
'location_id': self.warehouse.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
production_product_6.action_assign()
|
||||
|
||||
# ------------------------------------
|
||||
|
||||
@@ -307,11 +307,11 @@ class TestWorkOrderProcess(TestMrpCommon):
|
||||
man_order = man_order_form.save()
|
||||
# reset quantities
|
||||
self.product_1.type = "product"
|
||||
self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_1.id,
|
||||
'new_quantity': 0.0,
|
||||
'inventory_quantity': 0.0,
|
||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||
}).change_product_qty()
|
||||
})
|
||||
|
||||
(self.product_2 | self.product_4).write({
|
||||
'tracking': 'none',
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
'report/product_templates.xml',
|
||||
'report/product_packaging.xml',
|
||||
|
||||
'wizard/stock_change_product_qty_views.xml',
|
||||
'wizard/stock_picking_return_views.xml',
|
||||
'wizard/stock_scheduler_compute_views.xml',
|
||||
'wizard/stock_immediate_transfer_views.xml',
|
||||
|
||||
@@ -379,9 +379,6 @@ class Product(models.Model):
|
||||
res['fields']['qty_available']['string'] = _('Produced Qty')
|
||||
return res
|
||||
|
||||
def action_update_quantity_on_hand(self):
|
||||
return self.product_tmpl_id.with_context({'default_product_id': self.id}).action_update_quantity_on_hand()
|
||||
|
||||
def action_view_routes(self):
|
||||
return self.mapped('product_tmpl_id').action_view_routes()
|
||||
|
||||
@@ -408,12 +405,30 @@ class Product(models.Model):
|
||||
return action
|
||||
|
||||
def action_open_quants(self):
|
||||
self.ensure_one()
|
||||
self.env['stock.quant']._quant_tasks()
|
||||
action = self.env.ref('stock.product_open_quants').read()[0]
|
||||
location_domain = self._get_domain_locations()[0]
|
||||
action['domain'] = expression.AND([[('product_id', '=', self.id)], location_domain])
|
||||
return action
|
||||
domain = expression.AND([[('product_id', 'in', self.ids)], location_domain])
|
||||
self = self.with_context(hide_location=not self.user_has_groups('stock.group_stock_multi_locations'))
|
||||
|
||||
# If user have rights to write on quant, we define the view as editable.
|
||||
if self.user_has_groups('stock.group_stock_manager'):
|
||||
self = self.with_context(inventory_mode=True)
|
||||
# Set default location id if multilocations is inactive
|
||||
if not self.user_has_groups('stock.group_stock_multi_locations'):
|
||||
user_company = self.env.user.company_id
|
||||
warehouse = self.env['stock.warehouse'].search(
|
||||
[('company_id', '=', user_company.id)], limit=1
|
||||
)
|
||||
if warehouse:
|
||||
self = self.with_context(default_location_id=warehouse.lot_stock_id.id)
|
||||
# Set default product id if quants concern only one product
|
||||
if len(self) == 1:
|
||||
self = self.with_context(
|
||||
default_product_id=self.id,
|
||||
single_product=True
|
||||
)
|
||||
else:
|
||||
self = self.with_context(product_tmpl_id=self.product_tmpl_id.id)
|
||||
return self.env['stock.quant']._get_quants_action(domain)
|
||||
|
||||
@api.model
|
||||
def get_theoretical_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, to_uom=None):
|
||||
@@ -609,37 +624,8 @@ class ProductTemplate(models.Model):
|
||||
raise UserError(_("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."))
|
||||
return super(ProductTemplate, self).write(vals)
|
||||
|
||||
def action_update_quantity_on_hand(self):
|
||||
default_product_id = self.env.context.get('default_product_id', self.product_variant_id.id)
|
||||
if self.env.user.user_has_groups('stock.group_stock_multi_locations') or (self.env.user.user_has_groups('stock.group_production_lot') and self.tracking != 'none'):
|
||||
product_ref_name = self.name + ' - ' + datetime.today().strftime('%m/%d/%y')
|
||||
ctx = {'default_filter': 'product', 'default_product_id': default_product_id, 'default_name': product_ref_name}
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.inventory',
|
||||
'context': ctx,
|
||||
}
|
||||
else:
|
||||
wiz = self.env['stock.change.product.qty'].create({'product_id': default_product_id})
|
||||
return {
|
||||
'name': _('Update quantity on hand'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.change.product.qty',
|
||||
'target': 'new',
|
||||
'res_id': wiz.id,
|
||||
'context': {'default_product_id': self.env.context.get('default_product_id')}
|
||||
}
|
||||
|
||||
def action_open_quants(self):
|
||||
self.env['stock.quant']._quant_tasks()
|
||||
products = self.mapped('product_variant_ids')
|
||||
action = self.env.ref('stock.product_open_quants').read()[0]
|
||||
location_domain = products._get_domain_locations()[0]
|
||||
action['domain'] = expression.AND([[('product_id', 'in', products.ids)], location_domain])
|
||||
return action
|
||||
return self.product_variant_ids.action_open_quants()
|
||||
|
||||
def action_view_related_putaway_rules(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -59,7 +59,7 @@ class ProductionLot(models.Model):
|
||||
self.product_qty = sum(quants.mapped('quantity'))
|
||||
|
||||
def action_lot_open_quants(self):
|
||||
self.env['stock.quant']._quant_tasks()
|
||||
action = self.env.ref('stock.lot_open_quants').read()[0]
|
||||
action['context'] = {'search_default_lot_id': self.id}
|
||||
return action
|
||||
self = self.with_context(search_default_lot_id=self.id)
|
||||
if self.user_has_groups('stock.group_stock_manager'):
|
||||
self = self.with_context(inventory_mode=True)
|
||||
return self.env['stock.quant']._get_quants_action()
|
||||
|
||||
@@ -6,7 +6,7 @@ from psycopg2 import OperationalError, Error
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
||||
|
||||
import logging
|
||||
|
||||
@@ -18,8 +18,22 @@ class StockQuant(models.Model):
|
||||
_description = 'Quants'
|
||||
_rec_name = 'product_id'
|
||||
|
||||
def _domain_location_id(self):
|
||||
if not self._is_inventory_mode():
|
||||
return
|
||||
return ['&', ('company_id', '=', self.env.user.company_id.id), ('usage', 'in', ['internal', 'transit'])]
|
||||
|
||||
def _domain_product_id(self):
|
||||
if not self._is_inventory_mode():
|
||||
return
|
||||
domain = [('type', '=', 'product')]
|
||||
if self.env.context.get('product_tmpl_id'):
|
||||
domain = expression.AND([domain, [('product_tmpl_id', '=', self.env.context['product_tmpl_id'])]])
|
||||
return domain
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product',
|
||||
domain=lambda self: self._domain_product_id(),
|
||||
ondelete='restrict', readonly=True, required=True)
|
||||
# so user can filter on template in webclient
|
||||
product_tmpl_id = fields.Many2one(
|
||||
@@ -32,6 +46,7 @@ class StockQuant(models.Model):
|
||||
string='Company', store=True, readonly=True)
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Location',
|
||||
domain=lambda self: self._domain_location_id(),
|
||||
auto_join=True, ondelete='restrict', readonly=True, required=True)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Lot/Serial Number',
|
||||
@@ -45,13 +60,93 @@ class StockQuant(models.Model):
|
||||
quantity = fields.Float(
|
||||
'Quantity',
|
||||
help='Quantity of products in this quant, in the default unit of measure of the product',
|
||||
readonly=True, required=True, oldname='qty')
|
||||
readonly=True, oldname='qty')
|
||||
inventory_quantity = fields.Float(
|
||||
'Inventoried Quantity', compute='_compute_inventory_quantity',
|
||||
inverse='_set_inventory_quantity', groups='stock.group_stock_manager')
|
||||
reserved_quantity = fields.Float(
|
||||
'Reserved Quantity',
|
||||
default=0.0,
|
||||
help='Quantity of reserved products in this quant, in the default unit of measure of the product',
|
||||
readonly=True, required=True)
|
||||
in_date = fields.Datetime('Incoming Date', readonly=True)
|
||||
tracking = fields.Selection(related='product_id.tracking', readonly=True)
|
||||
|
||||
@api.depends('quantity')
|
||||
def _compute_inventory_quantity(self):
|
||||
if not self._is_inventory_mode():
|
||||
return
|
||||
for quant in self:
|
||||
quant.inventory_quantity = quant.quantity
|
||||
|
||||
def _set_inventory_quantity(self):
|
||||
""" Inverse method to create stock move when `inventory_quantity` is set
|
||||
(`inventory_quantity` is only accessible in inventory mode).
|
||||
"""
|
||||
if not self._is_inventory_mode():
|
||||
return
|
||||
for quant in self:
|
||||
# Get the quantity to create a move for.
|
||||
rounding = quant.product_id.uom_id.rounding
|
||||
diff = float_round(quant.inventory_quantity - quant.quantity, precision_rounding=rounding)
|
||||
diff_float_compared = float_compare(diff, 0, precision_rounding=rounding)
|
||||
# Create and vaidate a move so that the quant matches its `inventory_quantity`.
|
||||
if diff_float_compared == 0:
|
||||
continue
|
||||
elif diff_float_compared > 0:
|
||||
move_vals = self._get_inventory_move_values(diff, self.product_id.property_stock_inventory, self.location_id)
|
||||
else:
|
||||
move_vals = self._get_inventory_move_values(-diff, self.location_id, self.product_id.property_stock_inventory, out=True)
|
||||
move = self.env['stock.move'].with_context(inventory_mode=False).create(move_vals)
|
||||
move._action_done()
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
""" Override to handle the "inventory mode" and create a quant as
|
||||
superuser the conditions are met.
|
||||
"""
|
||||
if self._is_inventory_mode() and 'inventory_quantity' in vals:
|
||||
allowed_fields = self._get_inventory_fields_create()
|
||||
if any([field for field in vals.keys() if field not in allowed_fields]):
|
||||
raise UserError(_("Quant's creation is restricted, you can't do this operation."))
|
||||
inventory_quantity = vals.pop('inventory_quantity')
|
||||
|
||||
# Create an empty quant or write on a similar one.
|
||||
product = self.env['product.product'].browse(vals['product_id'])
|
||||
location = self.env['stock.location'].browse(vals['location_id'])
|
||||
lot_id = self.env['stock.production.lot'].browse(vals.get('lot_id'))
|
||||
package_id = self.env['stock.quant.package'].browse(vals.get('package_id'))
|
||||
owner_id = self.env['res.partner'].browse(vals.get('owner_id'))
|
||||
quant = self._gather(product, location, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
if quant:
|
||||
quant = quant[0]
|
||||
else:
|
||||
quant = self.sudo().create(vals)
|
||||
# Set the `inventory_quantity` field to create the necessary move.
|
||||
quant.inventory_quantity = inventory_quantity
|
||||
return quant
|
||||
return super(StockQuant, self).create(vals)
|
||||
|
||||
@api.model
|
||||
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
""" Override to handle the "inventory mode" and set the `inventory_quantity`
|
||||
in view list grouped.
|
||||
"""
|
||||
result = super(StockQuant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
|
||||
if self._is_inventory_mode():
|
||||
for record in result:
|
||||
record['inventory_quantity'] = record.get('quantity', 0)
|
||||
return result
|
||||
|
||||
def write(self, vals):
|
||||
""" Override to handle the "inventory mode" and create the inventory move. """
|
||||
if self._is_inventory_mode() and 'inventory_quantity' in vals:
|
||||
allowed_fields = self._get_inventory_fields_write()
|
||||
if any([field for field in vals.keys() if field not in allowed_fields]):
|
||||
raise UserError(_("Quant's edition is restricted, you can't do this operation."))
|
||||
self = self.sudo()
|
||||
return super(StockQuant, self).write(vals)
|
||||
return super(StockQuant, self).write(vals)
|
||||
|
||||
def action_view_stock_moves(self):
|
||||
self.ensure_one()
|
||||
@@ -180,6 +275,23 @@ class StockQuant(models.Model):
|
||||
else:
|
||||
return sum([available_quantity for available_quantity in availaible_quantities.values() if float_compare(available_quantity, 0, precision_rounding=rounding) > 0])
|
||||
|
||||
@api.onchange('location_id', 'product_id', 'lot_id', 'package_id', 'owner_id')
|
||||
def _onchange_location_or_product_id(self):
|
||||
if self.lot_id and self.tracking == 'none':
|
||||
self.lot_id = None
|
||||
if self.product_id and self.location_id:
|
||||
quants = self._gather(self.product_id, self.location_id, lot_id=self.lot_id, package_id=self.package_id, owner_id=self.owner_id, strict=True)
|
||||
quantity_on_hand = sum(quants.mapped('quantity'))
|
||||
reserved_quantity = sum(quants.mapped('reserved_quantity'))
|
||||
values_to_update = {
|
||||
'quantity': quantity_on_hand,
|
||||
'reserved_quantity': reserved_quantity
|
||||
}
|
||||
# We update 'inventory_quantity' only if user didn't modify it
|
||||
if self.inventory_quantity == self.quantity:
|
||||
values_to_update['inventory_quantity'] = quantity_on_hand
|
||||
self.update(values_to_update)
|
||||
|
||||
@api.model
|
||||
def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None):
|
||||
""" Increase or decrease `reserved_quantity` of a set of quants for a given set of
|
||||
@@ -341,6 +453,108 @@ class StockQuant(models.Model):
|
||||
self._merge_quants()
|
||||
self._unlink_zero_quants()
|
||||
|
||||
@api.model
|
||||
def _is_inventory_mode(self):
|
||||
""" Used to control whether a quant was written on or created during an
|
||||
"inventory session", meaning a mode where we need to create the stock.move
|
||||
record necessary to be consistent with the `inventory_quantity` field.
|
||||
"""
|
||||
return self.env.context.get('inventory_mode') is True and self.user_has_groups('stock.group_stock_manager')
|
||||
|
||||
@api.model
|
||||
def _get_inventory_fields_create(self):
|
||||
""" Returns a list of fields user can edit when he want to create a quant in `inventory_mode`.
|
||||
"""
|
||||
return ['product_id', 'location_id', 'lot_id', 'package_id', 'owner_id', 'inventory_quantity']
|
||||
|
||||
@api.model
|
||||
def _get_inventory_fields_write(self):
|
||||
""" Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`.
|
||||
"""
|
||||
return ['inventory_quantity']
|
||||
|
||||
def _get_inventory_move_values(self, qty, location_id, location_dest_id, out=False):
|
||||
""" Called when user manually set a new quantity (via `inventory_quantity`)
|
||||
just before creating the corresponding stock move.
|
||||
|
||||
:param location_id: `stock.location`
|
||||
:param location_dest_id: `stock.location`
|
||||
:param out: boolean to set on True when the move go to inventory adjustment location.
|
||||
:return: dict with all values needed to create a new `stock.move` with its move line.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Product Quantity Updated'),
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom': self.product_uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'company_id': self.company_id.id,
|
||||
'state': 'confirmed',
|
||||
'location_id': location_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'qty_done': qty,
|
||||
'location_id': location_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
'lot_id': self.lot_id.id,
|
||||
'package_id': out and self.package_id.id or False,
|
||||
'result_package_id': (not out) and self.package_id.id or False,
|
||||
'owner_id': self.owner_id.id,
|
||||
})]
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_quants_action(self, domain=None, extend=False):
|
||||
""" Returns an action to open quant view.
|
||||
Depending of the context (user have right to be inventory mode or not),
|
||||
the list view will be editable or readonly.
|
||||
|
||||
:param domain: List for the domain, empty by default.
|
||||
:param extend: If True, enables form, graph and pivot views. False by default.
|
||||
"""
|
||||
self._quant_tasks()
|
||||
action = {
|
||||
'name': _('Stock On Hand'),
|
||||
'view_type': 'tree',
|
||||
'view_mode': 'list',
|
||||
'res_model': 'stock.quant',
|
||||
'type': 'ir.actions.act_window',
|
||||
'context': self.env.context,
|
||||
'domain': domain or [],
|
||||
'help': """
|
||||
<p class="o_view_nocontent_empty_folder">No stock on hand</p>
|
||||
<p>This analysis gives you an overview on the current stock
|
||||
level of your products.</p>
|
||||
"""
|
||||
}
|
||||
|
||||
if self._is_inventory_mode():
|
||||
action['view_id'] = self.env.ref('stock.view_stock_quant_tree_editable').id
|
||||
else:
|
||||
action['view_id'] = self.env.ref('stock.view_stock_quant_tree').id
|
||||
# Enables form view in readonly list
|
||||
action.update({
|
||||
'view_mode': 'tree,form',
|
||||
'views': [
|
||||
(action['view_id'], 'list'),
|
||||
(self.env.ref('stock.view_stock_quant_form').id, 'form'),
|
||||
],
|
||||
})
|
||||
if extend:
|
||||
action.update({
|
||||
'view_mode': 'tree,form,pivot,graph',
|
||||
'views': [
|
||||
(action['view_id'], 'list'),
|
||||
(self.env.ref('stock.view_stock_quant_form').id, 'form'),
|
||||
(self.env.ref('stock.view_stock_quant_pivot').id, 'pivot'),
|
||||
(self.env.ref('stock.stock_quant_view_graph').id, 'graph'),
|
||||
],
|
||||
})
|
||||
return action
|
||||
|
||||
|
||||
class QuantPackage(models.Model):
|
||||
""" Packages containing quants and/or other packages """
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import test_product
|
||||
from . import test_warehouse
|
||||
from . import test_stock_location_search
|
||||
from . import test_quant
|
||||
from . import test_quant_inventory_mode
|
||||
from . import test_inventory
|
||||
from . import test_move
|
||||
from . import test_move2
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
|
||||
class TestEditableQuant(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestEditableQuant, cls).setUpClass()
|
||||
|
||||
# Shortcut to call `stock.quant` with `inventory mode` set in the context
|
||||
cls.Quant = cls.env['stock.quant'].with_context(inventory_mode=True)
|
||||
|
||||
Product = cls.env['product.product']
|
||||
Location = cls.env['stock.location']
|
||||
cls.product = Product.create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.product2 = Product.create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.product_tracked_sn = Product.create({
|
||||
'name': 'Product tracked by SN',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.warehouse = Location.create({
|
||||
'name': 'Warehouse',
|
||||
'usage': 'internal',
|
||||
})
|
||||
cls.stock = Location.create({
|
||||
'name': 'Stock',
|
||||
'usage': 'internal',
|
||||
'location_id': cls.warehouse.id,
|
||||
})
|
||||
cls.room1 = Location.create({
|
||||
'name': 'Room A',
|
||||
'usage': 'internal',
|
||||
'location_id': cls.stock.id,
|
||||
})
|
||||
cls.room2 = Location.create({
|
||||
'name': 'Room B',
|
||||
'usage': 'internal',
|
||||
'location_id': cls.stock.id,
|
||||
})
|
||||
cls.inventory_loss = cls.product.property_stock_inventory
|
||||
|
||||
def test_create_quant_1(self):
|
||||
""" Create a new quant who don't exist yet.
|
||||
"""
|
||||
# Checks we don't have any quant for this product.
|
||||
quants = self.env['stock.quant'].search([('product_id', '=', self.product.id)])
|
||||
self.assertEqual(len(quants), 0)
|
||||
self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.stock.id,
|
||||
'inventory_quantity': 24
|
||||
})
|
||||
quants = self.env['stock.quant'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
('quantity', '>', 0),
|
||||
])
|
||||
# Checks we have now a quant, and also checks the quantity is equals to
|
||||
# what we set in `inventory_quantity` field.
|
||||
self.assertEqual(len(quants), 1)
|
||||
self.assertEqual(quants.quantity, 24)
|
||||
|
||||
stock_move = self.env['stock.move'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
])
|
||||
self.assertEqual(stock_move.location_id.id, self.inventory_loss.id)
|
||||
self.assertEqual(stock_move.location_dest_id.id, self.stock.id)
|
||||
|
||||
def test_create_quant_2(self):
|
||||
""" Try to create a quant who already exist.
|
||||
Must update the existing quant instead of creating a new one.
|
||||
"""
|
||||
# Creates a quants...
|
||||
first_quant = self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 12,
|
||||
})
|
||||
quants = self.env['stock.quant'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
('quantity', '>', 0),
|
||||
])
|
||||
self.assertEqual(len(quants), 1)
|
||||
# ... then try to create an another quant for the same product/location.
|
||||
second_quant = self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'inventory_quantity': 24,
|
||||
})
|
||||
quants = self.env['stock.quant'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
('quantity', '>', 0),
|
||||
])
|
||||
# Checks we still have only one quant, and first quant quantity was
|
||||
# updated, and second quant had the same ID than the first quant.
|
||||
self.assertEqual(len(quants), 1)
|
||||
self.assertEqual(first_quant.quantity, 24)
|
||||
self.assertEqual(first_quant.id, second_quant.id)
|
||||
stock_move = self.env['stock.move'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
])
|
||||
self.assertEqual(len(stock_move), 1)
|
||||
|
||||
def test_create_quant_3(self):
|
||||
""" Try to create a quant with `inventory_quantity` but not in inventory mode.
|
||||
Creates two quants not in inventory mode:
|
||||
- One with `quantity` (this one must be OK)
|
||||
- One with `inventory_quantity` (this one must be null)
|
||||
"""
|
||||
valid_quant = self.env['stock.quant'].create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 10,
|
||||
})
|
||||
invalid_quant = self.env['stock.quant'].create({
|
||||
'product_id': self.product2.id,
|
||||
'location_id': self.room1.id,
|
||||
'inventory_quantity': 20,
|
||||
})
|
||||
self.assertEqual(valid_quant.quantity, 10)
|
||||
self.assertEqual(invalid_quant.quantity, 0)
|
||||
|
||||
def test_create_quant_4(self):
|
||||
""" Try to create tree quants in inventory mode with `quantity` and/or `inventory_quantity`.
|
||||
Creates two quants not in inventory mode:
|
||||
- One with `quantity` (this one must be OK, but `inventory_mode` is useless here as it
|
||||
doesn't enter in the inventory mode case and create quant as usual)
|
||||
- One with `inventory_quantity` (this one must be OK)
|
||||
- One with the two values (this one must raises an error as it enters in the inventory
|
||||
mode but user can't edit directly `quantity` in inventory mode)
|
||||
"""
|
||||
valid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 10,
|
||||
})
|
||||
inventoried_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product2.id,
|
||||
'location_id': self.room1.id,
|
||||
'inventory_quantity': 20,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
invalid_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room2.id,
|
||||
'quantity': 10,
|
||||
'inventory_quantity': 20,
|
||||
})
|
||||
self.assertEqual(valid_quant.quantity, 10)
|
||||
self.assertEqual(inventoried_quant.quantity, 20)
|
||||
|
||||
def test_edit_quant_1(self):
|
||||
""" Increases manually quantity of a quant.
|
||||
"""
|
||||
quant = self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 12,
|
||||
})
|
||||
quant.inventory_quantity = 24
|
||||
self.assertEqual(quant.quantity, 24)
|
||||
stock_move = self.env['stock.move'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
])
|
||||
self.assertEqual(stock_move.location_id.id, self.inventory_loss.id)
|
||||
self.assertEqual(stock_move.location_dest_id.id, self.room1.id)
|
||||
|
||||
def test_edit_quant_2(self):
|
||||
""" Decreases manually quantity of a quant.
|
||||
"""
|
||||
quant = self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 12,
|
||||
})
|
||||
quant.inventory_quantity = 8
|
||||
self.assertEqual(quant.quantity, 8)
|
||||
stock_move = self.env['stock.move'].search([
|
||||
('product_id', '=', self.product.id),
|
||||
])
|
||||
self.assertEqual(stock_move.location_id.id, self.room1.id)
|
||||
self.assertEqual(stock_move.location_dest_id.id, self.inventory_loss.id)
|
||||
|
||||
def test_edit_quant_3(self):
|
||||
""" Try to edit a record without the inventory mode.
|
||||
Must raise an error.
|
||||
"""
|
||||
user_demo = self.env.ref('base.user_demo')
|
||||
user_admin = self.env.ref('base.user_admin')
|
||||
quant = self.Quant.create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.room1.id,
|
||||
'quantity': 12
|
||||
})
|
||||
self.assertEqual(quant.quantity, 12)
|
||||
# Try to write on quant without permission
|
||||
with self.assertRaises(AccessError):
|
||||
quant.sudo(user_demo).write({'inventory_quantity': 8})
|
||||
self.assertEqual(quant.quantity, 12)
|
||||
|
||||
# Try to write on quant with permission
|
||||
quant.sudo(user_admin).write({'inventory_quantity': 8})
|
||||
self.assertEqual(quant.quantity, 8)
|
||||
@@ -7,12 +7,11 @@ from odoo.tests import Form
|
||||
class TestWarehouse(TestStockCommon):
|
||||
def test_inventory_product(self):
|
||||
self.product_1.type = 'product'
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
product_1_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product_1.id,
|
||||
'new_quantity': 50.0,
|
||||
'inventory_quantity': 50.0,
|
||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
inventory = self.env['stock.inventory'].sudo(self.user_stock_manager).create({
|
||||
'name': 'Starting for product_1',
|
||||
'filter': 'product',
|
||||
@@ -47,32 +46,6 @@ class TestWarehouse(TestStockCommon):
|
||||
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.warehouse_1.wh_input_stock_loc_id).quantity, 0.0)
|
||||
self.assertEqual(self.env['stock.quant']._gather(self.product_1, self.env.ref('stock.stock_location_stock')).quantity, 0.0)
|
||||
|
||||
def test_inventory_wizard(self):
|
||||
self.product_1.type = 'product'
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
'product_id': self.product_1.id,
|
||||
'new_quantity': 50.0,
|
||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
# Check inventory performed in setup was effectivley performed
|
||||
self.assertEqual(self.product_1.virtual_available, 50.0)
|
||||
self.assertEqual(self.product_1.qty_available, 50.0)
|
||||
|
||||
# Check inventory obj details (1 inventory with 1 line, because 1 product change)
|
||||
inventory = self.env['stock.inventory'].search([('id', 'not in', self.existing_inventories.ids)])
|
||||
self.assertEqual(len(inventory), 1)
|
||||
self.assertIn('INV: %s' % self.product_1.display_name, inventory.name)
|
||||
self.assertEqual(len(inventory.line_ids), 1)
|
||||
self.assertEqual(inventory.line_ids.product_id, self.product_1)
|
||||
self.assertEqual(inventory.line_ids.product_qty, 50.0)
|
||||
|
||||
# Check associated quants: 2 quants for the product and the quantity (1 in stock, 1 in inventory adjustment)
|
||||
quant = self.env['stock.quant'].search([('id', 'not in', self.existing_quants.ids)])
|
||||
self.assertEqual(len(quant), 2)
|
||||
# print quant.name, quant.product_id, quant.location_id
|
||||
# TDE TODO: expand this test
|
||||
|
||||
def test_basic_move(self):
|
||||
product = self.product_3.sudo(self.user_stock_manager)
|
||||
product.type = 'product'
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<sheet position="before">
|
||||
<header>
|
||||
<button name="action_update_quantity_on_hand" string="Update Qty On Hand" type="object" attrs="{'invisible': [('type', '!=', 'product')]}"/>
|
||||
<button string="Replenish" type="action"
|
||||
name="%(action_product_replenish)d"
|
||||
context="{'default_product_id': id}"
|
||||
@@ -198,10 +197,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<header position="inside">
|
||||
<button string="Update Qty On Hand"
|
||||
type="object"
|
||||
name="action_update_quantity_on_hand"
|
||||
attrs="{'invisible': [('type', '!=', 'product')]}"/>
|
||||
<button string="Replenish" type="action"
|
||||
name="%(action_product_replenish)d"
|
||||
context="{'default_product_id': id}"
|
||||
@@ -292,7 +287,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<header position="inside" >
|
||||
<button name="action_update_quantity_on_hand" string="Update Qty On Hand" type="object" attrs="{'invisible': [('type', '!=', 'product')]}"/>
|
||||
<button string="Replenish" type="action"
|
||||
name="%(action_product_replenish)d"
|
||||
context="{'default_product_tmpl_id': id}"
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.actions.act_window" id="lot_open_quants">
|
||||
<field name="name">Quants</field>
|
||||
<field name="context">{'search_default_lot_id': active_id}</field>
|
||||
<field name="res_model">stock.quant</field>
|
||||
</record>
|
||||
|
||||
<record id="view_production_lot_form" model="ir.ui.view">
|
||||
<field name="name">stock.production.lot.form</field>
|
||||
<field name="model">stock.production.lot</field>
|
||||
|
||||
@@ -69,19 +69,57 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_stock_quant_tree_editable">
|
||||
<field name="name">stock.quant.tree.editable</field>
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-danger="inventory_quantity < 0" editable="top"
|
||||
create="1" edit="1">
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"
|
||||
readonly="context.get('single_product', False)" force_save="1"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="location_id" attrs="{'readonly': [('id', '!=', False)]}"
|
||||
invisible="context.get('hide_location', False)"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot"
|
||||
attrs="{
|
||||
'readonly': ['|', ('id', '!=', False), ('tracking', 'not in', ['serial', 'lot'])],
|
||||
'required': [('tracking', '!=', 'none')]
|
||||
}"
|
||||
context="{'default_product_id': product_id}"
|
||||
domain="[('product_id', '=', product_id)]"/>
|
||||
<field name="package_id" groups="stock.group_tracking_lot"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="reserved_quantity"/>
|
||||
<field name="quantity" string="On Hand Quantity"
|
||||
invisible="1"/>
|
||||
<field name="inventory_quantity" string="On Hand Quantity"
|
||||
readonly="0"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
<field name='company_id' groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_stock_quant_tree">
|
||||
<field name="name">stock.quant.tree</field>
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Inventory Valuation" create="0" delete="0" decoration-danger="quantity < 0">
|
||||
<tree create="0" delete="0" decoration-danger="quantity < 0">
|
||||
<field name="product_id"/>
|
||||
<field name="location_id" options="{'no_create': True}"/>
|
||||
<field name="location_id" invisible="context.get('hide_location', False)"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" groups="stock.group_tracking_lot"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="reserved_quantity" string="Reserved"/>
|
||||
<field name="quantity" string="On Hand"/>
|
||||
<field name="reserved_quantity"/>
|
||||
<field name="quantity"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
<field name='company_id' groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
@@ -135,11 +173,6 @@
|
||||
<field name="context">{'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1}</field>
|
||||
<field name="domain">[('product_id.product_tmpl_id', '=', active_id)]</field>
|
||||
<field name="res_model">stock.quant</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="product_open_quants"> <!-- product_view + python -->
|
||||
<field name="name">Stock On Hand</field>
|
||||
<field name="domain">[('product_id', '=', active_id)]</field>
|
||||
<field name="res_model">stock.quant</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="location_open_quants"> <!-- Used in location -->
|
||||
<field name="context">{'search_default_productgroup': 1}</field>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_picking_return
|
||||
from . import stock_change_product_qty
|
||||
from . import stock_scheduler_compute
|
||||
from . import stock_immediate_transfer
|
||||
from . import stock_backorder_confirmation
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields, tools, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProductChangeQuantity(models.TransientModel):
|
||||
_name = "stock.change.product.qty"
|
||||
_description = "Change Product Quantity"
|
||||
|
||||
# TDE FIXME: strange dfeault method, was present before migration ? to check
|
||||
product_id = fields.Many2one('product.product', 'Product', required=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Template', required=True)
|
||||
product_variant_count = fields.Integer('Variant Count', related='product_tmpl_id.product_variant_count', readonly=False)
|
||||
new_quantity = fields.Float(
|
||||
'New Quantity on Hand', default=1,
|
||||
digits=dp.get_precision('Product Unit of Measure'), required=True,
|
||||
help='This quantity is expressed in the Default Unit of Measure of the product.')
|
||||
location_id = fields.Many2one('stock.location', 'Location', required=True, domain="[('usage', '=', 'internal')]")
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(ProductChangeQuantity, self).default_get(fields)
|
||||
if 'location_id' in fields and not res.get('location_id'):
|
||||
company_user = self.env.company
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
|
||||
if warehouse:
|
||||
res['location_id'] = warehouse.lot_stock_id.id
|
||||
return res
|
||||
|
||||
@api.onchange('location_id', 'product_id')
|
||||
def onchange_location_id(self):
|
||||
# TDE FIXME: should'nt we use context / location ?
|
||||
if self.location_id and self.product_id:
|
||||
availability = self.product_id.with_context(compute_child=False)._product_available()
|
||||
self.new_quantity = availability[self.product_id.id]['qty_available']
|
||||
|
||||
@api.onchange('product_id')
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.product_tmpl_id = self.onchange_product_id_dict(self.product_id.id)['product_tmpl_id']
|
||||
|
||||
def _action_start_line(self):
|
||||
product = self.product_id.with_context(location=self.location_id.id)
|
||||
th_qty = product.qty_available
|
||||
|
||||
res = {
|
||||
'product_qty': self.new_quantity,
|
||||
'location_id': self.location_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'theoretical_qty': th_qty,
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def onchange_product_id_dict(self, product_id):
|
||||
return {
|
||||
'product_tmpl_id': self.env['product.product'].browse(product_id).product_tmpl_id.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if values.get('product_id'):
|
||||
values.update(self.onchange_product_id_dict(values['product_id']))
|
||||
return super(ProductChangeQuantity, self).create(values)
|
||||
|
||||
@api.constrains('new_quantity')
|
||||
def check_new_quantity(self):
|
||||
if any(wizard.new_quantity < 0 for wizard in self):
|
||||
raise UserError(_('Quantity cannot be negative.'))
|
||||
|
||||
def change_product_qty(self):
|
||||
""" Changes the Product Quantity by making a Physical Inventory. """
|
||||
Inventory = self.env['stock.inventory']
|
||||
for wizard in self:
|
||||
product = wizard.product_id.with_context(location=wizard.location_id.id)
|
||||
line_data = wizard._action_start_line()
|
||||
|
||||
if wizard.product_id.id:
|
||||
inventory_filter = 'product'
|
||||
else:
|
||||
inventory_filter = 'none'
|
||||
inventory = Inventory.create({
|
||||
'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name),
|
||||
'filter': inventory_filter,
|
||||
'product_id': wizard.product_id.id,
|
||||
'location_id': wizard.location_id.id,
|
||||
'line_ids': [(0, 0, line_data)],
|
||||
})
|
||||
inventory.action_validate()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_change_product_quantity" model="ir.ui.view">
|
||||
<field name="name">Change Product Quantity</field>
|
||||
<field name="model">stock.change.product.qty</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Update Product Quantity">
|
||||
<group>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
<field name="product_variant_count" invisible="1"/>
|
||||
<field name="product_id" widget="selection"
|
||||
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
|
||||
attrs="{'readonly': [('product_variant_count', '=', 1)]}"
|
||||
readonly="context.get('default_product_id')"/>
|
||||
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="new_quantity"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="change_product_qty" string="_Apply" type="object" class="btn-primary"/>
|
||||
<button string="_Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -33,6 +33,25 @@ class StockQuantityHistory(models.TransientModel):
|
||||
}
|
||||
return action
|
||||
else:
|
||||
self.env['stock.quant']._merge_quants()
|
||||
self.env['stock.quant']._unlink_zero_quants()
|
||||
return self.env.ref('stock.quantsact').read()[0]
|
||||
self = self.with_context(search_default_internal_loc=1)
|
||||
if self.user_has_groups('stock.group_production_lot,stock.group_stock_multi_locations'):
|
||||
# fixme: erase the following condition when it'll be possible to create a new record
|
||||
# from a empty grouped editable list without go through the form view.
|
||||
if self.env['stock.quant'].search_count([
|
||||
('company_id', '=', self.env.user.company_id.id),
|
||||
('location_id.usage', 'in', ['internal', 'transit'])
|
||||
]) > 0:
|
||||
self = self.with_context(
|
||||
search_default_productgroup=1,
|
||||
search_default_locationgroup=1
|
||||
)
|
||||
if not self.user_has_groups('stock.group_stock_multi_locations'):
|
||||
company_user = self.env.user.company_id
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
|
||||
if warehouse:
|
||||
self = self.with_context(default_location_id=warehouse.lot_stock_id.id)
|
||||
|
||||
# If user have rights to write on quant, we set quants in inventory mode.
|
||||
if self.user_has_groups('stock.group_stock_manager'):
|
||||
self = self.with_context(inventory_mode=True)
|
||||
return self.env['stock.quant']._get_quants_action(extend=True)
|
||||
|
||||
@@ -44,20 +44,18 @@ class TestWebsiteSaleStockProductWarehouse(TestWebsiteSaleProductAttributeValueC
|
||||
})
|
||||
|
||||
# Update quantity of Product A in Warehouse 1
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_1.id,
|
||||
'new_quantity': 10.0,
|
||||
'inventory_quantity': 10.0,
|
||||
'location_id': warehouse_1.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
|
||||
# Update quantity of Product B in Warehouse 2
|
||||
inventory_wizard = self.env['stock.change.product.qty'].create({
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product_2.id,
|
||||
'new_quantity': 10.0,
|
||||
'inventory_quantity': 10.0,
|
||||
'location_id': warehouse_2.lot_stock_id.id,
|
||||
})
|
||||
inventory_wizard.change_product_qty()
|
||||
|
||||
# Get current website and set warehouse_id of Warehouse 1
|
||||
current_website = self.env['website'].get_current_website()
|
||||
|
||||
Reference in New Issue
Block a user