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