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