[FIX] sale_stock: product_id_change_with_wh should not call another method (product_i...
[odoo/odoo.git] / addons / sale_stock / sale_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
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22 from datetime import datetime, timedelta
23 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
24 from openerp.osv import fields, osv
25 from openerp.tools.safe_eval import safe_eval as eval
26 from openerp.tools.translate import _
27 import pytz
28 from openerp import SUPERUSER_ID
29
30 class sale_order(osv.osv):
31     _inherit = "sale.order"
32
33     def _get_default_warehouse(self, cr, uid, context=None):
34         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
35         warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
36         if not warehouse_ids:
37             return False
38         return warehouse_ids[0]
39
40     def _get_shipped(self, cr, uid, ids, name, args, context=None):
41         res = {}
42         for sale in self.browse(cr, uid, ids, context=context):
43             group = sale.procurement_group_id
44             if group:
45                 res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids])
46             else:
47                 res[sale.id] = False
48         return res
49
50     def _get_orders(self, cr, uid, ids, context=None):
51         res = set()
52         for move in self.browse(cr, uid, ids, context=context):
53             if move.procurement_id and move.procurement_id.sale_line_id:
54                 res.add(move.procurement_id.sale_line_id.order_id.id)
55         return list(res)
56
57     def _get_orders_procurements(self, cr, uid, ids, context=None):
58         res = set()
59         for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context):
60             if proc.state =='done' and proc.sale_line_id:
61                 res.add(proc.sale_line_id.order_id.id)
62         return list(res)
63
64     def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
65         res = {}
66         for sale in self.browse(cr, uid, ids, context=context):
67             if not sale.procurement_group_id:
68                 res[sale.id] = []
69                 continue
70             res[sale.id] = self.pool.get('stock.picking').search(cr, uid, [('group_id', '=', sale.procurement_group_id.id)], context=context)
71         return res
72
73     def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
74         vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
75         location_id = order.partner_shipping_id.property_stock_customer.id
76         vals['location_id'] = location_id
77         routes = line.route_id and [(4, line.route_id.id)] or []
78         vals['route_ids'] = routes
79         vals['warehouse_id'] = order.warehouse_id and order.warehouse_id.id or False
80         vals['partner_dest_id'] = order.partner_shipping_id.id
81         return vals
82
83     _columns = {
84         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
85         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
86             'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
87             help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
88         'order_policy': fields.selection([
89                 ('manual', 'On Demand'),
90                 ('picking', 'On Delivery Order'),
91                 ('prepaid', 'Before Delivery'),
92             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
93             help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""),
94         'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={
95                 'procurement.order': (_get_orders_procurements, ['state'], 10)
96             }),
97         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
98         'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'),
99     }
100     _defaults = {
101         'warehouse_id': _get_default_warehouse,
102         'picking_policy': 'direct',
103         'order_policy': 'manual',
104     }
105     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
106         val = {}
107         if warehouse_id:
108             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
109             if warehouse.company_id:
110                 val['company_id'] = warehouse.company_id.id
111         return {'value': val}
112
113     def action_view_delivery(self, cr, uid, ids, context=None):
114         '''
115         This function returns an action that display existing delivery orders
116         of given sales order ids. It can either be a in a list or in a form
117         view, if there is only one delivery order to show.
118         '''
119         
120         mod_obj = self.pool.get('ir.model.data')
121         act_obj = self.pool.get('ir.actions.act_window')
122
123         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree_all')
124         id = result and result[1] or False
125         result = act_obj.read(cr, uid, [id], context=context)[0]
126
127         #compute the number of delivery orders to display
128         pick_ids = []
129         for so in self.browse(cr, uid, ids, context=context):
130             pick_ids += [picking.id for picking in so.picking_ids]
131             
132         #choose the view_mode accordingly
133         if len(pick_ids) > 1:
134             result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
135         else:
136             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
137             result['views'] = [(res and res[1] or False, 'form')]
138             result['res_id'] = pick_ids and pick_ids[0] or False
139         return result
140
141     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
142         move_obj = self.pool.get("stock.move")
143         res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
144         for order in self.browse(cr, uid, ids, context=context):
145             if order.order_policy == 'picking':
146                 for picking in order.picking_ids:
147                     move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context)
148         return res
149
150     def action_cancel(self, cr, uid, ids, context=None):
151         if context is None:
152             context = {}
153         sale_order_line_obj = self.pool.get('sale.order.line')
154         proc_obj = self.pool.get('procurement.order')
155         stock_obj = self.pool.get('stock.picking')
156         for sale in self.browse(cr, uid, ids, context=context):
157             for pick in sale.picking_ids:
158                 if pick.state not in ('draft', 'cancel'):
159                     raise osv.except_osv(
160                         _('Cannot cancel sales order!'),
161                         _('You must first cancel all delivery order(s) attached to this sales order.'))
162             stock_obj.signal_workflow(cr, uid, [p.id for p in sale.picking_ids], 'button_cancel')
163         return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
164
165     def action_wait(self, cr, uid, ids, context=None):
166         res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
167         for o in self.browse(cr, uid, ids):
168             noprod = self.test_no_product(cr, uid, o, context)
169             if noprod and o.order_policy=='picking':
170                 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
171         return res
172
173     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
174         date_planned = super(sale_order, self)._get_date_planned(cr, uid, order, line, start_date, context=context)
175         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
176         return date_planned
177
178     def _prepare_procurement_group(self, cr, uid, order, context=None):
179         res = super(sale_order, self)._prepare_procurement_group(cr, uid, order, context=None)
180         res.update({'move_type': order.picking_policy})
181         return res
182
183     def action_ship_end(self, cr, uid, ids, context=None):
184         super(sale_order, self).action_ship_end(cr, uid, ids, context=context)
185         for order in self.browse(cr, uid, ids, context=context):
186             val = {'shipped': True}
187             if order.state == 'shipping_except':
188                 val['state'] = 'progress'
189                 if (order.order_policy == 'manual'):
190                     for line in order.order_line:
191                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
192                             val['state'] = 'manual'
193                             break
194             res = self.write(cr, uid, [order.id], val)
195         return True
196
197     def has_stockable_products(self, cr, uid, ids, *args):
198         for order in self.browse(cr, uid, ids):
199             for order_line in order.order_line:
200                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
201                     return True
202         return False
203
204
205 class sale_order_line(osv.osv):
206     _inherit = 'sale.order.line'
207
208     def need_procurement(self, cr, uid, ids, context=None):
209         #when sale is installed alone, there is no need to create procurements, but with sale_stock
210         #we must create a procurement for each product that is not a service.
211         for line in self.browse(cr, uid, ids, context=context):
212             if line.product_id and line.product_id.type != 'service':
213                 return True
214         return super(sale_order_line, self).need_procurement(cr, uid, ids, context=context)
215
216     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
217         res = {}
218         for line in self.browse(cr, uid, ids, context=context):
219             try:
220                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
221             except:
222                 res[line.id] = 1
223         return res
224
225     _columns = {
226         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
227         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
228         'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]),
229     }
230
231     _defaults = {
232         'product_packaging': False,
233     }
234
235     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
236                                    partner_id=False, packaging=False, flag=False, context=None):
237         if not product:
238             return {'value': {'product_packaging': False}}
239         product_obj = self.pool.get('product.product')
240         product_uom_obj = self.pool.get('product.uom')
241         pack_obj = self.pool.get('product.packaging')
242         warning = {}
243         result = {}
244         warning_msgs = ''
245         if flag:
246             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
247                     product=product, qty=qty, uom=uom, partner_id=partner_id,
248                     packaging=packaging, flag=False, context=context)
249             warning_msgs = res.get('warning') and res['warning'].get('message', '') or ''
250
251         products = product_obj.browse(cr, uid, product, context=context)
252         if not products.packaging_ids:
253             packaging = result['product_packaging'] = False
254
255         if packaging:
256             default_uom = products.uom_id and products.uom_id.id
257             pack = pack_obj.browse(cr, uid, packaging, context=context)
258             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
259 #            qty = qty - qty % q + q
260             if qty and (q and not (qty % q) == 0):
261                 ean = pack.ean or _('(n/a)')
262                 qty_pack = pack.qty
263                 type_ul = pack.ul
264                 if not warning_msgs:
265                     warn_msg = _("You selected a quantity of %d Units.\n"
266                                 "But it's not compatible with the selected packaging.\n"
267                                 "Here is a proposition of quantities according to the packaging:\n"
268                                 "EAN: %s Quantity: %s Type of ul: %s") % \
269                                     (qty, ean, qty_pack, type_ul.name)
270                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
271                 warning = {
272                        'title': _('Configuration Error!'),
273                        'message': warning_msgs
274                 }
275             result['product_uom_qty'] = qty
276
277         return {'value': result, 'warning': warning}
278
279
280     def product_id_change_with_wh(self, cr, uid, ids, pricelist, product, qty=0,
281             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
282             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, warehouse_id=False, context=None):
283         context = context or {}
284         product_uom_obj = self.pool.get('product.uom')
285         product_obj = self.pool.get('product.product')
286         warning = {}
287         res = self.product_id_change(cr, uid, ids, pricelist, product, qty=qty,
288             uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
289             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
290
291         if not product:
292             res['value'].update({'product_packaging': False})
293             return res
294
295         #update of result obtained in super function
296         product_obj = product_obj.browse(cr, uid, product, context=context)
297         res['value']['delay'] = (product_obj.sale_delay or 0.0)
298
299         # Calling product_packaging_change function after updating UoM
300         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
301         res['value'].update(res_packing.get('value', {}))
302         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
303
304         if product_obj.type == 'product':
305             #determine if the product is MTO or not (for a further check)
306             isMto = False
307             if warehouse_id:
308                 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
309                 for product_route in product_obj.route_ids:
310                     if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id:
311                         isMto = True
312                         break
313             else:
314                 try:
315                     mto_route_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'route_warehouse0_mto').id
316                 except:
317                     # if route MTO not found in ir_model_data, we treat the product as in MTS
318                     mto_route_id = False
319                 if mto_route_id:
320                     for product_route in product_obj.route_ids:
321                         if product_route.id == mto_route_id:
322                             isMto = True
323                             break
324
325             #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO
326             if not isMto:
327                 uom_record = False
328                 if uom:
329                     uom_record = product_uom_obj.browse(cr, uid, uom, context=context)
330                     if product_obj.uom_id.category_id.id != uom_record.category_id.id:
331                         uom_record = False
332                 if not uom_record:
333                     uom_record = product_obj.uom_id
334                 compare_qty = float_compare(product_obj.virtual_available, qty, precision_rounding=uom_record.rounding)
335                 if compare_qty == -1:
336                     warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
337                         (qty, uom_record.name,
338                          max(0,product_obj.virtual_available), uom_record.name,
339                          max(0,product_obj.qty_available), uom_record.name)
340                     warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
341
342         #update of warning messages
343         if warning_msgs:
344             warning = {
345                        'title': _('Configuration Error!'),
346                        'message' : warning_msgs
347                     }
348         res.update({'warning': warning})
349         return res
350
351 class stock_move(osv.osv):
352     _inherit = 'stock.move'
353
354     def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
355         invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
356         if move.procurement_id and move.procurement_id.sale_line_id:
357             sale_line = move.procurement_id.sale_line_id
358             self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], {
359                 'invoice_lines': [(4, invoice_line_id)]
360             }, context=context)
361             self.pool.get('sale.order').write(cr, uid, [sale_line.order_id.id], {
362                 'invoice_ids': [(4, invoice_line_vals['invoice_id'])],
363             })
364         return invoice_line_id
365
366     def _get_master_data(self, cr, uid, move, company, context=None):
367         if move.procurement_id and move.procurement_id.sale_line_id:
368             sale_order = move.procurement_id.sale_line_id.order_id
369             return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id
370         return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context)
371
372     def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None):
373         res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context)
374         if move.procurement_id and move.procurement_id.sale_line_id:
375             sale_line = move.procurement_id.sale_line_id
376             res['invoice_line_tax_id'] = [(6, 0, [x.id for x in sale_line.tax_id])]
377             res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False
378             res['discount'] = sale_line.discount
379             if move.product_id.id != sale_line.product_id.id:
380                 res['price_unit'] = self.pool['product.pricelist'].price_get(
381                     cr, uid, [sale_line.order_id.pricelist_id.id],
382                     move.product_id.id, move.product_uom_qty or 1.0,
383                     sale_line.order_id.partner_id, context=context)[sale_line.order_id.pricelist_id.id]
384             else:
385                 res['price_unit'] = sale_line.price_unit
386         return res
387
388
389 class stock_location_route(osv.osv):
390     _inherit = "stock.location.route"
391     _columns = {
392         'sale_selectable': fields.boolean("Selectable on Sales Order Line")
393         }
394
395
396 class stock_picking(osv.osv):
397     _inherit = "stock.picking"
398
399     def _get_sale_id(self, cr, uid, ids, name, args, context=None):
400         sale_obj = self.pool.get("sale.order")
401         res = {}
402         for picking in self.browse(cr, uid, ids, context=context):
403             res[picking.id] = False
404             if picking.group_id:
405                 sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
406                 if sale_ids:
407                     res[picking.id] = sale_ids[0]
408         return res
409     
410     _columns = {
411         'sale_id': fields.function(_get_sale_id, type="many2one", relation="sale.order", string="Sale Order"),
412     }
413
414     def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
415         sale_obj = self.pool.get('sale.order')
416         sale_line_obj = self.pool.get('sale.order.line')
417         invoice_line_obj = self.pool.get('account.invoice.line')
418         invoice_id = super(stock_picking, self)._create_invoice_from_picking(cr, uid, picking, vals, context=context)
419         if picking.group_id:
420             sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
421             if sale_ids:
422                 sale_line_ids = sale_line_obj.search(cr, uid, [('order_id', 'in', sale_ids), ('product_id.type', '=', 'service'), ('invoiced', '=', False)], context=context)
423                 if sale_line_ids:
424                     created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context)
425                     invoice_line_obj.write(cr, uid, created_lines, {'invoice_id': invoice_id}, context=context)
426         return invoice_id