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 ##############################################################################
23 from datetime import datetime, timedelta
24 from dateutil.relativedelta import relativedelta
27 from osv import fields, osv
28 from tools import config
29 from tools.translate import _
30 import decimal_precision as dp
32 class sale_shop(osv.osv):
34 _description = "Sale Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41 'company_id': fields.many2one('res.company', 'Company', required=True),
46 def _incoterm_get(self, cr, uid, context=None):
49 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
52 class sale_order(osv.osv):
54 _description = "Sale Order"
56 def copy(self, cr, uid, id, default=None, context=None):
66 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
68 return super(sale_order, self).copy(cr, uid, id, default, context=context)
70 def _amount_line_tax(self, cr, uid, line, context=None):
74 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']:
78 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
79 cur_obj = self.pool.get('res.currency')
83 for order in self.browse(cr, uid, ids, context=context):
85 'amount_untaxed': 0.0,
90 cur = order.pricelist_id.currency_id
91 for line in order.order_line:
92 val1 += line.price_subtotal
93 val += self._amount_line_tax(cr, uid, line, context=context)
94 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
95 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
96 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
100 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
109 p.sale_id,sum(m.product_qty), mp.state as mp_state
113 stock_picking p on (p.id=m.picking_id)
115 procurement_order mp on (mp.move_id=m.id)
117 p.sale_id IN %s GROUP BY mp.state, p.sale_id''',(tuple(ids),))
118 for oid, nbr, mp_state in cr.fetchall():
119 if mp_state == 'cancel':
121 if mp_state == 'done':
122 res[oid][0] += nbr or 0.0
123 res[oid][1] += nbr or 0.0
125 res[oid][1] += nbr or 0.0
130 res[r] = 100.0 * res[r][0] / res[r][1]
131 for order in self.browse(cr, uid, ids, context=context):
133 res[order.id] = 100.0
136 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
140 for sale in self.browse(cursor, user, ids, context=context):
145 for invoice in sale.invoice_ids:
146 if invoice.state not in ('draft', 'cancel'):
147 tot += invoice.amount_untaxed
149 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
154 def _invoiced(self, cursor, user, ids, name, arg, context=None):
158 for sale in self.browse(cursor, user, ids, context=context):
160 for invoice in sale.invoice_ids:
161 if invoice.state != 'paid':
164 if not sale.invoice_ids:
168 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
178 clause += 'AND inv.state = \'paid\''
180 clause += 'AND inv.state <> \'paid\''
182 cursor.execute('SELECT rel.order_id ' \
183 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
184 'WHERE rel.invoice_id = inv.id ' + clause)
185 res = cursor.fetchall()
187 cursor.execute('SELECT sale.id ' \
188 'FROM sale_order AS sale ' \
189 'WHERE sale.id NOT IN ' \
190 '(SELECT rel.order_id ' \
191 'FROM sale_order_invoice_rel AS rel)')
192 res.extend(cursor.fetchall())
194 return [('id', '=', 0)]
195 return [('id', 'in', [x[0] for x in res])]
197 def _get_order(self, cr, uid, ids, context=None):
201 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
202 result[line.order_id.id] = True
206 'name': fields.char('Order Reference', size=64, required=True,
207 readonly=True, states={'draft': [('readonly', False)]}, select=True),
208 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
209 'origin': fields.char('Source document', size=64, help="Reference of the document that generated this sale order request."),
210 'client_order_ref': fields.char('Customer Reference', size=64),
211 'state': fields.selection([
212 ('draft', 'Quotation'),
213 ('waiting_date', 'Waiting Schedule'),
214 ('manual', 'Manual In Progress'),
215 ('progress', 'In Progress'),
216 ('shipping_except', 'Shipping Exception'),
217 ('invoice_except', 'Invoice Exception'),
219 ('cancel', 'Cancelled')
220 ], '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),
221 'date_order': fields.date('Ordered Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
222 'create_date': fields.date('Creation Date', readonly=True, help="Date on which sale order is created"),
223 'date_confirm': fields.date('Confirmation Date', readonly=True, help="Date on which sale order is confirmed"),
224 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
225 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
226 '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"),
227 '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."),
228 '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"),
230 '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"),
231 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
232 '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?"""),
233 'order_policy': fields.selection([
234 ('prepaid', 'Payment Before Delivery'),
235 ('manual', 'Shipping & Manual Invoice'),
236 ('postpaid', 'Invoice on Order After Delivery'),
237 ('picking', 'Invoice from Picking'),
238 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
239 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
240 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
241 - 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.
242 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
243 - The 'Invoice from the picking' choice is used to create an invoice during the picking process."""),
244 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist version for current sale order"),
245 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sale order"),
247 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
248 '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)."),
249 '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"),
250 '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"),
251 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
252 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
253 'invoiced': fields.function(_invoiced, method=True, string='Paid',
254 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid"),
255 'note': fields.text('Notes'),
257 'amount_untaxed': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
259 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
260 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
262 multi='sums', help="The amount without tax"),
263 'amount_tax': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
265 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
266 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
268 multi='sums', help="The tax amount"),
269 'amount_total': fields.function(_amount_all, method=True, digits_compute= dp.get_precision('Sale Price'), string='Total',
271 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
272 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
274 multi='sums', help="The total amount"),
276 '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),
277 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
278 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
279 'company_id': fields.many2one('res.company','Company')
282 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', context=c),
283 'picking_policy': 'direct',
284 'date_order': time.strftime('%Y-%m-%d'),
285 'order_policy': 'manual',
287 'user_id': lambda obj, cr, uid, context: uid,
288 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
289 'invoice_quantity': 'order',
290 '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'],
291 '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'],
292 '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'],
297 def unlink(self, cr, uid, ids, context=None):
300 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
302 for s in sale_orders:
303 if s['state'] in ['draft', 'cancel']:
304 unlink_ids.append(s['id'])
306 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
307 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
309 def onchange_shop_id(self, cr, uid, ids, shop_id):
312 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
313 v['project_id'] = shop.project_id.id
314 # Que faire si le client a une pricelist a lui ?
315 if shop.pricelist_id.id:
316 v['pricelist_id'] = shop.pricelist_id.id
319 def action_cancel_draft(self, cr, uid, ids, *args):
322 cr.execute('select id from sale_order_line where order_id IN %s and state=%s',(tuple(ids),'cancel'))
323 line_ids = map(lambda x: x[0], cr.fetchall())
324 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
325 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
326 wf_service = netsvc.LocalService("workflow")
328 # Deleting the existing instance of workflow for SO
329 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
330 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
331 for (id,name) in self.name_get(cr, uid, ids):
332 message = _('Sale order ') + " '" + name + "' "+ _("is in draft state")
333 self.log(cr, uid, id, message)
336 def onchange_partner_id(self, cr, uid, ids, part):
338 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
340 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
341 part = self.pool.get('res.partner').browse(cr, uid, part)
342 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
343 payment_term = part.property_payment_term and part.property_payment_term.id or False
344 fiscal_position = part.property_account_position and part.property_account_position.id or False
345 dedicated_salesman = part.user_id and part.user_id.id or uid
348 'partner_invoice_id': addr['invoice'],
349 'partner_order_id': addr['contact'],
350 'partner_shipping_id': addr['delivery'],
351 'payment_term': payment_term,
352 'fiscal_position': fiscal_position,
353 'user_id': dedicated_salesman,
357 val['pricelist_id'] = pricelist
359 return {'value': val}
361 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
367 if policy == 'prepaid':
369 elif policy == 'picking':
370 inv_qty = 'procurement'
371 return {'value': {'invoice_quantity': inv_qty}}
373 def write(self, cr, uid, ids, vals, context=None):
376 if 'order_policy' in vals:
377 if vals['order_policy'] == 'prepaid':
378 vals.update({'invoice_quantity': 'order'})
379 elif vals['order_policy'] == 'picking':
380 vals.update({'invoice_quantity': 'procurement'})
381 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
383 def create(self, cr, uid, vals, context=None):
386 if 'order_policy' in vals:
387 if vals['order_policy'] == 'prepaid':
388 vals.update({'invoice_quantity': 'order'})
389 if vals['order_policy'] == 'picking':
390 vals.update({'invoice_quantity': 'procurement'})
391 return super(sale_order, self).create(cr, uid, vals, context=context)
393 def button_dummy(self, cr, uid, ids, context=None):
398 #FIXME: the method should return the list of invoices created (invoice_ids)
399 # and not the id of the last invoice created (res). The problem is that we
400 # cannot change it directly since the method is called by the sale order
401 # workflow and I suppose it expects a single id...
402 def _inv_get(self, cr, uid, order, context=None):
407 def _make_invoice(self, cr, uid, order, lines, context=None):
408 journal_obj = self.pool.get('account.journal')
409 inv_obj = self.pool.get('account.invoice')
414 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
416 raise osv.except_osv(_('Error !'),
417 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
418 a = order.partner_id.property_account_receivable.id
419 if order.payment_term:
420 pay_term = order.payment_term.id
423 for preinv in order.invoice_ids:
424 if preinv.state not in ('cancel',):
425 for preline in preinv.invoice_line:
426 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
427 lines.append(inv_line_id)
429 'name': order.client_order_ref or order.name,
430 'origin': order.name,
431 'type': 'out_invoice',
432 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
434 'partner_id': order.partner_id.id,
435 'journal_id': journal_ids[0],
436 'address_invoice_id': order.partner_invoice_id.id,
437 'address_contact_id': order.partner_order_id.id,
438 'invoice_line': [(6, 0, lines)],
439 'currency_id': order.pricelist_id.currency_id.id,
440 'comment': order.note,
441 'payment_term': pay_term,
442 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
443 'date_invoice' : context.get('date_invoice',False),
444 'company_id' : order.company_id.id,
445 'user_id':order.user_id and order.user_id.id or False
447 inv.update(self._inv_get(cr, uid, order))
448 inv_id = inv_obj.create(cr, uid, inv, context=context)
449 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
450 if data.get('value', False):
451 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
452 inv_obj.button_compute(cr, uid, [inv_id])
455 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
459 picking_obj = self.pool.get('stock.picking')
460 invoice = self.pool.get('account.invoice')
463 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
464 # last day of the last month as invoice date
466 context['date_inv'] = date_inv
467 for o in self.browse(cr, uid, ids):
469 for line in o.order_line:
471 raise osv.except_osv(_('Error !'), _('The Sale Order already has some lines invoiced. You should continue the billing process by line.'))
472 elif (line.state in states):
473 lines.append(line.id)
474 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
476 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
478 for o in self.browse(cr, uid, ids):
479 for i in o.invoice_ids:
480 if i.state == 'draft':
482 for val in invoices.values():
484 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
487 invoice_ref += o.name + '|'
488 self.write(cr, uid, [o.id], {'state': 'progress'})
489 if o.order_policy == 'picking':
490 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
491 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
492 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
494 for order, il in val:
495 res = self._make_invoice(cr, uid, order, il, context=context)
496 invoice_ids.append(res)
497 self.write(cr, uid, [order.id], {'state': 'progress'})
498 if order.order_policy == 'picking':
499 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
500 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
503 def action_invoice_cancel(self, cr, uid, ids, context=None):
506 for sale in self.browse(cr, uid, ids, context=context):
507 for line in sale.order_line:
509 for iline in line.invoice_lines:
510 if iline.invoice_id and iline.invoice_id.state == 'cancel':
514 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
515 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
518 def action_invoice_end(self, cr, uid, ids, context=None):
522 for order in self.browse(cr, uid, ids, context=context):
523 for line in order.order_line:
524 if line.state == 'exception':
525 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'state': 'confirmed'}, context=context)
527 if order.state == 'invoice_except':
528 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
532 def action_cancel(self, cr, uid, ids, context=None):
536 sale_order_line_obj = self.pool.get('sale.order.line')
537 for sale in self.browse(cr, uid, ids, context=context):
538 for pick in sale.picking_ids:
539 if pick.state not in ('draft', 'cancel'):
540 raise osv.except_osv(
541 _('Could not cancel sale order !'),
542 _('You must first cancel all picking attached to this sale order.'))
543 for r in self.read(cr, uid, ids, ['picking_ids']):
544 for pick in r['picking_ids']:
545 wf_service = netsvc.LocalService("workflow")
546 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
547 for inv in sale.invoice_ids:
548 if inv.state not in ('draft', 'cancel'):
549 raise osv.except_osv(
550 _('Could not cancel this sale order !'),
551 _('You must first cancel all invoices attached to this sale order.'))
552 for r in self.read(cr, uid, ids, ['invoice_ids']):
553 for inv in r['invoice_ids']:
554 wf_service = netsvc.LocalService("workflow")
555 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
556 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
558 message = _('Sale order') + " '" + sale.name + _(" is cancelled")
559 self.log(cr, uid, sale.id, message)
560 self.write(cr, uid, ids, {'state': 'cancel'})
563 def action_wait(self, cr, uid, ids, *args):
564 product_obj = self.pool.get('product.product')
565 for o in self.browse(cr, uid, ids):
566 if (o.order_policy == 'manual'):
567 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
569 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
570 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
571 message = _('Quotation') + " '" + o.name + "' "+ _("is converted to Sale order")
572 self.log(cr, uid, o.id, message)
575 def procurement_lines_get(self, cr, uid, ids, *args):
577 for order in self.browse(cr, uid, ids, context={}):
578 for line in order.order_line:
579 if line.procurement_id:
580 res.append(line.procurement_id.id)
583 # if mode == 'finished':
584 # returns True if all lines are done, False otherwise
585 # if mode == 'canceled':
586 # returns True if there is at least one canceled line, False otherwise
587 def test_state(self, cr, uid, ids, mode, *args):
588 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
593 write_cancel_ids = []
594 for order in self.browse(cr, uid, ids, context={}):
595 for line in order.order_line:
596 if (not line.procurement_id) or (line.procurement_id.state=='done'):
597 if line.state != 'done':
598 write_done_ids.append(line.id)
601 if line.procurement_id:
602 if (line.procurement_id.state == 'cancel'):
604 if line.state != 'exception':
605 write_cancel_ids.append(line.id)
609 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
611 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
613 if mode == 'finished':
615 elif mode == 'canceled':
621 def action_ship_create(self, cr, uid, ids, *args):
623 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
624 for order in self.browse(cr, uid, ids, context={}):
626 output_id = order.shop_id.warehouse_id.lot_output_id.id
628 for line in order.order_line:
630 date_planned = datetime.now() + relativedelta(days=line.delay or 0.0)
631 date_planned = (date_planned - timedelta(days=company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
633 if line.state == 'done':
636 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
637 location_id = order.shop_id.warehouse_id.lot_stock_id.id
639 loc_dest_id = order.partner_id.property_stock_customer.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 create_sale_order_line_invoice(self, cr, uid, ids, context=None):
850 context.update({'active_ids' : ids,'active_id' : ids})
851 self.pool.get('sale.order.line.make.invoice').make_invoices(cr, uid, ids, context=context)
854 def invoice_line_create(self, cr, uid, ids, context=None):
858 def _get_line_qty(line):
859 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
861 return line.product_uos_qty or 0.0
862 return line.product_uom_qty
864 return self.pool.get('procurement.order').quantity_get(cr, uid,
865 line.procurement_id.id, context=context)
867 def _get_line_uom(line):
868 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
870 return line.product_uos.id
871 return line.product_uom.id
873 return self.pool.get('procurement.order').uom_get(cr, uid,
874 line.procurement_id.id, context=context)
878 for line in self.browse(cr, uid, ids, context=context):
879 if not line.invoiced:
881 a = line.product_id.product_tmpl_id.property_account_income.id
883 a = line.product_id.categ_id.property_account_income_categ.id
885 raise osv.except_osv(_('Error !'),
886 _('There is no income account defined ' \
887 'for this product: "%s" (id:%d)') % \
888 (line.product_id.name, line.product_id.id,))
890 a = self.pool.get('ir.property').get(cr, uid,
891 'property_account_income_categ', 'product.category',
893 uosqty = _get_line_qty(line)
894 uos_id = _get_line_uom(line)
897 pu = round(line.price_unit * line.product_uom_qty / uosqty,
898 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
899 fpos = line.order_id.fiscal_position or False
900 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
902 raise osv.except_osv(_('Error !'),
903 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
904 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
906 'origin': line.order_id.name,
910 'discount': line.discount,
912 'product_id': line.product_id.id or False,
913 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
915 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
917 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
918 self.write(cr, uid, [line.id], {'invoiced': True})
919 sales[line.order_id.id] = True
920 create_ids.append(inv_id)
921 # Trigger workflow events
922 wf_service = netsvc.LocalService("workflow")
923 for sid in sales.keys():
924 wf_service.trg_write(uid, 'sale.order', sid, cr)
927 def button_cancel(self, cr, uid, ids, context=None):
930 for line in self.browse(cr, uid, ids, context=context):
932 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
933 for move_line in line.move_ids:
934 if move_line.state != 'cancel':
935 raise osv.except_osv(
936 _('Could not cancel sale order line!'),
937 _('You must first cancel stock moves attached to this sale order line.'))
938 message = _('Sale order line') + " '" + line.name + "' "+_("is cancelled")
939 self.log(cr, uid, id, message)
940 return self.write(cr, uid, ids, {'state': 'cancel'})
942 def button_confirm(self, cr, uid, ids, context=None):
945 return self.write(cr, uid, ids, {'state': 'confirmed'})
947 def button_done(self, cr, uid, ids, context=None):
950 wf_service = netsvc.LocalService("workflow")
951 res = self.write(cr, uid, ids, {'state': 'done'})
952 for line in self.browse(cr, uid, ids, context=context):
953 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
956 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
957 product_obj = self.pool.get('product.product')
959 return {'value': {'product_uom': product_uos,
960 'product_uom_qty': product_uos_qty}, 'domain': {}}
962 product = product_obj.browse(cr, uid, product_id)
964 'product_uom': product.uom_id.id,
966 # FIXME must depend on uos/uom of the product and not only of the coeff.
969 'product_uom_qty': product_uos_qty / product.uos_coeff,
970 'th_weight': product_uos_qty / product.uos_coeff * product.weight
972 except ZeroDivisionError:
974 return {'value': value}
976 def copy_data(self, cr, uid, id, default=None, context=None):
981 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
982 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
984 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
985 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
986 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
988 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.'))
990 product_uom_obj = self.pool.get('product.uom')
991 partner_obj = self.pool.get('res.partner')
992 product_obj = self.pool.get('product.product')
994 lang = partner_obj.browse(cr, uid, partner_id).lang
995 context = {'lang': lang, 'partner_id': partner_id}
998 return {'value': {'th_weight': 0, 'product_packaging': False,
999 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1003 date_order = time.strftime('%Y-%m-%d')
1006 product_obj = product_obj.browse(cr, uid, product, context=context)
1007 if not packaging and product_obj.packaging:
1008 packaging = product_obj.packaging[0].id
1009 result['product_packaging'] = packaging
1012 default_uom = product_obj.uom_id and product_obj.uom_id.id
1013 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1014 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1015 # qty = qty - qty % q + q
1016 if qty and (q and not (qty % q) == 0):
1020 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)
1021 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
1023 'title': _('Picking Information !'),
1026 result['product_uom_qty'] = qty
1030 uom2 = product_uom_obj.browse(cr, uid, uom)
1031 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1035 if product_obj.uos_id:
1036 uos2 = product_uom_obj.browse(cr, uid, uos)
1037 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1041 if product_obj.description_sale:
1042 result['notes'] = product_obj.description_sale
1043 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1044 if update_tax: #The quantity only have changed
1045 result['delay'] = (product_obj.sale_delay or 0.0)
1046 partner = partner_obj.browse(cr, uid, partner_id)
1047 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1048 result.update({'type': product_obj.procure_method})
1051 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1053 if (not uom) and (not uos):
1054 result['product_uom'] = product_obj.uom_id.id
1055 if product_obj.uos_id:
1056 result['product_uos'] = product_obj.uos_id.id
1057 result['product_uos_qty'] = qty * product_obj.uos_coeff
1058 uos_category_id = product_obj.uos_id.category_id.id
1060 result['product_uos'] = False
1061 result['product_uos_qty'] = qty
1062 uos_category_id = False
1063 result['th_weight'] = qty * product_obj.weight
1064 domain = {'product_uom':
1065 [('category_id', '=', product_obj.uom_id.category_id.id)],
1067 [('category_id', '=', uos_category_id)]}
1069 elif uos and not uom: # only happens if uom is False
1070 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1071 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1072 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1073 elif uom: # whether uos is set or not
1074 default_uom = product_obj.uom_id and product_obj.uom_id.id
1075 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1076 if product_obj.uos_id:
1077 result['product_uos'] = product_obj.uos_id.id
1078 result['product_uos_qty'] = qty * product_obj.uos_coeff
1080 result['product_uos'] = False
1081 result['product_uos_qty'] = qty
1082 result['th_weight'] = q * product_obj.weight # Round the quantity up
1085 uom2 = product_obj.uom_id
1086 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1087 and (product_obj.procure_method=='make_to_stock'):
1089 'title': _('Not enough stock !'),
1090 'message': _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') %
1091 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1092 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1093 max(0,product_obj.qty_available), product_obj.uom_id.name)
1101 'title': 'No Pricelist !',
1103 'You have to select a pricelist or a customer in the sale form !\n'
1104 'Please set one before choosing a product.'
1107 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1108 product, qty or 1.0, partner_id, {
1114 'title': 'No valid pricelist line found !',
1116 "Couldn't find a pricelist line matching this product and quantity.\n"
1117 "You have to change either the product, the quantity or the pricelist."
1120 result.update({'price_unit': price})
1121 return {'value': result, 'domain': domain, 'warning': warning}
1123 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1124 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1125 lang=False, update_tax=True, date_order=False):
1126 res = self.product_id_change(cursor, user, ids, pricelist, product,
1127 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1128 partner_id=partner_id, lang=lang, update_tax=update_tax,
1129 date_order=date_order)
1130 if 'product_uom' in res['value']:
1131 del res['value']['product_uom']
1133 res['value']['price_unit'] = 0.0
1136 def unlink(self, cr, uid, ids, context=None):
1139 """Allows to delete sale order lines in draft,cancel states"""
1140 for rec in self.browse(cr, uid, ids, context=context):
1141 if rec.state not in ['draft', 'cancel']:
1142 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1143 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1148 class sale_config_picking_policy(osv.osv_memory):
1149 _name = 'sale.config.picking_policy'
1150 _inherit = 'res.config'
1153 'name': fields.char('Name', size=64),
1154 'picking_policy': fields.selection([
1155 ('direct', 'Direct Delivery'),
1156 ('one', 'All at Once')
1157 ], 'Picking Default Policy', required=True, help="The Shipping Policy is used to synchronise invoices and delivery operations."),
1158 'order_policy': fields.selection([
1159 ('manual', 'Invoice Based on Sales Orders'),
1160 ('picking', 'Invoice Based on Deliveries'),
1161 ], 'Shipping Default Policy', required=True,
1162 help="You can generate invoices based on sales orders or based on shippings."),
1163 'step': fields.selection([
1164 ('one', 'Delivery Order Only'),
1165 ('two', 'Picking List & Delivery Order')
1166 ], 'Steps To Deliver a Sale Order', required=True,
1167 help="By default, OpenERP is able to manage complex routing and paths "\
1168 "of products in your warehouse and partner locations. This will configure "\
1169 "the most common and simple methods to deliver products to the customer "\
1170 "in one or two operations by the worker.")
1173 'picking_policy': 'direct',
1174 'order_policy': 'manual',
1178 def execute(self, cr, uid, ids, context=None):
1181 for o in self.browse(cr, uid, ids, context=context):
1182 ir_values_obj = self.pool.get('ir.values')
1183 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1184 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1186 md = self.pool.get('ir.model.data')
1187 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1188 group_id = md.browse(cr, uid, group_id, context=context).res_id
1189 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1190 menu_id = md.browse(cr, uid, menu_id, context=context).res_id
1191 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1192 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1193 location_id = md.browse(cr, uid, location_id, context=context).res_id
1194 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1196 sale_config_picking_policy()
1198 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: