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_order(osv.osv):
31 _inherit = "sale.order"
33 def copy(self, cr, uid, id, default=None, context=None):
40 return super(sale_order, self).copy(cr, uid, id, default, context=context)
42 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
46 if policy == 'prepaid':
48 elif policy == 'picking':
49 inv_qty = 'procurement'
50 return {'value': {'invoice_quantity': inv_qty}}
52 def write(self, cr, uid, ids, vals, context=None):
53 if vals.get('order_policy', False):
54 if vals['order_policy'] == 'prepaid':
55 vals.update({'invoice_quantity': 'order'})
56 elif vals['order_policy'] == 'picking':
57 vals.update({'invoice_quantity': 'procurement'})
58 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
60 def create(self, cr, uid, vals, context=None):
61 if vals.get('order_policy', False):
62 if vals['order_policy'] == 'prepaid':
63 vals.update({'invoice_quantity': 'order'})
64 if vals['order_policy'] == 'picking':
65 vals.update({'invoice_quantity': 'procurement'})
66 order = super(sale_order, self).create(cr, uid, vals, context=context)
69 def _get_default_warehouse(self, cr, uid, context=None):
70 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
71 warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
73 raise osv.except_osv(_('Error!'), _('There is no warehouse defined for current company.'))
74 return warehouse_ids[0]
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 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
144 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on',
145 help="The sales order will automatically create the invoice proposition (draft invoice).\
146 You have to choose if you want your invoice based on ordered ", required=True, readonly=True, states={'draft': [('readonly', False)]}),
149 'warehouse_id': _get_default_warehouse,
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 onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
170 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
171 if warehouse.company_id:
172 val['company_id'] = warehouse.company_id.id
173 return {'value': val}
175 def action_view_delivery(self, cr, uid, ids, context=None):
177 This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
179 mod_obj = self.pool.get('ir.model.data')
180 act_obj = self.pool.get('ir.actions.act_window')
182 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
183 id = result and result[1] or False
184 result = act_obj.read(cr, uid, [id], context=context)[0]
185 #compute the number of delivery orders to display
187 for so in self.browse(cr, uid, ids, context=context):
188 pick_ids += [picking.id for picking in so.picking_ids]
189 #choose the view_mode accordingly
190 if len(pick_ids) > 1:
191 result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
193 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
194 result['views'] = [(res and res[1] or False, 'form')]
195 result['res_id'] = pick_ids and pick_ids[0] or False
198 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
199 picking_obj = self.pool.get('stock.picking')
200 res = super(sale_order,self).action_invoice_create( cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
201 for order in self.browse(cr, uid, ids, context=context):
202 if order.order_policy == 'picking':
203 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
206 def action_cancel(self, cr, uid, ids, context=None):
209 sale_order_line_obj = self.pool.get('sale.order.line')
210 proc_obj = self.pool.get('procurement.order')
211 stock_obj = self.pool.get('stock.picking')
212 for sale in self.browse(cr, uid, ids, context=context):
213 for pick in sale.picking_ids:
214 if pick.state not in ('draft', 'cancel'):
215 raise osv.except_osv(
216 _('Cannot cancel sales order!'),
217 _('You must first cancel all delivery order(s) attached to this sales order.'))
218 if pick.state == 'cancel':
219 for mov in pick.move_lines:
220 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
222 proc_obj.signal_button_check(cr, uid, proc_ids)
223 for r in self.read(cr, uid, ids, ['picking_ids']):
224 stock_obj.signal_button_cancel(cr, uid, r['picking_ids'])
225 return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
227 def action_wait(self, cr, uid, ids, context=None):
228 res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
229 for o in self.browse(cr, uid, ids):
230 noprod = self.test_no_product(cr, uid, o, context)
231 if noprod and o.order_policy=='picking':
232 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
235 def procurement_lines_get(self, cr, uid, ids, *args):
237 for order in self.browse(cr, uid, ids, context={}):
238 for line in order.order_line:
239 if line.procurement_id:
240 res.append(line.procurement_id.id)
243 def date_to_datetime(self, cr, uid, userdate, context=None):
244 """ Convert date values expressed in user's timezone to
245 server-side UTC timestamp, assuming a default arbitrary
246 time of 12:00 AM - because a time is needed.
248 :param str userdate: date string in in user time zone
249 :return: UTC datetime string for server-side use
251 # TODO: move to fields.datetime in server after 7.0
252 user_date = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT)
253 if context and context.get('tz'):
254 tz_name = context['tz']
256 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
258 utc = pytz.timezone('UTC')
259 context_tz = pytz.timezone(tz_name)
260 user_datetime = user_date + relativedelta(hours=12.0)
261 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
262 user_datetime = local_timestamp.astimezone(utc)
263 return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
264 return user_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
266 # if mode == 'finished':
267 # returns True if all lines are done, False otherwise
268 # if mode == 'canceled':
269 # returns True if there is at least one canceled line, False otherwise
270 def test_state(self, cr, uid, ids, mode, *args):
271 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
275 write_cancel_ids = []
276 for order in self.browse(cr, uid, ids, context={}):
277 for line in order.order_line:
278 if (not line.procurement_id) or (line.procurement_id.state=='done'):
279 if line.state != 'done':
280 write_done_ids.append(line.id)
283 if line.procurement_id:
284 if (line.procurement_id.state == 'cancel'):
286 if line.state != 'exception':
287 write_cancel_ids.append(line.id)
289 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
291 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
293 if mode == 'finished':
295 elif mode == 'canceled':
298 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
301 'origin': order.name,
302 'date_planned': date_planned,
303 'product_id': line.product_id.id,
304 'product_qty': line.product_uom_qty,
305 'product_uom': line.product_uom.id,
306 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
307 or line.product_uom_qty,
308 'product_uos': (line.product_uos and line.product_uos.id)\
309 or line.product_uom.id,
310 'location_id': order.warehouse_id.lot_stock_id.id,
311 'procure_method': line.type,
313 'company_id': order.company_id.id,
315 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
318 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
319 location_id = order.warehouse_id.lot_stock_id.id
320 output_id = order.warehouse_id.lot_output_id.id
323 'picking_id': picking_id,
324 'product_id': line.product_id.id,
325 'date': date_planned,
326 'date_expected': date_planned,
327 'product_qty': line.product_uom_qty,
328 'product_uom': line.product_uom.id,
329 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
330 'product_uos': (line.product_uos and line.product_uos.id)\
331 or line.product_uom.id,
332 'product_packaging': line.product_packaging.id,
333 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
334 'location_id': location_id,
335 'location_dest_id': output_id,
336 'sale_line_id': line.id,
337 'tracking_id': False,
340 'company_id': order.company_id.id,
341 'price_unit': line.product_id.standard_price or 0.0
344 def _prepare_order_picking(self, cr, uid, order, context=None):
345 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
348 'origin': order.name,
349 'date': self.date_to_datetime(cr, uid, order.date_order, context),
352 'move_type': order.picking_policy,
354 'partner_id': order.partner_shipping_id.id,
356 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
357 'company_id': order.company_id.id,
360 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
361 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
363 Define ship_recreate for process after shipping exception
364 param order: sales order to which the order lines belong
365 param line: sales order line records to procure
366 param move_id: the ID of stock move
367 param proc_id: the ID of procurement
369 move_obj = self.pool.get('stock.move')
370 if order.state == 'shipping_except':
371 for pick in order.picking_ids:
372 for move in pick.move_lines:
373 if move.state == 'cancel':
374 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
376 for mov in move_obj.browse(cr, uid, mov_ids):
377 # 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?
378 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
379 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
382 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
383 start_date = self.date_to_datetime(cr, uid, start_date, context)
384 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
385 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
388 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
389 """Create the required procurements to supply sales order lines, also connecting
390 the procurements to appropriate stock moves in order to bring the goods to the
391 sales order's requested location.
393 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
394 a standard outgoing picking will be created to wrap the stock moves, as returned
395 by :meth:`~._prepare_order_picking`.
397 Modules that wish to customize the procurements or partition the stock moves over
398 multiple stock pickings may override this method and call ``super()`` with
399 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
401 :param browse_record order: sales order to which the order lines belong
402 :param list(browse_record) order_lines: sales order line records to procure
403 :param int picking_id: optional ID of a stock picking to which the created stock moves
404 will be added. A new picking will be created if ommitted.
407 move_obj = self.pool.get('stock.move')
408 picking_obj = self.pool.get('stock.picking')
409 procurement_obj = self.pool.get('procurement.order')
412 for line in order_lines:
413 if line.state == 'done':
416 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
419 if line.product_id.type in ('product', 'consu'):
421 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
422 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
424 # a service has no stock move
427 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
428 proc_ids.append(proc_id)
429 line.write({'procurement_id': proc_id})
430 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
433 picking_obj.signal_button_confirm(cr, uid, [picking_id])
434 procurement_obj.signal_button_confirm(cr, uid, proc_ids)
437 if order.state == 'shipping_except':
438 val['state'] = 'progress'
439 val['shipped'] = False
441 if (order.order_policy == 'manual'):
442 for line in order.order_line:
443 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
444 val['state'] = 'manual'
449 def action_ship_create(self, cr, uid, ids, context=None):
450 for order in self.browse(cr, uid, ids, context=context):
451 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
454 def action_ship_end(self, cr, uid, ids, context=None):
455 for order in self.browse(cr, uid, ids, context=context):
456 val = {'shipped': True}
457 if order.state == 'shipping_except':
458 val['state'] = 'progress'
459 if (order.order_policy == 'manual'):
460 for line in order.order_line:
461 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
462 val['state'] = 'manual'
464 for line in order.order_line:
466 if line.state == 'exception':
467 towrite.append(line.id)
469 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
470 res = self.write(cr, uid, [order.id], val)
473 def has_stockable_products(self, cr, uid, ids, *args):
474 for order in self.browse(cr, uid, ids):
475 for order_line in order.order_line:
476 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
481 class sale_order_line(osv.osv):
483 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
485 for line in self.browse(cr, uid, ids, context=context):
487 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
492 _inherit = 'sale.order.line'
494 '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)]}),
495 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
496 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
497 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
498 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
499 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
503 'product_packaging': False,
506 def _get_line_qty(self, cr, uid, line, context=None):
507 if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
508 return self.pool.get('procurement.order').quantity_get(cr, uid,
509 line.procurement_id.id, context=context)
511 return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
514 def _get_line_uom(self, cr, uid, line, context=None):
515 if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
516 return self.pool.get('procurement.order').uom_get(cr, uid,
517 line.procurement_id.id, context=context)
519 return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
521 def button_cancel(self, cr, uid, ids, context=None):
522 res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
523 for line in self.browse(cr, uid, ids, context=context):
524 for move_line in line.move_ids:
525 if move_line.state != 'cancel':
526 raise osv.except_osv(
527 _('Cannot cancel sales order line!'),
528 _('You must first cancel stock moves attached to this sales order line.'))
531 def copy_data(self, cr, uid, id, default=None, context=None):
534 default.update({'move_ids': []})
535 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
537 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
538 partner_id=False, packaging=False, flag=False, context=None):
540 return {'value': {'product_packaging': False}}
541 product_obj = self.pool.get('product.product')
542 product_uom_obj = self.pool.get('product.uom')
543 pack_obj = self.pool.get('product.packaging')
548 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
549 product=product, qty=qty, uom=uom, partner_id=partner_id,
550 packaging=packaging, flag=False, context=context)
551 warning_msgs = res.get('warning') and res['warning']['message']
553 products = product_obj.browse(cr, uid, product, context=context)
554 if not products.packaging:
555 packaging = result['product_packaging'] = False
556 elif not packaging and products.packaging and not flag:
557 packaging = products.packaging[0].id
558 result['product_packaging'] = packaging
561 default_uom = products.uom_id and products.uom_id.id
562 pack = pack_obj.browse(cr, uid, packaging, context=context)
563 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
564 # qty = qty - qty % q + q
565 if qty and (q and not (qty % q) == 0):
566 ean = pack.ean or _('(n/a)')
570 warn_msg = _("You selected a quantity of %d Units.\n"
571 "But it's not compatible with the selected packaging.\n"
572 "Here is a proposition of quantities according to the packaging:\n"
573 "EAN: %s Quantity: %s Type of ul: %s") % \
574 (qty, ean, qty_pack, type_ul.name)
575 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
577 'title': _('Configuration Error!'),
578 'message': warning_msgs
580 result['product_uom_qty'] = qty
582 return {'value': result, 'warning': warning}
584 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
585 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
586 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
587 context = context or {}
588 product_uom_obj = self.pool.get('product.uom')
589 partner_obj = self.pool.get('res.partner')
590 product_obj = self.pool.get('product.product')
592 res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
593 uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
594 lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
597 res['value'].update({'product_packaging': False})
600 #update of result obtained in super function
601 product_obj = product_obj.browse(cr, uid, product, context=context)
602 res['value']['delay'] = (product_obj.sale_delay or 0.0)
603 res['value']['type'] = product_obj.procure_method
605 #check if product is available, and if not: raise an error
608 uom2 = product_uom_obj.browse(cr, uid, uom)
609 if product_obj.uom_id.category_id.id != uom2.category_id.id:
612 uom2 = product_obj.uom_id
614 # Calling product_packaging_change function after updating UoM
615 res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
616 res['value'].update(res_packing.get('value', {}))
617 warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
618 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
619 if (product_obj.type=='product') and int(compare_qty) == -1 \
620 and (product_obj.procure_method=='make_to_stock'):
621 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
622 (qty, uom2 and uom2.name or product_obj.uom_id.name,
623 max(0,product_obj.virtual_available), product_obj.uom_id.name,
624 max(0,product_obj.qty_available), product_obj.uom_id.name)
625 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
627 #update of warning messages
630 'title': _('Configuration Error!'),
631 'message' : warning_msgs
633 res.update({'warning': warning})
637 class sale_advance_payment_inv(osv.osv_memory):
638 _inherit = "sale.advance.payment.inv"
640 def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
641 result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
642 sale_obj = self.pool.get('sale.order')
643 sale_line_obj = self.pool.get('sale.order.line')
644 wizard = self.browse(cr, uid, [result], context)
645 sale = sale_obj.browse(cr, uid, sale_id, context=context)
647 # If invoice on picking: add the cost on the SO
648 # If not, the advance will be deduced when generating the final invoice
649 line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
650 line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
651 if sale.order_policy == 'picking':
655 'price_unit': -inv_amount,
656 'product_uom_qty': wizard.qtty or 1.0,
657 'product_uos_qty': wizard.qtty or 1.0,
658 'product_uos': res.get('uos_id', False),
659 'product_uom': res.get('uom_id', False),
660 'product_id': wizard.product_id.id or False,
664 sale_line_obj.create(cr, uid, vals, context=context)