[FIX] stock: barcode_nomenclature_id field on stock.picking.type instead of stock...
[odoo/odoo.git] / addons / stock / procurement.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
24
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
30 import openerp
31
32 class procurement_group(osv.osv):
33     _inherit = 'procurement.group'
34     _columns = {
35         'partner_id': fields.many2one('res.partner', 'Partner')
36     }
37
38 class procurement_rule(osv.osv):
39     _inherit = 'procurement.rule'
40
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'))]
44
45     def _get_rules(self, cr, uid, ids, context=None):
46         res = []
47         for route in self.browse(cr, uid, ids):
48             res += [x.id for x in route.pull_ids]
49         return res
50
51     _columns = {
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',
60             store={
61                 'stock.location.route': (_get_rules, ['sequence'], 10),
62                 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
63         }),
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)"),
71     }
72
73     _defaults = {
74         'procure_method': 'make_to_stock',
75         'propagate': True,
76         'delay': 0,
77     }
78
79 class procurement_order(osv.osv):
80     _inherit = "procurement.order"
81     _columns = {
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'),
89     }
90
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)
94
95     def cancel(self, cr, uid, ids, context=None):
96         if context is None:
97             context = {}
98         to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context)
99         ctx = context.copy()
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)
105
106     def _find_parent_locations(self, cr, uid, procurement, context=None):
107         location = procurement.location_id
108         res = [location.id]
109         while location.location_id:
110             location = location.location_id
111             res.append(location.id)
112         return res
113
114     def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
115         if warehouse_id:
116             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
117             return {'value': {'location_id': warehouse.lot_stock_id.id}}
118         return {}
119
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)
130         if not res:
131             res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context)
132             if not res:
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 []
134                 if not res:
135                     res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
136         return res
137
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)
140         if not rule_id:
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
145         return rule_id
146
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.
150
151         :param procurement: browse record
152         :rtype: dictionary
153         '''
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')
155         group_id = False
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
162         already_done_qty = 0
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)
169         vals = {
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,
189             'date': newdate,
190             'date_expected': newdate,
191             'propagate': procurement.rule_id.propagate,
192             'priority': procurement.priority,
193         }
194         return vals
195
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)
200                 return False
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)
205             return True
206         return super(procurement_order, self)._run(cr, uid, procurement, context=context)
207
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)
211
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)
220         return res
221
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.
225         '''
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:
230                 return True
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:
237                 return False
238             elif all_done_or_cancel and not all_cancel:
239                 return True
240             elif 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)
243             return False
244
245         return super(procurement_order, self)._check(cr, uid, procurement, context)
246
247     def do_view_pickings(self, cr, uid, ids, context=None):
248         '''
249         This function returns an action that display the pickings of the procurements belonging
250         to the same procurement group of given ids.
251         '''
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))) + "])]"
259         return result
260
261     def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
262         '''
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.
266
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
275         '''
276         super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
277         if context is None:
278             context = {}
279         try:
280             if use_new_cursor:
281                 cr = openerp.registry(cr.dbname).cursor()
282
283             move_obj = self.pool.get('stock.move')
284
285             #Minimum stock rules
286             self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
287
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)
292                 if use_new_cursor:
293                     cr.commit()
294
295             if use_new_cursor:
296                 cr.commit()
297         finally:
298             if use_new_cursor:
299                 try:
300                     cr.close()
301                 except Exception:
302                     pass
303         return {}
304
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)
308
309     def _prepare_orderpoint_procurement(self, cr, uid, orderpoint, product_qty, context=None):
310         return {
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,
322         }
323
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']
329
330     def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
331         '''
332         Create procurement based on Orderpoint
333
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.
336         '''
337         if context is None:
338             context = {}
339         if use_new_cursor:
340             cr = openerp.registry(cr.dbname).cursor()
341         orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
342
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)
346         prev_ids = []
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):
351                 try:
352                     prods = self._product_virtual_get(cr, uid, op)
353                     if prods is None:
354                         continue
355                     if prods < op.product_min_qty:
356                         qty = max(op.product_min_qty, op.product_max_qty) - prods
357
358                         reste = op.qty_multiple > 0 and qty % op.qty_multiple or 0.0
359                         if reste > 0:
360                             qty += op.qty_multiple - reste
361
362                         if qty <= 0:
363                             continue
364
365                         qty -= orderpoint_obj.subtract_procurements(cr, uid, op, context=context)
366
367                         if qty > 0:
368                             proc_id = procurement_obj.create(cr, uid,
369                                                              self._prepare_orderpoint_procurement(cr, uid, op, qty, context=context),
370                                                              context=context)
371                             self.check(cr, uid, [proc_id])
372                             self.run(cr, uid, [proc_id])
373                     if use_new_cursor:
374                         cr.commit()
375                 except OperationalError:
376                     if use_new_cursor:
377                         orderpoint_ids.append(op.id)
378                         cr.rollback()
379                         continue
380                     else:
381                         raise
382             if use_new_cursor:
383                 cr.commit()
384             if prev_ids == ids:
385                 break
386             else:
387                 prev_ids = ids
388
389         if use_new_cursor:
390             cr.commit()
391             cr.close()
392         return {}