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 - - - + + + + + + - - + +