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