[REF] stock,stock_account: archived products and quantities
PURPOSE Keep consistent behavior of the stat buttons and the different stock reports when archiving a product SPECIFICATION Currently, you can archive a product but the behavior of the different stock reports, stat buttons isn't coherent afterwards. Here are the different behaviors that should be improved: 1.Keep quantity on hand and forecasted quantity visible (in the stat button + when opening it) 2.Automatically (un)archive RR and BOM if the product is (un)archived 3.Valuation report: show the archived products without the "archived" filter (behaves already this way in the inventory and forecast reports) 4.When a product has been archived and still has quantity in stock, include it in inventory adjustments for which counted quantity = default to stock on hand (dont do so for archived exhausted products). Also, revert #47478 since we don't have active field. LINKS Task 2178269 PR #45586 Fixes #41634 Related: odoo/upgrade#811 Related: odoo/enterprise#8513 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
@@ -30,6 +30,13 @@ class ProductTemplate(models.Model):
|
||||
template.used_in_bom_count = self.env['mrp.bom'].search_count(
|
||||
[('bom_line_ids.product_id', 'in', template.product_variant_ids.ids)])
|
||||
|
||||
def write(self, values):
|
||||
if 'active' in values:
|
||||
self.filtered(lambda p: p.active != values['active']).with_context(active_test=False).bom_ids.write({
|
||||
'active': values['active']
|
||||
})
|
||||
return super().write(values)
|
||||
|
||||
def action_used_in_bom(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('mrp.mrp_bom_form_action').read()[0]
|
||||
@@ -70,6 +77,13 @@ class ProductProduct(models.Model):
|
||||
for product in self:
|
||||
product.used_in_bom_count = self.env['mrp.bom'].search_count([('bom_line_ids.product_id', '=', product.id)])
|
||||
|
||||
def write(self, values):
|
||||
if 'active' in values:
|
||||
self.filtered(lambda p: p.active != values['active']).with_context(active_test=False).variant_bom_ids.write({
|
||||
'active': values['active']
|
||||
})
|
||||
return super().write(values)
|
||||
|
||||
def get_components(self):
|
||||
""" Return the components list ids in case of kit product.
|
||||
Return the product itself otherwise"""
|
||||
|
||||
@@ -164,8 +164,8 @@ class Product(models.Model):
|
||||
domain_move_in += date_date_expected_domain_to
|
||||
domain_move_out += date_date_expected_domain_to
|
||||
|
||||
Move = self.env['stock.move']
|
||||
Quant = self.env['stock.quant']
|
||||
Move = self.env['stock.move'].with_context(active_test=False)
|
||||
Quant = self.env['stock.quant'].with_context(active_test=False)
|
||||
domain_move_in_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_in
|
||||
domain_move_out_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_out
|
||||
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
|
||||
@@ -512,16 +512,11 @@ class Product(models.Model):
|
||||
return theoretical_quantity
|
||||
|
||||
def write(self, values):
|
||||
res = super(Product, self).write(values)
|
||||
if 'active' in values and not values['active']:
|
||||
products = self.mapped('orderpoint_ids').filtered(lambda r: r.active).mapped('product_id')
|
||||
if products:
|
||||
msg = _('You still have some active reordering rules on this product. Please archive or delete them first.')
|
||||
msg += '\n\n'
|
||||
for product in products:
|
||||
msg += '- %s \n' % product.display_name
|
||||
raise UserError(msg)
|
||||
return res
|
||||
if 'active' in values:
|
||||
self.filtered(lambda p: p.active != values['active']).with_context(active_test=False).orderpoint_ids.write({
|
||||
'active': values['active']
|
||||
})
|
||||
return super().write(values)
|
||||
|
||||
def _get_rules_from_location(self, location, seen_rules=False):
|
||||
if not seen_rules:
|
||||
@@ -611,14 +606,14 @@ class ProductTemplate(models.Model):
|
||||
|
||||
def _compute_quantities_dict(self):
|
||||
# TDE FIXME: why not using directly the function fields ?
|
||||
variants_available = self.mapped('product_variant_ids')._product_available()
|
||||
variants_available = self.with_context(active_test=False).mapped('product_variant_ids')._product_available()
|
||||
prod_available = {}
|
||||
for template in self:
|
||||
qty_available = 0
|
||||
virtual_available = 0
|
||||
incoming_qty = 0
|
||||
outgoing_qty = 0
|
||||
for p in template.product_variant_ids:
|
||||
for p in template.with_context(active_test=False).product_variant_ids:
|
||||
qty_available += variants_available[p.id]["qty_available"]
|
||||
virtual_available += variants_available[p.id]["virtual_available"]
|
||||
incoming_qty += variants_available[p.id]["incoming_qty"]
|
||||
@@ -711,7 +706,7 @@ class ProductTemplate(models.Model):
|
||||
|
||||
# Be aware that the exact same function exists in product.product
|
||||
def action_open_quants(self):
|
||||
return self.product_variant_ids.action_open_quants()
|
||||
return self.with_context(active_test=False).product_variant_ids.filtered(lambda p: p.qty_available != 0).action_open_quants()
|
||||
|
||||
def action_update_quantity_on_hand(self):
|
||||
advanced_option_groups = [
|
||||
@@ -773,8 +768,9 @@ class ProductTemplate(models.Model):
|
||||
|
||||
def action_product_tmpl_forecast_report(self):
|
||||
action = self.env.ref('stock.report_stock_quantity_action_product').read()[0]
|
||||
product_ids = self.with_context(active_test=False).product_variant_ids.filtered(lambda p: p.virtual_available != 0)
|
||||
action['domain'] = [
|
||||
('product_id', 'in', self.product_variant_ids.ids),
|
||||
('product_id', 'in', product_ids.ids)
|
||||
]
|
||||
return action
|
||||
|
||||
|
||||
@@ -224,8 +224,9 @@ class Inventory(models.Model):
|
||||
|
||||
domain = [('company_id', '=', self.company_id.id),
|
||||
('quantity', '!=', '0'),
|
||||
('product_id.active', '=', True),
|
||||
('location_id', 'in', locations_ids)]
|
||||
if self.prefill_counted_quantity == 'zero':
|
||||
domain.append(('product_id.active', '=', True))
|
||||
|
||||
if self.product_ids:
|
||||
domain = expression.AND([domain, [('product_id', 'in', self.product_ids.ids)]])
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Copyright 2015 Camptocamp SA
|
||||
|
||||
from odoo.addons.stock.tests.common2 import TestStockCommon
|
||||
from odoo.tests.common import Form
|
||||
|
||||
|
||||
class TestVirtualAvailable(TestStockCommon):
|
||||
@@ -81,3 +82,25 @@ class TestVirtualAvailable(TestStockCommon):
|
||||
self.picking_out_2.do_unreserve()
|
||||
# 8 units are available again
|
||||
self.assertAlmostEqual(40.0, self.product_3.free_qty)
|
||||
|
||||
def test_archive_product_1(self):
|
||||
"""`qty_available` and `virtual_available` are computed on archived products"""
|
||||
self.assertTrue(self.product_3.active)
|
||||
self.assertAlmostEqual(40.0, self.product_3.qty_available)
|
||||
self.assertAlmostEqual(40.0, self.product_3.virtual_available)
|
||||
self.product_3.active = False
|
||||
self.assertAlmostEqual(40.0, self.product_3.qty_available)
|
||||
self.assertAlmostEqual(40.0, self.product_3.virtual_available)
|
||||
|
||||
def test_archive_product_2(self):
|
||||
"""Archiving a product should archive its reordering rules"""
|
||||
self.assertTrue(self.product_3.active)
|
||||
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||||
orderpoint_form.product_id = self.product_3
|
||||
orderpoint_form.location_id = self.env.ref('stock.stock_location_stock')
|
||||
orderpoint_form.product_min_qty = 0.0
|
||||
orderpoint_form.product_max_qty = 5.0
|
||||
orderpoint = orderpoint_form.save()
|
||||
self.assertTrue(orderpoint.active)
|
||||
self.product_3.active = False
|
||||
self.assertFalse(orderpoint.active)
|
||||
|
||||
@@ -260,7 +260,8 @@ class StockMove(models.Model):
|
||||
todo_valued_moves._sanity_check_for_valuation()
|
||||
stock_valuation_layers |= getattr(todo_valued_moves, '_create_%s_svl' % valued_type)()
|
||||
|
||||
for svl in stock_valuation_layers.with_context(active_test=False):
|
||||
|
||||
for svl in stock_valuation_layers:
|
||||
if not svl.product_id.valuation == 'real_time':
|
||||
continue
|
||||
if svl.currency_id.is_zero(svl.value):
|
||||
@@ -272,7 +273,7 @@ class StockMove(models.Model):
|
||||
# For every in move, run the vacuum for the linked product.
|
||||
products_to_vacuum = valued_moves['in'].mapped('product_id')
|
||||
company = valued_moves['in'].mapped('company_id') and valued_moves['in'].mapped('company_id')[0] or self.env.company
|
||||
for product_to_vacuum in products_to_vacuum.with_context(active_test=False):
|
||||
for product_to_vacuum in products_to_vacuum:
|
||||
product_to_vacuum._run_fifo_vacuum(company)
|
||||
|
||||
return res
|
||||
|
||||
@@ -13,7 +13,6 @@ class StockValuationLayer(models.Model):
|
||||
|
||||
_rec_name = 'product_id'
|
||||
|
||||
active = fields.Boolean(related='product_id.active')
|
||||
company_id = fields.Many2one('res.company', 'Company', readonly=True, required=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', readonly=True, required=True, check_company=True)
|
||||
categ_id = fields.Many2one('product.category', related='product_id.categ_id')
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
<field name="categ_id" />
|
||||
<field name="product_tmpl_id" />
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group expand='0' string='Group by...'>
|
||||
<filter string='Product' name="group_by_product_id" context="{'group_by': 'product_id'}"/>
|
||||
</group>
|
||||
|
||||
Reference in New Issue
Block a user