diff --git a/addons/mrp/product.py b/addons/mrp/product.py
index 178ca3138b3..a1ab3ba2b11 100644
--- a/addons/mrp/product.py
+++ b/addons/mrp/product.py
@@ -27,7 +27,6 @@ class product_template(osv.osv):
'bom_count': fields.function(_bom_orders_count, string='# Bill of Material', type='integer', multi="_bom_order_count"),
'mo_count': fields.function(_bom_orders_count_mo, string='# Manufacturing Orders', type='integer'),
'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added."),
- 'track_production': fields.boolean('Track Manufacturing Lots', help="Forces to specify a Serial Number for all moves containing this product and generated by a Manufacturing Order"),
}
_defaults = {
diff --git a/addons/mrp/stock.py b/addons/mrp/stock.py
index 733cb6f40b1..18b6d772192 100644
--- a/addons/mrp/stock.py
+++ b/addons/mrp/stock.py
@@ -21,28 +21,9 @@ class StockMove(osv.osv):
def check_tracking(self, cr, uid, move, lot_id, context=None):
super(StockMove, self).check_tracking(cr, uid, move, lot_id, context=context)
- if move.product_id.track_production and (move.location_id.usage == 'production' or move.location_dest_id.usage == 'production') and not lot_id:
- raise UserError(_('You must assign a serial number for the product %s') % (move.product_id.name))
- if move.raw_material_production_id and move.location_dest_id.usage == 'production' and move.raw_material_production_id.product_id.track_production and not move.consumed_for:
+ if move.raw_material_production_id and move.product_id.tracking!='none' and move.location_dest_id.usage == 'production' and move.raw_material_production_id.product_id.tracking != 'none' and not move.consumed_for:
raise UserError(_("Because the product %s requires it, you must assign a serial number to your raw material %s to proceed further in your production. Please use the 'Produce' button to do so.") % (move.raw_material_production_id.product_id.name, move.product_id.name))
- # TODO master: remove me, no longer used
- def _check_phantom_bom(self, cr, uid, move, context=None):
- """check if product associated to move has a phantom bom
- return list of ids of mrp.bom for that product """
- user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
- #doing the search as SUPERUSER because a user with the permission to write on a stock move should be able to explode it
- #without giving him the right to read the boms.
- domain = [
- '|', ('product_id', '=', move.product_id.id),
- '&', ('product_id', '=', False), ('product_tmpl_id.product_variant_ids', '=', move.product_id.id),
- ('type', '=', 'phantom'),
- '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
- '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
- ('company_id', '=', user_company)]
- return self.pool.get('mrp.bom').search(cr, SUPERUSER_ID, domain, context=context)
-
-
def _action_explode(self, cr, uid, move, context=None):
""" Explodes pickings.
@param move: Stock moves
diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py
index 4ead80c75be..365f5f5a466 100644
--- a/addons/mrp/wizard/mrp_product_produce.py
+++ b/addons/mrp/wizard/mrp_product_produce.py
@@ -14,7 +14,6 @@ class mrp_product_produce_line(osv.osv_memory):
'product_qty': fields.float('Quantity (in default UoM)', digits_compute=dp.get_precision('Product Unit of Measure')),
'lot_id': fields.many2one('stock.production.lot', string='Lot'),
'produce_id': fields.many2one('mrp.product.produce', string="Produce"),
- 'track_production': fields.related('product_id', 'track_production', type='boolean', string="Track Production"),
}
class mrp_product_produce(osv.osv_memory):
@@ -31,7 +30,8 @@ class mrp_product_produce(osv.osv_memory):
"and it will finish the production order when total ordered quantities are produced."),
'lot_id': fields.many2one('stock.production.lot', 'Lot'), #Should only be visible when it is consume and produce mode
'consume_lines': fields.one2many('mrp.product.produce.line', 'produce_id', 'Products Consumed'),
- 'track_production': fields.boolean('Track production'),
+ 'tracking': fields.related('product_id', 'tracking', type='selection',
+ selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')]),
}
def on_change_qty(self, cr, uid, ids, product_qty, consume_lines, context=None):
@@ -88,13 +88,13 @@ class mrp_product_produce(osv.osv_memory):
def _get_track(self, cr, uid, context=None):
prod = self._get_product_id(cr, uid, context=context)
prod_obj = self.pool.get("product.product")
- return prod and prod_obj.browse(cr, uid, prod, context=context).track_production or False
+ return prod and prod_obj.browse(cr, uid, prod, context=context).tracking or 'none'
_defaults = {
'product_qty': _get_product_qty,
'mode': lambda *x: 'consume_produce',
'product_id': _get_product_id,
- 'track_production': _get_track,
+ 'tracking': _get_track,
}
def do_produce(self, cr, uid, ids, context=None):
diff --git a/addons/mrp/wizard/mrp_product_produce_view.xml b/addons/mrp/wizard/mrp_product_produce_view.xml
index 859bebc2b43..6535f390f9f 100644
--- a/addons/mrp/wizard/mrp_product_produce_view.xml
+++ b/addons/mrp/wizard/mrp_product_produce_view.xml
@@ -14,10 +14,10 @@
-
+
diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py
index ee1d9e8de44..0c894beef83 100644
--- a/addons/mrp_repair/mrp_repair.py
+++ b/addons/mrp_repair/mrp_repair.py
@@ -268,7 +268,7 @@ class mrp_repair(osv.osv):
else:
self.write(cr, uid, [o.id], {'state': 'confirmed'})
for line in o.operations:
- if line.product_id.track_production:
+ if line.product_id.tracking != 'none':
raise UserError(_("Serial number is required for operation line with product '%s'") % (line.product_id.name))
mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
return True
diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py
index 79ff3cbd060..56ad233c914 100644
--- a/addons/stock/__openerp__.py
+++ b/addons/stock/__openerp__.py
@@ -58,6 +58,7 @@ Dashboard / Reports for Warehouse Management will include:
'wizard/make_procurement_view.xml',
'wizard/orderpoint_procurement_view.xml',
'wizard/stock_pack_details.xml',
+ 'wizard/stock_lot_split.xml',
'stock_incoterms.xml',
'stock_report.xml',
'stock_view.xml',
@@ -74,7 +75,7 @@ Dashboard / Reports for Warehouse Management will include:
'report/report_stock_forecast.xml',
'stock_dashboard.xml',
'wizard/stock_immediate_transfer.xml',
- 'wizard/stock_backorder_confirmation.xml'
+ 'wizard/stock_backorder_confirmation.xml',
],
'test': [
'test/inventory.yml',
diff --git a/addons/stock/product.py b/addons/stock/product.py
index 67cba725017..0a106a70076 100644
--- a/addons/stock/product.py
+++ b/addons/stock/product.py
@@ -312,14 +312,14 @@ class product_product(osv.osv):
templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)]))
return template_obj.action_view_routes(cr, uid, templ_ids, context=context)
- def onchange_track_all(self, cr, uid, ids, track_all, context=None):
- if not track_all:
+ def onchange_tracking(self, cr, uid, ids, tracking, context=None):
+ if not tracking or tracking == 'none':
return {}
unassigned_quants = self.pool['stock.quant'].search_count(cr, uid, [('product_id','in', ids), ('lot_id','=', False), ('location_id.usage','=', 'internal')], context=context)
if unassigned_quants:
return {'warning' : {
'title': _('Warning!'),
- 'message' : _("Lots are not defined for all the existing inventory of this product. You should assign serial numbers (e.g. by creating an inventory) first.")
+ 'message' : _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ")
}}
return {}
@@ -392,10 +392,7 @@ class product_template(osv.osv):
'loc_rack': fields.char('Rack', size=16),
'loc_row': fields.char('Row', size=16),
'loc_case': fields.char('Case', size=16),
- 'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Serial Number for all moves containing this product and coming from a Supplier Location"),
- 'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Serial Number for all moves containing this product and going to a Customer Location"),
- 'track_all': fields.boolean('Full Lots Traceability', help="Forces to specify a Serial Number on each and every operation related to this product"),
-
+ 'tracking': fields.selection(selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", required=True),
# sum of product variant qty
# 'reception_count': fields.function(_product_available, multi='qty_available',
# fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
@@ -416,6 +413,7 @@ class product_template(osv.osv):
_defaults = {
'sale_delay': 7,
+ 'tracking': 'none',
}
def action_view_routes(self, cr, uid, ids, context=None):
@@ -432,12 +430,12 @@ class product_template(osv.osv):
result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]"
return result
- def onchange_track_all(self, cr, uid, ids, track_all, context=None):
- if not track_all:
+ def onchange_tracking(self, cr, uid, ids, tracking, context=None):
+ if not tracking:
return {}
product_product = self.pool['product.product']
variant_ids = product_product.search(cr, uid, [('product_tmpl_id', 'in', ids)], context=context)
- return product_product.onchange_track_all(cr, uid, variant_ids, track_all, context=context)
+ return product_product.onchange_tracking(cr, uid, variant_ids, tracking, context=context)
def _get_products(self, cr, uid, ids, context=None):
products = []
diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml
index f7c2bd06efd..7b83cc7bf17 100644
--- a/addons/stock/product_view.xml
+++ b/addons/stock/product_view.xml
@@ -135,9 +135,7 @@
-
-
-
+
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 9a13f778768..50243d79030 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -396,7 +396,7 @@ class stock_quant(osv.osv):
quants_reconcile.append(quant)
if to_move_quants:
to_recompute_move_ids = [x.reservation_id.id for x in to_move_quants if x.reservation_id and x.reservation_id.id != move.id]
- self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, context=context)
+ self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, lot_id=lot_id, context=context)
self.pool.get('stock.move').recalculate_move_state(cr, uid, to_recompute_move_ids, context=context)
if location_to.usage == 'internal':
# Do manual search for quant to avoid full table scan (order by id)
@@ -408,11 +408,13 @@ class stock_quant(osv.osv):
for quant in quants_reconcile:
self._quant_reconcile_negative(cr, uid, quant, move, context=context)
- def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
+ def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, lot_id = False, context=None):
context=context or {}
vals = {'location_id': location_dest_id.id,
'history_ids': [(4, move.id)],
'reservation_id': False}
+ if lot_id and any(x.id for x in quants if not x.lot_id.id):
+ vals['lot_id'] = lot_id
if not context.get('entire_pack'):
vals.update({'package_id': dest_package_id})
self.write(cr, SUPERUSER_ID, [q.id for q in quants], vals, context=context)
@@ -430,6 +432,19 @@ class stock_quant(osv.osv):
if location.usage in ['inventory', 'production', 'supplier']:
return quants
res_qty = qty
+ if restrict_lot_id:
+ if not prefered_domain_list:
+ prefered_domain_list = [[('lot_id', '=', restrict_lot_id)],[('lot_id', '=', False)]]
+ else:
+ lot_list = []
+ no_lot_list = []
+ for pref_domain in prefered_domain_list:
+ pref_lot_domain = pref_domain + [('lot_id', '=', restrict_lot_id)]
+ pref_no_lot_domain = pref_domain + [('lot_id', '=', False)]
+ lot_list.append(pref_lot_domain)
+ no_lot_list.append(pref_no_lot_domain)
+ prefered_domain_list = lot_list + no_lot_list
+
if not prefered_domain_list:
return self.quants_get(cr, uid, location, product, qty, domain=domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
for prefered_domain in prefered_domain_list:
@@ -458,7 +473,7 @@ class stock_quant(osv.osv):
if restrict_partner_id:
domain += [('owner_id', '=', restrict_partner_id)]
if restrict_lot_id:
- domain += [('lot_id', '=', restrict_lot_id)]
+ domain += ['|', ('lot_id', '=', restrict_lot_id), ('lot_id', '=', False)]
if location:
removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context)
result += self.apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context)
@@ -507,6 +522,16 @@ class stock_quant(osv.osv):
negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
vals.update({'propagated_from_id': negative_quant_id})
+ # In case of serial tracking, check if the product does not exist somewhere internally already
+ picking_type = move.picking_id and move.picking_id.picking_type_id or False
+ if move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)):
+ if qty != 1.0:
+ raise UserError(_('You should only receive by the piece with the same serial number'))
+ other_quants = self.search(cr, uid, [('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
+ ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')], context=context)
+ if other_quants:
+ raise UserError(_('The serial number %s is already in stock') % lot_id.name)
+
#create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
return self.browse(cr, uid, quant_id, context=context)
@@ -547,12 +572,11 @@ class stock_quant(osv.osv):
"""
solving_quant = quant
dom = [('qty', '<', 0)]
- if quant.lot_id:
- dom += [('lot_id', '=', quant.lot_id.id)]
- dom += [('owner_id', '=', quant.owner_id.id)]
- dom += [('package_id', '=', quant.package_id.id)]
+ if quant.package_id.id:
+ dom += [('package_id', '=', quant.package_id.id)]
dom += [('id', '!=', quant.propagated_from_id.id)]
- quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, dom, context=context)
+ quants = self.quants_get_prefered_domain(cr, uid, quant.location_id, quant.product_id, quant.qty, dom,
+ restrict_lot_id=quant.lot_id.id, restrict_partner_id=quant.owner_id.id, context=context)
product_uom_rounding = quant.product_id.uom_id.rounding
for quant_neg, qty in quants:
if not quant_neg or not solving_quant:
@@ -790,7 +814,8 @@ class stock_picking(models.Model):
if picking_type_id:
picking_type = self.pool['stock.picking.type'].browse(cr, uid, picking_type_id)
res['value'] = {'location_id': picking_type.default_location_src_id.id,
- 'location_dest_id': picking_type.default_location_dest_id.id}
+ 'location_dest_id': picking_type.default_location_dest_id.id,
+ 'lots_visible': picking_type.use_create_lots or picking_type.use_existing_lots,}
return res
def _default_location_destination(self):
@@ -1521,6 +1546,7 @@ class stock_picking(models.Model):
stock_move_obj = self.pool["stock.move"]
stock_operation_obj = self.pool["stock.pack.operation"]
package_obj = self.pool["stock.quant.package"]
+ package_id = False
for pick in self.browse(cr, uid, ids, context=context):
operations = [x for x in pick.pack_operation_ids if x.qty_done > 0 and (not x.result_package_id)]
pack_operation_ids = []
@@ -1532,11 +1558,13 @@ class stock_picking(models.Model):
stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context)
op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
pack_operation_ids.append(op.id)
- if op.product_id and op.location_id and op.location_dest_id:
- stock_move_obj.check_tracking_product(cr, uid, op.product_id, op.lot_id.id, op.location_id, op.location_dest_id, context=context)
if operations:
+ stock_operation_obj.check_tracking(cr, uid, pack_operation_ids, context=context)
package_id = package_obj.create(cr, uid, {}, context=context)
stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context)
+ else:
+ raise UserError(_('Please process some quantities to put in the pack first!'))
+ return package_id
class stock_production_lot(osv.osv):
@@ -2168,22 +2196,13 @@ class stock_move(osv.osv):
self.check_recompute_pack_op(cr, uid, ids, context=context)
return res
- def check_tracking_product(self, cr, uid, product, lot_id, location, location_dest, context=None):
- check = False
- if product.track_all and not location_dest.usage == 'inventory':
- check = True
- elif product.track_incoming and location.usage in ('supplier', 'transit', 'inventory') and location_dest.usage == 'internal':
- check = True
- elif product.track_outgoing and location_dest.usage in ('customer', 'transit') and location.usage == 'internal':
- check = True
- if check and not lot_id:
- raise UserError(_('You must assign a serial number for the product %s') % (product.name))
-
-
def check_tracking(self, cr, uid, move, lot_id, context=None):
""" Checks if serial number is assigned to stock move or not and raise an error if it had to.
"""
- self.check_tracking_product(cr, uid, move.product_id, lot_id, move.location_id, move.location_dest_id, context=context)
+ if move.picking_id and (move.picking_id.picking_type_id.use_existing_lots or move.picking_id.picking_type_id.use_create_lots) and \
+ move.product_id.tracking != 'none':
+ if not lot_id:
+ raise UserError(_('You need to provide a Lot/Serial Number for product %s') % move.product_id.name)
def check_recompute_pack_op(self, cr, uid, ids, context=None):
pickings = list(set([x.picking_id for x in self.browse(cr, uid, ids, context=context) if x.picking_id]))
@@ -2373,7 +2392,7 @@ class stock_move(osv.osv):
prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain_list=prefered_domain_list,
- restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
+ restrict_lot_id=ops.lot_id.id, restrict_partner_id=ops.owner_id.id, context=context)
if ops.product_id:
#If a product is given, the result is always put immediately in the result package (if it is False, they are without package)
quant_dest_package_id = ops.result_package_id.id
@@ -3383,6 +3402,8 @@ class stock_warehouse(osv.osv):
'name': _('Receipts'),
'warehouse_id': warehouse.id,
'code': 'incoming',
+ 'use_create_lots': True,
+ 'use_existing_lots': False,
'sequence_id': in_seq_id,
'default_location_src_id': supplier_loc.id,
'default_location_dest_id': input_loc.id,
@@ -3392,6 +3413,8 @@ class stock_warehouse(osv.osv):
'name': _('Delivery Orders'),
'warehouse_id': warehouse.id,
'code': 'outgoing',
+ 'use_create_lots': False,
+ 'use_existing_lots': True,
'sequence_id': out_seq_id,
'return_picking_type_id': in_type_id,
'default_location_src_id': output_loc.id,
@@ -3403,6 +3426,8 @@ class stock_warehouse(osv.osv):
'name': _('Internal Transfers'),
'warehouse_id': warehouse.id,
'code': 'internal',
+ 'use_create_lots': False,
+ 'use_existing_lots': True,
'sequence_id': int_seq_id,
'default_location_src_id': wh_stock_loc.id,
'default_location_dest_id': wh_stock_loc.id,
@@ -3413,6 +3438,8 @@ class stock_warehouse(osv.osv):
'name': _('Pack'),
'warehouse_id': warehouse.id,
'code': 'internal',
+ 'use_create_lots': False,
+ 'use_existing_lots': True,
'sequence_id': pack_seq_id,
'default_location_src_id': wh_pack_stock_loc.id,
'default_location_dest_id': output_loc.id,
@@ -3423,6 +3450,8 @@ class stock_warehouse(osv.osv):
'name': _('Pick'),
'warehouse_id': warehouse.id,
'code': 'internal',
+ 'use_create_lots': False,
+ 'use_existing_lots': True,
'sequence_id': pick_seq_id,
'default_location_src_id': wh_stock_loc.id,
'default_location_dest_id': wh_pack_stock_loc.id,
@@ -4029,6 +4058,27 @@ class stock_pack_operation(osv.osv):
self.write(cr, uid, [id], {'qty_done': 1.0}, context=context)
return True
+ def _compute_lots_visible(self, cr, uid, ids, field_name, arg, context=None):
+ res = {}
+ for pack in self.browse(cr, uid, ids, context=context):
+ pick = pack.picking_id
+ product_requires = (pack.product_id.tracking != 'none')
+ if pick.picking_type_id:
+ res[pack.id] = pick.picking_type_id.use_existing_lots or pick.picking_type_id.use_create_lots and product_requires
+ else:
+ res[pack.id] = product_requires
+ return res
+
+ def _get_default_from_loc(self, cr, uid, context=None):
+ default_loc = context.get('default_location_id')
+ if default_loc:
+ return self.pool['stock.location'].browse(cr, uid, default_loc, context=context).name
+
+ def _get_default_to_loc(self, cr, uid, context=None):
+ default_loc = context.get('default_location_dest_id')
+ if default_loc:
+ return self.pool['stock.location'].browse(cr, uid, default_loc, context=context).name
+
_columns = {
'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
@@ -4051,6 +4101,7 @@ class stock_pack_operation(osv.osv):
'from_loc': fields.function(_compute_location_description, type='char', string='From', multi='loc', readonly=True),
'to_loc': fields.function(_compute_location_description, type='char', string='To', multi='loc', readonly=True),
'fresh_record': fields.boolean('Newly created pack operation'),
+ 'lots_visible': fields.function(_compute_lots_visible, type='boolean'),
'state': fields.related('picking_id', 'state', type='selection', selection=[
('draft', 'Draft'),
('cancel', 'Cancelled'),
@@ -4068,6 +4119,8 @@ class stock_pack_operation(osv.osv):
'product_qty': 0.0,
'processed_boolean': lambda *a: False,
'fresh_record': True,
+ 'from_loc': _get_default_from_loc,
+ 'to_loc': _get_default_to_loc,
}
def write(self, cr, uid, ids, vals, context=None):
@@ -4093,97 +4146,59 @@ class stock_pack_operation(osv.osv):
raise UserError(_('You can not delete pack operations of a done picking'))
return super(stock_pack_operation, self).unlink(cr, uid, ids, context=context)
- def action_drop_down(self, cr, uid, ids, context=None):
- ''' Used by barcode interface to say that pack_operation has been moved from src location
- to destination location, if qty_done is less than product_qty than we have to split the
- operation in two to process the one with the qty moved
- '''
- processed_ids = []
- move_obj = self.pool.get("stock.move")
- for pack_op in self.browse(cr, uid, ids, context=None):
- if pack_op.product_id and pack_op.location_id and pack_op.location_dest_id:
- move_obj.check_tracking_product(cr, uid, pack_op.product_id, pack_op.lot_id.id, pack_op.location_id, pack_op.location_dest_id, context=context)
- op = pack_op.id
- if pack_op.qty_done < pack_op.product_qty:
- # we split the operation in two
- op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context)
- self.write(cr, uid, [pack_op.id], {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
- processed_ids.append(op)
- self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context)
+ def check_tracking(self, cr, uid, ids, context=None):
+ """ Checks if serial number is assigned to stock move or not and raise an error if it had to.
+ """
+ operations = self.browse(cr, uid, ids, context=context)
+ for ops in operations:
+ if ops.picking_id and (ops.picking_id.picking_type_id.use_existing_lots or ops.picking_id.picking_type_id.use_create_lots) and \
+ ops.product_id and ops.product_id.tracking != 'none':
+ if not ops.lot_id:
+ raise UserError(_('You need to provide a Lot/Serial Number for product %s') % ops.product_id.name)
+ if ops.product_id.tracking == 'serial' and ops.qty_done != 1.0:
+ raise UserError(_('You should provide a different Lot for each piece'))
- def create_and_assign_lot(self, cr, uid, id, name, context=None):
- ''' Used by barcode interface to create a new lot and assign it to the operation
- '''
- obj = self.browse(cr,uid,id,context)
- product_id = obj.product_id.id
- val = {'product_id': product_id}
- new_lot_id = False
- if name:
- lots = self.pool.get('stock.production.lot').search(cr, uid, ['&', ('name', '=', name), ('product_id', '=', product_id)], context=context)
- if lots:
- new_lot_id = lots[0]
- val.update({'name': name})
-
- if not new_lot_id:
- new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context)
- self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context)
-
- def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False, visible_op_ids=False, increment=1, context=None):
- '''Search for an operation with given 'domain' in a picking, if it exists increment the qty by the value of increment otherwise create it
-
- :param domain: list of tuple directly reusable as a domain
- context can receive a key 'current_package_id' with the package to consider for this operation
- returns True
- '''
- if context is None:
- context = {}
-
- #if current_package_id is given in the context, we increase the number of items in this package
- package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
- existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id)] + domain + package_clause, context=context)
- todo_operation_ids = []
- if existing_operation_ids:
- if filter_visible:
- todo_operation_ids = [val for val in existing_operation_ids if val in visible_op_ids]
+ def split_lot(self, cr, uid, ids, context=None):
+ assert len(ids) > 0
+ data_obj = self.pool['ir.model.data']
+ pack = self.browse(cr, uid, ids[0], context=context)
+ picking_type = pack.picking_id.picking_type_id
+ serial = (pack.product_id.tracking == 'serial')
+ view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_lot_split')
+ only_create = picking_type.use_create_lots and not picking_type.use_existing_lots
+ line_ids = []
+ if pack.qty_done > 0 and pack.lot_id:
+ if pack.product_id.tracking == 'serial':
+ product_qty = 1.0
else:
- todo_operation_ids = existing_operation_ids
- if todo_operation_ids:
- #existing operation found for the given domain and picking => increment its quantity
- operation_id = todo_operation_ids[0]
- op_obj = self.browse(cr, uid, operation_id, context=context)
- qty = op_obj.qty_done
- if increment > 0:
- qty += increment
- elif increment < 0:
- if qty == 0 and op_obj.product_qty == 0:
- #we have a line with 0 qty set, so delete it
- self.unlink(cr, uid, [operation_id], context=context)
- return False
- else:
- qty = max(0, qty-1)
- self.write(cr, uid, [operation_id], {'qty_done': qty}, context=context)
- else:
- #no existing operation found for the given domain and picking => create a new one
- picking_obj = self.pool.get("stock.picking")
- picking = picking_obj.browse(cr, uid, picking_id, context=context)
- values = {
- 'picking_id': picking_id,
- 'product_qty': 0,
- 'location_id': picking.location_id.id,
- 'location_dest_id': picking.location_dest_id.id,
- 'qty_done': increment,
- }
- for key in domain:
- var_name, dummy, value = key
- uom_id = False
- if var_name == 'product_id':
- uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
- update_dict = {var_name: value}
- if uom_id:
- update_dict['product_uom_id'] = uom_id
- values.update(update_dict)
- operation_id = self.create(cr, uid, values, context=context)
- return operation_id
+ product_qty = pack.qty_done
+ line_ids = [(0, 0, {'lot_id': pack.lot_id.id,
+ 'lot_name': pack.lot_id.name if only_create else '',
+ 'product_qty': product_qty,
+ })]
+ values = {
+ 'pack_id': pack.id,
+ 'product_id': pack.product_id.id,
+ 'product_uom_id': pack.product_uom_id.id,
+ 'product_qty': pack.product_qty,
+ 'qty_done': pack.qty_done,
+ 'only_create': only_create,
+ 'line_ids': line_ids,
+ }
+ wiz_id = self.pool['stock.lot.split'].create(cr, uid, values, context=context)
+ return {
+ 'name': _('Split Lot'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.lot.split',
+ 'views': [(view, 'form')],
+ 'view_id': view,
+ 'target': 'new',
+ 'res_id': wiz_id,
+ 'context': {'serial': serial,
+ 'only_create': only_create},
+ }
class stock_move_operation_link(osv.osv):
@@ -4216,8 +4231,8 @@ class stock_move_operation_link(osv.osv):
#if not given any information about package, we don't open boxes
domain.append(('package_id', '=', False))
#if lot info is given, we restrict choice to this lot otherwise we can take any
- if op.lot_id:
- domain.append(('lot_id', '=', op.lot_id.id))
+ #if op.lot_id:
+ # domain.append(('lot_id', '=', op.lot_id.id))
#if owner info is given, we restrict to this owner otherwise we restrict to no owner
if op.owner_id:
domain.append(('owner_id', '=', op.owner_id.id))
@@ -4471,9 +4486,11 @@ class stock_picking_type(osv.osv):
'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
'code': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True),
'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
- 'show_entire_packs': fields.boolean('Show entire packs to move'),
+ 'show_entire_packs': fields.boolean('Allow moving packs'),
'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
'active': fields.boolean('Active'),
+ 'use_create_lots': fields.boolean('Create New Lots'),
+ 'use_existing_lots': fields.boolean('Use Existing Lots'),
# Statistics for the kanban view
'last_done_picking': fields.function(_get_tristate_values,
@@ -4505,6 +4522,8 @@ class stock_picking_type(osv.osv):
_defaults = {
'warehouse_id': _default_warehouse,
'active': True,
+ 'use_existing_lots': True,
+ 'use_create_lots': True,
}
class barcode_rule(models.Model):
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index 8f8447f784e..da1234ed48c 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -729,13 +729,16 @@
-
+
-
+
+
+
@@ -751,7 +754,7 @@
-
+
@@ -764,13 +767,13 @@
+ states="confirmed,assigned,waiting,partially_available" groups="base.group_no_one"/>
-
+
@@ -1449,13 +1452,18 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/stock/wizard/__init__.py b/addons/stock/wizard/__init__.py
index 7a4969f4235..ecea997abdd 100644
--- a/addons/stock/wizard/__init__.py
+++ b/addons/stock/wizard/__init__.py
@@ -9,3 +9,4 @@ import orderpoint_procurement
import stock_pack_details
import stock_immediate_transfer
import stock_backorder_confirmation
+import stock_lot_split
\ No newline at end of file
diff --git a/addons/stock/wizard/stock_lot_split.py b/addons/stock/wizard/stock_lot_split.py
new file mode 100644
index 00000000000..0db5d933c12
--- /dev/null
+++ b/addons/stock/wizard/stock_lot_split.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import models, fields, api, _
+import openerp.addons.decimal_precision as dp
+from openerp.exceptions import UserError
+
+class stock_lot_split(models.TransientModel):
+ _name = 'stock.lot.split'
+ _description = 'Lot split'
+
+ @api.one
+ @api.depends('line_ids')
+ def _compute_qty_done(self):
+ self.qty_done = sum([x.product_qty for x in self.line_ids])
+
+ pack_id = fields.Many2one('stock.pack.operation', 'Pack operation')
+ product_qty = fields.Float('Quantity', digits=dp.get_precision('Product Unit of Measure'), readonly=True)
+ product_id = fields.Many2one('product.product', 'Product', readonly=True)
+ product_uom_id = fields.Many2one('product.uom', 'Product Unit of Measure', readonly=True)
+ line_ids = fields.One2many('stock.lot.split.line', 'split_id')
+ qty_done = fields.Float('Processed Qty', digits=dp.get_precision('Product Unit of Measure'), compute='_compute_qty_done')
+ picking_type_id = fields.Many2one('stock.picking.type', related='pack_id.picking_id.picking_type_id')
+
+ @api.multi
+ def process(self):
+ self.ensure_one()
+ if not self.line_ids:
+ raise UserError (_('Please provide at least one line'))
+ # Split pack operations
+ firstline = True
+ totals_other = 0.0
+ for line in self.line_ids:
+ if not line.lot_name and not line.lot_id:
+ raise UserError(_('Please provide a lot/serial number for every line'))
+ # In case of only creating lots, we will have the text in lot_name, otherwise the lot_id
+ if line.lot_name:
+ if self.pack_id.lot_id and line.lot_name == self.pack_id.lot_id.name:
+ lot = self.pack_id.lot_id
+ else:
+ lot = self.env['stock.production.lot'].create({'name': line.lot_name, 'product_id': self.pack_id.product_id.id})
+ else:
+ lot = line.lot_id
+ if firstline:
+ self.pack_id.write({'lot_id': lot.id,
+ 'qty_done': line.product_qty})
+ firstline = False
+ else:
+ pack_new = self.pack_id.copy()
+ pack_new.write({'lot_id': lot.id,
+ 'qty_done': line.product_qty,
+ 'product_qty': line.product_qty})
+ totals_other += line.product_qty
+ old_qty = self.pack_id.product_qty
+ if old_qty - totals_other > 0:
+ self.pack_id.product_qty = self.pack_id.product_qty - totals_other
+ else:
+ self.pack_id.product_qty = 0.0
+
+ return True
+
+
+class stock_lot_split_line(models.TransientModel):
+ _name = 'stock.lot.split.line'
+ _description = 'Lot split line'
+
+ split_id = fields.Many2one('stock.lot.split')
+ lot_id = fields.Many2one('stock.production.lot', string="Lot/Serial Number")
+ lot_name = fields.Char('Name')
+ product_qty = fields.Float('Quantity', digits=dp.get_precision('Product Unit of Measure'), default=1.0)
\ No newline at end of file
diff --git a/addons/stock/wizard/stock_lot_split.xml b/addons/stock/wizard/stock_lot_split.xml
new file mode 100644
index 00000000000..cee63bc0da1
--- /dev/null
+++ b/addons/stock/wizard/stock_lot_split.xml
@@ -0,0 +1,47 @@
+
+
+
+ Split Lot
+ stock.lot.split
+
+
+
+
+
+
+ Lot Split
+ ir.actions.act_window
+ stock.lot.split
+
+ form
+ form
+ new
+ {}
+
+
\ No newline at end of file
diff --git a/addons/stock/wizard/stock_pack_details.py b/addons/stock/wizard/stock_pack_details.py
index afab6d65b42..23240a88c92 100644
--- a/addons/stock/wizard/stock_pack_details.py
+++ b/addons/stock/wizard/stock_pack_details.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp import models, fields, api
+from openerp import models, fields, api, _
import openerp.addons.decimal_precision as dp
+from openerp.exceptions import UserError
class stock_pack_details(models.TransientModel):
_name = 'stock.pack.details'
@@ -44,17 +45,13 @@ class stock_pack_details(models.TransientModel):
@api.multi
def split_quantities(self):
for wiz in self:
- if wiz.quantity>1 and wiz.qty_done < wiz.quantity:
+ if wiz.quantity>0.0 and wiz.qty_done < wiz.quantity:
pack2 = self.pack_id.copy({'qty_done': 0.0, 'product_qty': wiz.quantity - wiz.qty_done})
wiz.quantity = wiz.qty_done
self.pack_id.write({'qty_done': wiz.qty_done, 'product_qty': wiz.quantity})
- return {
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.picking',
- 'type': 'ir.actions.act_window',
- 'res_id': self.pack_id.picking_id.id,
- }
+ return True
+ else:
+ raise UserError(_('Can not split 0 quantity'))
@api.one
def process(self):
@@ -69,4 +66,4 @@ class stock_pack_details(models.TransientModel):
'location_dest_id': self.location_dest_id.id,
'result_package_id': self.result_package_id.id,
})
- return {}
\ No newline at end of file
+ return True
\ No newline at end of file
diff --git a/addons/stock/wizard/stock_pack_details.xml b/addons/stock/wizard/stock_pack_details.xml
index 23b1ddbb551..920fcaeabc1 100644
--- a/addons/stock/wizard/stock_pack_details.xml
+++ b/addons/stock/wizard/stock_pack_details.xml
@@ -6,13 +6,12 @@