OK, so I am answering my own question. I have ended up doing it my defining 'group' moves which combine other moves. The other moves stay around, but the group moves appear on the order and picking. When the group move is complete, it knows to complete the other moves also.
Here is an attempt at a patch. It will operate as before, but if you tick 'full_merge' (the default) it will also merge lines. Comments from the code: +# this merges purchase orders which are to the same partner and location +# if full_merge is enabled, then it will also merge order lines for the same product, location, etc. +# The original code tried to do this but since each line had a different stock move it didn't work +# This code creates a new 'group' stock move for each one that it wants to merge. This group move +# will then appear on the purchase order and picking, so that orders look correct, and reception picking +# is easier. +# At the end when the picking is complete, the code (elsewhere) knows to look inside the group and +# process the grouped moves as well. +# +# Additions required: +# New fields in stock.move +# - group True if a group (i.e. not a real item, but created from 2 or more others) +# - move_group_id ID of the group move for this item, if this is a member of a group +# +# so members of the group will have move_group_id non-null, and group False +# the group itself will have move_group_id null, and 'group' True +# +# Also added a 'group' field to the order line, so that the user can see that the line is a group +# However there is no way to see what was grouped, since order lines don't let the user link back to Index: bin/addons/stock/stock.py =================================================================== --- bin/addons/stock/stock.py (revision 666) +++ bin/addons/stock/stock.py (working copy) @@ -484,6 +484,7 @@ self.write(cr,uid, ids, {'state':'done'}) return True + # this moves 'assigned' items in a picking to 'done'. This is their final resting place def action_move(self, cr, uid, ids, context={}): for pick in self.browse(cr, uid, ids): todo = [] @@ -742,6 +743,8 @@ 'tracking_id': fields.many2one('stock.tracking', 'Tracking lot', select=True, help="Tracking lot is the code that will be put on the logistic unit/pallet"), 'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True), + 'move_group_id': fields.many2one('stock.move', 'Grouped move', help = "Allows a number of moves to be grouped so they show as one on orders and pickings. This records the group that this move is in"), + 'group':fields.boolean('Group', readonly=True, select=True, help = "true if this stock move is a group of other moves"), 'move_dest_id': fields.many2one('stock.move', 'Dest. Move'), 'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'), 'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'), @@ -749,7 +752,9 @@ 'note': fields.text('Notes'), - 'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'State', readonly=True, select=True), + # grouped means that this move is grouped into another, so shouldn't be used by itself until the group disbands + # group means this is a group, containing other moves. It should generally be ignored as it is not a real stock move + 'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('grouped','Grouped'),('group','Group'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'State', readonly=True, select=True), 'price_unit': fields.float('Unit Price', digits=(16, int(config['price_accuracy']))), } @@ -870,19 +875,44 @@ return True def action_done(self, cr, uid, ids, context=None): + + # this function closes off a stock move, mark it as assigned (or group, for a group move) + def process_done (dest): + mid = dest.id + if dest.id: + cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, dest.id)) + if dest.state in ('waiting','confirmed','grouped'): + # update the state + self.write(cr, uid, [dest.id], dest.group and {'state':'group'} or {'state':'assigned'}) + + # if there is an attached picking list, give it a kick, as we may now have all the items it needs to complete + if dest.picking_id: + wf_service = netsvc.LocalService("workflow") + wf_service.trg_write(uid, 'stock.picking', dest.picking_id.id, cr) + #else: + # pass + # self.action_done(cr, uid, [move.move_dest_id.id]) + for move in self.browse(cr, uid, ids): - if move.move_dest_id.id and (move.state != 'done'): - mid = move.move_dest_id.id - if move.move_dest_id.id: - cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, move.move_dest_id.id)) - if move.move_dest_id.state in ('waiting','confirmed'): - self.write(cr, uid, [move.move_dest_id.id], {'state':'assigned'}) - if move.move_dest_id.picking_id: - wf_service = netsvc.LocalService("workflow") - wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr) - else: - pass - # self.action_done(cr, uid, [move.move_dest_id.id]) + dest = move.move_dest_id + if dest.id and (move.state != 'done'): + + # mark this move as assigned + process_done (dest) + + # if this move is a group, mark its memebers as assigned also + if dest.group: + # find the IDs of the members + item_ids = self.search(cr, uid, [('move_group_id', '=', dest.id)]) + + # process each in turn + for item in self.browse (cr, uid, item_ids): + process_done (item) + + # handle the attached move, if any + # this will be currently set to 'waiting', so change it to 'group' so it is dead + if dest.move_dest_id.id: + self.write(cr, uid, [dest.move_dest_id.id], {'state':'group'}) # # Accounting Entries Index: bin/addons/stock/stock_view.xml =================================================================== --- bin/addons/stock/stock_view.xml (revision 666) +++ bin/addons/stock/stock_view.xml (working copy) @@ -500,6 +500,8 @@ <field> <field> <field> + <field> + <field> <newline> <label> <button> @@ -751,6 +753,8 @@ <field> <field> <field> + <field> + <field> <newline> <field> <field> Index: bin/addons/mrp/mrp.py =================================================================== --- bin/addons/mrp/mrp.py (revision 666) +++ bin/addons/mrp/mrp.py (working copy) @@ -194,7 +194,7 @@ (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id']) ] - + def onchange_product_id(self, cr, uid, ids, product_id, name, context={}): if product_id: prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0] @@ -291,7 +291,7 @@ 'author_id': fields.many2one('res.users', 'Author'), 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True), } - + _defaults = { 'author_id': lambda x,y,z,c: z, 'date': lambda *a: time.strftime('%Y-%m-%d'), @@ -533,6 +533,7 @@ move_id=False newdate = production.date_planned if line.product_id.type in ('product', 'consu'): + # create the move to the production location res_dest_id = self.pool.get('stock.move').create(cr, uid, { 'name':'PROD:'+production.name, 'date_planned': production.date_planned, @@ -545,6 +546,8 @@ 'state': 'waiting', }) moves.append(res_dest_id) + + # create the move to itself for the procurement move_id = self.pool.get('stock.move').create(cr, uid, { 'name':'PROD:'+production.name, 'picking_id':picking_id, @@ -568,9 +571,11 @@ 'procure_method': line.product_id.procure_method, 'move_id': move_id, }) + # confirm the procurement automatically wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr) if toconfirm: + # confirm the stock picking automatically wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr) self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'}) Index: bin/addons/purchase/wizard/wizard_group.py =================================================================== --- bin/addons/purchase/wizard/wizard_group.py (revision 666) +++ bin/addons/purchase/wizard/wizard_group.py (working copy) @@ -34,15 +34,42 @@ import pooler from osv.orm import browse_record + +# enhanced from original version by Simon Glass, Bluewater Systems Ltd, www.bluewatersys.com +# +# this merges purchase orders which are to the same partner and location +# if full_merge is enabled, then it will also merge order lines for the same product, location, etc. +# The original code tried to do this but since each line had a different stock move it didn't work +# This code creates a new 'group' stock move for each one that it wants to merge. This group move +# will then appear on the purchase order and picking, so that orders look correct, and reception picking +# is easier. +# At the end when the picking is complete, the code (elsewhere) knows to look inside the group and +# process the grouped moves as well. +# +# Additions required: +# New fields in stock.move +# - group True if a group (i.e. not a real item, but created from 2 or more others) +# - move_group_id ID of the group move for this item, if this is a member of a group +# +# so members of the group will have move_group_id non-null, and group False +# the group itself will have move_group_id null, and 'group' True +# +# Also added a 'group' field to the order line, so that the user can see that the line is a group +# However there is no way to see what was grouped, since order lines don't let the user link back to +# the stock move + merge_form = """<xml> <form> <separator> <newline> <label> + <newline> + <field> </form> """ merge_fields = { + 'full_merge': {'string':'Merge order lines for the same product', 'type':'boolean', 'required':True, 'default' : True}, } ack_form = """<xml> @@ -53,15 +80,30 @@ ack_fields = {} def _mergeOrders(self, cr, uid, data, context): + # merge order lines also\ + full_merge = data ['form'] ['full_merge'] order_obj = pooler.get_pool(cr.dbname).get('purchase.order') - + + # patch to ensure that procurement is updated with new purchase order number + proc_obj = pooler.get_pool(cr.dbname).get('mrp.procurement') + stockmove_obj = pooler.get_pool(cr.dbname).get('stock.move') + + # make a key out of a set of fields. We will use this to compare order lines, and merge those that have the same key def make_key(br, fields): list_key = [] for field in fields: field_val = getattr(br, field) - if field in ('product_id','move_dest_id', 'account_analytic_id'): + + # for this, check the eventual location is the same, rather than the move id + # (since we can group the moves if needed) + if full_merge and field == 'move_dest_id': + field_val = field_val.location_dest_id + field = 'move_dest_id_loc' + elif field in ('product_id','move_dest_id', 'account_analytic_id'): if not field_val: field_val = False + + # convert objects to a list of IDs if isinstance(field_val, browse_record): field_val = field_val.id elif isinstance(field_val, list): @@ -69,7 +111,28 @@ list_key.append((field, field_val)) list_key.sort() return tuple(list_key) - + + # get the salient details from an existing move into a dict + def get_stock_move (move_id): + move = stockmove_obj.browse (cr, uid, [move_id]) [0] + return { + 'name': move.name, + 'picking_id':move.picking_id.id, + 'product_id': move.product_id.id, + 'product_qty': move.product_qty, + 'product_uom': move.product_uom.id, + 'date_planned': move.date_planned, + 'move_dest_id': move.move_dest_id.id, + 'location_id': move.location_id.id, + 'location_dest_id': move.location_dest_id.id, + 'state': move.state + } + + # change the moves in the list so that they are part of the group headed by id 'id' + def merge_moves (list, group_id): + stockmove_obj.write(cr, uid, [v.id for v in list], {'move_group_id': merge_id,'state' : 'grouped'}) + stockmove_obj.write(cr, uid, [group_id], {'group': True}) + # compute what the new orders should contain new_orders = {} for porder in [order for order in order_obj.browse(cr, uid, data['ids']) if order.state == 'draft']: @@ -77,6 +140,8 @@ new_order = new_orders.setdefault(order_key, ({}, [])) new_order[1].append(porder.id) order_infos = new_order[0] + + # if no order details yet (this is the first order for this partner/location), fill them in if not order_infos: order_infos.update({ 'origin': porder.origin, @@ -93,46 +158,114 @@ }) else: #order_infos['name'] += ', %s' % porder.name + # we already have the order details, so just append the notes from this order if porder.notes: order_infos['notes'] += ('\n%s' % (porder.notes,)) + # work through the order lines for order_line in porder.order_line: + # get a key based on the fields which must be unique line_key = make_key(order_line, ('name', 'date_planned', 'taxes_id', 'price_unit', 'notes', 'product_id', 'move_dest_id', 'account_analytic_id')) + + # get a pointer to a line with the same key values + # if no match, then it will be empty, and we will fill it o_line = order_infos['order_line'].setdefault(line_key, {}) + + # get the move_dest_id field + dest_val = getattr(order_line, 'move_dest_id') + if not dest_val: + dest_val = False + else: + dest_val = dest_val.id if o_line: # merge the line with an existing line o_line['product_qty'] += order_line.product_qty * order_line.product_uom.factor / o_line['uom_factor'] + if full_merge: + o_line['move_dest_id_loc'].append (order_line.move_dest_id) else: # append a new "standalone" line for field in ('product_qty', 'product_uom'): field_val = getattr(order_line, field) if isinstance(field_val, browse_record): field_val = field_val.id - o_line[field] = field_val + o_line[field] = field_val o_line['uom_factor'] = order_line.product_uom and order_line.product_uom.factor or 1.0 + if full_merge: + o_line['move_dest_id_loc'] = [order_line.move_dest_id,] + + # put in the move_dest_id value + o_line ['move_dest_id'] = dest_val wf_service = netsvc.LocalService("workflow") allorders = [] for order_key, (order_data, old_ids) in new_orders.iteritems(): - # skip merges with only one order - if len(old_ids) < 2: + # skip merges with only one order, unless we are merging items + if len(old_ids) <2> 1: + # look for associated stock moves (pointed to by move_dest_id) + merge_list = [] # list of move lines to group + merge_id = -1 # id of the group move created + for move in move_list: + # if this move has an associated move which is for the same product, add it to the list + rel_move = stockmove_obj.browse (cr, uid, [move.move_dest_id.id], {}) [0] + if rel_move.product_id.id == move.product_id.id: + merge_list.append (rel_move) + + # if we got any, group them + if len (merge_list) > 0: + move = get_stock_move (merge_list [0].id) + del move ['picking_id'] + move ['product_qty'] = value ['product_qty'] + merge_id = stockmove_obj.create(cr, uid, move); + + # indicate that these items are now grouped + merge_moves (merge_list, merge_id) + + # and now merge our original list, setting the quantity and move_dest_id correctly + # we remove the picking_id, since we don't want the merged item to appear in the picking (do we?) + move = get_stock_move (move_list [0].id) + del move ['picking_id'] + move ['product_qty'] = value ['product_qty'] + if merge_id != -1: + move ['move_dest_id'] = merge_id + merge_id = stockmove_obj.create(cr, uid, move) + + # indicate that these items are now grouped + merge_moves (move_list, merge_id) + + # update the order line to point to the new move + value ['move_dest_id'] = merge_id + value ['group'] = True + order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()] - + # create the new order neworder_id = order_obj.create(cr, uid, order_data) allorders.append(neworder_id) # make triggers pointing to the old orders point to the new order for old_id in old_ids: + proc_ids = proc_obj.search(cr, uid, [('purchase_id', '=', old_id)]) + proc_obj.write(cr, uid, proc_ids, {'purchase_id': neworder_id,}) wf_service.trg_redirect(uid, 'purchase.order', old_id, neworder_id, cr) + + # cancel the old order wf_service.trg_validate(uid, 'purchase.order', old_id, 'purchase_cancel', cr) return { Index: bin/addons/purchase/purchase.py =================================================================== --- bin/addons/purchase/purchase.py (revision 666) +++ bin/addons/purchase/purchase.py (working copy) @@ -252,7 +252,7 @@ continue if order_line.product_id.product_tmpl_id.type in ('product', 'consu'): dest = order.location_id.id - self.pool.get('stock.move').create(cr, uid, { + fields = { 'name': 'PO:'+order_line.name, 'product_id': order_line.product_id.id, 'product_qty': order_line.product_qty, @@ -266,7 +266,12 @@ 'move_dest_id': order_line.move_dest_id.id, 'state': 'assigned', 'purchase_line_id': order_line.id, - }) + } + # for a group, add the group info also + if order_line.group: + fields ['group'] = True + fields ['move_group_id'] = order_line.move_dest_id.move_group_id.id + self.pool.get('stock.move').create(cr, uid, fields) if order_line.move_dest_id: self.pool.get('stock.move').write(cr, uid, [order_line.move_dest_id.id], {'location_id':order.location_id.id}) wf_service = netsvc.LocalService("workflow") @@ -310,6 +315,7 @@ 'notes': fields.text('Notes'), 'order_id': fields.many2one('purchase.order', 'Order Ref', select=True, required=True, ondelete='cascade'), 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',), + 'group':fields.boolean('Group', readonly=True, select=True, help = "true if order line was created from a group of others"), } _defaults = { 'product_qty': lambda *a: 1.0 Index: bin/addons/purchase/purchase_view.xml =================================================================== --- bin/addons/purchase/purchase_view.xml (revision 666) +++ bin/addons/purchase/purchase_view.xml (working copy) @@ -138,6 +138,7 @@ <field> <field> <field> + <field> <field> </page><page> <field> Index: bin/workflow/workitem.py =================================================================== --- bin/workflow/workitem.py (revision 666) +++ bin/workflow/workitem.py (working copy) @@ -95,7 +95,6 @@ instance.validate(cr, i[0], (ident[0],i[1],i[2]), activity['signal_send'], force_running=True) - if activity['kind']=='dummy': if workitem['state']=='active': _state_set(cr, workitem, activity, 'complete', ident) ------------------------ -- Simon Glass Christchurch New Zealand _______________________________________________ Tinyerp-users mailing list http://tiny.be/mailman/listinfo/tinyerp-users
