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