From d2d0184587b7098a5bd6d7b4a6a44aacde30185c Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Thu, 7 Nov 2013 12:20:53 +0100 Subject: [PATCH] [IMP] pocurement, stock, purchase, sale_stock, stock_account: misc changes in usabilty, code refactoring and new option on procurement group propagation. 1) form view of procurement.order and procurement.rule reviewed 2) added buttons on procurement.order to jump on other procurements or pickings of same procurement.group 3) tracked some fields on procurement.order 4) used the short name of warehouse for the rule names 5) added a new choice for the procurement.group propagation in order to allow having only one reception picking for several po lines grouped in the same PO but belonging to different procurement.group at first 6) some code refactored for easiness, modularity, clarity and other obvious reasons bzr revid: qdp-launchpad@openerp.com-20131107112053-knracfslqkfitg8b --- addons/procurement/procurement.py | 36 ++++-- addons/procurement/procurement_view.xml | 70 +++++++++-- addons/purchase/purchase.py | 93 +++++++------- addons/sale_stock/sale_stock.py | 11 +- addons/stock/procurement.py | 59 +++++---- addons/stock/stock.py | 18 ++- addons/stock/stock_view.xml | 129 +++++++++----------- addons/stock_account/stock_account_view.xml | 8 +- 8 files changed, 245 insertions(+), 179 deletions(-) diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index d7475c5e489..9aa58040379 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -82,12 +82,19 @@ class procurement_rule(osv.osv): _columns = { 'name': fields.char('Name', required=True, help="This field will fill the packing origin and the name of its moves"), - 'group_id': fields.many2one('procurement.group', 'Procurement Group'), + 'group_propagation_option': fields.selection([('none', 'Leave Empty'), ('propagate', 'Propagate'), ('fixed', 'Fixed')], string="Propagation of Procurement Group"), + 'group_id': fields.many2one('procurement.group', 'Fixed Procurement Group'), 'action': fields.selection(selection=lambda s, cr, uid, context=None: s._get_action(cr, uid, context=context), string='Action', required=True), + 'sequence': fields.integer('Sequence'), 'company_id': fields.many2one('res.company', 'Company'), } + _defaults = { + 'group_propagation_option': 'propagate', + 'sequence': 20, + } + class procurement_order(osv.osv): """ @@ -107,11 +114,11 @@ class procurement_order(osv.osv): 'company_id': fields.many2one('res.company', 'Company', required=True), # These two fields are used for shceduling - 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', required=True, select=True), - 'date_planned': fields.datetime('Scheduled date', required=True, select=True), + 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', required=True, select=True, track_visibility='onchange'), + 'date_planned': fields.datetime('Scheduled Date', required=True, select=True, track_visibility='onchange'), 'group_id': fields.many2one('procurement.group', 'Procurement Group'), - 'rule_id': fields.many2one('procurement.rule', 'Rule'), + 'rule_id': fields.many2one('procurement.rule', 'Rule', track_visibility='onchange'), 'product_id': fields.many2one('product.product', 'Product', required=True, states={'confirmed': [('readonly', False)]}, readonly=True), 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'confirmed': [('readonly', False)]}, readonly=True), @@ -141,14 +148,27 @@ class procurement_order(osv.osv): procurements = self.read(cr, uid, ids, ['state'], context=context) unlink_ids = [] for s in procurements: - if s['state'] in ['draft','cancel']: + if s['state'] in ['draft', 'cancel']: unlink_ids.append(s['id']) else: raise osv.except_osv(_('Invalid Action!'), - _('Cannot delete Procurement Order(s) which are in %s state.') % \ - s['state']) + _('Cannot delete Procurement Order(s) which are in %s state.') % s['state']) return osv.osv.unlink(self, cr, uid, unlink_ids, context=context) + def do_view_procurements(self, cr, uid, ids, context=None): + ''' + This function returns an action that display existing procurement orders + of same procurement group of given ids. + ''' + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements') + id = result and result[1] or False + result = act_obj.read(cr, uid, [id], context=context)[0] + group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id]) + result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" + return result + def onchange_product_id(self, cr, uid, ids, product_id, context=None): """ Finds UoM and UoS of changed product. @param product_id: Changed id of product. @@ -215,8 +235,6 @@ class procurement_order(osv.osv): if procurement.product_id.type != 'service': rule_id = self._find_suitable_rule(cr, uid, procurement, context=context) if rule_id: - rule = self.pool.get('procurement.rule').browse(cr, uid, rule_id, context=context) - self.message_post(cr, uid, [procurement.id], body=_('Following rule %s for the procurement resolution.') % (rule.name), context=context) self.write(cr, uid, [procurement.id], {'rule_id': rule_id}, context=context) return True return False diff --git a/addons/procurement/procurement_view.xml b/addons/procurement/procurement_view.xml index 610c7288b80..316b0e7470b 100644 --- a/addons/procurement/procurement_view.xml +++ b/addons/procurement/procurement_view.xml @@ -34,6 +34,9 @@ +
+
@@ -50,25 +53,30 @@ - - - + - - - - - + + + + + + + + + + + +
@@ -80,7 +88,7 @@ - Procurement for Groups + Group's Procurements procurement.order form tree,form @@ -93,8 +101,8 @@
-
-
@@ -174,5 +182,45 @@ tree,form [('state','=','exception')] + + + + procurement.rule.tree + procurement.rule + + + + + + + + + + + + procurement.rule.form + procurement.rule + + + + +
+
+ + + + + + + + + + +
+ +
+
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index d37e826fdeb..87053924efe 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1186,67 +1186,69 @@ class procurement_order(osv.osv): seller_delay = int(procurement.product_id.seller_delay) return schedule_date - relativedelta(days=seller_delay) + def _get_product_supplier(self, cr, uid, procurement, context=None): + ''' returns the main supplier of the procurement's product given as argument''' + return procurement.product_id.seller_id + + def _get_po_line_values_from_proc(self, cr, uid, procurement, partner, company, schedule_date, context=None): + if context is None: + context = {} + uom_obj = self.pool.get('product.uom') + pricelist_obj = self.pool.get('product.pricelist') + prod_obj = self.pool.get('product.product') + acc_pos_obj = self.pool.get('account.fiscal.position') + + seller_qty = procurement.product_id.seller_qty + pricelist_id = partner.property_product_pricelist_purchase.id + uom_id = procurement.product_id.uom_po_id.id + qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id) + if seller_qty: + qty = max(qty, seller_qty) + price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner.id, {'uom': uom_id})[pricelist_id] + + #Passing partner_id to context for purchase order line integrity of Line name + new_context = context.copy() + new_context.update({'lang': partner.lang, 'partner_id': partner.id}) + product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context) + taxes_ids = procurement.product_id.supplier_taxes_id + taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids) + name = product.partner_ref + if product.description_purchase: + name += '\n' + product.description_purchase + + return { + 'name': name, + 'product_qty': qty, + 'product_id': procurement.product_id.id, + 'product_uom': uom_id, + 'price_unit': price or 0.0, + 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False, + 'taxes_id': [(6, 0, taxes)], + } + def make_po(self, cr, uid, ids, context=None): """ Make purchase order from procurement @return: New created Purchase Orders procurement wise """ res = {} - if context is None: - context = {} company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id - partner_obj = self.pool.get('res.partner') po_obj = self.pool.get('purchase.order') po_line_obj = self.pool.get('purchase.order.line') - uom_obj = self.pool.get('product.uom') - pricelist_obj = self.pool.get('product.pricelist') - prod_obj = self.pool.get('product.product') - acc_pos_obj = self.pool.get('account.fiscal.position') seq_obj = self.pool.get('ir.sequence') pass_ids = [] linked_po_ids = [] for procurement in self.browse(cr, uid, ids, context=context): - res_id = procurement.move_dest_id and procurement.move_dest_id.id or False - partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement. + partner = self._get_product_supplier(cr, uid, procurement, context=context) if not partner: self.message_post(cr, uid, [procurement.id], _('There is no supplier associated to product %s') % (procurement.product_id.name)) res[procurement.id] = False else: - seller_qty = procurement.product_id.seller_qty - partner_id = partner.id - address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery'] - pricelist_id = partner.property_product_pricelist_purchase.id - uom_id = procurement.product_id.uom_po_id.id - qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id) - if seller_qty: - qty = max(qty, seller_qty) - - price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, partner_id, {'uom': uom_id})[pricelist_id] - schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context) - purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) - - #Passing partner_id to context for purchase order line integrity of Line name - new_context = context.copy() - new_context.update({'lang': partner.lang, 'partner_id': partner_id}) - product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context) - taxes_ids = procurement.product_id.supplier_taxes_id - taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids) - name = product.partner_ref - if product.description_purchase: - name += '\n' + product.description_purchase - line_vals = { - 'name': name, - 'product_qty': qty, - 'product_id': procurement.product_id.id, - 'product_uom': uom_id, - 'price_unit': price or 0.0, - 'date_planned': schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'move_dest_id': res_id, - 'taxes_id': [(6, 0, taxes)], - } + line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context) #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one available_draft_po_ids = po_obj.search(cr, uid, [ - ('partner_id', '=', partner_id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id), + ('partner_id', '=', partner.id), ('state', '=', 'draft'), ('picking_type_id', '=', procurement.rule_id.picking_type_id.id), ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id)], context=context) if available_draft_po_ids: po_id = available_draft_po_ids[0] @@ -1254,20 +1256,21 @@ class procurement_order(osv.osv): po_line_id = po_line_obj.create(cr, uid, line_vals, context=context) linked_po_ids.append(procurement.id) else: + purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name po_vals = { 'name': name, 'origin': procurement.origin, - 'partner_id': partner_id, + 'partner_id': partner.id, 'location_id': procurement.location_id.id, 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'pricelist_id': pricelist_id, + 'pricelist_id': partner.property_product_pricelist_purchase.id, 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'company_id': procurement.company_id.id, 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False, 'payment_term_id': partner.property_supplier_payment_term.id or False, } - po_id = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=new_context) + po_id = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=context) po_line_id = po_obj.browse(cr, uid, po_id, context=context).order_line[0].id pass_ids.append(procurement.id) res[procurement.id] = po_line_id diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 648c3c3444e..49ec6d5bd4f 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -63,26 +63,21 @@ class sale_order(osv.osv): if move.procurement_id and move.procurement_id.sale_line_id: res.add(move.procurement_id.sale_line_id.order_id.id) return list(res) - + def _get_orders_procurements(self, cr, uid, ids, context=None): res = set() for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context): if proc.sale_line_id: res.add(proc.sale_line_id.order_id.id) return list(res) - + def _get_picking_ids(self, cr, uid, ids, name, args, context=None): res = {} for sale in self.browse(cr, uid, ids, context=context): if not sale.procurement_group_id: res[sale.id] = [] continue - picking_ids = set() - for procurement in sale.procurement_group_id.procurement_ids: - for move in procurement.move_ids: - if move.picking_id: - picking_ids.add(move.picking_id.id) - res[sale.id] = list(picking_ids) + res[sale.id] = self.pool.get('stock.picking').search(cr, uid, [('group_id', '=', sale.procurement_group_id.id)], context=context) return res def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index 653894a6615..706945e32ee 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -72,7 +72,6 @@ class procurement_rule(osv.osv): 'stock.location.route': (_get_rules, ['sequence'], 10), 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10), }), - 'sequence': fields.integer('Sequence'), 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', help="Picking Type determines the way the picking should be shown in the view, reports, ..."), 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={ @@ -88,10 +87,9 @@ class procurement_rule(osv.osv): _defaults = { 'procure_method': 'make_to_stock', - 'sequence': 20, - 'active': True, - 'propagate': True, - 'delay': 0, + 'active': True, + 'propagate': True, + 'delay': 0, } class procurement_order(osv.osv): @@ -100,7 +98,7 @@ class procurement_order(osv.osv): 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with project_mrp 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"), 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"), - 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Followed Route', help="Preferred route to be followed by the procurement order"), + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Preferred Routes', help="Preferred route to be followed by the procurement order. Usually copied from the generating document (SO) but could be set up manually."), 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"), } @@ -161,7 +159,19 @@ class procurement_order(osv.osv): return rule_id def _run_move_create(self, cr, uid, procurement, context=None): - vals = { + ''' Returns a dictionary of values that will be sued to create a stock move from a procurement. + This function assumes that the given procurement has a rule (action == 'move') set on it. + + :param procurement: browse record + :rtype: dictionary + ''' + newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') + group_id = False + if procurement.rule_id.group_propagation_option == 'propagate': + group_id = procurement.group_id and procurement.group_id.id or False + elif procurement.rule_id.group_propagation_option == 'fixed': + group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False + vals = { 'name': procurement.name, 'company_id': procurement.company_id.id, 'product_id': procurement.product_id.id, @@ -170,12 +180,9 @@ class procurement_order(osv.osv): 'product_qty': procurement.product_qty, 'product_uom': procurement.product_uom.id, 'product_uom_qty': procurement.product_qty, - 'product_uos_qty': (procurement.product_uos and procurement.product_uos_qty)\ - or procurement.product_qty, - 'product_uos': (procurement.product_uos and procurement.product_uos.id)\ - or procurement.product_uom.id, - 'partner_id': procurement.group_id and procurement.group_id.partner_id and \ - procurement.group_id.partner_id.id or False, + 'product_uos_qty': (procurement.product_uos and procurement.product_uos_qty) or procurement.product_qty, + 'product_uos': (procurement.product_uos and procurement.product_uos.id) or procurement.product_uom.id, + 'partner_id': procurement.group_id and procurement.group_id.partner_id and procurement.group_id.partner_id.id or False, 'location_id': procurement.rule_id.location_src_id.id, 'location_dest_id': procurement.rule_id.location_id.id, 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False, @@ -184,16 +191,12 @@ class procurement_order(osv.osv): 'procure_method': procurement.rule_id.procure_method, 'origin': procurement.origin, 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'group_id': procurement.group_id and procurement.group_id.id or False, + 'group_id': group_id, 'route_ids': [(4, x.id) for x in procurement.route_ids], 'warehouse_id': procurement.rule_id.propagate_warehouse_id and procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id, - } - if procurement.rule_id: - newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') - vals.update({ - 'date': newdate, - 'propagate': procurement.rule_id.propagate, - }) + 'date': newdate, + 'propagate': procurement.rule_id.propagate, + } return vals def _run(self, cr, uid, procurement, context=None): @@ -229,7 +232,19 @@ class procurement_order(osv.osv): return super(procurement_order, self)._check(cr, uid, procurement, context) - + def do_view_pickings(self, cr, uid, ids, context=None): + ''' + This function returns an action that display the pickings of the procurements belonging + to the same procurement group of given ids. + ''' + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings') + id = result and result[1] or False + result = act_obj.read(cr, uid, [id], context=context)[0] + group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id]) + result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" + return result # # Scheduler diff --git a/addons/stock/stock.py b/addons/stock/stock.py index c79590a5041..c1ceeecc6d7 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1322,7 +1322,7 @@ class stock_move(osv.osv): 'stock.quant': (_get_move, ['reservation_id'], 10)}), 'procurement_id': fields.many2one('procurement.order', 'Procurement'), 'group_id': fields.many2one('procurement.group', 'Procurement Group'), - 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule'), + 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'), 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'), 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'), 'inventory_id': fields.many2one('stock.inventory', 'Inventory'), @@ -1384,8 +1384,14 @@ class stock_move(osv.osv): } def _prepare_procurement_from_move(self, cr, uid, move, context=None): - origin = (move.group_id and (move.group_id.name+":") or "") + (move.rule_id and move.rule_id.name or "/") - return { + origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or "/") + group_id = move.group_id and move.group_id.id or False + if move.rule_id: + if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id: + group_id = move.rule_id.group_id.id + elif move.rule_id.group_propagation_option == 'none': + group_id = False + return { 'name': move.rule_id and move.rule_id.name or "/", 'origin': origin, 'company_id': move.company_id and move.company_id.id or False, @@ -1397,8 +1403,8 @@ class stock_move(osv.osv): 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id, 'location_id': move.location_id.id, 'move_dest_id': move.id, - 'group_id': move.group_id and move.group_id.id or False, - 'route_ids' : [(4, x.id) for x in move.route_ids], + 'group_id': group_id, + 'route_ids': [(4, x.id) for x in move.route_ids], 'warehouse_id': move.warehouse_id and move.warehouse_id.id or False, } @@ -2690,7 +2696,7 @@ class stock_warehouse(osv.osv): return new_id def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None): - return obj.name + ': ' + from_loc.name + ' -> ' + dest_loc.name + return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name def _format_routename(self, cr, uid, obj, name, context=None): return obj.name + ': ' + name diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index b4e0940daa9..cd10100f232 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -1540,7 +1540,7 @@
- + All Operations stock.picking.type ir.actions.act_window @@ -1554,10 +1554,10 @@ parent="menu_stock_warehouse_mgmt" sequence="1"/> + id="menu_pickingtype" + name="Types of Operation" + parent="stock.menu_stock_configuration" + action="action_picking_type_list" /> @@ -1669,12 +1669,15 @@ procurement.order - - - + + + + + + - - + +