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,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 from osv import fields, osv
25 from tools.translate import _
26 import decimal_precision as dp
31 class sale_shop(osv.osv):
33 _description = "Sale Shop"
35 'name': fields.char('Shop Name', size=64, required=True),
36 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
37 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
38 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40 'company_id': fields.many2one('res.company', 'Company', required=False),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 def _incoterm_get(self, cr, uid, context=None):
51 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
54 class sale_order(osv.osv):
56 _description = "Sale Order"
58 def copy(self, cr, uid, id, default=None, context=None):
68 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
70 return super(sale_order, self).copy(cr, uid, id, default, context=context)
72 def _amount_line_tax(self, cr, uid, line, context=None):
76 for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)['taxes']:
80 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
81 cur_obj = self.pool.get('res.currency')
85 for order in self.browse(cr, uid, ids, context=context):
87 'amount_untaxed': 0.0,
92 cur = order.pricelist_id.currency_id
93 for line in order.order_line:
94 val1 += line.price_subtotal
95 val += self._amount_line_tax(cr, uid, line, context=context)
96 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
97 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
98 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
102 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
111 p.sale_id,sum(m.product_qty), mp.state as mp_state
115 stock_picking p on (p.id=m.picking_id)
117 procurement_order mp on (mp.move_id=m.id)
119 p.sale_id IN %s GROUP BY mp.state, p.sale_id''',(tuple(ids),))
120 for oid, nbr, mp_state in cr.fetchall():
121 if mp_state == 'cancel':
123 if mp_state == 'done':
124 res[oid][0] += nbr or 0.0
125 res[oid][1] += nbr or 0.0
127 res[oid][1] += nbr or 0.0
132 res[r] = 100.0 * res[r][0] / res[r][1]
133 for order in self.browse(cr, uid, ids, context=context):
135 res[order.id] = 100.0
138 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
142 for sale in self.browse(cursor, user, ids, context=context):
147 for invoice in sale.invoice_ids:
148 if invoice.state not in ('draft', 'cancel'):
149 tot += invoice.amount_untaxed
151 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
156 def _invoiced(self, cursor, user, ids, name, arg, context=None):
160 for sale in self.browse(cursor, user, ids, context=context):
162 for invoice in sale.invoice_ids:
163 if invoice.state != 'paid':
166 if not sale.invoice_ids:
170 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
180 clause += 'AND inv.state = \'paid\''
182 clause += 'AND inv.state <> \'paid\''
184 cursor.execute('SELECT rel.order_id ' \
185 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
186 'WHERE rel.invoice_id = inv.id ' + clause)
187 res = cursor.fetchall()
189 cursor.execute('SELECT sale.id ' \
190 'FROM sale_order AS sale ' \
191 'WHERE sale.id NOT IN ' \
192 '(SELECT rel.order_id ' \
193 'FROM sale_order_invoice_rel AS rel)')
194 res.extend(cursor.fetchall())
196 return [('id', '=', 0)]
197 return [('id', 'in', [x[0] for x in res])]
199 def _get_order(self, cr, uid, ids, context=None):
203 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
204 result[line.order_id.id] = True
208 'name': fields.char('Order Reference', size=64, required=True,
209 readonly=True, states={'draft': [('readonly', False)]}, select=True),
210 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
211 'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
212 'client_order_ref': fields.char('Customer Reference', size=64),
213 'state': fields.selection([
214 ('draft', 'Quotation'),
215 ('waiting_date', 'Waiting Schedule'),
216 ('manual', 'Manual In Progress'),
217 ('progress', 'In Progress'),
218 ('shipping_except', 'Shipping Exception'),
219 ('invoice_except', 'Invoice Exception'),
221 ('cancel', 'Cancelled')
222 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Ordered Date'.", select=True),
223 'date_order': fields.date('Ordered Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224 'create_date': fields.date('Creation Date', readonly=True, help="Date on which sale order is created"),
225 'date_confirm': fields.date('Confirmation Date', readonly=True, help="Date on which sale order is confirmed"),
226 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
227 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
228 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sale order"),
229 'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact who requested the order or quotation."),
230 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sale order"),
232 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3, help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction"),
233 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
234 'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
235 'order_policy': fields.selection([
236 ('prepaid', 'Payment Before Delivery'),
237 ('manual', 'Shipping & Manual Invoice'),
238 ('postpaid', 'Invoice on Order After Delivery'),
239 ('picking', 'Invoice from Picking'),
240 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
241 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
242 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
243 - The 'Shipping & Manual Invoice' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
244 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
245 - The 'Invoice from the picking' choice is used to create an invoice during the picking process."""),
246 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist version for current sale order"),
247 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sale order"),
249 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
250 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
251 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
252 'shipped': fields.boolean('Picked', readonly=True, help="It indicates that a picking has been done. It will be set to True if the ordered quantities are available and the picking is done. If the ordered quantities are not available Procurement generates a Purchase/Manufacturing order. Unless its Picking and Purchase/Manufacturing order are not in the done state it wont be set to True"),
253 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
254 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
255 'invoiced': fields.function(_invoiced, method=True, string='Paid',
256 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid"),
257 'note': fields.text('Notes'),
259 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
261 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
262 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
264 multi='sums', help="The amount without tax"),
265 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
267 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
268 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
270 multi='sums', help="The tax amount"),
271 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Total',
273 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
274 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
276 multi='sums', help="The total amount"),
278 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True),
279 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
280 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
281 'company_id': fields.many2one('res.company','Company')
284 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', context=c),
285 'picking_policy': 'direct',
286 'date_order': time.strftime('%Y-%m-%d'),
287 'order_policy': 'manual',
289 'user_id': lambda obj, cr, uid, context: uid,
290 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
291 'invoice_quantity': 'order',
292 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
293 'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
294 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
299 def unlink(self, cr, uid, ids, context=None):
302 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
304 for s in sale_orders:
305 if s['state'] in ['draft', 'cancel']:
306 unlink_ids.append(s['id'])
308 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
309 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
311 def onchange_shop_id(self, cr, uid, ids, shop_id):
314 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
315 v['project_id'] = shop.project_id.id
316 # Que faire si le client a une pricelist a lui ?
317 if shop.pricelist_id.id:
318 v['pricelist_id'] = shop.pricelist_id.id
321 def action_cancel_draft(self, cr, uid, ids, *args):
324 cr.execute('select id from sale_order_line where order_id IN %s and state=%s',(tuple(ids),'cancel'))
325 line_ids = map(lambda x: x[0], cr.fetchall())
326 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
327 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
328 wf_service = netsvc.LocalService("workflow")
330 # Deleting the existing instance of workflow for SO
331 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
332 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
333 for (id,name) in self.name_get(cr, uid, ids):
334 message = _('Sale order ') + " '" + name + "' "+ _("is in draft state")
335 self.log(cr, uid, id, message)
338 def onchange_partner_id(self, cr, uid, ids, part):
340 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
342 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
343 part = self.pool.get('res.partner').browse(cr, uid, part)
344 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
345 payment_term = part.property_payment_term and part.property_payment_term.id or False
346 fiscal_position = part.property_account_position and part.property_account_position.id or False
347 dedicated_salesman = part.user_id and part.user_id.id or uid
350 'partner_invoice_id': addr['invoice'],
351 'partner_order_id': addr['contact'],
352 'partner_shipping_id': addr['delivery'],
353 'payment_term': payment_term,
354 'fiscal_position': fiscal_position,
355 'user_id': dedicated_salesman,
359 val['pricelist_id'] = pricelist
361 return {'value': val}
363 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
369 if policy == 'prepaid':
371 elif policy == 'picking':
372 inv_qty = 'procurement'
373 return {'value': {'invoice_quantity': inv_qty}}
375 def write(self, cr, uid, ids, vals, context=None):
378 if 'order_policy' in vals:
379 if vals['order_policy'] == 'prepaid':
380 vals.update({'invoice_quantity': 'order'})
381 elif vals['order_policy'] == 'picking':
382 vals.update({'invoice_quantity': 'procurement'})
383 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
385 def create(self, cr, uid, vals, context=None):
388 if 'order_policy' in vals:
389 if vals['order_policy'] == 'prepaid':
390 vals.update({'invoice_quantity': 'order'})
391 if vals['order_policy'] == 'picking':
392 vals.update({'invoice_quantity': 'procurement'})
393 return super(sale_order, self).create(cr, uid, vals, context=context)
395 def button_dummy(self, cr, uid, ids, context=None):
400 #FIXME: the method should return the list of invoices created (invoice_ids)
401 # and not the id of the last invoice created (res). The problem is that we
402 # cannot change it directly since the method is called by the sale order
403 # workflow and I suppose it expects a single id...
404 def _inv_get(self, cr, uid, order, context=None):
409 def _make_invoice(self, cr, uid, order, lines, context=None):
410 journal_obj = self.pool.get('account.journal')
411 inv_obj = self.pool.get('account.invoice')
416 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
418 raise osv.except_osv(_('Error !'),
419 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
420 a = order.partner_id.property_account_receivable.id
421 if order.payment_term:
422 pay_term = order.payment_term.id
425 for preinv in order.invoice_ids:
426 if preinv.state not in ('cancel',):
427 for preline in preinv.invoice_line:
428 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
429 lines.append(inv_line_id)
431 'name': order.client_order_ref or order.name,
432 'origin': order.name,
433 'type': 'out_invoice',
434 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
436 'partner_id': order.partner_id.id,
437 'journal_id': journal_ids[0],
438 'address_invoice_id': order.partner_invoice_id.id,
439 'address_contact_id': order.partner_order_id.id,
440 'invoice_line': [(6, 0, lines)],
441 'currency_id': order.pricelist_id.currency_id.id,
442 'comment': order.note,
443 'payment_term': pay_term,
444 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
445 'date_invoice' : context.get('date_invoice',False),
446 'company_id' : order.company_id.id,
447 'user_id':order.user_id and order.user_id.id or False
449 inv.update(self._inv_get(cr, uid, order))
450 inv_id = inv_obj.create(cr, uid, inv, context=context)
451 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
452 if data.get('value', False):
453 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
454 inv_obj.button_compute(cr, uid, [inv_id])
457 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
461 picking_obj = self.pool.get('stock.picking')
462 invoice = self.pool.get('account.invoice')
465 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
466 # last day of the last month as invoice date
468 context['date_inv'] = date_inv
469 for o in self.browse(cr, uid, ids):
471 for line in o.order_line:
473 #raise osv.except_osv(_('Error !'), _('The Sale Order already has some lines invoiced. You should continue the billing process by line.'))
475 elif (line.state in states):
476 lines.append(line.id)
477 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
479 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
481 for o in self.browse(cr, uid, ids):
482 for i in o.invoice_ids:
483 if i.state == 'draft':
485 for val in invoices.values():
487 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
490 invoice_ref += o.name + '|'
491 self.write(cr, uid, [o.id], {'state': 'progress'})
492 if o.order_policy == 'picking':
493 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
494 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
495 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
497 for order, il in val:
498 res = self._make_invoice(cr, uid, order, il, context=context)
499 invoice_ids.append(res)
500 self.write(cr, uid, [order.id], {'state': 'progress'})
501 if order.order_policy == 'picking':
502 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
503 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
506 def action_invoice_cancel(self, cr, uid, ids, context=None):
509 for sale in self.browse(cr, uid, ids, context=context):
510 for line in sale.order_line:
512 for iline in line.invoice_lines:
513 if iline.invoice_id and iline.invoice_id.state == 'cancel':
517 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
518 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
521 def action_invoice_end(self, cr, uid, ids, context=None):
525 for order in self.browse(cr, uid, ids, context=context):
526 for line in order.order_line:
527 if line.state == 'exception':
528 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'state': 'confirmed'}, context=context)
530 if order.state == 'invoice_except':
531 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
535 def action_cancel(self, cr, uid, ids, context=None):
538 sale_order_line_obj = self.pool.get('sale.order.line')
539 for sale in self.browse(cr, uid, ids, context=context):
540 for pick in sale.picking_ids:
541 if pick.state not in ('draft', 'cancel'):
542 raise osv.except_osv(
543 _('Could not cancel sale order !'),
544 _('You must first cancel all picking attached to this sale order.'))
545 for r in self.read(cr, uid, ids, ['picking_ids']):
546 for pick in r['picking_ids']:
547 wf_service = netsvc.LocalService("workflow")
548 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
549 for inv in sale.invoice_ids:
550 if inv.state not in ('draft', 'cancel'):
551 raise osv.except_osv(
552 _('Could not cancel this sale order !'),
553 _('You must first cancel all invoices attached to this sale order.'))
554 for r in self.read(cr, uid, ids, ['invoice_ids']):
555 for inv in r['invoice_ids']:
556 wf_service = netsvc.LocalService("workflow")
557 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
558 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
560 message = _('Sale order') + " '" + sale.name + _(" is cancelled")
561 self.log(cr, uid, sale.id, message)
562 self.write(cr, uid, ids, {'state': 'cancel'})
565 def action_wait(self, cr, uid, ids, *args):
566 for o in self.browse(cr, uid, ids):
567 if (o.order_policy == 'manual'):
568 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
570 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
571 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
572 message = _('Quotation') + " '" + o.name + "' "+ _("is converted to Sale order")
573 self.log(cr, uid, o.id, message)
576 def procurement_lines_get(self, cr, uid, ids, *args):
578 for order in self.browse(cr, uid, ids, context={}):
579 for line in order.order_line:
580 if line.procurement_id:
581 res.append(line.procurement_id.id)
584 # if mode == 'finished':
585 # returns True if all lines are done, False otherwise
586 # if mode == 'canceled':
587 # returns True if there is at least one canceled line, False otherwise
588 def test_state(self, cr, uid, ids, mode, *args):
589 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
594 write_cancel_ids = []
595 for order in self.browse(cr, uid, ids, context={}):
596 for line in order.order_line:
597 if (not line.procurement_id) or (line.procurement_id.state=='done'):
598 if line.state != 'done':
599 write_done_ids.append(line.id)
602 if line.procurement_id:
603 if (line.procurement_id.state == 'cancel'):
605 if line.state != 'exception':
606 write_cancel_ids.append(line.id)
610 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
612 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
614 if mode == 'finished':
616 elif mode == 'canceled':
622 def action_ship_create(self, cr, uid, ids, *args):
624 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
625 for order in self.browse(cr, uid, ids, context={}):
627 output_id = order.shop_id.warehouse_id.lot_output_id.id
629 for line in order.order_line:
631 date_planned = datetime.now() + relativedelta(days=line.delay or 0.0)
632 date_planned = (date_planned - timedelta(days=company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
634 if line.state == 'done':
637 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
638 location_id = order.shop_id.warehouse_id.lot_stock_id.id
640 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
641 picking_id = self.pool.get('stock.picking').create(cr, uid, {
643 'origin': order.name,
646 'move_type': order.picking_policy,
648 'address_id': order.partner_shipping_id.id,
650 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
651 'company_id': order.company_id.id,
653 move_id = self.pool.get('stock.move').create(cr, uid, {
654 'name': line.name[:64],
655 'picking_id': picking_id,
656 'product_id': line.product_id.id,
657 'date_planned': date_planned,
658 'date_expected': date_planned,
659 'product_qty': line.product_uom_qty,
660 'product_uom': line.product_uom.id,
661 'product_uos_qty': line.product_uos_qty,
662 'product_uos': (line.product_uos and line.product_uos.id)\
663 or line.product_uom.id,
664 'product_packaging': line.product_packaging.id,
665 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
666 'location_id': location_id,
667 'location_dest_id': output_id,
668 'sale_line_id': line.id,
669 'tracking_id': False,
673 'company_id': order.company_id.id,
676 proc_id = self.pool.get('procurement.order').create(cr, uid, {
678 'origin': order.name,
679 'date_planned': date_planned,
680 'product_id': line.product_id.id,
681 'product_qty': line.product_uom_qty,
682 'product_uom': line.product_uom.id,
683 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
684 or line.product_uom_qty,
685 'product_uos': (line.product_uos and line.product_uos.id)\
686 or line.product_uom.id,
687 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
688 'procure_method': line.type,
690 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
691 'company_id': order.company_id.id,
693 proc_ids.append(proc_id)
694 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
697 for proc_id in proc_ids:
698 wf_service = netsvc.LocalService("workflow")
699 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
702 wf_service = netsvc.LocalService("workflow")
703 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
705 if order.state == 'shipping_except':
706 val['state'] = 'progress'
708 if (order.order_policy == 'manual'):
709 for line in order.order_line:
710 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
711 val['state'] = 'manual'
713 self.write(cr, uid, [order.id], val)
716 def action_ship_end(self, cr, uid, ids, context=None):
719 for order in self.browse(cr, uid, ids, context=context):
720 val = {'shipped': True}
721 if order.state == 'shipping_except':
722 val['state'] = 'progress'
723 if (order.order_policy == 'manual'):
724 for line in order.order_line:
725 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
726 val['state'] = 'manual'
728 for line in order.order_line:
730 if line.state == 'exception':
731 towrite.append(line.id)
733 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
734 self.write(cr, uid, [order.id], val)
737 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
738 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
740 part = inv['partner_id'] and inv['partner_id'][0]
741 pr = inv['amount_untaxed'] or 0.0
742 partnertype = 'customer'
745 'name': 'Order: '+name,
747 'description': 'Order '+str(inv['id']),
750 'date': time.strftime('%Y-%m-%d'),
753 'partner_type': partnertype,
755 'planned_revenue': pr,
759 self.pool.get('res.partner.event').create(cr, uid, event)
761 def has_stockable_products(self, cr, uid, ids, *args):
762 for order in self.browse(cr, uid, ids):
763 for order_line in order.order_line:
764 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
769 # TODO add a field price_unit_uos
770 # - update it on change product and unit price
771 # - use it in report if there is a uos
772 class sale_order_line(osv.osv):
774 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
775 tax_obj = self.pool.get('account.tax')
776 cur_obj = self.pool.get('res.currency')
778 context = context or {}
779 for line in self.browse(cr, uid, ids, context=context):
780 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
781 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
782 cur = line.order_id.pricelist_id.currency_id
783 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
786 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
790 for line in self.browse(cr, uid, ids, context=context):
792 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
797 _name = 'sale.order.line'
798 _description = 'Sale Order Line'
800 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
801 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
802 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sale order lines."),
803 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the the shipping of the products to the customer", readonly=True, states={'draft':[('readonly',False)]}),
804 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
805 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
806 'invoiced': fields.boolean('Invoiced', readonly=True),
807 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
808 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft':[('readonly',False)]}),
809 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
810 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
811 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
812 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
813 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
814 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
815 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft':[('readonly',False)]}),
816 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
817 'product_uos': fields.many2one('product.uom', 'Product UoS'),
818 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
819 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
820 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
821 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
822 'notes': fields.text('Notes'),
823 'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
824 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
825 help='* The \'Draft\' state is set when the related sale order in draft state. \
826 \n* The \'Confirmed\' state is set when the related sale order is confirmed. \
827 \n* The \'Exception\' state is set when the related sale order is set as exception. \
828 \n* The \'Done\' state is set when the sale order line has been picked. \
829 \n* The \'Cancelled\' state is set when a user cancel the sale order related.'),
830 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
831 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='Salesman'),
832 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True, states={'draft':[('readonly',False)]}),
834 _order = 'sequence, id'
838 'product_uom_qty': 1,
839 'product_uos_qty': 1,
843 'type': 'make_to_stock',
844 'product_packaging': False
847 def invoice_line_create(self, cr, uid, ids, context=None):
851 def _get_line_qty(line):
852 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
854 return line.product_uos_qty or 0.0
855 return line.product_uom_qty
857 return self.pool.get('procurement.order').quantity_get(cr, uid,
858 line.procurement_id.id, context=context)
860 def _get_line_uom(line):
861 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
863 return line.product_uos.id
864 return line.product_uom.id
866 return self.pool.get('procurement.order').uom_get(cr, uid,
867 line.procurement_id.id, context=context)
871 for line in self.browse(cr, uid, ids, context=context):
872 if not line.invoiced:
874 a = line.product_id.product_tmpl_id.property_account_income.id
876 a = line.product_id.categ_id.property_account_income_categ.id
878 raise osv.except_osv(_('Error !'),
879 _('There is no income account defined ' \
880 'for this product: "%s" (id:%d)') % \
881 (line.product_id.name, line.product_id.id,))
883 a = self.pool.get('ir.property').get(cr, uid,
884 'property_account_income_categ', 'product.category',
886 uosqty = _get_line_qty(line)
887 uos_id = _get_line_uom(line)
890 pu = round(line.price_unit * line.product_uom_qty / uosqty,
891 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
892 fpos = line.order_id.fiscal_position or False
893 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
895 raise osv.except_osv(_('Error !'),
896 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
897 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
899 'origin': line.order_id.name,
903 'discount': line.discount,
905 'product_id': line.product_id.id or False,
906 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
908 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
910 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
911 self.write(cr, uid, [line.id], {'invoiced': True})
912 sales[line.order_id.id] = True
913 create_ids.append(inv_id)
914 # Trigger workflow events
915 wf_service = netsvc.LocalService("workflow")
916 for sid in sales.keys():
917 wf_service.trg_write(uid, 'sale.order', sid, cr)
920 def button_cancel(self, cr, uid, ids, context=None):
923 for line in self.browse(cr, uid, ids, context=context):
925 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
926 for move_line in line.move_ids:
927 if move_line.state != 'cancel':
928 raise osv.except_osv(
929 _('Could not cancel sale order line!'),
930 _('You must first cancel stock moves attached to this sale order line.'))
931 message = _('Sale order line') + " '" + line.name + "' "+_("is cancelled")
932 self.log(cr, uid, id, message)
933 return self.write(cr, uid, ids, {'state': 'cancel'})
935 def button_confirm(self, cr, uid, ids, context=None):
938 return self.write(cr, uid, ids, {'state': 'confirmed'})
940 def button_done(self, cr, uid, ids, context=None):
943 wf_service = netsvc.LocalService("workflow")
944 res = self.write(cr, uid, ids, {'state': 'done'})
945 for line in self.browse(cr, uid, ids, context=context):
946 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
949 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
950 product_obj = self.pool.get('product.product')
952 return {'value': {'product_uom': product_uos,
953 'product_uom_qty': product_uos_qty}, 'domain': {}}
955 product = product_obj.browse(cr, uid, product_id)
957 'product_uom': product.uom_id.id,
959 # FIXME must depend on uos/uom of the product and not only of the coeff.
962 'product_uom_qty': product_uos_qty / product.uos_coeff,
963 'th_weight': product_uos_qty / product.uos_coeff * product.weight
965 except ZeroDivisionError:
967 return {'value': value}
969 def copy_data(self, cr, uid, id, default=None, context=None):
974 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
975 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
977 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
978 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
979 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
981 raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sale form !\nPlease set one customer before choosing a product.'))
983 product_uom_obj = self.pool.get('product.uom')
984 partner_obj = self.pool.get('res.partner')
985 product_obj = self.pool.get('product.product')
987 lang = partner_obj.browse(cr, uid, partner_id).lang
988 context = {'lang': lang, 'partner_id': partner_id}
991 return {'value': {'th_weight': 0, 'product_packaging': False,
992 'product_uos_qty': qty}, 'domain': {'product_uom': [],
996 date_order = time.strftime('%Y-%m-%d')
999 product_obj = product_obj.browse(cr, uid, product, context=context)
1000 if not packaging and product_obj.packaging:
1001 packaging = product_obj.packaging[0].id
1002 result['product_packaging'] = packaging
1005 default_uom = product_obj.uom_id and product_obj.uom_id.id
1006 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1007 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1008 # qty = qty - qty % q + q
1009 if qty and (q and not (qty % q) == 0):
1013 warn_msg = _("You selected a quantity of %d Units.\nBut it's not compatible with the selected packaging.\nHere is a proposition of quantities according to the packaging: ") % (qty)
1014 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
1016 'title': _('Picking Information !'),
1019 result['product_uom_qty'] = qty
1023 uom2 = product_uom_obj.browse(cr, uid, uom)
1024 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1028 if product_obj.uos_id:
1029 uos2 = product_uom_obj.browse(cr, uid, uos)
1030 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1034 if product_obj.description_sale:
1035 result['notes'] = product_obj.description_sale
1036 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1037 if update_tax: #The quantity only have changed
1038 result['delay'] = (product_obj.sale_delay or 0.0)
1039 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1040 result.update({'type': product_obj.procure_method})
1043 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1045 if (not uom) and (not uos):
1046 result['product_uom'] = product_obj.uom_id.id
1047 if product_obj.uos_id:
1048 result['product_uos'] = product_obj.uos_id.id
1049 result['product_uos_qty'] = qty * product_obj.uos_coeff
1050 uos_category_id = product_obj.uos_id.category_id.id
1052 result['product_uos'] = False
1053 result['product_uos_qty'] = qty
1054 uos_category_id = False
1055 result['th_weight'] = qty * product_obj.weight
1056 domain = {'product_uom':
1057 [('category_id', '=', product_obj.uom_id.category_id.id)],
1059 [('category_id', '=', uos_category_id)]}
1061 elif uos and not uom: # only happens if uom is False
1062 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1063 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1064 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1065 elif uom: # whether uos is set or not
1066 default_uom = product_obj.uom_id and product_obj.uom_id.id
1067 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1068 if product_obj.uos_id:
1069 result['product_uos'] = product_obj.uos_id.id
1070 result['product_uos_qty'] = qty * product_obj.uos_coeff
1072 result['product_uos'] = False
1073 result['product_uos_qty'] = qty
1074 result['th_weight'] = q * product_obj.weight # Round the quantity up
1077 uom2 = product_obj.uom_id
1078 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1079 and (product_obj.procure_method=='make_to_stock'):
1081 'title': _('Not enough stock !'),
1082 'message': _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') %
1083 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1084 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1085 max(0,product_obj.qty_available), product_obj.uom_id.name)
1093 'title': 'No Pricelist !',
1095 'You have to select a pricelist or a customer in the sale form !\n'
1096 'Please set one before choosing a product.'
1099 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1100 product, qty or 1.0, partner_id, {
1106 'title': 'No valid pricelist line found !',
1108 "Couldn't find a pricelist line matching this product and quantity.\n"
1109 "You have to change either the product, the quantity or the pricelist."
1112 result.update({'price_unit': price})
1113 return {'value': result, 'domain': domain, 'warning': warning}
1115 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1116 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1117 lang=False, update_tax=True, date_order=False):
1118 res = self.product_id_change(cursor, user, ids, pricelist, product,
1119 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1120 partner_id=partner_id, lang=lang, update_tax=update_tax,
1121 date_order=date_order)
1122 if 'product_uom' in res['value']:
1123 del res['value']['product_uom']
1125 res['value']['price_unit'] = 0.0
1128 def unlink(self, cr, uid, ids, context=None):
1131 """Allows to delete sale order lines in draft,cancel states"""
1132 for rec in self.browse(cr, uid, ids, context=context):
1133 if rec.state not in ['draft', 'cancel']:
1134 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1135 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1140 class sale_config_picking_policy(osv.osv_memory):
1141 _name = 'sale.config.picking_policy'
1142 _inherit = 'res.config'
1145 'name': fields.char('Name', size=64),
1146 'picking_policy': fields.selection([
1147 ('direct', 'Direct Delivery'),
1148 ('one', 'All at Once')
1149 ], 'Picking Default Policy', required=True, help="The Shipping Policy is used to synchronise invoices and delivery operations."),
1150 'order_policy': fields.selection([
1151 ('manual', 'Invoice Based on Sales Orders'),
1152 ('picking', 'Invoice Based on Deliveries'),
1153 ], 'Shipping Default Policy', required=True,
1154 help="You can generate invoices based on sales orders or based on shippings."),
1155 'step': fields.selection([
1156 ('one', 'Delivery Order Only'),
1157 ('two', 'Picking List & Delivery Order')
1158 ], 'Steps To Deliver a Sale Order', required=True,
1159 help="By default, OpenERP is able to manage complex routing and paths "\
1160 "of products in your warehouse and partner locations. This will configure "\
1161 "the most common and simple methods to deliver products to the customer "\
1162 "in one or two operations by the worker.")
1165 'picking_policy': 'direct',
1166 'order_policy': 'manual',
1170 def execute(self, cr, uid, ids, context=None):
1173 for o in self.browse(cr, uid, ids, context=context):
1174 ir_values_obj = self.pool.get('ir.values')
1175 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1176 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1178 md = self.pool.get('ir.model.data')
1179 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1180 group_id = md.browse(cr, uid, group_id, context=context).res_id
1181 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1182 menu_id = md.browse(cr, uid, menu_id, context=context).res_id
1183 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1184 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1185 location_id = md.browse(cr, uid, location_id, context=context).res_id
1186 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1188 sale_config_picking_policy()
1190 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: