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 openerp import SUPERUSER_ID
27 from dateutil.relativedelta import relativedelta
28 from datetime import datetime
29 from psycopg2 import OperationalError
32 class procurement_group(osv.osv):
33 _inherit = 'procurement.group'
35 'partner_id': fields.many2one('res.partner', 'Partner')
38 class procurement_rule(osv.osv):
39 _inherit = 'procurement.rule'
41 def _get_action(self, cr, uid, context=None):
42 result = super(procurement_rule, self)._get_action(cr, uid, context=context)
43 return result + [('move', _('Move From Another Location'))]
45 def _get_rules(self, cr, uid, ids, context=None):
47 for route in self.browse(cr, uid, ids):
48 res += [x.id for x in route.pull_ids]
52 'location_id': fields.many2one('stock.location', 'Procurement Location'),
53 'location_src_id': fields.many2one('stock.location', 'Source Location',
54 help="Source location is action=move"),
55 'route_id': fields.many2one('stock.location.route', 'Route',
56 help="If route_id is False, the rule is global"),
57 'procure_method': fields.selection([('make_to_stock', 'Take From Stock'), ('make_to_order', 'Create Procurement')], 'Move Supply Method', required=True,
58 help="""Determines the procurement method of the stock move that will be generated: whether it will need to 'take from the available stock' in its source location or needs to ignore its stock and create a procurement over there."""),
59 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence',
61 'stock.location.route': (_get_rules, ['sequence'], 10),
62 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
64 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type',
65 help="Picking Type determines the way the picking should be shown in the view, reports, ..."),
66 'delay': fields.integer('Number of Days'),
67 'partner_address_id': fields.many2one('res.partner', 'Partner Address'),
68 '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'),
69 'warehouse_id': fields.many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for'),
70 '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)"),
74 'procure_method': 'make_to_stock',
79 class procurement_order(osv.osv):
80 _inherit = "procurement.order"
82 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service
83 'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"),
84 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"),
85 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"),
86 '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."),
87 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"),
88 'orderpoint_id': fields.many2one('stock.warehouse.orderpoint', 'Minimum Stock Rule'),
91 def propagate_cancel(self, cr, uid, procurement, context=None):
92 if procurement.rule_id.action == 'move' and procurement.move_ids:
93 self.pool.get('stock.move').action_cancel(cr, uid, [m.id for m in procurement.move_ids], context=context)
95 def cancel(self, cr, uid, ids, context=None):
98 to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
100 #set the context for the propagation of the procurement cancelation
101 ctx['cancel_procurement'] = True
102 for procurement in self.browse(cr, uid, to_cancel_ids, context=ctx):
103 self.propagate_cancel(cr, uid, procurement, context=ctx)
104 return super(procurement_order, self).cancel(cr, uid, to_cancel_ids, context=ctx)
106 def _find_parent_locations(self, cr, uid, procurement, context=None):
107 location = procurement.location_id
109 while location.location_id:
110 location = location.location_id
111 res.append(location.id)
114 def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
116 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
117 return {'value': {'location_id': warehouse.lot_stock_id.id}}
120 def _search_suitable_rule(self, cr, uid, procurement, domain, context=None):
121 '''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'''
122 pull_obj = self.pool.get('procurement.rule')
123 warehouse_route_ids = []
124 if procurement.warehouse_id:
125 domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)]
126 warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids]
127 product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids]
128 procurement_route_ids = [x.id for x in procurement.route_ids]
129 res = pull_obj.search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context)
131 res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context)
133 res = warehouse_route_ids and pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context) or []
135 res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
138 def _find_suitable_rule(self, cr, uid, procurement, context=None):
139 rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, procurement, context=context)
141 #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A'
142 all_parent_location_ids = self._find_parent_locations(cr, uid, procurement, context=context)
143 rule_id = self._search_suitable_rule(cr, uid, procurement, [('location_id', 'in', all_parent_location_ids)], context=context)
144 rule_id = rule_id and rule_id[0] or False
147 def _run_move_create(self, cr, uid, procurement, context=None):
148 ''' Returns a dictionary of values that will be used to create a stock move from a procurement.
149 This function assumes that the given procurement has a rule (action == 'move') set on it.
151 :param procurement: browse record
154 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')
156 if procurement.rule_id.group_propagation_option == 'propagate':
157 group_id = procurement.group_id and procurement.group_id.id or False
158 elif procurement.rule_id.group_propagation_option == 'fixed':
159 group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False
160 #it is possible that we've already got some move done, so check for the done qty and create
161 #a new move with the correct qty
163 already_done_qty_uos = 0
164 for move in procurement.move_ids:
165 already_done_qty += move.product_uom_qty if move.state == 'done' else 0
166 already_done_qty_uos += move.product_uos_qty if move.state == 'done' else 0
167 qty_left = max(procurement.product_qty - already_done_qty, 0)
168 qty_uos_left = max(procurement.product_uos_qty - already_done_qty_uos, 0)
170 'name': procurement.name,
171 'company_id': procurement.rule_id.company_id.id or procurement.rule_id.location_src_id.company_id.id or procurement.rule_id.location_id.company_id.id or procurement.company_id.id,
172 'product_id': procurement.product_id.id,
173 'product_uom': procurement.product_uom.id,
174 'product_uom_qty': qty_left,
175 'product_uos_qty': (procurement.product_uos and qty_uos_left) or qty_left,
176 'product_uos': (procurement.product_uos and procurement.product_uos.id) or procurement.product_uom.id,
177 'partner_id': procurement.rule_id.partner_address_id.id or (procurement.group_id and procurement.group_id.partner_id.id) or False,
178 'location_id': procurement.rule_id.location_src_id.id,
179 'location_dest_id': procurement.location_id.id,
180 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False,
181 'procurement_id': procurement.id,
182 'rule_id': procurement.rule_id.id,
183 'procure_method': procurement.rule_id.procure_method,
184 'origin': procurement.origin,
185 'picking_type_id': procurement.rule_id.picking_type_id.id,
186 'group_id': group_id,
187 'route_ids': [(4, x.id) for x in procurement.route_ids],
188 'warehouse_id': procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id,
190 'date_expected': newdate,
191 'propagate': procurement.rule_id.propagate,
192 'priority': procurement.priority,
196 def _run(self, cr, uid, procurement, context=None):
197 if procurement.rule_id and procurement.rule_id.action == 'move':
198 if not procurement.rule_id.location_src_id:
199 self.message_post(cr, uid, [procurement.id], body=_('No source location defined!'), context=context)
201 move_obj = self.pool.get('stock.move')
202 move_dict = self._run_move_create(cr, uid, procurement, context=context)
203 #create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
204 move_obj.create(cr, SUPERUSER_ID, move_dict, context=context)
206 return super(procurement_order, self)._run(cr, uid, procurement, context=context)
208 def run(self, cr, uid, ids, autocommit=False, context=None):
209 new_ids = [x.id for x in self.browse(cr, uid, ids, context=context) if x.state not in ('running', 'done', 'cancel')]
210 res = super(procurement_order, self).run(cr, uid, new_ids, autocommit=autocommit, context=context)
212 #after all the procurements are run, check if some created a draft stock move that needs to be confirmed
213 #(we do that in batch because it fasts the picking assignation and the picking state computation)
214 move_to_confirm_ids = []
215 for procurement in self.browse(cr, uid, new_ids, context=context):
216 if procurement.state == "running" and procurement.rule_id and procurement.rule_id.action == "move":
217 move_to_confirm_ids += [m.id for m in procurement.move_ids if m.state == 'draft']
218 if move_to_confirm_ids:
219 self.pool.get('stock.move').action_confirm(cr, uid, move_to_confirm_ids, context=context)
222 def _check(self, cr, uid, procurement, context=None):
223 ''' Implement the procurement checking for rules of type 'move'. The procurement will be satisfied only if all related
224 moves are done/cancel and if the requested quantity is moved.
226 if procurement.rule_id and procurement.rule_id.action == 'move':
227 uom_obj = self.pool.get('product.uom')
228 # In case Phantom BoM splits only into procurements
229 if not procurement.move_ids:
231 cancel_test_list = [x.state == 'cancel' for x in procurement.move_ids]
232 done_cancel_test_list = [x.state in ('done', 'cancel') for x in procurement.move_ids]
233 at_least_one_cancel = any(cancel_test_list)
234 all_done_or_cancel = all(done_cancel_test_list)
235 all_cancel = all(cancel_test_list)
236 if not all_done_or_cancel:
238 elif all_done_or_cancel and not all_cancel:
241 self.message_post(cr, uid, [procurement.id], body=_('All stock moves have been cancelled for this procurement.'), context=context)
242 self.write(cr, uid, [procurement.id], {'state': 'cancel'}, context=context)
245 return super(procurement_order, self)._check(cr, uid, procurement, context)
247 def do_view_pickings(self, cr, uid, ids, context=None):
249 This function returns an action that display the pickings of the procurements belonging
250 to the same procurement group of given ids.
252 mod_obj = self.pool.get('ir.model.data')
253 act_obj = self.pool.get('ir.actions.act_window')
254 result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings')
255 id = result and result[1] or False
256 result = act_obj.read(cr, uid, [id], context=context)[0]
257 group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id])
258 result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
261 def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
263 Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules
264 and the availability of moves. This function is intended to be run for all the companies at the same time, so
265 we run functions as SUPERUSER to avoid intercompanies and access rights issues.
267 @param self: The object pointer
268 @param cr: The current row, from the database cursor,
269 @param uid: The current user ID for security checks
270 @param ids: List of selected IDs
271 @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
272 This is appropriate for batch jobs only.
273 @param context: A standard dictionary for contextual values
274 @return: Dictionary of values
276 super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
281 cr = openerp.registry(cr.dbname).cursor()
283 move_obj = self.pool.get('stock.move')
286 self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
288 #Search all confirmed stock_moves and try to assign them
289 confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context)
290 for x in xrange(0, len(confirmed_ids), 100):
291 move_obj.action_assign(cr, uid, confirmed_ids[x:x + 100], context=context)
305 def _get_orderpoint_date_planned(self, cr, uid, orderpoint, start_date, context=None):
306 date_planned = start_date + relativedelta(days=orderpoint.product_id.seller_delay or 0.0)
307 return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT)
309 def _prepare_orderpoint_procurement(self, cr, uid, orderpoint, product_qty, context=None):
311 'name': orderpoint.name,
312 'date_planned': self._get_orderpoint_date_planned(cr, uid, orderpoint, datetime.today(), context=context),
313 'product_id': orderpoint.product_id.id,
314 'product_qty': product_qty,
315 'company_id': orderpoint.company_id.id,
316 'product_uom': orderpoint.product_uom.id,
317 'location_id': orderpoint.location_id.id,
318 'origin': orderpoint.name,
319 'warehouse_id': orderpoint.warehouse_id.id,
320 'orderpoint_id': orderpoint.id,
321 'group_id': orderpoint.group_id.id,
324 def _product_virtual_get(self, cr, uid, order_point):
325 product_obj = self.pool.get('product.product')
326 return product_obj._product_available(cr, uid,
327 [order_point.product_id.id],
328 context={'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available']
330 def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
332 Create procurement based on Orderpoint
334 :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
335 This is appropriate for batch jobs only.
340 cr = openerp.registry(cr.dbname).cursor()
341 orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
343 procurement_obj = self.pool.get('procurement.order')
344 dom = company_id and [('company_id', '=', company_id)] or []
345 orderpoint_ids = orderpoint_obj.search(cr, uid, dom)
347 while orderpoint_ids:
348 ids = orderpoint_ids[:100]
349 del orderpoint_ids[:100]
350 for op in orderpoint_obj.browse(cr, uid, ids, context=context):
352 prods = self._product_virtual_get(cr, uid, op)
355 if prods < op.product_min_qty:
356 qty = max(op.product_min_qty, op.product_max_qty) - prods
358 reste = op.qty_multiple > 0 and qty % op.qty_multiple or 0.0
360 qty += op.qty_multiple - reste
365 qty -= orderpoint_obj.subtract_procurements(cr, uid, op, context=context)
368 proc_id = procurement_obj.create(cr, uid,
369 self._prepare_orderpoint_procurement(cr, uid, op, qty, context=context),
371 self.check(cr, uid, [proc_id])
372 self.run(cr, uid, [proc_id])
375 except OperationalError:
377 orderpoint_ids.append(op.id)