[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:
svs-odoo
2019-05-29 13:59:01 +00:00
co-authored by sle-odoo
parent be27955136
commit da33eb36b8
18 changed files with 545 additions and 243 deletions
+6 -6
View File
@@ -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
+6 -6
View File
@@ -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()
# ------------------------------------
+3 -3
View File
@@ -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',
-1
View File
@@ -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',
+24 -38
View File
@@ -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()
+4 -4
View File
@@ -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()
+216 -2
View File
@@ -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 """
+1
View File
@@ -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)
+2 -29
View File
@@ -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'
-6
View File
@@ -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>
+42 -9
View File
@@ -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 &lt; 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 &lt; 0">
<tree create="0" delete="0" decoration-danger="quantity &lt; 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>
-1
View File
@@ -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>
+22 -3
View File
@@ -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()