[IMP] safe_eval: do not log exceptions, when re-raising a new exception, make the...
[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 dateutil.relativedelta import relativedelta
25 from openerp.osv import fields, osv
26 from openerp.tools.translate import _
27
28 class sale_shop(osv.osv):
29     _inherit = "sale.shop"
30     _columns = {
31         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
32     }
33
34 sale_shop()
35
36 class sale_order(osv.osv):
37     _inherit = "sale.order"
38     
39     def copy(self, cr, uid, id, default=None, context=None):
40         if not default:
41             default = {}
42         default.update({
43             'shipped': False,
44             'picking_ids': [],
45         })
46         return super(sale_order, self).copy(cr, uid, id, default, context=context)
47     
48     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
49         if not policy:
50             return {}
51         inv_qty = 'order'
52         if policy == 'prepaid':
53             inv_qty = 'order'
54         elif policy == 'picking':
55             inv_qty = 'procurement'
56         return {'value': {'invoice_quantity': inv_qty}}
57
58     def write(self, cr, uid, ids, vals, context=None):
59         if vals.get('order_policy', False):
60             if vals['order_policy'] == 'prepaid':
61                 vals.update({'invoice_quantity': 'order'})
62             elif vals['order_policy'] == 'picking':
63                 vals.update({'invoice_quantity': 'procurement'})
64         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
65
66     def create(self, cr, uid, vals, context=None):
67         if vals.get('order_policy', False):
68             if vals['order_policy'] == 'prepaid':
69                 vals.update({'invoice_quantity': 'order'})
70             if vals['order_policy'] == 'picking':
71                 vals.update({'invoice_quantity': 'procurement'})
72         order =  super(sale_order, self).create(cr, uid, vals, context=context)
73         return order
74
75     # This is False
76     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
77         if not ids:
78             return {}
79         res = {}
80         tmp = {}
81         for id in ids:
82             tmp[id] = {'picked': 0.0, 'total': 0.0}
83         cr.execute('''SELECT
84                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
85             FROM
86                 stock_move m
87             LEFT JOIN
88                 stock_picking p on (p.id=m.picking_id)
89             LEFT JOIN
90                 procurement_order mp on (mp.move_id=m.id)
91             WHERE
92                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
93
94         for item in cr.dictfetchall():
95             if item['move_state'] == 'cancel':
96                 continue
97
98             if item['picking_type'] == 'in':#this is a returned picking
99                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
100                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
101                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
102             else:
103                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
104                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
105                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
106
107         for order in self.browse(cr, uid, ids, context=context):
108             if order.shipped:
109                 res[order.id] = 100.0
110             else:
111                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
112         return res
113     
114     _columns = {
115           'state': fields.selection([
116             ('draft', 'Draft Quotation'),
117             ('sent', 'Quotation Sent'),
118             ('cancel', 'Cancelled'),
119             ('waiting_date', 'Waiting Schedule'),
120             ('progress', 'Sales Order'),
121             ('manual', 'Sale to Invoice'),
122             ('shipping_except', 'Shipping Exception'),
123             ('invoice_except', 'Invoice Exception'),
124             ('done', 'Done'),
125             ], 'Status', readonly=True,help="Gives the status of the quotation or sales order.\
126               \nThe exception status is automatically set when a cancel operation occurs \
127               in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
128                but waiting for the scheduler to run on the order date.", select=True),
129         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
130         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
131             'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
132             help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
133         'order_policy': fields.selection([
134                 ('manual', 'On Demand'),
135                 ('picking', 'On Delivery Order'),
136                 ('prepaid', 'Before Delivery'),
137             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
138             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."""),
139         'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
140         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
141         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
142         'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', 
143                                              help="The sales order will automatically create the invoice proposition (draft invoice).\
144                                               You have to choose  if you want your invoice based on ordered ", required=True, readonly=True, states={'draft': [('readonly', False)]}),
145     }
146     _defaults = {
147              'picking_policy': 'direct',
148              'order_policy': 'manual',
149              'invoice_quantity': 'order',
150          }
151
152     # Form filling
153     def unlink(self, cr, uid, ids, context=None):
154         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
155         unlink_ids = []
156         for s in sale_orders:
157             if s['state'] in ['draft', 'cancel']:
158                 unlink_ids.append(s['id'])
159             else:
160                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
161
162         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
163
164     def action_view_delivery(self, cr, uid, ids, context=None):
165         '''
166         This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
167         '''
168         mod_obj = self.pool.get('ir.model.data')
169         act_obj = self.pool.get('ir.actions.act_window')
170
171         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
172         id = result and result[1] or False
173         result = act_obj.read(cr, uid, [id], context=context)[0]
174         #compute the number of delivery orders to display
175         pick_ids = []
176         for so in self.browse(cr, uid, ids, context=context):
177             pick_ids += [picking.id for picking in so.picking_ids]
178         #choose the view_mode accordingly
179         if len(pick_ids) > 1:
180             result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
181         else:
182             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
183             result['views'] = [(res and res[1] or False, 'form')]
184             result['res_id'] = pick_ids and pick_ids[0] or False
185         return result
186
187     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
188         picking_obj = self.pool.get('stock.picking')
189         res = super(sale_order,self).action_invoice_create( cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
190         for order in self.browse(cr, uid, ids, context=context):
191             if order.order_policy == 'picking':
192                 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
193         return res
194
195     def action_cancel(self, cr, uid, ids, context=None):
196         if context is None:
197             context = {}
198         sale_order_line_obj = self.pool.get('sale.order.line')
199         proc_obj = self.pool.get('procurement.order')
200         stock_obj = self.pool.get('stock.picking')
201         for sale in self.browse(cr, uid, ids, context=context):
202             for pick in sale.picking_ids:
203                 if pick.state not in ('draft', 'cancel'):
204                     raise osv.except_osv(
205                         _('Cannot cancel sales order!'),
206                         _('You must first cancel all delivery order(s) attached to this sales order.'))
207                 if pick.state == 'cancel':
208                     for mov in pick.move_lines:
209                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
210                         if proc_ids:
211                             proc_obj.signal_button_check(cr, uid, proc_ids)            
212             for r in self.read(cr, uid, ids, ['picking_ids']):
213                 stock_obj.signal_button_cancel(cr, uid, r['picking_ids'])
214         return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
215
216     def action_wait(self, cr, uid, ids, context=None):
217         res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
218         for o in self.browse(cr, uid, ids):
219             noprod = self.test_no_product(cr, uid, o, context)
220             if noprod and o.order_policy=='picking':
221                 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
222         return res
223
224     def procurement_lines_get(self, cr, uid, ids, *args):
225         res = []
226         for order in self.browse(cr, uid, ids, context={}):
227             for line in order.order_line:
228                 if line.procurement_id:
229                     res.append(line.procurement_id.id)
230         return res
231
232     # if mode == 'finished':
233     #   returns True if all lines are done, False otherwise
234     # if mode == 'canceled':
235     #   returns True if there is at least one canceled line, False otherwise
236     def test_state(self, cr, uid, ids, mode, *args):
237         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
238         finished = True
239         canceled = False
240         write_done_ids = []
241         write_cancel_ids = []
242         for order in self.browse(cr, uid, ids, context={}):
243             for line in order.order_line:
244                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
245                     if line.state != 'done':
246                         write_done_ids.append(line.id)
247                 else:
248                     finished = False
249                 if line.procurement_id:
250                     if (line.procurement_id.state == 'cancel'):
251                         canceled = True
252                         if line.state != 'exception':
253                             write_cancel_ids.append(line.id)
254         if write_done_ids:
255             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
256         if write_cancel_ids:
257             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
258
259         if mode == 'finished':
260             return finished
261         elif mode == 'canceled':
262             return canceled
263
264     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
265         return {
266             'name': line.name,
267             'origin': order.name,
268             'date_planned': date_planned,
269             'product_id': line.product_id.id,
270             'product_qty': line.product_uom_qty,
271             'product_uom': line.product_uom.id,
272             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
273                     or line.product_uom_qty,
274             'product_uos': (line.product_uos and line.product_uos.id)\
275                     or line.product_uom.id,
276             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
277             'procure_method': line.type,
278             'move_id': move_id,
279             'company_id': order.company_id.id,
280             'note': line.name,
281         }
282
283     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
284         location_id = order.shop_id.warehouse_id.lot_stock_id.id
285         output_id = order.shop_id.warehouse_id.lot_output_id.id
286         return {
287             'name': line.name,
288             'picking_id': picking_id,
289             'product_id': line.product_id.id,
290             'date': date_planned,
291             'date_expected': date_planned,
292             'product_qty': line.product_uom_qty,
293             'product_uom': line.product_uom.id,
294             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
295             'product_uos': (line.product_uos and line.product_uos.id)\
296                     or line.product_uom.id,
297             'product_packaging': line.product_packaging.id,
298             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
299             'location_id': location_id,
300             'location_dest_id': output_id,
301             'sale_line_id': line.id,
302             'tracking_id': False,
303             'state': 'draft',
304             #'state': 'waiting',
305             'company_id': order.company_id.id,
306             'price_unit': line.product_id.standard_price or 0.0
307         }
308
309     def _prepare_order_picking(self, cr, uid, order, context=None):
310         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
311         return {
312             'name': pick_name,
313             'origin': order.name,
314             'date': order.date_order,
315             'type': 'out',
316             'state': 'auto',
317             'move_type': order.picking_policy,
318             'sale_id': order.id,
319             'partner_id': order.partner_shipping_id.id,
320             'note': order.note,
321             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
322             'company_id': order.company_id.id,
323         }
324
325     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
326         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
327         """
328         Define ship_recreate for process after shipping exception
329         param order: sales order to which the order lines belong
330         param line: sales order line records to procure
331         param move_id: the ID of stock move
332         param proc_id: the ID of procurement
333         """
334         move_obj = self.pool.get('stock.move')
335         if order.state == 'shipping_except':
336             for pick in order.picking_ids:
337                 for move in pick.move_lines:
338                     if move.state == 'cancel':
339                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
340                         if mov_ids:
341                             for mov in move_obj.browse(cr, uid, mov_ids):
342                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
343                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
344                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
345         return True
346
347     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
348         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
349         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
350         return date_planned
351
352     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
353         """Create the required procurements to supply sales order lines, also connecting
354         the procurements to appropriate stock moves in order to bring the goods to the
355         sales order's requested location.
356
357         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
358         a standard outgoing picking will be created to wrap the stock moves, as returned
359         by :meth:`~._prepare_order_picking`.
360
361         Modules that wish to customize the procurements or partition the stock moves over
362         multiple stock pickings may override this method and call ``super()`` with
363         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
364
365         :param browse_record order: sales order to which the order lines belong
366         :param list(browse_record) order_lines: sales order line records to procure
367         :param int picking_id: optional ID of a stock picking to which the created stock moves
368                                will be added. A new picking will be created if ommitted.
369         :return: True
370         """
371         move_obj = self.pool.get('stock.move')
372         picking_obj = self.pool.get('stock.picking')
373         procurement_obj = self.pool.get('procurement.order')
374         proc_ids = []
375
376         for line in order_lines:
377             if line.state == 'done':
378                 continue
379
380             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
381
382             if line.product_id:
383                 if line.product_id.type in ('product', 'consu'):
384                     if not picking_id:
385                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
386                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
387                 else:
388                     # a service has no stock move
389                     move_id = False
390
391                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
392                 proc_ids.append(proc_id)
393                 line.write({'procurement_id': proc_id})
394                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
395
396         if picking_id:
397             picking_obj.signal_button_confirm(cr, uid, [picking_id])
398         procurement_obj.signal_button_confirm(cr, uid, proc_ids)
399
400         val = {}
401         if order.state == 'shipping_except':
402             val['state'] = 'progress'
403             val['shipped'] = False
404
405             if (order.order_policy == 'manual'):
406                 for line in order.order_line:
407                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
408                         val['state'] = 'manual'
409                         break
410         order.write(val)
411         return True
412
413     def action_ship_create(self, cr, uid, ids, context=None):
414         for order in self.browse(cr, uid, ids, context=context):
415             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
416         return True
417
418     def action_ship_end(self, cr, uid, ids, context=None):
419         for order in self.browse(cr, uid, ids, context=context):
420             val = {'shipped': True}
421             if order.state == 'shipping_except':
422                 val['state'] = 'progress'
423                 if (order.order_policy == 'manual'):
424                     for line in order.order_line:
425                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
426                             val['state'] = 'manual'
427                             break
428             for line in order.order_line:
429                 towrite = []
430                 if line.state == 'exception':
431                     towrite.append(line.id)
432                 if towrite:
433                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
434             res = self.write(cr, uid, [order.id], val)
435         return True
436
437     def has_stockable_products(self, cr, uid, ids, *args):
438         for order in self.browse(cr, uid, ids):
439             for order_line in order.order_line:
440                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
441                     return True
442         return False
443
444
445 class sale_order_line(osv.osv):
446
447     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
448         res = {}
449         for line in self.browse(cr, uid, ids, context=context):
450             try:
451                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
452             except:
453                 res[line.id] = 1
454         return res
455
456     _inherit = 'sale.order.line'
457     _columns = { 
458         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
459         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
460         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
461         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
462         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
463         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
464     }
465     _defaults = {
466         'delay': 0.0,
467         'product_packaging': False,
468     }
469
470     def _get_line_qty(self, cr, uid, line, context=None):
471         if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
472             return self.pool.get('procurement.order').quantity_get(cr, uid,
473                    line.procurement_id.id, context=context)
474         else:
475             return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
476
477
478     def _get_line_uom(self, cr, uid, line, context=None):
479         if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
480             return self.pool.get('procurement.order').uom_get(cr, uid,
481                     line.procurement_id.id, context=context)
482         else:
483             return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
484
485     def button_cancel(self, cr, uid, ids, context=None):
486         res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
487         for line in self.browse(cr, uid, ids, context=context):
488             for move_line in line.move_ids:
489                 if move_line.state != 'cancel':
490                     raise osv.except_osv(
491                             _('Cannot cancel sales order line!'),
492                             _('You must first cancel stock moves attached to this sales order line.'))   
493         return res
494
495     def copy_data(self, cr, uid, id, default=None, context=None):
496         if not default:
497             default = {}
498         default.update({'move_ids': []})
499         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
500
501     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
502                                    partner_id=False, packaging=False, flag=False, context=None):
503         if not product:
504             return {'value': {'product_packaging': False}}
505         product_obj = self.pool.get('product.product')
506         product_uom_obj = self.pool.get('product.uom')
507         pack_obj = self.pool.get('product.packaging')
508         warning = {}
509         result = {}
510         warning_msgs = ''
511         if flag:
512             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
513                     product=product, qty=qty, uom=uom, partner_id=partner_id,
514                     packaging=packaging, flag=False, context=context)
515             warning_msgs = res.get('warning') and res['warning']['message']
516
517         products = product_obj.browse(cr, uid, product, context=context)
518         if not products.packaging:
519             packaging = result['product_packaging'] = False
520         elif not packaging and products.packaging and not flag:
521             packaging = products.packaging[0].id
522             result['product_packaging'] = packaging
523
524         if packaging:
525             default_uom = products.uom_id and products.uom_id.id
526             pack = pack_obj.browse(cr, uid, packaging, context=context)
527             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
528 #            qty = qty - qty % q + q
529             if qty and (q and not (qty % q) == 0):
530                 ean = pack.ean or _('(n/a)')
531                 qty_pack = pack.qty
532                 type_ul = pack.ul
533                 if not warning_msgs:
534                     warn_msg = _("You selected a quantity of %d Units.\n"
535                                 "But it's not compatible with the selected packaging.\n"
536                                 "Here is a proposition of quantities according to the packaging:\n"
537                                 "EAN: %s Quantity: %s Type of ul: %s") % \
538                                     (qty, ean, qty_pack, type_ul.name)
539                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
540                 warning = {
541                        'title': _('Configuration Error!'),
542                        'message': warning_msgs
543                 }
544             result['product_uom_qty'] = qty
545
546         return {'value': result, 'warning': warning}
547
548     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
549             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
550             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
551         context = context or {}
552         product_uom_obj = self.pool.get('product.uom')
553         partner_obj = self.pool.get('res.partner')
554         product_obj = self.pool.get('product.product')
555         warning = {}
556         res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
557             uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
558             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
559
560         if not product:
561             res['value'].update({'product_packaging': False})
562             return res
563
564         #update of result obtained in super function
565         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
566         res['value'].update(res_packing.get('value', {}))
567         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
568         product_obj = product_obj.browse(cr, uid, product, context=context)
569         res['value']['delay'] = (product_obj.sale_delay or 0.0)
570         res['value']['type'] = product_obj.procure_method
571
572         #check if product is available, and if not: raise an error
573         uom2 = False
574         if uom:
575             uom2 = product_uom_obj.browse(cr, uid, uom)
576             if product_obj.uom_id.category_id.id != uom2.category_id.id:
577                 uom = False
578         if not uom2:
579             uom2 = product_obj.uom_id
580         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
581         if (product_obj.type=='product') and int(compare_qty) == -1 \
582           and (product_obj.procure_method=='make_to_stock'):
583             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
584                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
585                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
586                      max(0,product_obj.qty_available), product_obj.uom_id.name)
587             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
588
589         #update of warning messages
590         if warning_msgs:
591             warning = {
592                        'title': _('Configuration Error!'),
593                        'message' : warning_msgs
594                     }
595         res.update({'warning': warning})
596         return res
597
598
599 class sale_advance_payment_inv(osv.osv_memory):
600     _inherit = "sale.advance.payment.inv"
601
602     def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
603         result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
604         sale_obj = self.pool.get('sale.order')
605         sale_line_obj = self.pool.get('sale.order.line')
606         wizard = self.browse(cr, uid, [result], context)
607         sale = sale_obj.browse(cr, uid, sale_id, context=context)
608         if sale.order_policy == 'postpaid':
609             raise osv.except_osv(
610                 _('Error!'),
611                 _("You cannot make an advance on a sales order \
612                      that is defined as 'Automatic Invoice after delivery'."))
613
614         # If invoice on picking: add the cost on the SO
615         # If not, the advance will be deduced when generating the final invoice
616         line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
617         line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
618         if sale.order_policy == 'picking':
619             vals = {
620                 'order_id': sale.id,
621                 'name': line_name,
622                 'price_unit': -inv_amount,
623                 'product_uom_qty': wizard.qtty or 1.0,
624                 'product_uos_qty': wizard.qtty or 1.0,
625                 'product_uos': res.get('uos_id', False),
626                 'product_uom': res.get('uom_id', False),
627                 'product_id': wizard.product_id.id or False,
628                 'discount': False,
629                 'tax_id': line_tax,
630             }
631             sale_line_obj.create(cr, uid, vals, context=context)
632         return result