11f3e0225fe5fbbd1eb380c4eb78daa1667beabf
[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         }
310
311     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
312         location_id = order.shop_id.warehouse_id.lot_stock_id.id
313         output_id = order.shop_id.warehouse_id.lot_output_id.id
314         return {
315             'name': line.name,
316             'picking_id': picking_id,
317             'product_id': line.product_id.id,
318             'date': date_planned,
319             'date_expected': date_planned,
320             'product_qty': line.product_uom_qty,
321             'product_uom': line.product_uom.id,
322             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
323             'product_uos': (line.product_uos and line.product_uos.id)\
324                     or line.product_uom.id,
325             'product_packaging': line.product_packaging.id,
326             'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
327             'location_id': location_id,
328             'location_dest_id': output_id,
329             'sale_line_id': line.id,
330             'tracking_id': False,
331             'state': 'draft',
332             #'state': 'waiting',
333             'company_id': order.company_id.id,
334             'price_unit': line.product_id.standard_price or 0.0
335         }
336
337     def _prepare_order_picking(self, cr, uid, order, context=None):
338         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
339         return {
340             'name': pick_name,
341             'origin': order.name,
342             'date': self.date_to_datetime(cr, uid, order.date_order, context),
343             'type': 'out',
344             'state': 'auto',
345             'move_type': order.picking_policy,
346             'sale_id': order.id,
347             'partner_id': order.partner_shipping_id.id,
348             'note': order.note,
349             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
350             'company_id': order.company_id.id,
351         }
352
353     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
354         """
355         Define ship_recreate for process after shipping exception
356         param order: sales order to which the order lines belong
357         param line: sales order line records to procure
358         param move_id: the ID of stock move
359         param proc_id: the ID of procurement
360         """
361         move_obj = self.pool.get('stock.move')
362         proc_obj = self.pool.get('procurement.order')
363         if move_id and order.state == 'shipping_except':
364             current_move = move_obj.browse(cr, uid, move_id)
365             moves = []
366             for picking in order.picking_ids:
367                 if picking.id != current_move.picking_id.id and picking.state != 'cancel':
368                     moves.extend(move for move in picking.move_lines if move.state != 'cancel' and move.sale_line_id.id == line.id)
369             if moves:
370                 product_qty = current_move.product_qty
371                 product_uos_qty = current_move.product_uos_qty
372                 for move in moves:
373                     product_qty -= move.product_qty
374                     product_uos_qty -= move.product_uos_qty
375                 if product_qty > 0 or product_uos_qty > 0:
376                     move_obj.write(cr, uid, [move_id], {'product_qty': product_qty, 'product_uos_qty': product_uos_qty})
377                     proc_obj.write(cr, uid, [proc_id], {'product_qty': product_qty, 'product_uos_qty': product_uos_qty})
378                 else:
379                     current_move.unlink()
380                     proc_obj.unlink(cr, uid, [proc_id])
381         return True
382
383     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
384         start_date = self.date_to_datetime(cr, uid, start_date, context)
385         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
386         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
387         return date_planned
388
389     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
390         """Create the required procurements to supply sales order lines, also connecting
391         the procurements to appropriate stock moves in order to bring the goods to the
392         sales order's requested location.
393
394         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
395         a standard outgoing picking will be created to wrap the stock moves, as returned
396         by :meth:`~._prepare_order_picking`.
397
398         Modules that wish to customize the procurements or partition the stock moves over
399         multiple stock pickings may override this method and call ``super()`` with
400         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
401
402         :param browse_record order: sales order to which the order lines belong
403         :param list(browse_record) order_lines: sales order line records to procure
404         :param int picking_id: optional ID of a stock picking to which the created stock moves
405                                will be added. A new picking will be created if ommitted.
406         :return: True
407         """
408         move_obj = self.pool.get('stock.move')
409         picking_obj = self.pool.get('stock.picking')
410         procurement_obj = self.pool.get('procurement.order')
411         proc_ids = []
412
413         for line in order_lines:
414             if line.state == 'done':
415                 continue
416
417             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
418
419             if line.product_id:
420                 if line.product_id.type in ('product', 'consu'):
421                     if not picking_id:
422                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
423                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
424                 else:
425                     # a service has no stock move
426                     move_id = False
427
428                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
429                 proc_ids.append(proc_id)
430                 line.write({'procurement_id': proc_id})
431                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
432
433         wf_service = netsvc.LocalService("workflow")
434         if picking_id:
435             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
436         for proc_id in proc_ids:
437             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
438
439         val = {}
440         if order.state == 'shipping_except':
441             val['state'] = 'progress'
442             val['shipped'] = False
443
444             if (order.order_policy == 'manual'):
445                 for line in order.order_line:
446                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
447                         val['state'] = 'manual'
448                         break
449         order.write(val)
450         return True
451
452     def action_ship_create(self, cr, uid, ids, context=None):
453         for order in self.browse(cr, uid, ids, context=context):
454             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
455         return True
456
457     def action_ship_end(self, cr, uid, ids, context=None):
458         for order in self.browse(cr, uid, ids, context=context):
459             val = {'shipped': True}
460             if order.state == 'shipping_except':
461                 val['state'] = 'progress'
462                 if (order.order_policy == 'manual'):
463                     for line in order.order_line:
464                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
465                             val['state'] = 'manual'
466                             break
467             for line in order.order_line:
468                 towrite = []
469                 if line.state == 'exception':
470                     towrite.append(line.id)
471                 if towrite:
472                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
473             res = self.write(cr, uid, [order.id], val)
474         return True
475
476     def has_stockable_products(self, cr, uid, ids, *args):
477         for order in self.browse(cr, uid, ids):
478             for order_line in order.order_line:
479                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
480                     return True
481         return False
482
483
484 class sale_order_line(osv.osv):
485
486     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
487         res = {}
488         for line in self.browse(cr, uid, ids, context=context):
489             try:
490                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
491             except:
492                 res[line.id] = 1
493         return res
494
495     _inherit = 'sale.order.line'
496     _columns = { 
497         '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)]}),
498         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
499         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
500         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
501         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
502         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
503     }
504     _defaults = {
505         'delay': 0.0,
506         'product_packaging': False,
507     }
508
509     def _get_line_qty(self, cr, uid, line, context=None):
510         if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
511             return self.pool.get('procurement.order').quantity_get(cr, uid,
512                    line.procurement_id.id, context=context)
513         else:
514             return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
515
516
517     def _get_line_uom(self, cr, uid, line, context=None):
518         if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
519             return self.pool.get('procurement.order').uom_get(cr, uid,
520                     line.procurement_id.id, context=context)
521         else:
522             return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
523
524     def button_cancel(self, cr, uid, ids, context=None):
525         res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
526         for line in self.browse(cr, uid, ids, context=context):
527             for move_line in line.move_ids:
528                 if move_line.state != 'cancel':
529                     raise osv.except_osv(
530                             _('Cannot cancel sales order line!'),
531                             _('You must first cancel stock moves attached to this sales order line.'))   
532         return res
533
534     def copy_data(self, cr, uid, id, default=None, context=None):
535         if not default:
536             default = {}
537         default.update({'move_ids': []})
538         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
539
540     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
541                                    partner_id=False, packaging=False, flag=False, context=None):
542         if not product:
543             return {'value': {'product_packaging': False}}
544         product_obj = self.pool.get('product.product')
545         product_uom_obj = self.pool.get('product.uom')
546         pack_obj = self.pool.get('product.packaging')
547         warning = {}
548         result = {}
549         warning_msgs = ''
550         if flag:
551             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
552                     product=product, qty=qty, uom=uom, partner_id=partner_id,
553                     packaging=packaging, flag=False, context=context)
554             warning_msgs = res.get('warning') and res['warning']['message']
555
556         products = product_obj.browse(cr, uid, product, context=context)
557         if not products.packaging:
558             packaging = result['product_packaging'] = False
559         elif not packaging and products.packaging and not flag:
560             packaging = products.packaging[0].id
561             result['product_packaging'] = packaging
562
563         if packaging:
564             default_uom = products.uom_id and products.uom_id.id
565             pack = pack_obj.browse(cr, uid, packaging, context=context)
566             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
567 #            qty = qty - qty % q + q
568             if qty and (q and not (qty % q) == 0):
569                 ean = pack.ean or _('(n/a)')
570                 qty_pack = pack.qty
571                 type_ul = pack.ul
572                 if not warning_msgs:
573                     warn_msg = _("You selected a quantity of %d Units.\n"
574                                 "But it's not compatible with the selected packaging.\n"
575                                 "Here is a proposition of quantities according to the packaging:\n"
576                                 "EAN: %s Quantity: %s Type of ul: %s") % \
577                                     (qty, ean, qty_pack, type_ul.name)
578                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
579                 warning = {
580                        'title': _('Configuration Error!'),
581                        'message': warning_msgs
582                 }
583             result['product_uom_qty'] = qty
584
585         return {'value': result, 'warning': warning}
586
587     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
588             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
589             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
590         context = context or {}
591         product_uom_obj = self.pool.get('product.uom')
592         partner_obj = self.pool.get('res.partner')
593         product_obj = self.pool.get('product.product')
594         warning = {}
595         res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
596             uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
597             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
598
599         if not product:
600             res['value'].update({'product_packaging': False})
601             return res
602
603         #update of result obtained in super function
604         product_obj = product_obj.browse(cr, uid, product, context=context)
605         res['value']['delay'] = (product_obj.sale_delay or 0.0)
606         res['value']['type'] = product_obj.procure_method
607
608         #check if product is available, and if not: raise an error
609         uom2 = False
610         if uom:
611             uom2 = product_uom_obj.browse(cr, uid, uom, context=context)
612             if product_obj.uom_id.category_id.id != uom2.category_id.id:
613                 uom = False
614         if not uom2:
615             uom2 = product_obj.uom_id
616
617         # Calling product_packaging_change function after updating UoM
618         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
619         res['value'].update(res_packing.get('value', {}))
620         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
621         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
622         if (product_obj.type=='product') and int(compare_qty) == -1 \
623           and (product_obj.procure_method=='make_to_stock'):
624             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
625                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
626                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
627                      max(0,product_obj.qty_available), product_obj.uom_id.name)
628             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
629
630         #update of warning messages
631         if warning_msgs:
632             warning = {
633                        'title': _('Configuration Error!'),
634                        'message' : warning_msgs
635                     }
636         res.update({'warning': warning})
637         return res
638
639
640 class sale_advance_payment_inv(osv.osv_memory):
641     _inherit = "sale.advance.payment.inv"
642
643     def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
644         result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
645         sale_obj = self.pool.get('sale.order')
646         sale_line_obj = self.pool.get('sale.order.line')
647         wizard = self.browse(cr, uid, [result], context)
648         sale = sale_obj.browse(cr, uid, sale_id, context=context)
649
650         # If invoice on picking: add the cost on the SO
651         # If not, the advance will be deduced when generating the final invoice
652         line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
653         line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
654         if sale.order_policy == 'picking':
655             vals = {
656                 'order_id': sale.id,
657                 'name': line_name,
658                 'price_unit': -inv_amount,
659                 'product_uom_qty': wizard.qtty or 1.0,
660                 'product_uos_qty': wizard.qtty or 1.0,
661                 'product_uos': res.get('uos_id', False),
662                 'product_uom': res.get('uom_id', False),
663                 'product_id': wizard.product_id.id or False,
664                 'discount': False,
665                 'tax_id': line_tax,
666             }
667             sale_line_obj.create(cr, uid, vals, context=context)
668         return result