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.tools.translate import _
28 from openerp import SUPERUSER_ID
30 class sale_shop(osv.osv):
31 _inherit = "sale.shop"
33 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
37 class sale_order(osv.osv):
38 _inherit = "sale.order"
40 def copy(self, cr, uid, id, default=None, context=None):
47 return super(sale_order, self).copy(cr, uid, id, default, context=context)
49 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
53 if policy == 'prepaid':
55 elif policy == 'picking':
56 inv_qty = 'procurement'
57 return {'value': {'invoice_quantity': inv_qty}}
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)
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)
77 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
83 tmp[id] = {'picked': 0.0, 'total': 0.0}
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
89 stock_picking p on (p.id=m.picking_id)
91 procurement_order mp on (mp.move_id=m.id)
93 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
95 for item in cr.dictfetchall():
96 if item['move_state'] == 'cancel':
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
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
108 for order in self.browse(cr, uid, ids, context=context):
110 res[order.id] = 100.0
112 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
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'),
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)]}),
148 'picking_policy': 'direct',
149 'order_policy': 'manual',
150 'invoice_quantity': 'order',
154 def unlink(self, cr, uid, ids, context=None):
155 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
157 for s in sale_orders:
158 if s['state'] in ['draft', 'cancel']:
159 unlink_ids.append(s['id'])
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.'))
163 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
165 def action_view_delivery(self, cr, uid, ids, context=None):
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.
169 mod_obj = self.pool.get('ir.model.data')
170 act_obj = self.pool.get('ir.actions.act_window')
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
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))+"])]"
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
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'})
196 def action_cancel(self, cr, uid, ids, context=None):
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)])
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)
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)
225 def procurement_lines_get(self, cr, uid, ids, *args):
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)
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.
238 :param str userdate: date string in in user time zone
239 :return: UTC datetime string for server-side use
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']
246 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
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)
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")
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)
273 if line.procurement_id:
274 if (line.procurement_id.state == 'cancel'):
276 if line.state != 'exception':
277 write_cancel_ids.append(line.id)
279 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
281 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
283 if mode == 'finished':
285 elif mode == 'canceled':
288 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
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,
303 'company_id': order.company_id.id,
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
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,
329 'company_id': order.company_id.id,
330 'price_unit': line.product_id.standard_price or 0.0
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')
337 'origin': order.name,
338 'date': self.date_to_datetime(cr, uid, order.date_order, context),
341 'move_type': order.picking_policy,
343 'partner_id': order.partner_shipping_id.id,
345 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
346 'company_id': order.company_id.id,
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)
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
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)])
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})
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)
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.
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`.
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.
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.
396 move_obj = self.pool.get('stock.move')
397 picking_obj = self.pool.get('stock.picking')
398 procurement_obj = self.pool.get('procurement.order')
401 for line in order_lines:
402 if line.state == 'done':
405 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
408 if line.product_id.type in ('product', 'consu'):
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))
413 # a service has no stock move
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)
422 picking_obj.signal_button_confirm(cr, uid, [picking_id])
423 procurement_obj.signal_button_confirm(cr, uid, proc_ids)
426 if order.state == 'shipping_except':
427 val['state'] = 'progress'
428 val['shipped'] = False
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'
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)
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'
453 for line in order.order_line:
455 if line.state == 'exception':
456 towrite.append(line.id)
458 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
459 res = self.write(cr, uid, [order.id], val)
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'):
470 class sale_order_line(osv.osv):
472 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
474 for line in self.browse(cr, uid, ids, context=context):
476 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
481 _inherit = 'sale.order.line'
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'),
492 'product_packaging': False,
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)
500 return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
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)
508 return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
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.'))
520 def copy_data(self, cr, uid, id, default=None, context=None):
523 default.update({'move_ids': []})
524 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
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):
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')
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']
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
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)')
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"
566 'title': _('Configuration Error!'),
567 'message': warning_msgs
569 result['product_uom_qty'] = qty
571 return {'value': result, 'warning': warning}
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')
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)
586 res['value'].update({'product_packaging': False})
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
597 #check if product is available, and if not: raise an error
600 uom2 = product_uom_obj.browse(cr, uid, uom)
601 if product_obj.uom_id.category_id.id != uom2.category_id.id:
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"
614 #update of warning messages
617 'title': _('Configuration Error!'),
618 'message' : warning_msgs
620 res.update({'warning': warning})
624 class sale_advance_payment_inv(osv.osv_memory):
625 _inherit = "sale.advance.payment.inv"
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)
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':
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,
651 sale_line_obj.create(cr, uid, vals, context=context)