a5f62b3d3c041ec10768fab2d5baeecb6d6e9804
[odoo/odoo.git] / addons / mrp / stock.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 import time
23
24 from openerp.osv import fields
25 from openerp.osv import osv
26 from openerp.tools.translate import _
27 from openerp import SUPERUSER_ID
28 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
29
30 class StockMove(osv.osv):
31     _inherit = 'stock.move'
32
33     _columns = {
34         'production_id': fields.many2one('mrp.production', 'Production Order for Produced Products', select=True, copy=False),
35         'raw_material_production_id': fields.many2one('mrp.production', 'Production Order for Raw Materials', select=True),
36         'consumed_for': fields.many2one('stock.move', 'Consumed for', help='Technical field used to make the traceability of produced products'),
37     }
38
39     def check_tracking(self, cr, uid, move, lot_id, context=None):
40         super(StockMove, self).check_tracking(cr, uid, move, lot_id, context=context)
41         if move.product_id.track_production and (move.location_id.usage == 'production' or move.location_dest_id.usage == 'production') and not lot_id:
42             raise osv.except_osv(_('Warning!'), _('You must assign a serial number for the product %s') % (move.product_id.name))
43         if move.raw_material_production_id and move.location_dest_id.usage == 'production' and move.raw_material_production_id.product_id.track_production and not move.consumed_for:
44             raise osv.except_osv(_('Warning!'), _("Because the product %s requires it, you must assign a serial number to your raw material %s to proceed further in your production. Please use the 'Produce' button to do so.") % (move.raw_material_production_id.product_id.name, move.product_id.name))
45
46     def _check_phantom_bom(self, cr, uid, move, context=None):
47         """check if product associated to move has a phantom bom
48             return list of ids of mrp.bom for that product """
49         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
50         #doing the search as SUPERUSER because a user with the permission to write on a stock move should be able to explode it
51         #without giving him the right to read the boms.
52         domain = [
53             '|', ('product_id', '=', move.product_id.id),
54             '&', ('product_id', '=', False), ('product_tmpl_id.product_variant_ids', '=', move.product_id.id),
55             ('type', '=', 'phantom'),
56             '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
57             '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
58             ('company_id', '=', user_company)]
59         return self.pool.get('mrp.bom').search(cr, SUPERUSER_ID, domain, context=context)
60
61     def _action_explode(self, cr, uid, move, context=None):
62         """ Explodes pickings.
63         @param move: Stock moves
64         @return: True
65         """
66         bom_obj = self.pool.get('mrp.bom')
67         move_obj = self.pool.get('stock.move')
68         prod_obj = self.pool.get("product.product")
69         proc_obj = self.pool.get("procurement.order")
70         uom_obj = self.pool.get("product.uom")
71         to_explode_again_ids = []
72         processed_ids = []
73         bis = self._check_phantom_bom(cr, uid, move, context=context)
74         if bis:
75             bom_point = bom_obj.browse(cr, SUPERUSER_ID, bis[0], context=context)
76             factor = uom_obj._compute_qty(cr, SUPERUSER_ID, move.product_uom.id, move.product_uom_qty, bom_point.product_uom.id) / bom_point.product_qty
77             res = bom_obj._bom_explode(cr, SUPERUSER_ID, bom_point, move.product_id, factor, [], context=context)
78             
79             for line in res[0]:
80                 product = prod_obj.browse(cr, uid, line['product_id'], context=context)
81                 if product.type != 'service':
82                     valdef = {
83                         'picking_id': move.picking_id.id if move.picking_id else False,
84                         'product_id': line['product_id'],
85                         'product_uom': line['product_uom'],
86                         'product_uom_qty': line['product_qty'],
87                         'product_uos': line['product_uos'],
88                         'product_uos_qty': line['product_uos_qty'],
89                         'state': 'draft',  #will be confirmed below
90                         'name': line['name'],
91                         'procurement_id': move.procurement_id.id,
92                         'split_from': move.id, #Needed in order to keep sale connection, but will be removed by unlink
93                     }
94                     mid = move_obj.copy(cr, uid, move.id, default=valdef, context=context)
95                     to_explode_again_ids.append(mid)
96                 else:
97                     if prod_obj.need_procurement(cr, uid, [product.id], context=context):
98                         valdef = {
99                             'name': move.rule_id and move.rule_id.name or "/",
100                             'origin': move.origin,
101                             'company_id': move.company_id and move.company_id.id or False,
102                             'date_planned': move.date,
103                             'product_id': line['product_id'],
104                             'product_qty': line['product_qty'],
105                             'product_uom': line['product_uom'],
106                             'product_uos_qty': line['product_uos_qty'],
107                             'product_uos': line['product_uos'],
108                             'group_id': move.group_id.id,
109                             'priority': move.priority,
110                             'partner_dest_id': move.partner_id.id,
111                             }
112                         if move.procurement_id:
113                             proc = proc_obj.copy(cr, uid, move.procurement_id.id, default=valdef, context=context)
114                         else:
115                             proc = proc_obj.create(cr, uid, valdef, context=context)
116                         proc_obj.run(cr, uid, [proc], context=context) #could be omitted
117
118             
119             #check if new moves needs to be exploded
120             if to_explode_again_ids:
121                 for new_move in self.browse(cr, uid, to_explode_again_ids, context=context):
122                     processed_ids.extend(self._action_explode(cr, uid, new_move, context=context))
123             
124             if not move.split_from and move.procurement_id:
125                 # Check if procurements have been made to wait for
126                 moves = move.procurement_id.move_ids
127                 if len(moves) == 1:
128                     proc_obj.write(cr, uid, [move.procurement_id.id], {'state': 'done'}, context=context)
129
130             if processed_ids and move.state == 'assigned':
131                 # Set the state of resulting moves according to 'assigned' as the original move is assigned
132                 move_obj.write(cr, uid, list(set(processed_ids) - set([move.id])), {'state': 'assigned'}, context=context)
133                 
134             #delete the move with original product which is not relevant anymore
135             move_obj.unlink(cr, SUPERUSER_ID, [move.id], context=context)
136         #return list of newly created move or the move id otherwise, unless there is no move anymore
137         return processed_ids or (not bis and [move.id]) or []
138
139     def action_confirm(self, cr, uid, ids, context=None):
140         move_ids = []
141         for move in self.browse(cr, uid, ids, context=context):
142             #in order to explode a move, we must have a picking_type_id on that move because otherwise the move
143             #won't be assigned to a picking and it would be weird to explode a move into several if they aren't
144             #all grouped in the same picking.
145             if move.picking_type_id:
146                 move_ids.extend(self._action_explode(cr, uid, move, context=context))
147             else:
148                 move_ids.append(move.id)
149
150         #we go further with the list of ids potentially changed by action_explode
151         return super(StockMove, self).action_confirm(cr, uid, move_ids, context=context)
152
153     def action_consume(self, cr, uid, ids, product_qty, location_id=False, restrict_lot_id=False, restrict_partner_id=False,
154                        consumed_for=False, context=None):
155         """ Consumed product with specific quantity from specific source location.
156         @param product_qty: Consumed/produced product quantity (= in quantity of UoM of product)
157         @param location_id: Source location
158         @param restrict_lot_id: optionnal parameter that allows to restrict the choice of quants on this specific lot
159         @param restrict_partner_id: optionnal parameter that allows to restrict the choice of quants to this specific partner
160         @param consumed_for: optionnal parameter given to this function to make the link between raw material consumed and produced product, for a better traceability
161         @return: New lines created if not everything was consumed for this line
162         """
163         if context is None:
164             context = {}
165         res = []
166         production_obj = self.pool.get('mrp.production')
167
168         if product_qty <= 0:
169             raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
170         #because of the action_confirm that can create extra moves in case of phantom bom, we need to make 2 loops
171         ids2 = []
172         for move in self.browse(cr, uid, ids, context=context):
173             if move.state == 'draft':
174                 ids2.extend(self.action_confirm(cr, uid, [move.id], context=context))
175             else:
176                 ids2.append(move.id)
177
178         prod_orders = set()
179         for move in self.browse(cr, uid, ids2, context=context):
180             prod_orders.add(move.raw_material_production_id.id or move.production_id.id)
181             move_qty = move.product_qty
182             if move_qty <= 0:
183                 raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
184             quantity_rest = move_qty - product_qty
185             # Compare with numbers of move uom as we want to avoid a split with 0 qty
186             quantity_rest_uom = move.product_uom_qty - self.pool.get("product.uom")._compute_qty_obj(cr, uid, move.product_id.uom_id, product_qty, move.product_uom)
187             if float_compare(quantity_rest_uom, 0, precision_rounding=move.product_uom.rounding) != 0:
188                 new_mov = self.split(cr, uid, move, quantity_rest, context=context)
189                 res.append(new_mov)
190             vals = {'restrict_lot_id': restrict_lot_id,
191                     'restrict_partner_id': restrict_partner_id,
192                     'consumed_for': consumed_for}
193             if location_id:
194                 vals.update({'location_id': location_id})
195             self.write(cr, uid, [move.id], vals, context=context)
196         # Original moves will be the quantities consumed, so they need to be done
197         self.action_done(cr, uid, ids2, context=context)
198         if res:
199             self.action_assign(cr, uid, res, context=context)
200         if prod_orders:
201             production_obj.signal_workflow(cr, uid, list(prod_orders), 'button_produce')
202         return res
203
204     def action_scrap(self, cr, uid, ids, product_qty, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
205         """ Move the scrap/damaged product into scrap location
206         @param product_qty: Scraped product quantity
207         @param location_id: Scrap location
208         @return: Scraped lines
209         """
210         res = []
211         production_obj = self.pool.get('mrp.production')
212         for move in self.browse(cr, uid, ids, context=context):
213             new_moves = super(StockMove, self).action_scrap(cr, uid, [move.id], product_qty, location_id,
214                                                             restrict_lot_id=restrict_lot_id,
215                                                             restrict_partner_id=restrict_partner_id, context=context)
216             #If we are not scrapping our whole move, tracking and lot references must not be removed
217             production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
218             for prod_id in production_ids:
219                 production_obj.signal_workflow(cr, uid, [prod_id], 'button_produce')
220             for new_move in new_moves:
221                 production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new_move)]})
222                 res.append(new_move)
223         return res
224
225     def write(self, cr, uid, ids, vals, context=None):
226         if isinstance(ids, (int, long)):
227             ids = [ids]
228         res = super(StockMove, self).write(cr, uid, ids, vals, context=context)
229         from openerp import workflow
230         if vals.get('state') == 'assigned':
231             moves = self.browse(cr, uid, ids, context=context)
232             orders = list(set([x.raw_material_production_id.id for x in moves if x.raw_material_production_id and x.raw_material_production_id.state == 'confirmed']))
233             for order_id in orders:
234                 if self.pool.get('mrp.production').test_ready(cr, uid, [order_id]):
235                     workflow.trg_validate(uid, 'mrp.production', order_id, 'moves_ready', cr)
236         return res
237
238 class stock_warehouse(osv.osv):
239     _inherit = 'stock.warehouse'
240     _columns = {
241         'manufacture_to_resupply': fields.boolean('Manufacture in this Warehouse', 
242                                                   help="When products are manufactured, they can be manufactured in this warehouse."),
243         'manufacture_pull_id': fields.many2one('procurement.rule', 'Manufacture Rule'),
244     }
245
246     def _get_manufacture_pull_rule(self, cr, uid, warehouse, context=None):
247         route_obj = self.pool.get('stock.location.route')
248         data_obj = self.pool.get('ir.model.data')
249         try:
250             manufacture_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_manufacture')[1]
251         except:
252             manufacture_route_id = route_obj.search(cr, uid, [('name', 'like', _('Manufacture'))], context=context)
253             manufacture_route_id = manufacture_route_id and manufacture_route_id[0] or False
254         if not manufacture_route_id:
255             raise osv.except_osv(_('Error!'), _('Can\'t find any generic Manufacture route.'))
256
257         return {
258             'name': self._format_routename(cr, uid, warehouse, _(' Manufacture'), context=context),
259             'location_id': warehouse.lot_stock_id.id,
260             'route_id': manufacture_route_id,
261             'action': 'manufacture',
262             'picking_type_id': warehouse.int_type_id.id,
263             'propagate': False, 
264             'warehouse_id': warehouse.id,
265         }
266
267     def create_routes(self, cr, uid, ids, warehouse, context=None):
268         pull_obj = self.pool.get('procurement.rule')
269         res = super(stock_warehouse, self).create_routes(cr, uid, ids, warehouse, context=context)
270         if warehouse.manufacture_to_resupply:
271             manufacture_pull_vals = self._get_manufacture_pull_rule(cr, uid, warehouse, context=context)
272             manufacture_pull_id = pull_obj.create(cr, uid, manufacture_pull_vals, context=context)
273             res['manufacture_pull_id'] = manufacture_pull_id
274         return res
275
276     def write(self, cr, uid, ids, vals, context=None):
277         pull_obj = self.pool.get('procurement.rule')
278         if isinstance(ids, (int, long)):
279             ids = [ids]
280
281         if 'manufacture_to_resupply' in vals:
282             if vals.get("manufacture_to_resupply"):
283                 for warehouse in self.browse(cr, uid, ids, context=context):
284                     if not warehouse.manufacture_pull_id:
285                         manufacture_pull_vals = self._get_manufacture_pull_rule(cr, uid, warehouse, context=context)
286                         manufacture_pull_id = pull_obj.create(cr, uid, manufacture_pull_vals, context=context)
287                         vals['manufacture_pull_id'] = manufacture_pull_id
288             else:
289                 for warehouse in self.browse(cr, uid, ids, context=context):
290                     if warehouse.manufacture_pull_id:
291                         pull_obj.unlink(cr, uid, warehouse.manufacture_pull_id.id, context=context)
292         return super(stock_warehouse, self).write(cr, uid, ids, vals, context=None)
293
294     def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
295         all_routes = super(stock_warehouse, self).get_all_routes_for_wh(cr, uid, warehouse, context=context)
296         if warehouse.manufacture_to_resupply and warehouse.manufacture_pull_id and warehouse.manufacture_pull_id.route_id:
297             all_routes += [warehouse.manufacture_pull_id.route_id.id]
298         return all_routes
299
300     def _handle_renaming(self, cr, uid, warehouse, name, code, context=None):
301         res = super(stock_warehouse, self)._handle_renaming(cr, uid, warehouse, name, code, context=context)
302         pull_obj = self.pool.get('procurement.rule')
303         #change the manufacture pull rule name
304         if warehouse.manufacture_pull_id:
305             pull_obj.write(cr, uid, warehouse.manufacture_pull_id.id, {'name': warehouse.manufacture_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
306         return res
307
308     def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None):
309         res = super(stock_warehouse, self)._get_all_products_to_resupply(cr, uid, warehouse, context=context)
310         if warehouse.manufacture_pull_id and warehouse.manufacture_pull_id.route_id:
311             for product_id in res:
312                 for route in self.pool.get('product.product').browse(cr, uid, product_id, context=context).route_ids:
313                     if route.id == warehouse.manufacture_pull_id.route_id.id:
314                         res.remove(product_id)
315                         break
316         return res