[IMP] stock: Improve lots with serial numbers, define lot tracking by picking type and convert stock with lot False to a lot when necessary
Instead of having only outgoing, manufacturing, incoming, all... traceability on the product, we can define on the product if there is no tracking, lot tracking or unique serial number tracking. On the picking type we can define whether we will only create lots for the picking type or use existing lots. The lots are required when the product and the picking type demand it. The option for serial number will check that each piece (quantity 1) has its own serial number. A lot splitting wizard is added on the pack operations in the picking form. We want to be able to handle the case where we receive a lot of iPads e.g. and we only assign a serial number when the product is delivered. For this, when the product is delivered, we see that we only have products with no lots. When we see that we take something without lot for something that needed one, we assign the lot to that piece of stock. For negative quants: a positive quant with no lot can reconcile with a negative quant with/without lot, a positive quant with lot, will preferably reconcile with a negative quant with the same lot, afterwards with that without lot.
This commit is contained in:
@@ -555,9 +555,6 @@
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.view_template_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='lot']" position="inside">
|
||||
<field name="track_production" groups="stock.group_production_lot" attrs="{'invisible': [('track_all', '=', True)]}"/>
|
||||
</xpath>
|
||||
<group name="sale_condition" position="inside">
|
||||
<label for="produce_delay" attrs="{'invisible':[('type','=','service')]}"/>
|
||||
<div attrs="{'invisible':[('type','=','service')]}">
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+1
-20
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<field name="mode"/>
|
||||
<field name="product_qty" colspan="2" on_change="on_change_qty(product_qty, consume_lines, context)"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="track_production" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]"
|
||||
context="{'default_product_id':product_id}"
|
||||
attrs="{'required': [('track_production', '=', True), ('mode', '=', 'consume_produce')]}"
|
||||
attrs="{'required': [('tracking', '!=', 'none'), ('mode', '=', 'consume_produce')]}"
|
||||
groups="stock.group_production_lot"/>
|
||||
</group>
|
||||
<group string="To Consume">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
+8
-10
@@ -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 = []
|
||||
|
||||
@@ -135,9 +135,7 @@
|
||||
</group>
|
||||
<group name="status" position="before">
|
||||
<group name="lot" groups="stock.group_production_lot" string="Lots" attrs="{'invisible':[('type','=','service')]}">
|
||||
<field name="track_all" groups="stock.group_production_lot" on_change="onchange_track_all(track_all)"/>
|
||||
<field name="track_incoming" groups="stock.group_production_lot" attrs="{'invisible': [('track_all', '=', True)]}"/>
|
||||
<field name="track_outgoing" groups="stock.group_production_lot" attrs="{'invisible': [('track_all', '=', True)]}"/>
|
||||
<field name="tracking" on_change="onchange_tracking(tracking)" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="weight" position="before">
|
||||
|
||||
+136
-117
@@ -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):
|
||||
|
||||
@@ -729,13 +729,16 @@
|
||||
<button name="do_prepare_partial" type="object" string="Recompute" attrs="{'invisible': [('recompute_pack_op','=', False)]}"/>
|
||||
<field name="recompute_pack_op" invisible="1"/>
|
||||
<field name="pack_operation_ids" invisible="1"/>
|
||||
<field name="pack_operation_product_ids" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<field name="pack_operation_product_ids" options="{'reload_on_button': True}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<tree editable="bottom" colors="grey:result_package_id;red:qty_done>product_qty;green:qty_done==product_qty and state!='done'">
|
||||
<field name="package_id" groups="stock.group_tracking_lot" invisible="1"/>
|
||||
<field name="product_id" on_change="product_id_change(product_id, product_uom_id, product_qty)" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="fresh_record" invisible="1"/>
|
||||
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
||||
<field name="lot_id" domain="[('product_id','=?', product_id)]" context="{'product_id': product_id}" groups="stock.group_production_lot" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="lot_id" domain="[('product_id','=', product_id)]" context="{'product_id': product_id}" groups="stock.group_production_lot" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<button name="split_lot" string="Lot Split" type="object" icon="STOCK_INDENT" groups="stock.group_production_lot"
|
||||
attrs="{'invisible': ['|', ('lots_visible', '=', False), ('state', 'not in', ['confirmed', 'assigned', 'waiting', 'partially_available'])]}"/>
|
||||
<field name="lots_visible" invisible="1"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
||||
@@ -751,7 +754,7 @@
|
||||
</tree>
|
||||
</field>
|
||||
<field name="picking_type_entire_packs" invisible="1"/>
|
||||
<field name="pack_operation_pack_ids" attrs="{'invisible': [('pack_operation_pack_ids', '=', []), ('picking_type_entire_packs', '=', False)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_picking_id': id}" groups="stock.group_tracking_lot">
|
||||
<field name="pack_operation_pack_ids" options="{'reload_on_button': True}" attrs="{'invisible': [('pack_operation_pack_ids', '=', []), ('picking_type_entire_packs', '=', False)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_picking_id': id}" groups="stock.group_tracking_lot">
|
||||
<tree editable="bottom" colors="grey:qty_done>0;">
|
||||
<field name="package_id" required="1" string="Package To Move"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
@@ -764,13 +767,13 @@
|
||||
<field name="qty_done" invisible="1"/>
|
||||
<field name="processed_boolean"/>
|
||||
<button name="%(stock.pack_details)d" string="Modify" type="action" icon="STOCK_EXECUTE"
|
||||
states="confirmed,assigned,waiting,partially_available"/>
|
||||
states="confirmed,assigned,waiting,partially_available" groups="base.group_no_one"/>
|
||||
</tree>
|
||||
</field>
|
||||
<button class="oe_link oe_right" name="put_in_pack" type="object" string="↳Put in Pack" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}" groups="stock.group_tracking_lot"/>
|
||||
</page>
|
||||
<page string="Initial Demand" attrs="{'invisible': [('state', 'not in', ('draft', 'confirmed', 'waiting'))]}">
|
||||
<field name="move_lines" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'stock.view_move_picking_tree', 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<field name="move_lines" options="{'reload_on_button': True}" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'stock.view_move_picking_tree', 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<kanban class="o_kanban_mobile o_kanban_one2many">
|
||||
<field name="product_id"/>
|
||||
<field name="availability"/>
|
||||
@@ -1449,13 +1452,18 @@
|
||||
<field name="code" on_change="onchange_picking_code(code)"/>
|
||||
<field name="return_picking_type_id"/>
|
||||
<field name="barcode_nomenclature_id"/>
|
||||
<field name="show_entire_packs" groups="stock.group_tracking_lot"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Locations"/>
|
||||
<group>
|
||||
<field name="default_location_src_id" attrs="{'required': [('code', '=', 'internal')]}"/>
|
||||
<field name="default_location_dest_id" attrs="{'required': [('code', 'in', ('internal', 'incoming'))]}"/>
|
||||
<group string="Packs and Lots">
|
||||
<field name="show_entire_packs" groups="stock.group_tracking_lot"/>
|
||||
<field name="use_create_lots" groups="stock.group_production_lot"/>
|
||||
<field name="use_existing_lots" groups="stock.group_production_lot"/>
|
||||
</group>
|
||||
<group string="Locations">
|
||||
<field name="default_location_src_id" attrs="{'required': [('code', '=', 'internal')]}"/>
|
||||
<field name="default_location_dest_id" attrs="{'required': [('code', 'in', ('internal', 'incoming'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
@@ -9,3 +9,4 @@ import orderpoint_procurement
|
||||
import stock_pack_details
|
||||
import stock_immediate_transfer
|
||||
import stock_backorder_confirmation
|
||||
import stock_lot_split
|
||||
@@ -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)
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_lot_split" model="ir.ui.view">
|
||||
<field name="name">Split Lot</field>
|
||||
<field name="model">stock.lot.split</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Lot Tracker">
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<label for="qty_done"/>
|
||||
<span>
|
||||
<field name="qty_done" class="oe_inline"/> of
|
||||
<field name="product_qty" class="oe_inline"/>
|
||||
<field name="product_uom_id" class="oe_inline" groups="product.group_uom"/>
|
||||
</span>
|
||||
</group>
|
||||
<group>
|
||||
<field name="line_ids" nolabel="1">
|
||||
<tree editable="bottom">
|
||||
<field name="lot_name" invisible="not context.get('only_create', False)"/>
|
||||
<field name="lot_id" invisible="context.get('only_create', False)"
|
||||
domain="[('product_id','=', parent.product_id)]"
|
||||
context="{'product_id': parent.product_id}"/>
|
||||
<field name="product_qty" invisible="context.get('serial')"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="process" string="_Apply" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="_Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lot_split" model="ir.actions.act_window">
|
||||
<field name="name">Lot Split</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.lot.split</field>
|
||||
<field name="view_id" ref="view_lot_split"/>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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 {}
|
||||
return True
|
||||
@@ -6,13 +6,12 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Enter Pack Details">
|
||||
<group>
|
||||
<group string="#Products">
|
||||
<group string="Products">
|
||||
<field name="pack_id" invisible="1"/>
|
||||
<field name="picking_source_location_id" invisible="True"/>
|
||||
<field name="picking_destination_location_id" invisible="True"/>
|
||||
|
||||
<field name="product_id" attrs="{'required': [('package_id', '=', False)]}"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot" domain="[('product_id','=?', product_id)]" context="{'product_id': product_id}"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot" domain="[('product_id','=', product_id)]" context="{'product_id': product_id}"/>
|
||||
</group>
|
||||
<group string="From">
|
||||
<field name="location_id" domain="[('id', 'child_of', picking_source_location_id)]"/>
|
||||
@@ -20,15 +19,15 @@
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Actions">
|
||||
<label for="qty_done" string="Done"/>
|
||||
<div class="oe_inline">
|
||||
<field name="qty_done"/>
|
||||
OF
|
||||
<group string="Quantities">
|
||||
<label for="qty_done"/>
|
||||
<span class="oe_inline">
|
||||
<field name="qty_done" nolabel="1"/>
|
||||
/
|
||||
<field name="quantity"/>
|
||||
<field name="product_uom_id" options="{"no_open": True}" groups="product.group_uom"/>
|
||||
<button name="split_quantities" string="Split" type="object" icon="STOCK_PREFERENCES" attrs="{'invisible': ['|', ('quantity', '=', 1), ('qty_done', '=', 0.0)]}"/>
|
||||
</div>
|
||||
</span>
|
||||
<button name="split_quantities" class="oe_link" string="Split" type="object"/>
|
||||
</group>
|
||||
<group string="To">
|
||||
<field name="location_dest_id" domain="[('id', 'child_of', picking_destination_location_id)]"/>
|
||||
@@ -36,7 +35,7 @@
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="process" string="_Apply" type="object" class="oe_highlight"/>
|
||||
<button name="process" string="Save" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="_Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
|
||||
@@ -250,8 +250,8 @@ class stock_quant(osv.osv):
|
||||
self._account_entry_move(cr, uid, [quant], move, context)
|
||||
return quant
|
||||
|
||||
def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
|
||||
res = super(stock_quant, self).move_quants_write(cr, uid, quants, move, location_dest_id, dest_package_id, context=context)
|
||||
def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, lot_id=False, context=None):
|
||||
res = super(stock_quant, self).move_quants_write(cr, uid, quants, move, location_dest_id, dest_package_id, lot_id=lot_id, context=context)
|
||||
if move.product_id.valuation == 'real_time':
|
||||
self._account_entry_move(cr, uid, quants, move, context=context)
|
||||
return res
|
||||
|
||||
Reference in New Issue
Block a user