1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
25 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
26 from dateutil.relativedelta import relativedelta
27 from datetime import datetime
30 class procurement_group(osv.osv):
31 _inherit = 'procurement.group'
33 'partner_id': fields.many2one('res.partner', 'Partner')
36 class procurement_rule(osv.osv):
37 _inherit = 'procurement.rule'
39 def _get_action(self, cr, uid, context=None):
40 result = super(procurement_rule, self)._get_action(cr, uid, context=context)
41 return result + [('move', 'Move From Another Location')]
43 def _get_rules(self, cr, uid, ids, context=None):
45 for route in self.browse(cr, uid, ids):
46 res += [x.id for x in route.pull_ids]
49 def _get_route(self, cr, uid, ids, context=None):
50 #WARNING TODO route_id is not required, so a field related seems a bad idea >-<
56 context_with_inactive = context.copy()
57 context_with_inactive['active_test']=False
58 for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
59 for pull_rule in route.pull_ids:
60 result[pull_rule.id] = True
64 'location_id': fields.many2one('stock.location', 'Procurement Location'),
65 'location_src_id': fields.many2one('stock.location', 'Source Location',
66 help="Source location is action=move"),
67 'route_id': fields.many2one('stock.location.route', 'Route',
68 help="If route_id is False, the rule is global"),
69 'procure_method': fields.selection([('make_to_stock', 'Make to Stock'), ('make_to_order', 'Make to Order')], 'Procure Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
70 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence',
72 'stock.location.route': (_get_rules, ['sequence'], 10),
73 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
75 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type',
76 help="Picking Type determines the way the picking should be shown in the view, reports, ..."),
77 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
78 'stock.location.route': (_get_route, ['active'], 20),
79 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20)},
80 help="If the active field is set to False, it will allow you to hide the rule without removing it."),
81 'delay': fields.integer('Number of Days'),
82 'partner_address_id': fields.many2one('res.partner', 'Partner Address'),
83 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'),
84 'warehouse_id': fields.many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for'),
85 'propagate_warehouse_id': fields.many2one('stock.warehouse', 'Warehouse to Propagate', help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)"),
89 'procure_method': 'make_to_stock',
95 class procurement_order(osv.osv):
96 _inherit = "procurement.order"
98 '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
99 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"),
100 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"),
101 '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."),
102 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"),
105 def propagate_cancel(self, cr, uid, procurement, context=None):
106 if procurement.rule_id.action == 'move' and procurement.move_ids:
107 self.pool.get('stock.move').action_cancel(cr, uid, [m.id for m in procurement.move_ids], context=context)
109 def cancel(self, cr, uid, ids, context=None):
112 to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
114 #set the context for the propagation of the procurement cancelation
115 ctx['cancel_procurement'] = True
116 for procurement in self.browse(cr, uid, to_cancel_ids, context=ctx):
117 if procurement.rule_id and procurement.rule_id.propagate:
118 self.propagate_cancel(cr, uid, procurement, context=ctx)
119 return super(procurement_order, self).cancel(cr, uid, to_cancel_ids, context=ctx)
121 def _find_parent_locations(self, cr, uid, procurement, context=None):
122 location = procurement.location_id
124 while location.location_id:
125 location = location.location_id
126 res.append(location.id)
129 def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
131 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
132 return {'value': {'location_id': warehouse.lot_stock_id.id}}
135 def _search_suitable_rule(self, cr, uid, procurement, domain, context=None):
136 '''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior'''
137 pull_obj = self.pool.get('procurement.rule')
138 warehouse_route_ids = []
139 if procurement.warehouse_id:
140 domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)]
141 warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids]
142 product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids]
143 procurement_route_ids = [x.id for x in procurement.route_ids]
144 res = pull_obj.search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context)
146 res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context)
148 res = warehouse_route_ids and pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context) or []
150 res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
153 def _find_suitable_rule(self, cr, uid, procurement, context=None):
154 rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, procurement, context=context)
156 #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A'
157 all_parent_location_ids = self._find_parent_locations(cr, uid, procurement, context=context)
158 rule_id = self._search_suitable_rule(cr, uid, procurement, [('location_id', 'in', all_parent_location_ids)], context=context)
159 rule_id = rule_id and rule_id[0] or False
162 def _run_move_create(self, cr, uid, procurement, context=None):
163 ''' Returns a dictionary of values that will be sued to create a stock move from a procurement.
164 This function assumes that the given procurement has a rule (action == 'move') set on it.
166 :param procurement: browse record
169 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')
171 if procurement.rule_id.group_propagation_option == 'propagate':
172 group_id = procurement.group_id and procurement.group_id.id or False
173 elif procurement.rule_id.group_propagation_option == 'fixed':
174 group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False
176 'name': procurement.name,
177 'company_id': procurement.company_id.id,
178 'product_id': procurement.product_id.id,
179 'product_qty': procurement.product_qty,
180 'product_uom': procurement.product_uom.id,
181 'product_uom_qty': procurement.product_qty,
182 'product_uos_qty': (procurement.product_uos and procurement.product_uos_qty) or procurement.product_qty,
183 'product_uos': (procurement.product_uos and procurement.product_uos.id) or procurement.product_uom.id,
184 'partner_id': procurement.group_id and procurement.group_id.partner_id and procurement.group_id.partner_id.id or False,
185 'location_id': procurement.rule_id.location_src_id.id,
186 'location_dest_id': procurement.rule_id.location_id.id,
187 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False,
188 'procurement_id': procurement.id,
189 'rule_id': procurement.rule_id.id,
190 'procure_method': procurement.rule_id.procure_method,
191 'origin': procurement.origin,
192 'picking_type_id': procurement.rule_id.picking_type_id.id,
193 'group_id': group_id,
194 'route_ids': [(4, x.id) for x in procurement.route_ids],
195 'warehouse_id': procurement.rule_id.propagate_warehouse_id and procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id,
197 'date_expected': newdate,
198 'propagate': procurement.rule_id.propagate,
200 #look if the procurement was in exception (because all its moves were cancelled) and cancel the previously made attempt to avoid duplicates
201 cancelled_moves = [m.id for m in procurement.move_ids if m.state == 'cancel']
203 previous_attempt = self.search(cr, uid, [('move_dest_id', 'in', cancelled_moves)], context=context)
205 self.cancel(cr, uid, previous_attempt, context=context)
208 def _run(self, cr, uid, procurement, context=None):
209 if procurement.rule_id and procurement.rule_id.action == 'move':
210 if not procurement.rule_id.location_src_id:
211 self.message_post(cr, uid, [procurement.id], body=_('No source location defined!'), context=context)
213 move_obj = self.pool.get('stock.move')
214 move_dict = self._run_move_create(cr, uid, procurement, context=context)
215 move_id = move_obj.create(cr, uid, move_dict, context=context)
216 move_obj.action_confirm(cr, uid, [move_id], context=context)
218 return super(procurement_order, self)._run(cr, uid, procurement, context)
220 def _check(self, cr, uid, procurement, context=None):
221 if procurement.rule_id and procurement.rule_id.action == 'move':
223 done_cancel_test_list = []
224 for move in procurement.move_ids:
225 done_test_list.append(move.state == 'done')
226 done_cancel_test_list.append(move.state in ('done', 'cancel'))
227 at_least_one_done = any(done_test_list)
228 all_done_or_cancel = all(done_cancel_test_list)
229 if not all_done_or_cancel:
231 elif at_least_one_done and all_done_or_cancel:
234 #all move are cancelled
235 self.write(cr, uid, [procurement.id], {'state': 'exception'}, context=context)
236 self.message_post(cr, uid, [procurement.id], body=_('All stock moves have been cancelled for this procurement.'), context=context)
239 return super(procurement_order, self)._check(cr, uid, procurement, context)
241 def do_view_pickings(self, cr, uid, ids, context=None):
243 This function returns an action that display the pickings of the procurements belonging
244 to the same procurement group of given ids.
246 mod_obj = self.pool.get('ir.model.data')
247 act_obj = self.pool.get('ir.actions.act_window')
248 result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings')
249 id = result and result[1] or False
250 result = act_obj.read(cr, uid, [id], context=context)[0]
251 group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
252 result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
257 # When stock is installed, it should also check for the different confirmed stock moves
258 # if they can not be installed
261 def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
263 Call the scheduler in order to
265 @param self: The object pointer
266 @param cr: The current row, from the database cursor,
267 @param uid: The current user ID for security checks
268 @param ids: List of selected IDs
269 @param use_new_cursor: False or the dbname
270 @param context: A standard dictionary for contextual values
271 @return: Dictionary of values
274 super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, context=context)
279 cr = openerp.registry(use_new_cursor).db.cursor()
281 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
282 move_obj = self.pool.get('stock.move')
284 self. _procure_orderpoint_confirm(cr, uid, automatic=False,use_new_cursor=False, context=context, user_id=False)
286 #Search all confirmed stock_moves and try to assign them
287 confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed'), ('company_id','=', company.id)], limit = None, context=context) #Type = stockable product?
288 for x in xrange(0, len(confirmed_ids), 100):
289 move_obj.action_assign(cr, uid, confirmed_ids[x:x+100], context=context)
304 def _prepare_automatic_op_procurement(self, cr, uid, product, warehouse, location_id, context=None):
305 return {'name': _('Automatic OP: %s') % (product.name,),
306 'origin': _('SCHEDULER'),
307 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
308 'product_id': product.id,
309 'product_qty': -product.virtual_available,
310 'product_uom': product.uom_id.id,
311 'location_id': location_id,
312 'company_id': warehouse.company_id.id,
315 def create_automatic_op(self, cr, uid, context=None):
317 Create procurement of virtual stock < 0
319 @param self: The object pointer
320 @param cr: The current row, from the database cursor,
321 @param uid: The current user ID for security checks
322 @param context: A standard dictionary for contextual values
323 @return: Dictionary of values
327 product_obj = self.pool.get('product.product')
328 proc_obj = self.pool.get('procurement.order')
329 warehouse_obj = self.pool.get('stock.warehouse')
331 warehouse_ids = warehouse_obj.search(cr, uid, [], context=context)
332 products_ids = product_obj.search(cr, uid, [], order='id', context=context)
334 for warehouse in warehouse_obj.browse(cr, uid, warehouse_ids, context=context):
335 context['warehouse'] = warehouse
336 # Here we check products availability.
337 # We use the method 'read' for performance reasons, because using the method 'browse' may crash the server.
338 for product_read in product_obj.read(cr, uid, products_ids, ['virtual_available'], context=context):
339 if product_read['virtual_available'] >= 0.0:
342 product = product_obj.browse(cr, uid, [product_read['id']], context=context)[0]
344 location_id = warehouse.lot_stock_id.id
346 proc_id = proc_obj.create(cr, uid,
347 self._prepare_automatic_op_procurement(cr, uid, product, warehouse, location_id, context=context),
349 self.assign(cr, uid, [proc_id])
350 self.run(cr, uid, [proc_id])
353 def _get_orderpoint_date_planned(self, cr, uid, orderpoint, start_date, context=None):
354 date_planned = start_date + \
355 relativedelta(days=orderpoint.product_id.seller_delay or 0.0)
356 return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT)
358 def _prepare_orderpoint_procurement(self, cr, uid, orderpoint, product_qty, context=None):
359 return {'name': orderpoint.name,
360 'date_planned': self._get_orderpoint_date_planned(cr, uid, orderpoint, datetime.today(), context=context),
361 'product_id': orderpoint.product_id.id,
362 'product_qty': product_qty,
363 'company_id': orderpoint.company_id.id,
364 'product_uom': orderpoint.product_uom.id,
365 'location_id': orderpoint.location_id.id,
366 'origin': orderpoint.name}
368 def _product_virtual_get(self, cr, uid, order_point):
369 product_obj = self.pool.get('product.product')
370 return product_obj._product_available(cr, uid,
371 [order_point.product_id.id],
372 {'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available']
374 def _procure_orderpoint_confirm(self, cr, uid, automatic=False,\
375 use_new_cursor=False, context=None, user_id=False):
377 Create procurement based on Orderpoint
378 use_new_cursor: False or the dbname
380 @param self: The object pointer
381 @param cr: The current row, from the database cursor,
382 @param user_id: The current user ID for security checks
383 @param context: A standard dictionary for contextual values
384 @param param: False or the dbname
385 @return: Dictionary of values
391 cr = openerp.registry(use_new_cursor).db.cursor()
392 orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
394 procurement_obj = self.pool.get('procurement.order')
398 self.create_automatic_op(cr, uid, context=context)
400 ids = orderpoint_obj.search(cr, uid, [], offset=offset, limit=100)
401 for op in orderpoint_obj.browse(cr, uid, ids, context=context):
402 prods = self._product_virtual_get(cr, uid, op)
405 if prods < op.product_min_qty:
406 qty = max(op.product_min_qty, op.product_max_qty)-prods
408 reste = qty % op.qty_multiple
410 qty += op.qty_multiple - reste
414 if op.product_id.type not in ('consu'):
415 procurement_draft_ids = orderpoint_obj.get_draft_procurements(cr, uid, op. id, context=context)
416 if procurement_draft_ids:
417 # Check draft procurement related to this order point
418 procure_datas = procurement_obj.read(
419 cr, uid, procurement_draft_ids, ['id', 'product_qty'], context=context)
421 for proc_data in procure_datas:
422 if to_generate >= proc_data['product_qty']:
423 self.signal_button_confirm(cr, uid, [proc_data['id']])
424 procurement_obj.write(cr, uid, [proc_data['id']], {'origin': op.name}, context=context)
425 to_generate -= proc_data['product_qty']
431 proc_id = procurement_obj.create(cr, uid,
432 self._prepare_orderpoint_procurement(cr, uid, op, qty, context=context),
434 self.check(cr, uid, [proc_id])
435 self.run(cr, uid, [proc_id])
436 #TODO: check if we can remove this field because it doesn't seem used at all
437 #orderpoint_obj.write(cr, uid, [op.id],
438 # {'procurement_id': proc_id}, context=context)