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