1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
12 # This program is distributed in the hope that it will be useful,
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.
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/>.
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 _
29 from openerp import SUPERUSER_ID
31 class sale_shop(osv.osv):
32 _inherit = "sale.shop"
34 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 class sale_order(osv.osv):
40 _inherit = "sale.order"
42 def copy(self, cr, uid, id, default=None, context=None):
49 return super(sale_order, self).copy(cr, uid, id, default, context=context)
51 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
55 if policy == 'prepaid':
57 elif policy == 'picking':
58 inv_qty = 'procurement'
59 return {'value': {'invoice_quantity': inv_qty}}
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)
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)
79 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
85 tmp[id] = {'picked': 0.0, 'total': 0.0}
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
91 stock_picking p on (p.id=m.picking_id)
93 procurement_order mp on (mp.move_id=m.id)
95 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
97 for item in cr.dictfetchall():
98 if item['move_state'] == 'cancel':
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
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
110 for order in self.browse(cr, uid, ids, context=context):
112 res[order.id] = 100.0
114 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
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'),
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)]}),
150 'picking_policy': 'direct',
151 'order_policy': 'manual',
152 'invoice_quantity': 'order',
156 def unlink(self, cr, uid, ids, context=None):
157 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
159 for s in sale_orders:
160 if s['state'] in ['draft', 'cancel']:
161 unlink_ids.append(s['id'])
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.'))
165 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
167 def action_view_delivery(self, cr, uid, ids, context=None):
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.
171 mod_obj = self.pool.get('ir.model.data')
172 act_obj = self.pool.get('ir.actions.act_window')
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
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))+"])]"
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
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'})
198 def action_cancel(self, cr, uid, ids, context=None):
199 wf_service = netsvc.LocalService("workflow")
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)])
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)
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)
229 def procurement_lines_get(self, cr, uid, ids, *args):
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)
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.
242 :param str userdate: date string in in user time zone
243 :return: UTC datetime string for server-side use
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']
250 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
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)
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")
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)
277 if line.procurement_id:
278 if (line.procurement_id.state == 'cancel'):
280 if line.state != 'exception':
281 write_cancel_ids.append(line.id)
283 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
285 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
287 if mode == 'finished':
289 elif mode == 'canceled':
292 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
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,
307 'company_id': order.company_id.id,
309 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
312 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
313 location_id = order.shop_id.warehouse_id.lot_stock_id.id
314 output_id = order.shop_id.warehouse_id.lot_output_id.id
317 'picking_id': picking_id,
318 'product_id': line.product_id.id,
319 'date': date_planned,
320 'date_expected': date_planned,
321 'product_qty': line.product_uom_qty,
322 'product_uom': line.product_uom.id,
323 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
324 'product_uos': (line.product_uos and line.product_uos.id)\
325 or line.product_uom.id,
326 'product_packaging': line.product_packaging.id,
327 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
328 'location_id': location_id,
329 'location_dest_id': output_id,
330 'sale_line_id': line.id,
331 'tracking_id': False,
334 'company_id': order.company_id.id,
335 'price_unit': line.product_id.standard_price or 0.0
338 def _prepare_order_picking(self, cr, uid, order, context=None):
339 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
342 'origin': order.name,
343 'date': self.date_to_datetime(cr, uid, order.date_order, context),
346 'move_type': order.picking_policy,
348 'partner_id': order.partner_shipping_id.id,
350 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
351 'company_id': order.company_id.id,
354 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
355 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
357 Define ship_recreate for process after shipping exception
358 param order: sales order to which the order lines belong
359 param line: sales order line records to procure
360 param move_id: the ID of stock move
361 param proc_id: the ID of procurement
363 move_obj = self.pool.get('stock.move')
364 if order.state == 'shipping_except':
365 for pick in order.picking_ids:
366 for move in pick.move_lines:
367 if move.state == 'cancel':
368 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
370 for mov in move_obj.browse(cr, uid, mov_ids):
371 # 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?
372 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
373 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
376 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
377 start_date = self.date_to_datetime(cr, uid, start_date, context)
378 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
379 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
382 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
383 """Create the required procurements to supply sales order lines, also connecting
384 the procurements to appropriate stock moves in order to bring the goods to the
385 sales order's requested location.
387 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
388 a standard outgoing picking will be created to wrap the stock moves, as returned
389 by :meth:`~._prepare_order_picking`.
391 Modules that wish to customize the procurements or partition the stock moves over
392 multiple stock pickings may override this method and call ``super()`` with
393 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
395 :param browse_record order: sales order to which the order lines belong
396 :param list(browse_record) order_lines: sales order line records to procure
397 :param int picking_id: optional ID of a stock picking to which the created stock moves
398 will be added. A new picking will be created if ommitted.
401 move_obj = self.pool.get('stock.move')
402 picking_obj = self.pool.get('stock.picking')
403 procurement_obj = self.pool.get('procurement.order')
406 for line in order_lines:
407 if line.state == 'done':
410 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
413 if line.product_id.type in ('product', 'consu'):
415 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
416 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
418 # a service has no stock move
421 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
422 proc_ids.append(proc_id)
423 line.write({'procurement_id': proc_id})
424 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
426 wf_service = netsvc.LocalService("workflow")
428 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
429 for proc_id in proc_ids:
430 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
433 if order.state == 'shipping_except':
434 val['state'] = 'progress'
435 val['shipped'] = False
437 if (order.order_policy == 'manual'):
438 for line in order.order_line:
439 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
440 val['state'] = 'manual'
445 def action_ship_create(self, cr, uid, ids, context=None):
446 for order in self.browse(cr, uid, ids, context=context):
447 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
450 def action_ship_end(self, cr, uid, ids, context=None):
451 for order in self.browse(cr, uid, ids, context=context):
452 val = {'shipped': True}
453 if order.state == 'shipping_except':
454 val['state'] = 'progress'
455 if (order.order_policy == 'manual'):
456 for line in order.order_line:
457 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
458 val['state'] = 'manual'
460 for line in order.order_line:
462 if line.state == 'exception':
463 towrite.append(line.id)
465 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
466 res = self.write(cr, uid, [order.id], val)
469 def has_stockable_products(self, cr, uid, ids, *args):
470 for order in self.browse(cr, uid, ids):
471 for order_line in order.order_line:
472 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
477 class sale_order_line(osv.osv):
479 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
481 for line in self.browse(cr, uid, ids, context=context):
483 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
488 _inherit = 'sale.order.line'
490 '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)]}),
491 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
492 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
493 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
494 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
495 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
499 'product_packaging': False,
502 def _get_line_qty(self, cr, uid, line, context=None):
503 if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
504 return self.pool.get('procurement.order').quantity_get(cr, uid,
505 line.procurement_id.id, context=context)
507 return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
510 def _get_line_uom(self, cr, uid, line, context=None):
511 if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
512 return self.pool.get('procurement.order').uom_get(cr, uid,
513 line.procurement_id.id, context=context)
515 return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
517 def button_cancel(self, cr, uid, ids, context=None):
518 res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
519 for line in self.browse(cr, uid, ids, context=context):
520 for move_line in line.move_ids:
521 if move_line.state != 'cancel':
522 raise osv.except_osv(
523 _('Cannot cancel sales order line!'),
524 _('You must first cancel stock moves attached to this sales order line.'))
527 def copy_data(self, cr, uid, id, default=None, context=None):
530 default.update({'move_ids': []})
531 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
533 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
534 partner_id=False, packaging=False, flag=False, context=None):
536 return {'value': {'product_packaging': False}}
537 product_obj = self.pool.get('product.product')
538 product_uom_obj = self.pool.get('product.uom')
539 pack_obj = self.pool.get('product.packaging')
544 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
545 product=product, qty=qty, uom=uom, partner_id=partner_id,
546 packaging=packaging, flag=False, context=context)
547 warning_msgs = res.get('warning') and res['warning']['message']
549 products = product_obj.browse(cr, uid, product, context=context)
550 if not products.packaging:
551 packaging = result['product_packaging'] = False
552 elif not packaging and products.packaging and not flag:
553 packaging = products.packaging[0].id
554 result['product_packaging'] = packaging
557 default_uom = products.uom_id and products.uom_id.id
558 pack = pack_obj.browse(cr, uid, packaging, context=context)
559 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
560 # qty = qty - qty % q + q
561 if qty and (q and not (qty % q) == 0):
562 ean = pack.ean or _('(n/a)')
566 warn_msg = _("You selected a quantity of %d Units.\n"
567 "But it's not compatible with the selected packaging.\n"
568 "Here is a proposition of quantities according to the packaging:\n"
569 "EAN: %s Quantity: %s Type of ul: %s") % \
570 (qty, ean, qty_pack, type_ul.name)
571 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
573 'title': _('Configuration Error!'),
574 'message': warning_msgs
576 result['product_uom_qty'] = qty
578 return {'value': result, 'warning': warning}
580 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
581 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
582 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
583 context = context or {}
584 product_uom_obj = self.pool.get('product.uom')
585 partner_obj = self.pool.get('res.partner')
586 product_obj = self.pool.get('product.product')
588 res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
589 uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
590 lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
593 res['value'].update({'product_packaging': False})
596 #update of result obtained in super function
597 product_obj = product_obj.browse(cr, uid, product, context=context)
598 res['value']['delay'] = (product_obj.sale_delay or 0.0)
599 res['value']['type'] = product_obj.procure_method
601 #check if product is available, and if not: raise an error
604 uom2 = product_uom_obj.browse(cr, uid, uom)
605 if product_obj.uom_id.category_id.id != uom2.category_id.id:
608 uom2 = product_obj.uom_id
610 # Calling product_packaging_change function after updating UoM
611 res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
612 res['value'].update(res_packing.get('value', {}))
613 warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
614 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
615 if (product_obj.type=='product') and int(compare_qty) == -1 \
616 and (product_obj.procure_method=='make_to_stock'):
617 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
618 (qty, uom2 and uom2.name or product_obj.uom_id.name,
619 max(0,product_obj.virtual_available), product_obj.uom_id.name,
620 max(0,product_obj.qty_available), product_obj.uom_id.name)
621 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
623 #update of warning messages
626 'title': _('Configuration Error!'),
627 'message' : warning_msgs
629 res.update({'warning': warning})
633 class sale_advance_payment_inv(osv.osv_memory):
634 _inherit = "sale.advance.payment.inv"
636 def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
637 result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
638 sale_obj = self.pool.get('sale.order')
639 sale_line_obj = self.pool.get('sale.order.line')
640 wizard = self.browse(cr, uid, [result], context)
641 sale = sale_obj.browse(cr, uid, sale_id, context=context)
643 # If invoice on picking: add the cost on the SO
644 # If not, the advance will be deduced when generating the final invoice
645 line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
646 line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
647 if sale.order_policy == 'picking':
651 'price_unit': -inv_amount,
652 'product_uom_qty': wizard.qtty or 1.0,
653 'product_uos_qty': wizard.qtty or 1.0,
654 'product_uos': res.get('uos_id', False),
655 'product_uom': res.get('uom_id', False),
656 'product_id': wizard.product_id.id or False,
660 sale_line_obj.create(cr, uid, vals, context=context)