[ADD]:Added Upstream Patch for stock
This commit is contained in:
parent
f313b7e8e8
commit
227710041f
@ -77,7 +77,7 @@ class ProcurementRule(models.Model):
|
||||
|
||||
data = self._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
|
||||
# Since action_confirm launch following procurement_group we should activate it.
|
||||
move = self.env['stock.move'].sudo().create(data)
|
||||
move = self.env['stock.move'].sudo().with_context(force_company=data.get('company_id', False)).create(data)
|
||||
move._action_confirm()
|
||||
return True
|
||||
|
||||
|
@ -98,13 +98,13 @@ class Product(models.Model):
|
||||
|
||||
domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc
|
||||
domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc
|
||||
if lot_id:
|
||||
if lot_id is not None:
|
||||
domain_quant += [('lot_id', '=', lot_id)]
|
||||
if owner_id:
|
||||
if owner_id is not None:
|
||||
domain_quant += [('owner_id', '=', owner_id)]
|
||||
domain_move_in += [('restrict_partner_id', '=', owner_id)]
|
||||
domain_move_out += [('restrict_partner_id', '=', owner_id)]
|
||||
if package_id:
|
||||
if package_id is not None:
|
||||
domain_quant += [('package_id', '=', package_id)]
|
||||
if dates_in_the_past:
|
||||
domain_move_in_done = list(domain_move_in)
|
||||
@ -221,6 +221,19 @@ class Product(models.Model):
|
||||
domain + loc_domain + ['!'] + dest_loc_domain if dest_loc_domain else domain + loc_domain
|
||||
)
|
||||
|
||||
def _search_qty_available(self, operator, value):
|
||||
# In the very specific case we want to retrieve products with stock available, we only need
|
||||
# to use the quants, not the stock moves. Therefore, we bypass the usual
|
||||
# '_search_product_quantity' method and call '_search_qty_available_new' instead. This
|
||||
# allows better performances.
|
||||
if value == 0.0 and operator == '>' and not ({'from_date', 'to_date'} & set(self.env.context.keys())):
|
||||
product_ids = self._search_qty_available_new(
|
||||
operator, value, self.env.context.get('lot_id'), self.env.context.get('owner_id'),
|
||||
self.env.context.get('package_id')
|
||||
)
|
||||
return [('id', 'in', product_ids)]
|
||||
return self._search_product_quantity(operator, value, 'qty_available')
|
||||
|
||||
def _search_virtual_available(self, operator, value):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
return self._search_product_quantity(operator, value, 'virtual_available')
|
||||
@ -245,24 +258,13 @@ class Product(models.Model):
|
||||
|
||||
# TODO: Still optimization possible when searching virtual quantities
|
||||
ids = []
|
||||
for product in self.search([]):
|
||||
for product in self.with_context(prefetch_fields=False).search([]):
|
||||
if OPERATORS[operator](product[field], value):
|
||||
ids.append(product.id)
|
||||
return [('id', 'in', ids)]
|
||||
|
||||
def _search_qty_available(self, operator, value):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
if value == 0.0 and operator in ('=', '>=', '<='):
|
||||
return self._search_product_quantity(operator, value, 'qty_available')
|
||||
product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'))
|
||||
if (value > 0 and operator in ('<=', '<')) or (value < 0 and operator in ('>=', '>')):
|
||||
# include also unavailable products
|
||||
domain = self._search_product_quantity(operator, value, 'qty_available')
|
||||
product_ids += domain[0][2]
|
||||
return [('id', 'in', product_ids)]
|
||||
|
||||
def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
''' Optimized method which doesn't search on stock.moves, only on stock.quants. '''
|
||||
product_ids = set()
|
||||
domain_quant = self._get_domain_locations()[0]
|
||||
if lot_id:
|
||||
@ -410,8 +412,11 @@ class ProductTemplate(models.Model):
|
||||
outgoing_qty = fields.Float(
|
||||
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
location_id = fields.Many2one('stock.location', 'Location')
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
|
||||
# The goal of these fields is not to be able to search a location_id/warehouse_id but
|
||||
# to properly make these fields "dummy": only used to put some keys in context from
|
||||
# the search view in order to influence computed field
|
||||
location_id = fields.Many2one('stock.location', 'Location', store=False, search=lambda operator, operand, vals: [])
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False, search=lambda operator, operand, vals: [])
|
||||
route_ids = fields.Many2many(
|
||||
'stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes',
|
||||
domain=[('product_selectable', '=', True)],
|
||||
@ -503,7 +508,7 @@ class ProductTemplate(models.Model):
|
||||
if 'uom_id' in vals:
|
||||
new_uom = self.env['product.uom'].browse(vals['uom_id'])
|
||||
updated = self.filtered(lambda template: template.uom_id != new_uom)
|
||||
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
|
||||
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.with_context(active_test=False).mapped('product_variant_ids').ids)], limit=1)
|
||||
if done_moves:
|
||||
raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
|
||||
if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0:
|
||||
@ -578,3 +583,27 @@ class ProductCategory(models.Model):
|
||||
category = category.parent_id
|
||||
routes |= category.route_ids
|
||||
self.total_route_ids = routes
|
||||
|
||||
|
||||
class ProductUoM(models.Model):
|
||||
_inherit = 'product.uom'
|
||||
|
||||
def write(self, values):
|
||||
# Users can not update the factor if open stock moves are based on it
|
||||
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
|
||||
changed = self.filtered(
|
||||
lambda u: any(u[f] != values[f] if f in values else False
|
||||
for f in {'factor', 'factor_inv', 'category_id'}))
|
||||
if changed:
|
||||
stock_move_lines = self.env['stock.move.line'].search_count([
|
||||
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
|
||||
('state', '!=', 'cancel'),
|
||||
])
|
||||
|
||||
if stock_move_lines:
|
||||
raise UserError(_(
|
||||
"You cannot change the ratio of this unit of mesure as some"
|
||||
" products with this UoM have already been moved or are "
|
||||
"currently reserved."
|
||||
))
|
||||
return super(ProductUoM, self).write(values)
|
||||
|
@ -28,12 +28,18 @@ class Company(models.Model):
|
||||
location.sudo().write({'company_id': self.id})
|
||||
self.write({'internal_transit_location_id': location.id})
|
||||
|
||||
warehouses = self.env['stock.warehouse'].search([('partner_id', '=', self.partner_id.id)])
|
||||
warehouses.mapped('partner_id').with_context(force_company=self.id).write({
|
||||
'property_stock_customer': location.id,
|
||||
'property_stock_supplier': location.id,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(Company, self).create(vals)
|
||||
|
||||
# multi-company rules prevents creating warehouse and sub-locations
|
||||
company.create_transit_location()
|
||||
# mutli-company rules prevents creating warehouse and sub-locations
|
||||
self.env['stock.warehouse'].check_access_rights('create')
|
||||
self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id, 'partner_id': company.partner_id.id})
|
||||
company.create_transit_location()
|
||||
return company
|
||||
|
@ -339,7 +339,7 @@ class InventoryLine(models.Model):
|
||||
# TDE FIXME: necessary ? -> replace by location_id
|
||||
prodlot_name = fields.Char(
|
||||
'Serial Number Name',
|
||||
related='prod_lot_id.name', store=True)
|
||||
related='prod_lot_id.name', store=True, readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', related='inventory_id.company_id',
|
||||
index=True, readonly=True, store=True)
|
||||
|
@ -91,6 +91,21 @@ class Location(models.Model):
|
||||
if 'usage' in values and values['usage'] == 'view':
|
||||
if self.mapped('quant_ids'):
|
||||
raise UserError(_("This location's usage cannot be changed to view as it contains products."))
|
||||
if 'usage' in values or 'scrap_location' in values:
|
||||
|
||||
modified_locations = self.filtered(
|
||||
lambda l: any(l[f] != values[f] if f in values else False
|
||||
for f in {'usage', 'scrap_location'}))
|
||||
reserved_quantities = self.env['stock.move.line'].search_count([
|
||||
('location_id', 'in', modified_locations.ids),
|
||||
('product_qty', '>', 0),
|
||||
])
|
||||
if reserved_quantities:
|
||||
raise UserError(_(
|
||||
"You cannot change the location type or its use as a scrap"
|
||||
" location as there are products reserved in this location."
|
||||
" Please unreserve the products first."
|
||||
))
|
||||
return super(Location, self).write(values)
|
||||
|
||||
@api.multi
|
||||
|
@ -275,14 +275,23 @@ class StockMove(models.Model):
|
||||
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
|
||||
In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
|
||||
self.ensure_one()
|
||||
return self.move_line_ids
|
||||
return self.move_line_ids or self.move_line_nosuggest_ids
|
||||
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id')
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done')
|
||||
def _quantity_done_compute(self):
|
||||
""" This field represents the sum of the move lines `qty_done`. It allows the user to know
|
||||
if there is still work to do.
|
||||
|
||||
We take care of rounding this value at the general decimal precision and not the rounding
|
||||
of the move's UOM to make sure this value is really close to the real sum, because this
|
||||
field will be used in `_action_done` in order to know if the move will need a backorder or
|
||||
an extra move.
|
||||
"""
|
||||
for move in self:
|
||||
quantity_done = 0
|
||||
for move_line in move._get_move_lines():
|
||||
# Transform the move_line quantity_done into the move uom.
|
||||
move.quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom)
|
||||
quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom, round=False)
|
||||
move.quantity_done = quantity_done
|
||||
|
||||
def _quantity_done_set(self):
|
||||
quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
|
||||
@ -459,10 +468,10 @@ class StockMove(models.Model):
|
||||
if propagated_date_field:
|
||||
current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
delta = relativedelta.relativedelta(new_date, current_date)
|
||||
if abs(delta.days) >= move.company_id.propagation_minimum_delta:
|
||||
delta_days = (new_date - current_date).total_seconds() / 86400
|
||||
if abs(delta_days) >= move.company_id.propagation_minimum_delta:
|
||||
old_move_date = datetime.strptime(move.move_dest_ids[0].date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_move_date = (old_move_date + relativedelta.relativedelta(days=delta_days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
propagated_changes_dict['date_expected'] = new_move_date
|
||||
#For pushed moves as well as for pulled moves, propagate by recursive call of write().
|
||||
#Note that, for pulled moves we intentionally don't propagate on the procurement.
|
||||
@ -521,9 +530,19 @@ class StockMove(models.Model):
|
||||
}
|
||||
|
||||
def _do_unreserve(self):
|
||||
if any(move.state in ('done', 'cancel') for move in self):
|
||||
raise UserError(_('Cannot unreserve a done move'))
|
||||
self.mapped('move_line_ids').unlink()
|
||||
moves_to_unreserve = self.env['stock.move']
|
||||
for move in self:
|
||||
if move.state == 'cancel':
|
||||
# We may have cancelled move in an open picking in a "propagate_cancel" scenario.
|
||||
continue
|
||||
if move.state == 'done':
|
||||
if move.scrapped:
|
||||
# We may have done move in an open picking in a scrap scenario.
|
||||
continue
|
||||
else:
|
||||
raise UserError(_('Cannot unreserve a done move'))
|
||||
moves_to_unreserve |= move
|
||||
moves_to_unreserve.mapped('move_line_ids').unlink()
|
||||
return True
|
||||
|
||||
def _push_apply(self):
|
||||
@ -618,7 +637,7 @@ class StockMove(models.Model):
|
||||
# We are using propagate to False in order to not cancel destination moves merged in moves[0]
|
||||
moves_to_unlink.write({'propagate': False})
|
||||
moves_to_unlink._action_cancel()
|
||||
moves_to_unlink.unlink()
|
||||
moves_to_unlink.sudo().unlink()
|
||||
return (self | self.env['stock.move'].concat(*moves_to_merge)) - moves_to_unlink
|
||||
|
||||
def _get_relevant_state_among_moves(self):
|
||||
@ -679,7 +698,7 @@ class StockMove(models.Model):
|
||||
self.product_uom = product.uom_id.id
|
||||
return {'domain': {'product_uom': [('category_id', '=', product.uom_id.category_id.id)]}}
|
||||
|
||||
@api.onchange('date')
|
||||
@api.onchange('date_expected')
|
||||
def onchange_date(self):
|
||||
if self.date_expected:
|
||||
self.date = self.date_expected
|
||||
@ -805,7 +824,7 @@ class StockMove(models.Model):
|
||||
group_id = False
|
||||
return {
|
||||
'company_id': self.company_id,
|
||||
'date_planned': self.date,
|
||||
'date_planned': self.date_expected,
|
||||
'move_dest_ids': self,
|
||||
'group_id': group_id,
|
||||
'route_ids': self.route_ids,
|
||||
@ -834,7 +853,12 @@ class StockMove(models.Model):
|
||||
}
|
||||
if quantity:
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
vals = dict(vals, product_uom_qty=uom_quantity)
|
||||
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
vals = dict(vals, product_uom_qty=uom_quantity)
|
||||
else:
|
||||
vals = dict(vals, product_uom_qty=quantity, product_uom_id=self.product_id.uom_id.id)
|
||||
if reserved_quant:
|
||||
vals = dict(
|
||||
vals,
|
||||
@ -859,18 +883,32 @@ class StockMove(models.Model):
|
||||
|
||||
taken_quantity = min(available_quantity, need)
|
||||
|
||||
# `taken_quantity` is in the quants unit of measure. There's a possibility that the move's
|
||||
# unit of measure won't be respected if we blindly reserve this quantity, a common usecase
|
||||
# is if the move's unit of measure's rounding does not allow fractional reservation. We chose
|
||||
# to convert `taken_quantity` to the move's unit of measure with a down rounding method and
|
||||
# then get it back in the quants unit of measure with an half-up rounding_method. This
|
||||
# way, we'll never reserve more than allowed. We do not apply this logic if
|
||||
# `available_quantity` is brought by a chained move line. In this case, `_prepare_move_line_vals`
|
||||
# will take care of changing the UOM to the UOM of the product.
|
||||
if not strict:
|
||||
taken_quantity_move_uom = self.product_id.uom_id._compute_quantity(taken_quantity, self.product_uom, rounding_method='DOWN')
|
||||
taken_quantity = self.product_uom._compute_quantity(taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
|
||||
quants = []
|
||||
|
||||
if self.product_id.tracking == 'serial':
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
|
||||
taken_quantity = 0
|
||||
|
||||
try:
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
if not float_is_zero(taken_quantity, precision_rounding=self.product_id.uom_id.rounding):
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
except UserError:
|
||||
# If it raises here, it means that the `available_quantity` brought by a done move line
|
||||
# is not available on the quants itself. This could be the result of an inventory
|
||||
# adjustment that removed totally of partially `available_quantity`. When this happens, we
|
||||
# chose to do nothing. This situation could not happen on MTS move, because in this case
|
||||
# `available_quantity` is directly the quantity on the quants themselves.
|
||||
taken_quantity = 0
|
||||
|
||||
# Find a candidate move line to update or create a new one.
|
||||
@ -878,7 +916,7 @@ class StockMove(models.Model):
|
||||
to_update = self.move_line_ids.filtered(lambda m: m.product_id.tracking != 'serial' and
|
||||
m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
|
||||
if to_update:
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
|
||||
else:
|
||||
if self.product_id.tracking == 'serial':
|
||||
for i in range(0, int(quantity)):
|
||||
@ -896,7 +934,7 @@ class StockMove(models.Model):
|
||||
assigned_moves = self.env['stock.move']
|
||||
partially_available_moves = self.env['stock.move']
|
||||
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
|
||||
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
|
||||
if move.location_id.should_bypass_reservation()\
|
||||
or move.product_id.type == 'consu':
|
||||
# create the move line(s) but do not impact quants
|
||||
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
|
||||
@ -919,12 +957,18 @@ class StockMove(models.Model):
|
||||
if not move.move_orig_ids:
|
||||
if move.procure_method == 'make_to_order':
|
||||
continue
|
||||
# If we don't need any quantity, consider the move assigned.
|
||||
need = move.product_qty - move.reserved_availability
|
||||
if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding):
|
||||
assigned_moves |= move
|
||||
continue
|
||||
# Reserve new quants and create move lines accordingly.
|
||||
available_quantity = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id)
|
||||
if available_quantity <= 0:
|
||||
continue
|
||||
need = move.product_qty - move.reserved_availability
|
||||
taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, strict=False)
|
||||
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
if need == taken_quantity:
|
||||
assigned_moves |= move
|
||||
else:
|
||||
@ -980,7 +1024,19 @@ class StockMove(models.Model):
|
||||
available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty
|
||||
for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
|
||||
need = move.product_qty - sum(move.move_line_ids.mapped('product_qty'))
|
||||
taken_quantity = move._update_reserved_quantity(need, quantity, location_id, lot_id, package_id, owner_id)
|
||||
# `quantity` is what is brought by chained done move lines. We double check
|
||||
# here this quantity is available on the quants themselves. If not, this
|
||||
# could be the result of an inventory adjustment that removed totally of
|
||||
# partially `quantity`. When this happens, we chose to reserve the maximum
|
||||
# still available. This situation could not happen on MTS move, because in
|
||||
# this case `quantity` is directly the quantity on the quants themselves.
|
||||
available_quantity = self.env['stock.quant']._get_available_quantity(
|
||||
move.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
if float_is_zero(available_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id)
|
||||
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
if need - taken_quantity == 0.0:
|
||||
assigned_moves |= move
|
||||
break
|
||||
@ -1031,8 +1087,8 @@ class StockMove(models.Model):
|
||||
# create the extra moves
|
||||
extra_move_quantity = float_round(
|
||||
self.quantity_done - self.product_uom_qty,
|
||||
precision_rounding=self.product_uom.rounding,
|
||||
rounding_method ='UP')
|
||||
precision_rounding=rounding,
|
||||
rounding_method='HALF-UP')
|
||||
extra_move_vals = self._prepare_extra_move_vals(extra_move_quantity)
|
||||
extra_move = self.copy(default=extra_move_vals)
|
||||
if extra_move.picking_id:
|
||||
@ -1060,6 +1116,9 @@ class StockMove(models.Model):
|
||||
break
|
||||
return extra_move
|
||||
|
||||
def _unreserve_initial_demand(self, new_move):
|
||||
pass
|
||||
|
||||
def check_move_bal_qty(self):
|
||||
for move in self:
|
||||
if move.move_type == 'in':
|
||||
@ -1106,8 +1165,10 @@ class StockMove(models.Model):
|
||||
|
||||
# Split moves where necessary and move quants
|
||||
for move in moves_todo:
|
||||
rounding = move.product_uom.rounding
|
||||
if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0:
|
||||
# To know whether we need to create a backorder or not, round to the general product's
|
||||
# decimal precision and not the product's UOM.
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(move.quantity_done, move.product_uom_qty, precision_digits=rounding) < 0:
|
||||
# Need to do some kind of conversion here
|
||||
qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
new_move = move._split(qty_split)
|
||||
@ -1120,9 +1181,7 @@ class StockMove(models.Model):
|
||||
move_line.write({'product_uom_qty': move_line.qty_done})
|
||||
except UserError:
|
||||
pass
|
||||
|
||||
# If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
|
||||
move.move_line_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move})
|
||||
move._unreserve_initial_demand(new_move)
|
||||
move.move_line_ids._action_done()
|
||||
# Check the consistency of the result packages; there should be an unique location across
|
||||
# the contained quants.
|
||||
@ -1154,15 +1213,17 @@ class StockMove(models.Model):
|
||||
self.mapped('move_line_ids').unlink()
|
||||
return super(StockMove, self).unlink()
|
||||
|
||||
def _prepare_move_split_vals(self, uom_qty):
|
||||
def _prepare_move_split_vals(self, qty):
|
||||
vals = {
|
||||
'product_uom_qty': uom_qty,
|
||||
'product_uom_qty': qty,
|
||||
'procure_method': 'make_to_stock',
|
||||
'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
|
||||
'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
|
||||
'origin_returned_move_id': self.origin_returned_move_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
}
|
||||
if self.env.context.get('force_split_uom_id'):
|
||||
vals['product_uom'] = self.env.context['force_split_uom_id']
|
||||
return vals
|
||||
|
||||
def _split(self, qty, restrict_partner_id=False):
|
||||
@ -1181,9 +1242,18 @@ class StockMove(models.Model):
|
||||
raise UserError(_('You cannot split a draft move. It needs to be confirmed first.'))
|
||||
if float_is_zero(qty, precision_rounding=self.product_id.uom_id.rounding) or self.product_qty <= qty:
|
||||
return self.id
|
||||
# HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
|
||||
|
||||
decimal_precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
# `qty` passed as argument is the quantity to backorder and is always expressed in the
|
||||
# quants UOM. If we're able to convert back and forth this quantity in the move's and the
|
||||
# quants UOM, the backordered move can keep the UOM of the move. Else, we'll create is in
|
||||
# the UOM of the quants.
|
||||
uom_qty = self.product_id.uom_id._compute_quantity(qty, self.product_uom, rounding_method='HALF-UP')
|
||||
defaults = self._prepare_move_split_vals(uom_qty)
|
||||
if float_compare(qty, self.product_uom._compute_quantity(uom_qty, self.product_id.uom_id, rounding_method='HALF-UP'), precision_digits=decimal_precision) == 0:
|
||||
defaults = self._prepare_move_split_vals(uom_qty)
|
||||
else:
|
||||
defaults = self.with_context(force_split_uom_id=self.product_id.uom_id.id)._prepare_move_split_vals(qty)
|
||||
|
||||
if restrict_partner_id:
|
||||
defaults['restrict_partner_id'] = restrict_partner_id
|
||||
@ -1192,21 +1262,15 @@ class StockMove(models.Model):
|
||||
if self.env.context.get('source_location_id'):
|
||||
defaults['location_id'] = self.env.context['source_location_id']
|
||||
new_move = self.with_context(rounding_method='HALF-UP').copy(defaults)
|
||||
# ctx = context.copy()
|
||||
# TDE CLEANME: used only in write in this file, to clean
|
||||
# ctx['do_not_propagate'] = True
|
||||
|
||||
# FIXME: pim fix your crap
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||
|
||||
# if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
|
||||
# new_move_prop = self.move_dest_id.split(qty)
|
||||
# new_move.write({'move_dest_id': new_move_prop})
|
||||
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
|
||||
# thus the result of action_confirm should always be a list of 1 element length)
|
||||
# In this case we don't merge move since the new move with 0 quantity done will be used for the backorder.
|
||||
# Update the original `product_qty` of the move. Use the general product's decimal
|
||||
# precision and not the move's UOM to handle case where the `quantity_done` is not
|
||||
# compatible with the move's UOM.
|
||||
new_product_qty = self.product_id.uom_id._compute_quantity(self.product_qty - qty, self.product_uom, round=False)
|
||||
new_product_qty = float_round(new_product_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure'))
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': new_product_qty})
|
||||
new_move = new_move._action_confirm(merge=False)
|
||||
# TDE FIXME: due to action confirm change
|
||||
return new_move.id
|
||||
|
||||
def _recompute_state(self):
|
||||
|
@ -52,7 +52,6 @@ class StockMoveLine(models.Model):
|
||||
reference = fields.Char(related='move_id.reference', store=True)
|
||||
in_entire_package = fields.Boolean(compute='_compute_in_entire_package')
|
||||
|
||||
@api.one
|
||||
def _compute_location_description(self):
|
||||
for operation, operation_sudo in izip(self, self.sudo()):
|
||||
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
|
||||
@ -141,7 +140,7 @@ class StockMoveLine(models.Model):
|
||||
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||
"""
|
||||
res = {}
|
||||
if self.product_id.tracking == 'serial':
|
||||
if self.qty_done and self.product_id.tracking == 'serial':
|
||||
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
|
||||
message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
|
||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||
@ -269,7 +268,7 @@ class StockMoveLine(models.Model):
|
||||
except UserError:
|
||||
pass
|
||||
if new_product_qty != ml.product_qty:
|
||||
new_product_uom_qty = self.product_id.uom_id._compute_quantity(new_product_qty, self.product_uom_id, rounding_method='HALF-UP')
|
||||
new_product_uom_qty = ml.product_id.uom_id._compute_quantity(new_product_qty, ml.product_uom_id, rounding_method='HALF-UP')
|
||||
ml.with_context(bypass_reservation_update=True).product_uom_qty = new_product_uom_qty
|
||||
|
||||
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
|
||||
@ -370,6 +369,15 @@ class StockMoveLine(models.Model):
|
||||
# `action_done` on the next move lines.
|
||||
ml_to_delete = self.env['stock.move.line']
|
||||
for ml in self:
|
||||
# Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`.
|
||||
uom_qty = float_round(ml.qty_done, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP')
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
qty_done = float_round(ml.qty_done, precision_digits=precision_digits, rounding_method='HALF-UP')
|
||||
if float_compare(uom_qty, qty_done, precision_digits=precision_digits) != 0:
|
||||
raise UserError(_('The quantity done for the product "%s" doesn\'t respect the rounding precision \
|
||||
defined on the unit of measure "%s". Please change the quantity done or the \
|
||||
rounding precision of your unit of measure.') % (ml.product_id.display_name, ml.product_uom_id.name))
|
||||
|
||||
qty_done_float_compared = float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)
|
||||
if qty_done_float_compared > 0:
|
||||
if ml.product_id.tracking != 'none':
|
||||
|
@ -320,7 +320,7 @@ class Picking(models.Model):
|
||||
for picking in self:
|
||||
if self.env.context.get('force_detailed_view'):
|
||||
picking.show_operations = True
|
||||
break
|
||||
continue
|
||||
if picking.picking_type_id.show_operations:
|
||||
if (picking.state == 'draft' and not self.env.context.get('planned_picking')) or picking.state != 'draft':
|
||||
picking.show_operations = True
|
||||
@ -669,6 +669,7 @@ class Picking(models.Model):
|
||||
all_in = True
|
||||
pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package)
|
||||
keys = ['product_id', 'lot_id']
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
grouped_quants = {}
|
||||
for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
@ -677,8 +678,8 @@ class Picking(models.Model):
|
||||
grouped_ops = {}
|
||||
for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty'))
|
||||
if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \
|
||||
or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops):
|
||||
if any(not float_is_zero(grouped_quants.get(key, 0) - grouped_ops.get(key, 0), precision_digits=precision_digits) for key in grouped_quants) \
|
||||
or any(not float_is_zero(grouped_ops.get(key, 0) - grouped_quants.get(key, 0), precision_digits=precision_digits) for key in grouped_ops):
|
||||
all_in = False
|
||||
return all_in
|
||||
|
||||
@ -704,7 +705,8 @@ class Picking(models.Model):
|
||||
|
||||
# If no lots when needed, raise error
|
||||
picking_type = self.picking_type_id
|
||||
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids)
|
||||
no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
if no_reserved_quantities and no_quantities_done:
|
||||
raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.'))
|
||||
@ -760,7 +762,7 @@ class Picking(models.Model):
|
||||
if self._check_backorder():
|
||||
return self.action_generate_backorder_wizard()
|
||||
self.action_done()
|
||||
return
|
||||
return True
|
||||
|
||||
def action_generate_backorder_wizard(self):
|
||||
view = self.env.ref('stock.view_backorder_confirmation')
|
||||
|
@ -37,6 +37,18 @@ class ProductionLot(models.Model):
|
||||
raise UserError(_("You are not allowed to create a lot for this picking type"))
|
||||
return super(ProductionLot, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'product_id' in vals:
|
||||
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids)])
|
||||
if move_lines:
|
||||
raise UserError(_(
|
||||
'You are not allowed to change the product linked to a serial or lot number ' +
|
||||
'if some stock moves have already been created with that number. ' +
|
||||
'This would lead to inconsistencies in your stock.'
|
||||
))
|
||||
return super(ProductionLot, self).write(vals)
|
||||
|
||||
@api.one
|
||||
def _product_qty(self):
|
||||
# We only care for the quants in internal or transit locations.
|
||||
|
@ -81,12 +81,6 @@ class StockQuant(models.Model):
|
||||
if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
|
||||
raise ValidationError(_('A serial number should only be linked to a single product.'))
|
||||
|
||||
@api.constrains('in_date', 'lot_id')
|
||||
def check_in_date(self):
|
||||
for quant in self:
|
||||
if quant.in_date and not quant.lot_id:
|
||||
raise ValidationError(_('An incoming date cannot be set to an untracked product.'))
|
||||
|
||||
@api.constrains('location_id')
|
||||
def check_location_id(self):
|
||||
for quant in self:
|
||||
@ -111,9 +105,9 @@ class StockQuant(models.Model):
|
||||
@api.model
|
||||
def _get_removal_strategy_order(self, removal_strategy):
|
||||
if removal_strategy == 'fifo':
|
||||
return 'in_date, id'
|
||||
return 'in_date ASC NULLS FIRST, id'
|
||||
elif removal_strategy == 'lifo':
|
||||
return 'in_date desc, id desc'
|
||||
return 'in_date DESC NULLS LAST, id desc'
|
||||
raise UserError(_('Removal strategy %s not implemented.') % (removal_strategy,))
|
||||
|
||||
def _gather(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
|
||||
@ -136,7 +130,17 @@ class StockQuant(models.Model):
|
||||
domain = expression.AND([[('owner_id', '=', owner_id and owner_id.id or False)], domain])
|
||||
domain = expression.AND([[('location_id', '=', location_id.id)], domain])
|
||||
|
||||
return self.search(domain, order=removal_strategy_order)
|
||||
# Copy code of _search for special NULLS FIRST/LAST order
|
||||
self.sudo(self._uid).check_access_rights('read')
|
||||
query = self._where_calc(domain)
|
||||
self._apply_ir_rules(query, 'read')
|
||||
from_clause, where_clause, where_clause_params = query.get_sql()
|
||||
where_str = where_clause and (" WHERE %s" % where_clause) or ''
|
||||
query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY "+ removal_strategy_order
|
||||
self._cr.execute(query_str, where_clause_params)
|
||||
res = self._cr.fetchall()
|
||||
# No uniquify list necessary as auto_join is not applied anyways...
|
||||
return self.browse([x[0] for x in res])
|
||||
|
||||
@api.model
|
||||
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
|
||||
@ -198,17 +202,16 @@ class StockQuant(models.Model):
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
rounding = product_id.uom_id.rounding
|
||||
|
||||
if lot_id:
|
||||
incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
incoming_dates = [d for d in quants.mapped('in_date') if d]
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
|
||||
for quant in quants:
|
||||
try:
|
||||
@ -255,13 +258,21 @@ class StockQuant(models.Model):
|
||||
self = self.sudo()
|
||||
rounding = product_id.uom_id.rounding
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0 and float_compare(quantity, available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0 and float_compare(abs(quantity), sum(quants.mapped('reserved_quantity')), precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
|
||||
reserved_quants = []
|
||||
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
# if we want to reserve
|
||||
available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
|
||||
# if we want to unreserve
|
||||
available_quantity = sum(quants.mapped('reserved_quantity'))
|
||||
if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
else:
|
||||
return reserved_quants
|
||||
|
||||
for quant in quants:
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
max_quantity_on_quant = quant.quantity - quant.reserved_quantity
|
||||
@ -450,6 +461,12 @@ class QuantPackage(models.Model):
|
||||
if move_lines_to_remove:
|
||||
move_lines_to_remove.write({'result_package_id': False})
|
||||
else:
|
||||
move_line_to_modify = self.env['stock.move.line'].search([
|
||||
('package_id', '=', package.id),
|
||||
('state', 'in', ('assigned', 'partially_available')),
|
||||
('product_qty', '!=', 0),
|
||||
])
|
||||
move_line_to_modify.write({'package_id': False})
|
||||
package.mapped('quant_ids').write({'package_id': False})
|
||||
|
||||
def action_view_picking(self):
|
||||
|
@ -21,47 +21,55 @@ class MrpStockReport(models.TransientModel):
|
||||
|
||||
@api.model
|
||||
def get_move_lines_upstream(self, move_lines):
|
||||
res = self.env['stock.move.line']
|
||||
for move_line in move_lines:
|
||||
lines_seen = move_lines
|
||||
lines_todo = list(move_lines)
|
||||
while lines_todo:
|
||||
move_line = lines_todo.pop(0)
|
||||
# if MTO
|
||||
if move_line.move_id.move_orig_ids:
|
||||
res |= move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id.id == move_line.lot_id.id)
|
||||
lines = move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id == move_line.lot_id
|
||||
) - lines_seen
|
||||
# if MTS
|
||||
elif move_line.location_id.usage == 'internal':
|
||||
lines = self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_dest_id', '=', move_line.location_id.id),
|
||||
('id', 'not in', lines_seen.ids),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
else:
|
||||
if move_line.location_id.usage == 'internal':
|
||||
res |= self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_dest_id', '=', move_line.location_id.id),
|
||||
('id', '!=', move_line.id),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
if res:
|
||||
res |= self.get_move_lines_upstream(res)
|
||||
return res
|
||||
continue
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
|
||||
@api.model
|
||||
def get_move_lines_downstream(self, move_lines):
|
||||
res = self.env['stock.move.line']
|
||||
for move_line in move_lines:
|
||||
lines_seen = move_lines
|
||||
lines_todo = list(move_lines)
|
||||
while lines_todo:
|
||||
move_line = lines_todo.pop(0)
|
||||
# if MTO
|
||||
if move_line.move_id.move_dest_ids:
|
||||
res |= move_line.move_id.move_dest_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id.id == move_line.lot_id.id)
|
||||
lines = move_line.move_id.move_dest_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id == move_line.lot_id
|
||||
) - lines_seen
|
||||
# if MTS
|
||||
elif move_line.location_dest_id.usage == 'internal':
|
||||
lines = self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_id', '=', move_line.location_dest_id.id),
|
||||
('id', 'not in', lines_seen.ids),
|
||||
('date', '>', move_line.date),
|
||||
])
|
||||
else:
|
||||
if move_line.location_dest_id.usage == 'internal':
|
||||
res |= self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_id', '=', move_line.location_dest_id.id),
|
||||
('id', '!=', move_line.id),
|
||||
('date', '>', move_line.date),
|
||||
])
|
||||
if res:
|
||||
res |= self.get_move_lines_downstream(res)
|
||||
return res
|
||||
continue
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
|
||||
@api.model
|
||||
def get_lines(self, line_id=None, **kw):
|
||||
|
@ -163,11 +163,14 @@ class Warehouse(models.Model):
|
||||
# If another partner assigned
|
||||
if vals.get('partner_id'):
|
||||
warehouses._update_partner_data(vals['partner_id'], vals.get('company_id'))
|
||||
|
||||
res = super(Warehouse, self).write(vals)
|
||||
|
||||
# check if we need to delete and recreate route
|
||||
if vals.get('reception_steps') or vals.get('delivery_steps'):
|
||||
warehouses._update_routes()
|
||||
route_vals = warehouses._update_routes()
|
||||
if route_vals:
|
||||
self.write(route_vals)
|
||||
|
||||
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
|
||||
for warehouse in warehouses:
|
||||
@ -188,9 +191,10 @@ class Warehouse(models.Model):
|
||||
ResCompany = self.env['res.company']
|
||||
if company_id:
|
||||
transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).with_context(force_company=company_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
else:
|
||||
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
|
||||
def create_sequences_and_picking_types(self):
|
||||
IrSequenceSudo = self.env['ir.sequence'].sudo()
|
||||
@ -293,7 +297,7 @@ class Warehouse(models.Model):
|
||||
reception_route.pull_ids.write({'active': False})
|
||||
reception_route.push_ids.write({'active': False})
|
||||
else:
|
||||
reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
|
||||
warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
|
||||
# push / procurement (pull) rules for reception
|
||||
routings = routes_data[warehouse.id][warehouse.reception_steps]
|
||||
push_rules_list, pull_rules_list = warehouse._get_push_pull_rules_values(
|
||||
@ -406,7 +410,7 @@ class Warehouse(models.Model):
|
||||
|
||||
pull_rules_list = supplier_wh._get_supply_pull_rules_values(
|
||||
[self.Routing(output_location, transit_location, supplier_wh.out_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id})
|
||||
values={'route_id': inter_wh_route.id})
|
||||
pull_rules_list += self._get_supply_pull_rules_values(
|
||||
[self.Routing(transit_location, input_location, self.in_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id})
|
||||
@ -606,11 +610,18 @@ class Warehouse(models.Model):
|
||||
routes_data = self.get_routes_dict()
|
||||
# change the default source and destination location and (de)activate operation types
|
||||
self._update_picking_type()
|
||||
self._create_or_update_delivery_route(routes_data)
|
||||
self._create_or_update_reception_route(routes_data)
|
||||
self._create_or_update_crossdock_route(routes_data)
|
||||
self._create_or_update_mto_pull(routes_data)
|
||||
return True
|
||||
delivery_route = self._create_or_update_delivery_route(routes_data)
|
||||
reception_route = self._create_or_update_reception_route(routes_data)
|
||||
crossdock_route = self._create_or_update_crossdock_route(routes_data)
|
||||
mto_pull = self._create_or_update_mto_pull(routes_data)
|
||||
|
||||
return {
|
||||
'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route],
|
||||
'mto_pull_id': mto_pull.id,
|
||||
'reception_route_id': reception_route.id,
|
||||
'delivery_route_id': delivery_route.id,
|
||||
'crossdock_route_id': crossdock_route.id,
|
||||
}
|
||||
|
||||
@api.one
|
||||
def _update_picking_type(self):
|
||||
|
@ -9,8 +9,8 @@
|
||||
</template>
|
||||
|
||||
<template id="report_location_barcode">
|
||||
<t t-call="web.basic_layout">
|
||||
<div t-foreach="[docs[x:x+4] for x in xrange(0, len(docs), 4)]" t-as="page_docs" class="page page_stock_location_barcodes">
|
||||
<t t-call="web.html_container">
|
||||
<div t-foreach="[docs[x:x+4] for x in xrange(0, len(docs), 4)]" t-as="page_docs" class="page article page_stock_location_barcodes">
|
||||
<t t-foreach="page_docs" t-as="o">
|
||||
<t t-if="o.barcode"><t t-set="content" t-value="o.barcode"/></t>
|
||||
<t t-if="not o.barcode"><t t-set="content" t-value="o.name"/></t>
|
||||
|
@ -61,7 +61,7 @@ class ReportStockForecat(models.Model):
|
||||
LEFT JOIN
|
||||
stock_location source_location ON sm.location_id = source_location.id
|
||||
WHERE
|
||||
sm.state IN ('confirmed','assigned','waiting') and
|
||||
sm.state IN ('confirmed','partially_available','assigned','waiting') and
|
||||
source_location.usage != 'internal' and dest_location.usage = 'internal'
|
||||
GROUP BY sm.date_expected,sm.product_id
|
||||
UNION ALL
|
||||
@ -82,7 +82,7 @@ class ReportStockForecat(models.Model):
|
||||
LEFT JOIN
|
||||
stock_location dest_location ON sm.location_dest_id = dest_location.id
|
||||
WHERE
|
||||
sm.state IN ('confirmed','assigned','waiting') and
|
||||
sm.state IN ('confirmed','partially_available','assigned','waiting') and
|
||||
source_location.usage = 'internal' and dest_location.usage != 'internal'
|
||||
GROUP BY sm.date_expected,sm.product_id)
|
||||
as MAIN
|
||||
|
@ -45,7 +45,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
|
||||
<field name="context">{'search_default_pivot_by':1, 'search_default_graph_by':1, 'search_default_product_tmpl_id': active_id}</field>
|
||||
<field name="context">{'search_default_product_tmpl_id': active_id}</field>
|
||||
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
|
||||
</record>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
|
||||
<field name="context">{'search_default_pivot_by':1, 'search_default_graph_by':1, 'search_default_product_id': active_id}</field>
|
||||
<field name="context">{'search_default_product_id': active_id}</field>
|
||||
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
|
||||
</record>
|
||||
</flectra>
|
||||
|
@ -102,14 +102,16 @@
|
||||
<span t-field="move.product_id.description_picking"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-if="move.product_qty" t-esc="move.product_qty"/>
|
||||
<span t-if="move.product_qty" t-field="move.product_qty"/>
|
||||
<span t-if="not move.product_qty" t-esc="move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')"/>
|
||||
<span t-field="move.product_id.uom_id" groups="product.group_uom"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="has_barcode">
|
||||
<span t-if="move.product_id and move.product_id.barcode">
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-if="move.product_id.barcode and len(move.product_id.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-elif="move.product_id.barcode and len(move.product_id.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
|
@ -9,4 +9,5 @@ from . import test_quant
|
||||
from . import test_inventory
|
||||
from . import test_move
|
||||
from . import test_move2
|
||||
from . import test_robustness
|
||||
from . import test_stock_branch
|
||||
|
@ -827,6 +827,129 @@ class StockMove(TransactionCase):
|
||||
|
||||
self.assertEqual(len(move.move_line_ids), 4.0)
|
||||
|
||||
def test_availability_6(self):
|
||||
""" Check that, in the scenario where a move is in a bigger uom than the uom of the quants
|
||||
and this uom only allows entire numbers, we don't make a partial reservation when the
|
||||
quantity available is not enough to reserve the move. Check also that it is not possible
|
||||
to set `quantity_done` with a value not honouring the UOM's rounding.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 6.0)
|
||||
|
||||
# the move should not be reserved
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'test_availability_6',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'confirmed')
|
||||
|
||||
# the quants should be left untouched
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 6.0)
|
||||
|
||||
# make 8 units available, the move should again not be reservabale
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 2.0)
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'confirmed')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 8.0)
|
||||
|
||||
# make 12 units available, this time the move should be reservable
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 4.0)
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'assigned')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
|
||||
# Check it isn't possible to set any value to quantity_done
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 0.1
|
||||
move._action_done()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 1.1
|
||||
move._action_done()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 0.9
|
||||
move._action_done()
|
||||
|
||||
move.quantity_done = 1
|
||||
move._action_done()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 12.0)
|
||||
|
||||
def test_availability_7(self):
|
||||
""" Check that, in the scenario where a move is in a bigger uom than the uom of the quants
|
||||
and this uom only allows entire numbers, we only reserve quantity honouring the uom's
|
||||
rounding even if the quantity is set across multiple quants.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# make 12 quants of 1
|
||||
for i in range(1, 13):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
# the move should be reserved
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'test_availability_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'assigned')
|
||||
self.assertEqual(len(move.move_line_ids.mapped('product_uom_id')), 1)
|
||||
self.assertEqual(move.move_line_ids.mapped('product_uom_id'), self.uom_unit)
|
||||
|
||||
for move_line in move.move_line_ids:
|
||||
move_line.qty_done = 1
|
||||
move._action_done()
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1)
|
||||
self.assertEqual(move.product_uom.id, self.uom_dozen.id)
|
||||
self.assertEqual(move.state, 'done')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 12.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.customer_location)), 12)
|
||||
|
||||
def test_availability_8(self):
|
||||
""" Test the assignment mechanism when the product quantity is decreased on a partially
|
||||
reserved stock move.
|
||||
"""
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 3.0)
|
||||
self.assertAlmostEqual(self.product1.qty_available, 3.0)
|
||||
|
||||
move_partial = self.env['stock.move'].create({
|
||||
'name': 'test_partial',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 5.0,
|
||||
})
|
||||
|
||||
move_partial._action_confirm()
|
||||
move_partial._action_assign()
|
||||
self.assertAlmostEqual(self.product1.virtual_available, -2.0)
|
||||
self.assertEqual(move_partial.state, 'partially_available')
|
||||
move_partial.product_uom_qty = 3.0
|
||||
move_partial._action_assign()
|
||||
self.assertEqual(move_partial.state, 'assigned')
|
||||
|
||||
def test_unreserve_1(self):
|
||||
""" Check that unreserving a stock move sets the products reserved as available and
|
||||
set the state back to confirmed.
|
||||
@ -1022,6 +1145,47 @@ class StockMove(TransactionCase):
|
||||
for quant in quants:
|
||||
self.assertEqual(quant.reserved_quantity, 0)
|
||||
|
||||
def test_unreserve_6(self):
|
||||
""" In a situation with a negative and a positive quant, reserve and unreserve.
|
||||
"""
|
||||
q1 = self.env['stock.quant'].create({
|
||||
'product_id': self.product1.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': -10,
|
||||
'reserved_quantity': 0,
|
||||
})
|
||||
|
||||
q2 = self.env['stock.quant'].create({
|
||||
'product_id': self.product1.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': 30.0,
|
||||
'reserved_quantity': 10.0,
|
||||
})
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_unreserve_6',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10.0,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(len(move1.move_line_ids), 1)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 10)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
self.assertEqual(q2.reserved_quantity, 20)
|
||||
|
||||
move1._do_unreserve()
|
||||
self.assertEqual(move1.state, 'confirmed')
|
||||
self.assertEqual(len(move1.move_line_ids), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
|
||||
self.assertEqual(q2.reserved_quantity, 10)
|
||||
|
||||
def test_link_assign_1(self):
|
||||
""" Test the assignment mechanism when two chained stock moves try to move one unit of an
|
||||
untracked product.
|
||||
@ -1356,6 +1520,319 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_stock_stock_1.state, 'assigned')
|
||||
self.assertEqual(move_stock_stock_2.state, 'waiting')
|
||||
|
||||
def test_link_assign_7(self):
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 6.0)
|
||||
|
||||
# create pickings and moves for a pick -> pack mto scenario
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
# the pick should not be reservable because of the rounding of the dozen
|
||||
move_stock_pack._action_assign()
|
||||
self.assertEqual(move_stock_pack.state, 'confirmed')
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'waiting')
|
||||
|
||||
# move the 6 units by adding an unreserved move line
|
||||
move_stock_pack.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'qty_done': 6,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': False,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': move_stock_pack.location_id.id,
|
||||
'location_dest_id': move_stock_pack.location_dest_id.id,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})]})
|
||||
|
||||
# the quantity done on the move should not respect the rounding of the move line
|
||||
self.assertEqual(move_stock_pack.quantity_done, 0.5)
|
||||
|
||||
# create the backorder in the uom of the quants
|
||||
backorder_wizard_dict = picking_stock_pack.button_validate()
|
||||
backorder_wizard = self.env[backorder_wizard_dict['res_model']].browse(backorder_wizard_dict['res_id'])
|
||||
backorder_wizard.process()
|
||||
self.assertEqual(move_stock_pack.state, 'done')
|
||||
self.assertEqual(move_stock_pack.quantity_done, 0.5)
|
||||
self.assertEqual(move_stock_pack.product_uom_qty, 0.5)
|
||||
|
||||
# the second move should not be reservable because of the rounding on the dozen
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
move_line_pack_cust = move_pack_cust.move_line_ids
|
||||
self.assertEqual(move_line_pack_cust.product_uom_qty, 6)
|
||||
self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
|
||||
|
||||
# move a dozen on the backorder to see how we handle the extra move
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_stock_pack.id)])
|
||||
backorder.move_lines.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': False,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': backorder.location_id.id,
|
||||
'location_dest_id': backorder.location_dest_id.id,
|
||||
'picking_id': backorder.id,
|
||||
})]})
|
||||
overprocessed_wizard_dict = backorder.button_validate()
|
||||
overprocessed_wizard = self.env[overprocessed_wizard_dict['res_model']].browse(overprocessed_wizard_dict['res_id'])
|
||||
overprocessed_wizard.action_confirm()
|
||||
backorder_move = backorder.move_lines
|
||||
self.assertEqual(backorder_move.state, 'done')
|
||||
self.assertEqual(backorder_move.quantity_done, 12.0)
|
||||
self.assertEqual(backorder_move.product_uom_qty, 12.0)
|
||||
self.assertEqual(backorder_move.product_uom, self.uom_unit)
|
||||
|
||||
# the second move should now be reservable
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'assigned')
|
||||
self.assertEqual(move_line_pack_cust.product_uom_qty, 12)
|
||||
self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, move_stock_pack.location_dest_id), 6)
|
||||
|
||||
def test_link_assign_8(self):
|
||||
""" Set the rounding of the dozen to 1.0, create a chain of two move for a dozen, the product
|
||||
concerned is tracked by serial number. Check that the flow is ok.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
for i in range(1, 13):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
# create pickings and moves for a pick -> pack mto scenario
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
move_stock_pack._action_assign()
|
||||
self.assertEqual(move_stock_pack.state, 'assigned')
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'waiting')
|
||||
|
||||
for ml in move_stock_pack.move_line_ids:
|
||||
ml.qty_done = 1
|
||||
picking_stock_pack.button_validate()
|
||||
self.assertEqual(move_pack_cust.state, 'assigned')
|
||||
for ml in move_pack_cust.move_line_ids:
|
||||
self.assertEqual(ml.product_uom_qty, 1)
|
||||
self.assertEqual(ml.product_uom_id.id, self.uom_unit.id)
|
||||
self.assertTrue(bool(ml.lot_id.id))
|
||||
|
||||
def test_link_assign_9(self):
|
||||
""" Create an uom "3 units" which is 3 times the units but without rounding. Create 3
|
||||
quants in stock and two chained moves. The first move will bring the 3 quants but the
|
||||
second only validate 2 and create a backorder for the last one. Check that the reservation
|
||||
is correctly cleared up for the last one.
|
||||
"""
|
||||
uom_3units = self.env['product.uom'].create({
|
||||
'name': '3 units',
|
||||
'category_id': self.uom_unit.category_id.id,
|
||||
'factor_inv': 3,
|
||||
'rounding': 1,
|
||||
})
|
||||
for i in range(1, 4):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_9',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': uom_3units.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_0',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': uom_3units.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
picking_stock_pack.action_assign()
|
||||
for ml in picking_stock_pack.move_lines.move_line_ids:
|
||||
ml.qty_done = 1
|
||||
picking_stock_pack.button_validate()
|
||||
self.assertEqual(picking_pack_cust.state, 'assigned')
|
||||
for ml in picking_pack_cust.move_lines.move_line_ids:
|
||||
if ml.lot_id.name != 'lot3':
|
||||
ml.qty_done = 1
|
||||
res_dict_for_back_order = picking_pack_cust.button_validate()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
|
||||
backordered_move = backorder.move_lines
|
||||
|
||||
# due to the rounding, the backordered quantity is 0.999 ; we shoudln't be able to reserve
|
||||
# 0.999 on a tracked by serial number quant
|
||||
backordered_move._action_assign()
|
||||
self.assertEqual(backordered_move.reserved_availability, 0)
|
||||
|
||||
# force the serial number and validate
|
||||
lot3 = self.env['stock.production.lot'].search([('name', '=', "lot3")])
|
||||
backorder.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product2.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': lot3.id,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': backordered_move.location_id.id,
|
||||
'location_dest_id': backordered_move.location_dest_id.id,
|
||||
'move_id': backordered_move.id,
|
||||
})]})
|
||||
|
||||
overprocessed_wizard = backorder.button_validate()
|
||||
overprocessed_wizard = self.env['stock.overprocessed.transfer'].browse(overprocessed_wizard['res_id'])
|
||||
overprocessed_wizard.action_confirm()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 3)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.pack_location), 0)
|
||||
|
||||
def test_link_assign_10(self):
|
||||
""" Test the assignment mechanism with partial availability.
|
||||
"""
|
||||
# make some stock:
|
||||
# stock location: 2.0
|
||||
# pack location: -1.0
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 2.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||
|
||||
move_out = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_out',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
move_out._action_confirm()
|
||||
move_out._action_assign()
|
||||
move_out.quantity_done = 1.0
|
||||
move_out._action_done()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.pack_location)), 1.0)
|
||||
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_2',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
move_stock_pack._action_assign()
|
||||
move_stock_pack.quantity_done = 2.0
|
||||
move_stock_pack._action_done()
|
||||
self.assertEqual(len(move_pack_cust.move_line_ids), 1)
|
||||
|
||||
self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
|
||||
def test_use_unreserved_move_line_1(self):
|
||||
""" Test that validating a stock move linked to an untracked product reserved by another one
|
||||
correctly unreserves the other one.
|
||||
@ -1784,7 +2261,7 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_line.product_qty, 5)
|
||||
move_line.qty_done = 5.0
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation
|
||||
move_line.with_context(debug=True).lot_id = lot1
|
||||
move_line.lot_id = lot1
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation when assgning a lot now
|
||||
|
||||
move1._action_done()
|
||||
@ -2598,9 +3075,9 @@ class StockMove(TransactionCase):
|
||||
return picking
|
||||
|
||||
def test_immediate_validate_5(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
Test that tracked products can be received without specifying a serial
|
||||
number when the picking type is configured that way.
|
||||
""" In a receipt with a single tracked by serial numbers move, clicking on validate without
|
||||
filling any quantities nor lot should open an UserError except if the picking type is
|
||||
configured to allow otherwise.
|
||||
"""
|
||||
picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
product_id = self.product2
|
||||
@ -3166,39 +3643,6 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(self.product1.qty_available, 5.0)
|
||||
self.assertEqual(self.product1.with_context(company_owned=True).qty_available, 10.0)
|
||||
|
||||
def test_split_1(self):
|
||||
""" When we split a move line and having one without quantity done, we want to keep reservation
|
||||
on the new one as it has not been unreserved during the copy.
|
||||
"""
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_split_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10.0,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 10)
|
||||
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move_line = move1.move_line_ids
|
||||
|
||||
default = {'product_uom_qty': 3,
|
||||
'qty_done': 3}
|
||||
move_line.copy(default=default)
|
||||
move_line.with_context(bypass_reservation_update=True).write({'product_uom_qty': 7, 'qty_done': 0})
|
||||
move1._action_done()
|
||||
|
||||
new_move = self.env['stock.move'].search([('name', '=', 'test_split_1'), ('state', '=', 'confirmed')])
|
||||
|
||||
self.assertEqual(move1.move_line_ids.product_uom_qty, 0.0)
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 3.0)
|
||||
self.assertEqual(new_move.move_line_ids.product_uom_qty, 7.0)
|
||||
self.assertEqual(new_move.move_line_ids.qty_done, 0.0)
|
||||
|
||||
def test_edit_initial_demand_1(self):
|
||||
""" Increase initial demand once everything is reserved and check if
|
||||
the existing move_line is updated.
|
||||
@ -3410,4 +3854,3 @@ class StockMove(TransactionCase):
|
||||
picking.button_validate()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 2)
|
||||
|
||||
|
@ -185,6 +185,73 @@ class TestPickShip(TestStockCommon):
|
||||
# the client picking should not be assigned anymore, as we returned partially what we took
|
||||
self.assertEqual(picking_client.state, 'confirmed')
|
||||
|
||||
def test_mto_moves_return_return(self):
|
||||
picking_pick, picking_client = self.create_pick_ship()
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.productA.id,
|
||||
'name': '123456789'
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||||
|
||||
picking_pick.action_assign()
|
||||
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
picking_pick.button_validate()
|
||||
self.assertEqual(picking_pick.state, 'done')
|
||||
self.assertEqual(picking_client.state, 'assigned')
|
||||
|
||||
# return this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_pick.button_validate()
|
||||
# return this return of this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_id=return_pick.id)\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_return_pick.button_validate()
|
||||
# test computation of traceability
|
||||
vals = {
|
||||
'line_id': 1,
|
||||
'model_name': 'stock.move.line',
|
||||
'level': 11,
|
||||
'parent_quant': False,
|
||||
}
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=return_return_pick.move_line_ids[0].id,
|
||||
stream='upstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[return_return_pick.id, return_pick.id, picking_pick.id],
|
||||
"Upstream computation from return of return worked"
|
||||
)
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=picking_pick.move_line_ids[0].id,
|
||||
stream='downstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[picking_pick.id, return_pick.id, return_return_pick.id],
|
||||
"Downstream computation from original picking worked"
|
||||
)
|
||||
|
||||
def test_mto_resupply_cancel_ship(self):
|
||||
""" This test simulates a pick pack ship with a resupply route
|
||||
set. Pick and pack are validated, ship is cancelled. This test
|
||||
@ -963,6 +1030,82 @@ class TestSinglePicking(TestStockCommon):
|
||||
self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
def test_extra_move_4(self):
|
||||
""" Create a picking with similar moves (created after
|
||||
confirmation). Action done should propagate all the extra
|
||||
quantity and only merge extra moves in their original moves.
|
||||
"""
|
||||
delivery = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
})
|
||||
self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 5,
|
||||
'quantity_done': 10,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': delivery.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
})
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 5)
|
||||
delivery.action_confirm()
|
||||
delivery.action_assign()
|
||||
|
||||
delivery.write({
|
||||
'move_lines': [(0, 0, {
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 0,
|
||||
'quantity_done': 10,
|
||||
'state': 'assigned',
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': delivery.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
})]
|
||||
})
|
||||
delivery.action_done()
|
||||
self.assertEqual(len(delivery.move_lines), 2, 'Move should not be merged together')
|
||||
for move in delivery.move_lines:
|
||||
self.assertEqual(move.quantity_done, move.product_uom_qty, 'Initial demand should be equals to quantity done')
|
||||
|
||||
def test_extra_move_5(self):
|
||||
""" Create a picking a move that is problematic with
|
||||
rounding (5.95 - 5.5 = 0.4500000000000002). Ensure that
|
||||
initial demand is corrct afer action_done and backoder
|
||||
are not created.
|
||||
"""
|
||||
delivery = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
})
|
||||
self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 5.5,
|
||||
'quantity_done': 5.95,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': delivery.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
})
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 5.5)
|
||||
delivery.action_confirm()
|
||||
delivery.action_assign()
|
||||
delivery.action_done()
|
||||
self.assertEqual(delivery.move_lines.product_uom_qty, 5.95, 'Move initial demand should be 5.95')
|
||||
|
||||
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery.id)])
|
||||
self.assertFalse(back_order, 'There should be no back order')
|
||||
|
||||
def test_recheck_availability_1(self):
|
||||
""" Check the good behavior of check availability. I create a DO for 2 unit with
|
||||
only one in stock. After the first check availability, I should have 1 reserved
|
||||
|
@ -629,7 +629,24 @@ class StockQuant(TransactionCase):
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0)
|
||||
self.assertEqual(quantity, 1)
|
||||
self.assertEqual(in_date, None)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_1b(self):
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 2.0)
|
||||
self.assertEqual(quantity, 3)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_2(self):
|
||||
""" Check that an incoming date is correctly set when updating the quantity of a tracked
|
||||
@ -707,6 +724,34 @@ class StockQuant(TransactionCase):
|
||||
# Removal strategy is LIFO, so lot1 should be received as it was received later.
|
||||
self.assertEqual(quants[0][0].lot_id.id, lot1.id)
|
||||
|
||||
def test_in_date_4b(self):
|
||||
""" Check for LIFO and max with/without in_date that it handles the LIFO NULLS LAST well
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
stock_location1 = self.env.ref('stock.stock_location_components')
|
||||
stock_location2 = self.env.ref('stock.stock_location_14')
|
||||
lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
|
||||
stock_location.removal_strategy_id = lifo_strategy
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location1.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
|
||||
in_date_location2 = datetime.now()
|
||||
self.env['stock.quant']._update_available_quantity(product1, stock_location2, 1.0, in_date=in_date_location2)
|
||||
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1)
|
||||
|
||||
# Removal strategy is LIFO, so the one with date is the most recent one and should be selected
|
||||
self.assertEqual(quants[0][0].location_id.id, stock_location2.id)
|
||||
|
||||
def test_in_date_5(self):
|
||||
""" Receive the same lot at different times, once they're in the same location, the quants
|
||||
are merged and only the earliest incoming date is kept.
|
||||
|
147
addons/stock/tests/test_robustness.py
Normal file
147
addons/stock/tests/test_robustness.py
Normal file
@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestRobustness(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestRobustness, self).setUp()
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.customer_location = self.env.ref('stock.stock_location_customers')
|
||||
self.uom_unit = self.env.ref('product.product_uom_unit')
|
||||
self.uom_dozen = self.env.ref('product.product_uom_dozen')
|
||||
self.product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_uom_factor(self):
|
||||
""" Changing the factor of a unit of measure shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be consistent
|
||||
with the `reserved_quantity` on quants.
|
||||
"""
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
12,
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# change the factor
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
move1.product_uom.factor = 0.05
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_location_usage(self):
|
||||
""" Changing the usage of a location shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be
|
||||
consistent with the `reserved_quantity` on the quants.
|
||||
"""
|
||||
# change stock usage
|
||||
self.stock_location.scrap_location = True
|
||||
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
1,
|
||||
)
|
||||
|
||||
# reserve a unit
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_location_archive',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location
|
||||
self.assertEqual(move1.product_qty, 1)
|
||||
|
||||
# change the stock usage
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
self.stock_location.scrap_location = False
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_package_unpack(self):
|
||||
""" Unpack a package that contains quants with a reservation
|
||||
should also remove the package on the reserved move lines.
|
||||
"""
|
||||
package = self.env['stock.quant.package'].create({
|
||||
'name': 'Shell Helix HX7 10W30',
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
10,
|
||||
package_id=package
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 10,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
|
||||
move1.result_package_id = False
|
||||
|
||||
package.unpack()
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0)
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0)
|
@ -229,6 +229,215 @@ class TestWarehouse(TestStockCommon):
|
||||
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
|
||||
self.assertEqual(len(quant), 1)
|
||||
|
||||
def test_resupply_route(self):
|
||||
""" Simulate a resupply chain between warehouses.
|
||||
Stock -> transit -> Dist. -> transit -> Shop -> Customer
|
||||
Create the move from Shop to Customer and ensure that all the pull
|
||||
rules are triggered in order to complete the move chain to Stock.
|
||||
"""
|
||||
warehouse_stock = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock.',
|
||||
'code': 'STK',
|
||||
})
|
||||
|
||||
warehouse_distribution = self.env['stock.warehouse'].create({
|
||||
'name': 'Dist.',
|
||||
'code': 'DIST',
|
||||
'default_resupply_wh_id': warehouse_stock.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
|
||||
})
|
||||
|
||||
warehouse_shop = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop',
|
||||
'code': 'SHOP',
|
||||
'default_resupply_wh_id': warehouse_distribution.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
|
||||
})
|
||||
|
||||
route_stock_to_dist = warehouse_distribution.resupply_route_ids
|
||||
route_dist_to_shop = warehouse_shop.resupply_route_ids
|
||||
|
||||
# Change the procure_method on the pull rules between dist and shop
|
||||
# warehouses. Since mto and resupply routes are both on product it will
|
||||
# select one randomly between them and if it select the resupply it is
|
||||
# 'make to stock' and it will not create the picking between stock and
|
||||
# dist warehouses.
|
||||
route_dist_to_shop.pull_ids.write({'procure_method': 'make_to_order'})
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
picking_out = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
'warehouse_id': warehouse_shop.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out.action_confirm()
|
||||
|
||||
moves = self.env['stock.move'].search([('product_id', '=', product.id)])
|
||||
# Shop/Stock -> Customer
|
||||
# Transit -> Shop/Stock
|
||||
# Dist/Stock -> Transit
|
||||
# Transit -> Dist/Stock
|
||||
# Stock/Stock -> Transit
|
||||
self.assertEqual(len(moves), 5, 'Invalid moves number.')
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
|
||||
def test_mutiple_resupply_warehouse(self):
|
||||
""" Simulate the following situation:
|
||||
- 2 shops with stock are resupply by 2 distinct warehouses
|
||||
- Shop Namur is resupply by the warehouse stock Namur
|
||||
- Shop Wavre is resupply by the warehouse stock Wavre
|
||||
- Simulate 2 moves for the same product but in different shop.
|
||||
This test ensure that the move are supplied by the correct distribution
|
||||
warehouse.
|
||||
"""
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
warehouse_distribution_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Wavre.',
|
||||
'code': 'WV',
|
||||
})
|
||||
|
||||
warehouse_shop_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Wavre',
|
||||
'code': 'SHWV',
|
||||
'default_resupply_wh_id': warehouse_distribution_wavre.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
|
||||
})
|
||||
|
||||
warehouse_distribution_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Namur.',
|
||||
'code': 'NM',
|
||||
})
|
||||
|
||||
warehouse_shop_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Namur',
|
||||
'code': 'SHNM',
|
||||
'default_resupply_wh_id': warehouse_distribution_namur.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
|
||||
})
|
||||
|
||||
route_shop_namur = warehouse_shop_namur.resupply_route_ids
|
||||
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
|
||||
|
||||
# The product contains the 2 resupply routes.
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
# Add 1 quant in each distribution warehouse.
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
|
||||
|
||||
# Create the move for the shop Namur. Should create a resupply from
|
||||
# distribution warehouse Namur.
|
||||
picking_out_namur = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_namur.id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_namur.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_namur.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Namur -> transit Location -> Shop Namur
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_namur)
|
||||
picking_transit_shop_namur.action_assign()
|
||||
picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_namur.action_done()
|
||||
|
||||
picking_out_namur.action_assign()
|
||||
picking_out_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_out_namur.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 0)
|
||||
|
||||
# Create the move for the shop Wavre. Should create a resupply from
|
||||
# distribution warehouse Wavre.
|
||||
picking_out_wavre = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_wavre.id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_wavre.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_wavre.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Wavre -> transit Location -> Shop Wavre
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_wavre)
|
||||
picking_transit_shop_wavre.action_assign()
|
||||
picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_wavre.action_done()
|
||||
|
||||
picking_out_wavre.action_assign()
|
||||
picking_out_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_out_wavre.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 0)
|
||||
|
||||
class TestResupply(TestStockCommon):
|
||||
def setUp(self):
|
||||
|
@ -78,6 +78,7 @@
|
||||
<group string="Applied On">
|
||||
<field name="location_id"/>
|
||||
<field name="warehouse_id" groups="base.group_no_one"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group string="Creates">
|
||||
<field name="location_src_id" attrs="{'required': [('action', '=', 'move')], 'invisible':[('action', '!=', 'move')]}" domain="[('usage','!=','view')]"/>
|
||||
|
@ -273,7 +273,7 @@
|
||||
<field name="push_ids" colspan="4" nolabel="1"/>
|
||||
</group>
|
||||
<group string="Procurement Rules" colspan="4" >
|
||||
<field name="pull_ids" colspan="4" nolabel="1"/>
|
||||
<field name="pull_ids" colspan="4" nolabel="1" context="{'default_company_id': company_id}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@ -285,6 +285,7 @@
|
||||
<field name="model">stock.location.route</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Route">
|
||||
<field name="name"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
|
@ -25,6 +25,8 @@
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="in_entire_package" invisible="1"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
@ -44,7 +46,8 @@
|
||||
<field name="qty_done"/>
|
||||
<field name="product_uom_id" options="{'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
</div>
|
||||
<field name="lot_id" string="Lot/Serial Number" groups="stock.group_production_lot"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('in_entire_package', '=', True)]}" domain="[('product_id', '=', product_id)]" groups="stock.group_production_lot" context="{'default_product_id': product_id, 'active_picking_id': picking_id}"/>
|
||||
<field name="lot_name" attrs="{'readonly': [('in_entire_package', '=', True)]}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" string="Source Package" groups="product.group_stock_packaging"/>
|
||||
<field name="result_package_id" string="Destination Package" groups="stock.group_tracking_lot"/>
|
||||
<field name="owner_id" string="Owner" groups="stock.group_tracking_owner"/>
|
||||
@ -86,6 +89,7 @@
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="in_entire_package"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
@ -93,6 +97,8 @@
|
||||
<field name="picking_id"/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<field name="lot_id" invisible="not context.get('show_lots_m2o')" domain="[('product_id', '=', product_id)]" groups="stock.group_production_lot" context="{'default_product_id': product_id, 'active_picking_id': picking_id}"/>
|
||||
<field name="lot_name" invisible="not context.get('show_lots_text')" groups="stock.group_production_lot"/>
|
||||
<field name="qty_done" string="Quantity Done"/>
|
||||
<field name="product_uom_id" string="Unit of Measure" groups="product.group_uom"/>
|
||||
</div>
|
||||
|
@ -77,9 +77,9 @@
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
<field name="priority"/>
|
||||
<field name="state"/>
|
||||
<field name="show_details_visible"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
@ -93,7 +93,11 @@
|
||||
<field name="product_id"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left"/>
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<button name="action_show_details" string="Register lots, packs, location"
|
||||
class="o_icon_button fa fa-list" type="object"
|
||||
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span><field name="product_uom_qty"/></span>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user