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