1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
25 from osv import fields, osv
27 from mx import DateTime
28 from tools import config
29 from tools.translate import _
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 'payment_account_id': fields.many2many('account.account','sale_shop_account','shop_id','account_id','Payment Accounts'),
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'),
44 def _incoterm_get(self, cr, uid, context={}):
45 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
48 class sale_order(osv.osv):
50 _description = "Sale Order"
51 def copy(self, cr, uid, id, default=None,context={}):
59 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
61 return super(sale_order, self).copy(cr, uid, id, default, context)
63 def _amount_line_tax(self, cr, uid, line, context={}):
65 for c in self.pool.get('account.tax').compute(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):
69 def _amount_all(self, cr, uid, ids, field_name, arg, context):
71 cur_obj=self.pool.get('res.currency')
72 for order in self.browse(cr, uid, ids):
74 'amount_untaxed': 0.0,
79 cur=order.pricelist_id.currency_id
80 for line in order.order_line:
81 val1 += line.price_subtotal
82 val += self._amount_line_tax(cr, uid, line, context)
83 res[order.id]['amount_tax']=cur_obj.round(cr, uid, cur, val)
84 res[order.id]['amount_untaxed']=cur_obj.round(cr, uid, cur, val1)
85 res[order.id]['amount_total']=res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
88 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
94 p.sale_id,sum(m.product_qty), m.state
98 stock_picking p on (p.id=m.picking_id)
100 p.sale_id in ('''+','.join(map(str,ids))+''')
101 GROUP BY m.state, p.sale_id''')
102 for oid,nbr,state in cr.fetchall():
106 res[oid][0] += nbr or 0.0
107 res[oid][1] += nbr or 0.0
109 res[oid][1] += nbr or 0.0
114 res[r] = 100.0 * res[r][0] / res[r][1]
115 for order in self.browse(cr, uid, ids, context):
117 res[order.id] = 100.0
120 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
122 for sale in self.browse(cursor, user, ids, context=context):
127 for invoice in sale.invoice_ids:
128 if invoice.state not in ('draft','cancel'):
129 tot += invoice.amount_untaxed
132 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
137 def _invoiced(self, cursor, user, ids, name, arg, context=None):
139 for sale in self.browse(cursor, user, ids, context=context):
141 for invoice in sale.invoice_ids:
142 if invoice.state <> 'paid':
145 if not sale.invoice_ids:
149 def _invoiced_search(self, cursor, user, obj, name, args):
157 clause += 'AND inv.state = \'paid\''
159 clause += 'AND inv.state <> \'paid\''
161 cursor.execute('SELECT rel.order_id ' \
162 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
163 'WHERE rel.invoice_id = inv.id ' + clause)
164 res = cursor.fetchall()
166 cursor.execute('SELECT sale.id ' \
167 'FROM sale_order AS sale ' \
168 'WHERE sale.id NOT IN ' \
169 '(SELECT rel.order_id ' \
170 'FROM sale_order_invoice_rel AS rel)')
171 res.extend(cursor.fetchall())
173 return [('id', '=', 0)]
174 return [('id', 'in', [x[0] for x in res])]
176 def _get_order(self, cr, uid, ids, context={}):
178 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
179 result[line.order_id.id] = True
183 'name': fields.char('Order Reference', size=64, required=True, select=True),
184 'shop_id':fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft':[('readonly',False)]}),
185 'origin': fields.char('Origin', size=64),
186 'client_order_ref': fields.char('Customer Ref',size=64),
188 'state': fields.selection([
189 ('draft','Quotation'),
190 ('waiting_date','Waiting Schedule'),
191 ('manual','Manual In Progress'),
192 ('progress','In Progress'),
193 ('shipping_except','Shipping Exception'),
194 ('invoice_except','Invoice Exception'),
196 ('cancel','Cancelled')
197 ], 'Order State', readonly=True, help="Gives the state of the quotation or sale order. The exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the packing list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the date 'Date Ordered'.", select=True),
198 'date_order':fields.date('Date Ordered', required=True, readonly=True, states={'draft':[('readonly',False)]}),
200 'user_id':fields.many2one('res.users', 'Salesman', states={'draft':[('readonly',False)]}, select=True),
201 'partner_id':fields.many2one('res.partner', 'Customer', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, select=True),
202 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
203 '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 that requested the order or quotation."),
204 'partner_shipping_id':fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
206 'incoterm': fields.selection(_incoterm_get, 'Incoterm',size=3),
207 'picking_policy': fields.selection([('direct','Partial Delivery'),('one','Complete Delivery')],
208 'Packing Policy', required=True, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
209 'order_policy': fields.selection([
210 ('prepaid','Payment Before Delivery'),
211 ('manual','Shipping & Manual Invoice'),
212 ('postpaid','Invoice on Order After Delivery'),
213 ('picking','Invoice from the Packing'),
214 ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]},
215 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
216 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
217 - The 'Shipping & Manual Invoice' will create the packing order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice.
218 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all packing lists have been finished.
219 - The 'Invoice from the packing' choice is used to create an invoice during the packing process."""),
220 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}),
221 'project_id':fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft':[('readonly', False)]}),
223 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}),
224 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoice', 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)."),
225 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Packing', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
226 'shipped':fields.boolean('Picked', readonly=True),
227 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
228 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
229 'invoiced': fields.function(_invoiced, method=True, string='Paid',
230 fnct_search=_invoiced_search, type='boolean'),
231 'note': fields.text('Notes'),
233 'amount_untaxed': fields.function(_amount_all, method=True, string='Untaxed Amount',
235 'sale.order': (lambda self, cr, uid, ids, c={}: ids, None, 10),
236 'sale.order.line': (_get_order, None, 10),
239 'amount_tax': fields.function(_amount_all, method=True, string='Taxes',
241 'sale.order': (lambda self, cr, uid, ids, c={}: ids, None, 10),
242 'sale.order.line': (_get_order, None, 10),
245 'amount_total': fields.function(_amount_all, method=True, string='Total',
247 'sale.order': (lambda self, cr, uid, ids, c={}: ids, None, 10),
248 'sale.order.line': (_get_order, None, 10),
252 '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 invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.",required=True),
253 'payment_term' : fields.many2one('account.payment.term', 'Payment Term'),
254 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
257 'picking_policy': lambda *a: 'direct',
258 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
259 'order_policy': lambda *a: 'manual',
260 'state': lambda *a: 'draft',
261 'user_id': lambda obj, cr, uid, context: uid,
262 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
263 'invoice_quantity': lambda *a: 'order',
264 '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'],
265 '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'],
266 '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'],
267 'pricelist_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').browse(cr, uid, context['partner_id']).property_product_pricelist.id,
272 def unlink(self, cr, uid, ids, context=None):
273 sale_orders = self.read(cr, uid, ids, ['state'])
275 for s in sale_orders:
276 if s['state'] in ['draft','cancel']:
277 unlink_ids.append(s['id'])
279 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
280 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
283 def onchange_shop_id(self, cr, uid, ids, shop_id):
286 shop=self.pool.get('sale.shop').browse(cr,uid,shop_id)
287 v['project_id']=shop.project_id.id
288 # Que faire si le client a une pricelist a lui ?
289 if shop.pricelist_id.id:
290 v['pricelist_id']=shop.pricelist_id.id
291 #v['payment_default_id']=shop.payment_default_id.id
294 def action_cancel_draft(self, cr, uid, ids, *args):
297 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
298 line_ids = map(lambda x: x[0], cr.fetchall())
299 self.write(cr, uid, ids, {'state':'draft', 'invoice_ids':[], 'shipped':0})
300 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced':False, 'state':'draft', 'invoice_lines':[(6,0,[])]})
301 wf_service = netsvc.LocalService("workflow")
303 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
306 def onchange_partner_id(self, cr, uid, ids, part):
308 return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False, 'payment_term' : False, 'fiscal_position': False}}
309 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact'])
310 part = self.pool.get('res.partner').browse(cr, uid, part)
311 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
312 payment_term = part.property_payment_term and part.property_payment_term.id or False
313 fiscal_position = part.property_account_position and part.property_account_position.id or False
314 val = {'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'payment_term' : payment_term, 'fiscal_position': fiscal_position}
316 val['pricelist_id'] = pricelist
319 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
323 if policy=='prepaid':
325 elif policy=='picking':
326 inv_qty = 'procurement'
327 return {'value':{'invoice_quantity':inv_qty}}
329 def write(self, cr, uid, ids, vals, context=None):
330 if vals.has_key('order_policy'):
331 if vals['order_policy']=='prepaid':
332 vals.update({'invoice_quantity':'order'})
333 elif vals['order_policy']=='picking':
334 vals.update({'invoice_quantity':'procurement'})
335 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
337 def create(self, cr, uid, vals, context={}):
338 if vals.has_key('order_policy'):
339 if vals['order_policy']=='prepaid':
340 vals.update({'invoice_quantity':'order'})
341 if vals['order_policy']=='picking':
342 vals.update({'invoice_quantity':'procurement'})
343 return super(sale_order, self).create(cr, uid, vals, context=context)
345 def button_dummy(self, cr, uid, ids, context={}):
348 #FIXME: the method should return the list of invoices created (invoice_ids)
349 # and not the id of the last invoice created (res). The problem is that we
350 # cannot change it directly since the method is called by the sale order
351 # workflow and I suppose it expects a single id...
352 def _inv_get(self, cr, uid, order, context={}):
355 def _make_invoice(self, cr, uid, order, lines,context={}):
356 a = order.partner_id.property_account_receivable.id
357 if order.payment_term:
358 pay_term = order.payment_term.id
361 for preinv in order.invoice_ids:
362 if preinv.state not in ('cancel',):
363 for preline in preinv.invoice_line:
364 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id':False, 'price_unit':-preline.price_unit})
365 lines.append(inv_line_id)
367 'name': order.client_order_ref or order.name,
368 'origin': order.name,
369 'type': 'out_invoice',
370 'reference': "P%dSO%d"%(order.partner_id.id,order.id),
372 'partner_id': order.partner_id.id,
373 'address_invoice_id': order.partner_invoice_id.id,
374 'address_contact_id': order.partner_invoice_id.id,
375 'invoice_line': [(6,0,lines)],
376 'currency_id' : order.pricelist_id.currency_id.id,
377 'comment': order.note,
378 'payment_term': pay_term,
379 'fiscal_position': order.partner_id.property_account_position.id
381 inv_obj = self.pool.get('account.invoice')
382 inv.update(self._inv_get(cr, uid, order))
383 inv_id = inv_obj.create(cr, uid, inv)
384 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id],
385 pay_term,time.strftime('%Y-%m-%d'))
386 if data.get('value',False):
387 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
388 inv_obj.button_compute(cr, uid, [inv_id])
392 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done','exception']):
397 for o in self.browse(cr,uid,ids):
399 for line in o.order_line:
400 if (line.state in states) and not line.invoiced:
401 lines.append(line.id)
402 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
404 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
407 for o in self.browse(cr, uid, ids):
408 for i in o.invoice_ids:
409 if i.state == 'draft':
411 picking_obj=self.pool.get('stock.picking')
412 for val in invoices.values():
414 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []))
416 self.write(cr, uid, [o.id], {'state' : 'progress'})
417 if o.order_policy=='picking':
418 picking_obj.write(cr,uid,map(lambda x:x.id,o.picking_ids),{'invoice_state':'invoiced'})
419 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
421 for order, il in val:
422 res = self._make_invoice(cr, uid, order, il)
423 invoice_ids.append(res)
424 self.write(cr, uid, [order.id], {'state' : 'progress'})
425 if order.order_policy=='picking':
426 picking_obj.write(cr,uid,map(lambda x:x.id,order.picking_ids),{'invoice_state':'invoiced'})
427 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
430 def action_invoice_cancel(self, cr, uid, ids, context={}):
431 for sale in self.browse(cr, uid, ids):
432 for line in sale.order_line:
434 for iline in line.invoice_lines:
435 if iline.invoice_id and iline.invoice_id.state == 'cancel':
439 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
440 self.write(cr, uid, ids, {'state':'invoice_except', 'invoice_ids':False})
444 def action_cancel(self, cr, uid, ids, context={}):
446 sale_order_line_obj = self.pool.get('sale.order.line')
447 for sale in self.browse(cr, uid, ids):
448 for pick in sale.picking_ids:
449 if pick.state not in ('draft','cancel'):
450 raise osv.except_osv(
451 _('Could not cancel sale order !'),
452 _('You must first cancel all packing attached to this sale order.'))
453 for r in self.read(cr,uid,ids,['picking_ids']):
454 for pick in r['picking_ids']:
455 wf_service = netsvc.LocalService("workflow")
456 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
457 for inv in sale.invoice_ids:
458 if inv.state not in ('draft','cancel'):
459 raise osv.except_osv(
460 _('Could not cancel this sale order !'),
461 _('You must first cancel all invoices attached to this sale order.'))
462 for r in self.read(cr,uid,ids,['invoice_ids']):
463 for inv in r['invoice_ids']:
464 wf_service = netsvc.LocalService("workflow")
465 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
466 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
468 self.write(cr,uid,ids,{'state':'cancel'})
471 def action_wait(self, cr, uid, ids, *args):
472 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
473 event_obj = self.pool.get('res.partner.event')
474 for o in self.browse(cr, uid, ids):
476 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
477 'partner_id': o.partner_id.id,\
478 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
479 'user_id': (o.user_id and o.user_id.id) or uid,\
480 'partner_type': 'customer', 'probability': 1.0,\
481 'planned_revenue': o.amount_untaxed})
482 if (o.order_policy == 'manual'):
483 self.write(cr, uid, [o.id], {'state': 'manual'})
485 self.write(cr, uid, [o.id], {'state': 'progress'})
486 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
488 def procurement_lines_get(self, cr, uid, ids, *args):
490 for order in self.browse(cr, uid, ids, context={}):
491 for line in order.order_line:
492 if line.procurement_id:
493 res.append(line.procurement_id.id)
496 # if mode == 'finished':
497 # returns True if all lines are done, False otherwise
498 # if mode == 'canceled':
499 # returns True if there is at least one canceled line, False otherwise
500 def test_state(self, cr, uid, ids, mode, *args):
501 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
506 write_cancel_ids = []
507 for order in self.browse(cr, uid, ids, context={}):
508 for line in order.order_line:
509 if (not line.procurement_id) or (line.procurement_id.state=='done'):
511 if line.state != 'done':
512 write_done_ids.append(line.id)
515 if line.procurement_id:
516 if (line.procurement_id.state == 'cancel'):
518 if line.state != 'exception':
519 write_cancel_ids.append(line.id)
524 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
526 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
530 elif mode=='canceled':
536 def action_ship_create(self, cr, uid, ids, *args):
538 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
539 for order in self.browse(cr, uid, ids, context={}):
540 output_id = order.shop_id.warehouse_id.lot_output_id.id
542 for line in order.order_line:
544 date_planned = DateTime.now() + DateTime.RelativeDateTime(days=line.delay or 0.0)
545 date_planned = (date_planned - DateTime.RelativeDateTime(days=company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
546 if line.state == 'done':
548 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
549 location_id = order.shop_id.warehouse_id.lot_stock_id.id
551 loc_dest_id = order.partner_id.property_stock_customer.id
552 picking_id = self.pool.get('stock.picking').create(cr, uid, {
553 'origin': order.name,
556 'move_type': order.picking_policy,
558 'address_id': order.partner_shipping_id.id,
560 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
564 move_id = self.pool.get('stock.move').create(cr, uid, {
565 'name': line.name[:64],
566 'picking_id': picking_id,
567 'product_id': line.product_id.id,
568 'date_planned': date_planned,
569 'product_qty': line.product_uom_qty,
570 'product_uom': line.product_uom.id,
571 'product_uos_qty': line.product_uos_qty,
572 'product_uos': (line.product_uos and line.product_uos.id)\
573 or line.product_uom.id,
574 'product_packaging' : line.product_packaging.id,
575 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id,
576 'location_id': location_id,
577 'location_dest_id': output_id,
578 'sale_line_id': line.id,
579 'tracking_id': False,
584 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
586 'origin': order.name,
587 'date_planned': date_planned,
588 'product_id': line.product_id.id,
589 'product_qty': line.product_uom_qty,
590 'product_uom': line.product_uom.id,
591 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
592 or line.product_uom_qty,
593 'product_uos': (line.product_uos and line.product_uos.id)\
594 or line.product_uom.id,
595 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
596 'procure_method': line.type,
598 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
600 wf_service = netsvc.LocalService("workflow")
601 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
602 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
603 elif line.product_id and line.product_id.product_tmpl_id.type=='service':
604 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
606 'origin': order.name,
607 'date_planned': date_planned,
608 'product_id': line.product_id.id,
609 'product_qty': line.product_uom_qty,
610 'product_uom': line.product_uom.id,
611 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
612 'procure_method': line.type,
613 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
615 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
616 wf_service = netsvc.LocalService("workflow")
617 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
620 # No procurement because no product in the sale.order.line.
626 wf_service = netsvc.LocalService("workflow")
627 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
629 if order.state=='shipping_except':
630 val['state'] = 'progress'
632 if (order.order_policy == 'manual'):
633 for line in order.order_line:
634 if (not line.invoiced) and (line.state not in ('cancel','draft')):
635 val['state'] = 'manual'
637 self.write(cr, uid, [order.id], val)
641 def action_ship_end(self, cr, uid, ids, context={}):
642 for order in self.browse(cr, uid, ids):
643 val = {'shipped':True}
644 if order.state=='shipping_except':
645 val['state'] = 'progress'
646 if (order.order_policy == 'manual'):
647 for line in order.order_line:
648 if (not line.invoiced) and (line.state not in ('cancel','draft')):
649 val['state'] = 'manual'
651 for line in order.order_line:
653 if line.state=='exception':
654 towrite.append(line.id)
656 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state':'done'},context=context)
657 self.write(cr, uid, [order.id], val)
660 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
661 invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed'])
663 part=inv['partner_id'] and inv['partner_id'][0]
664 pr = inv['amount_untaxed'] or 0.0
665 partnertype = 'customer'
667 self.pool.get('res.partner.event').create(cr, uid, {'name':'Order: '+name, 'som':False, 'description':'Order '+str(inv['id']), 'document':'', 'partner_id':part, 'date':time.strftime('%Y-%m-%d'), 'canal_id':False, 'user_id':uid, 'partner_type':partnertype, 'probability':1.0, 'planned_revenue':pr, 'planned_cost':0.0, 'type':eventtype})
669 def has_stockable_products(self,cr, uid, ids, *args):
670 for order in self.browse(cr, uid, ids):
671 for order_line in order.order_line:
672 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
677 # TODO add a field price_unit_uos
678 # - update it on change product and unit price
679 # - use it in report if there is a uos
680 class sale_order_line(osv.osv):
681 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
683 for line in self.browse(cr, uid, ids):
684 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
687 def _amount_line(self, cr, uid, ids, field_name, arg, context):
689 cur_obj=self.pool.get('res.currency')
690 for line in self.browse(cr, uid, ids):
691 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
692 cur = line.order_id.pricelist_id.currency_id
693 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
696 def _number_packages(self, cr, uid, ids, field_name, arg, context):
698 for line in self.browse(cr, uid, ids):
700 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
705 _name = 'sale.order.line'
706 _description = 'Sale Order line'
708 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
709 'name': fields.char('Description', size=256, required=True, select=True),
710 'sequence': fields.integer('Sequence'),
711 'delay': fields.float('Delivery Delay', required=True),
712 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True),
713 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines', readonly=True),
714 'invoiced': fields.boolean('Invoiced', readonly=True),
715 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
716 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
717 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
718 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
719 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
720 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True),
721 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
722 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'),
723 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
724 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
725 'product_uos_qty': fields.float('Quantity (UoS)'),
726 'product_uos': fields.many2one('product.uom', 'Product UoS'),
727 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
728 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
729 'discount': fields.float('Discount (%)', digits=(16,2)),
730 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
731 'notes': fields.text('Notes'),
732 'th_weight' : fields.float('Weight'),
733 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Cancelled'),('exception','Exception')], 'Status', required=True, readonly=True),
734 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
736 _order = 'sequence, id'
738 'discount': lambda *a: 0.0,
739 'delay': lambda *a: 0.0,
740 'product_uom_qty': lambda *a: 1,
741 'product_uos_qty': lambda *a: 1,
742 'sequence': lambda *a: 10,
743 'invoiced': lambda *a: 0,
744 'state': lambda *a: 'draft',
745 'type': lambda *a: 'make_to_stock',
746 'product_packaging': lambda *a: False
748 def invoice_line_create(self, cr, uid, ids, context={}):
749 def _get_line_qty(line):
750 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
752 return line.product_uos_qty or 0.0
753 return line.product_uom_qty
755 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
756 line.procurement_id.id, context)
758 def _get_line_uom(line):
759 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
761 return line.product_uos.id
762 return line.product_uom.id
764 return self.pool.get('mrp.procurement').uom_get(cr, uid,
765 line.procurement_id.id, context)
769 for line in self.browse(cr, uid, ids, context):
770 if not line.invoiced:
772 a = line.product_id.product_tmpl_id.property_account_income.id
774 a = line.product_id.categ_id.property_account_income_categ.id
776 raise osv.except_osv(_('Error !'),
777 _('There is no income account defined ' \
778 'for this product: "%s" (id:%d)') % \
779 (line.product_id.name, line.product_id.id,))
781 a = self.pool.get('ir.property').get(cr, uid,
782 'property_account_income_categ', 'product.category',
784 uosqty = _get_line_qty(line)
785 uos_id = _get_line_uom(line)
788 pu = round(line.price_unit * line.product_uom_qty / uosqty,
789 int(config['price_accuracy']))
790 fpos = line.order_id.fiscal_position or False
791 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
792 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
794 'origin':line.order_id.name,
798 'discount': line.discount,
800 'product_id': line.product_id.id or False,
801 'invoice_line_tax_id': [(6,0,[x.id for x in line.tax_id])],
803 'account_analytic_id': line.order_id.project_id.id,
805 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
806 self.write(cr, uid, [line.id], {'invoiced':True})
808 sales[line.order_id.id] = True
809 create_ids.append(inv_id)
811 # Trigger workflow events
812 wf_service = netsvc.LocalService("workflow")
813 for sid in sales.keys():
814 wf_service.trg_write(uid, 'sale.order', sid, cr)
817 def button_cancel(self, cr, uid, ids, context={}):
818 for line in self.browse(cr, uid, ids, context=context):
820 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
821 return self.write(cr, uid, ids, {'state':'cancel'})
823 def button_confirm(self, cr, uid, ids, context={}):
824 return self.write(cr, uid, ids, {'state':'confirmed'})
826 def button_done(self, cr, uid, ids, context={}):
827 wf_service = netsvc.LocalService("workflow")
828 res = self.write(cr, uid, ids, {'state':'done'})
829 for line in self.browse(cr,uid,ids,context):
830 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
834 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
835 product_obj = self.pool.get('product.product')
837 return {'value': {'product_uom': product_uos,
838 'product_uom_qty': product_uos_qty}, 'domain':{}}
840 product = product_obj.browse(cr, uid, product_id)
842 'product_uom' : product.uom_id.id,
844 # FIXME must depend on uos/uom of the product and not only of the coeff.
847 'product_uom_qty' : product_uos_qty / product.uos_coeff,
848 'th_weight' : product_uos_qty / product.uos_coeff * product.weight
850 except ZeroDivisionError:
852 return {'value' : value}
854 def copy_data(self, cr, uid, id, default=None,context={}):
857 default.update({'state':'draft', 'move_ids':[], 'invoiced':False, 'invoice_lines':[]})
858 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
860 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
861 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
862 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
864 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.'))
866 product_uom_obj = self.pool.get('product.uom')
867 partner_obj = self.pool.get('res.partner')
868 product_obj = self.pool.get('product.product')
870 lang = partner_obj.browse(cr, uid, partner_id).lang
871 context = {'lang': lang, 'partner_id': partner_id}
874 return {'value': {'th_weight' : 0, 'product_packaging': False,
875 'product_uos_qty': qty}, 'domain': {'product_uom': [],
879 date_order = time.strftime('%Y-%m-%d')
882 product_obj = product_obj.browse(cr, uid, product, context=context)
883 if not packaging and product_obj.packaging:
884 packaging = product_obj.packaging[0].id
885 result['product_packaging'] = packaging
888 default_uom = product_obj.uom_id and product_obj.uom_id.id
889 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
890 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
891 # qty = qty - qty % q + q
892 if qty and (q and not (qty % q) == 0):
896 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)
897 warn_msg = warn_msg + "\n\n"+_("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
899 'title':_('Packing Information !'),
902 result['product_uom_qty'] = qty
905 uom2 = product_uom_obj.browse(cr, uid, uom)
906 if product_obj.uom_id.category_id.id <> uom2.category_id.id:
910 if product_obj.uos_id:
911 uos2 = product_uom_obj.browse(cr, uid, uos)
912 if product_obj.uos_id.category_id.id <> uos2.category_id.id:
916 result.update({'type': product_obj.procure_method})
917 if product_obj.description_sale:
918 result['notes'] = product_obj.description_sale
919 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
920 if update_tax: #The quantity only have changed
921 result['delay'] = (product_obj.sale_delay or 0.0)
922 partner = partner_obj.browse(cr, uid, partner_id)
923 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
925 result['name'] = product_obj.partner_ref
927 if (not uom) and (not uos):
928 result['product_uom'] = product_obj.uom_id.id
929 if product_obj.uos_id:
930 result['product_uos'] = product_obj.uos_id.id
931 result['product_uos_qty'] = qty * product_obj.uos_coeff
932 uos_category_id = product_obj.uos_id.category_id.id
934 result['product_uos'] = False
935 result['product_uos_qty'] = qty
936 uos_category_id = False
937 result['th_weight'] = qty * product_obj.weight
938 domain = {'product_uom':
939 [('category_id', '=', product_obj.uom_id.category_id.id)],
941 [('category_id', '=', uos_category_id)]}
943 elif uos: # only happens if uom is False
944 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
945 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
946 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
947 elif uom: # whether uos is set or not
948 default_uom = product_obj.uom_id and product_obj.uom_id.id
949 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
950 if product_obj.uos_id:
951 result['product_uos'] = product_obj.uos_id.id
952 result['product_uos_qty'] = qty * product_obj.uos_coeff
954 result['product_uos'] = False
955 result['product_uos_qty'] = qty
956 result['th_weight'] = q * product_obj.weight # Round the quantity up
962 'title':'No Pricelist !',
964 'You have to select a pricelist in the sale form !\n'
965 'Please set one before choosing a product.'
968 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
969 product, qty or 1.0, partner_id, {
975 'title':'No valid pricelist line found !',
977 "Couldn't find a pricelist line matching this product and quantity.\n"
978 "You have to change either the product, the quantity or the pricelist."
981 result.update({'price_unit': price})
982 return {'value': result, 'domain': domain,'warning':warning}
984 def product_uom_change(self, cursor, user, 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):
987 res = self.product_id_change(cursor, user, ids, pricelist, product,
988 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
989 partner_id=partner_id, lang=lang, update_tax=update_tax,
990 date_order=date_order)
991 if 'product_uom' in res['value']:
992 del res['value']['product_uom']
994 res['value']['price_unit'] = 0.0
997 def unlink(self, cr, uid, ids, context={}):
998 """Allows to delete sale order lines in draft,cancel states"""
999 for rec in self.browse(cr, uid, ids, context=context):
1000 if rec.state not in ['draft','cancel']:
1001 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1002 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1007 class sale_config_picking_policy(osv.osv_memory):
1008 _name='sale.config.picking_policy'
1010 'name':fields.char('Name', size=64),
1011 'picking_policy': fields.selection([
1012 ('direct','Direct Delivery'),
1013 ('one','All at Once')
1014 ], 'Packing Default Policy', required=True ),
1015 'order_policy': fields.selection([
1016 ('manual','Invoice Based on Sales Orders'),
1017 ('picking','Invoice Based on Deliveries'),
1018 ], 'Shipping Default Policy', required=True),
1019 'step': fields.selection([
1020 ('one','Delivery Order Only'),
1021 ('two','Packing List & Delivery Order')
1022 ], 'Steps To Deliver a Sale Order', required=True,
1023 help="By default, Open ERP is able to manage complex routing and paths "\
1024 "of products in your warehouse and partner locations. This will configure "\
1025 "the most common and simple methods to deliver products to the customer "\
1026 "in one or two operations by the worker.")
1029 'picking_policy': lambda *a: 'direct',
1030 'order_policy': lambda *a: 'picking',
1031 'step': lambda *a: 'one'
1033 def set_default(self, cr, uid, ids, context=None):
1034 for o in self.browse(cr, uid, ids, context=context):
1035 ir_values_obj = self.pool.get('ir.values')
1036 ir_values_obj.set(cr,uid,'default',False,'picking_policy',['sale.order'],o.picking_policy)
1037 ir_values_obj.set(cr,uid,'default',False,'order_policy',['sale.order'],o.order_policy)
1040 md = self.pool.get('ir.model.data')
1041 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1042 group_id = md.browse(cr, uid, group_id, context).res_id
1043 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1044 menu_id = md.browse(cr, uid, menu_id, context).res_id
1045 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id':[(6,0,[group_id])]})
1047 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1048 location_id = md.browse(cr, uid, location_id, context).res_id
1049 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing':'transparent'})
1052 'view_type': 'form',
1053 "view_mode": 'form',
1054 'res_model': 'ir.actions.configuration.wizard',
1055 'type': 'ir.actions.act_window',
1058 def action_cancel(self,cr,uid,ids,conect=None):
1060 'view_type': 'form',
1061 "view_mode": 'form',
1062 'res_model': 'ir.actions.configuration.wizard',
1063 'type': 'ir.actions.act_window',
1066 sale_config_picking_policy()
1069 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: