1 ##############################################################################
3 # Copyright (c) 2004-2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 # $Id: sale.py 1005 2005-07-25 08:41:42Z nicoe $
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 ##############################################################################
32 from osv import fields, osv
34 from mx import DateTime
35 from tools import config
37 class sale_shop(osv.osv):
39 _description = "Sale Shop"
41 'name': fields.char('Shop name',size=64, required=True),
42 'payment_default_id': fields.many2one('account.payment.term','Default Payment Term',required=True),
43 'payment_account_id': fields.many2many('account.account','sale_shop_account','shop_id','account_id','Payment accounts'),
44 'warehouse_id': fields.many2one('stock.warehouse','Warehouse'),
45 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
46 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
50 def _incoterm_get(self, cr, uid, context={}):
51 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
54 class sale_order(osv.osv):
56 _description = "Sale Order"
57 def copy(self, cr, uid, id, default=None,context={}):
66 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
68 return super(sale_order, self).copy(cr, uid, id, default, context)
70 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
72 cur_obj=self.pool.get('res.currency')
73 for sale in self.browse(cr, uid, ids):
75 for line in sale.order_line:
76 res[sale.id] += line.price_subtotal
77 cur = sale.pricelist_id.currency_id
78 res[sale.id] = cur_obj.round(cr, uid, cur, res[sale.id])
81 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
83 cur_obj=self.pool.get('res.currency')
84 for order in self.browse(cr, uid, ids):
86 cur=order.pricelist_id.currency_id
87 for line in order.order_line:
88 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, order.partner_invoice_id.id, line.product_id, order.partner_id):
89 val+= cur_obj.round(cr, uid, cur, c['amount'])
90 res[order.id]=cur_obj.round(cr, uid, cur, val)
93 def _amount_total(self, cr, uid, ids, field_name, arg, context):
95 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
96 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
97 cur_obj=self.pool.get('res.currency')
99 order=self.browse(cr, uid, [id])[0]
100 cur=order.pricelist_id.currency_id
101 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
105 'name': fields.char('Order Description', size=64, required=True, select=True),
106 'shop_id':fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft':[('readonly',False)]}),
107 'origin': fields.char('Origin', size=64),
108 'client_order_ref': fields.char('Partner Ref.',size=64),
110 'state': fields.selection([
111 ('draft','Quotation'),
112 ('waiting_date','Waiting Schedule'),
113 ('manual','Manual in progress'),
114 ('progress','In progress'),
115 ('shipping_except','Shipping Exception'),
116 ('invoice_except','Invoice Exception'),
119 ], '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 be on the date 'Date Ordered'.", select=True),
120 'date_order':fields.date('Date Ordered', required=True, readonly=True, states={'draft':[('readonly',False)]}),
122 'user_id':fields.many2one('res.users', 'Salesman', states={'draft':[('readonly',False)]}, select=True),
123 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, select=True),
124 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
125 '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."),
126 'partner_shipping_id':fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
128 'incoterm': fields.selection(_incoterm_get, 'Incoterm',size=3),
129 'picking_policy': fields.selection([('direct','Direct Delivery'),('one','All at once')], 'Packing Policy', required=True ),
130 'order_policy': fields.selection([
131 ('prepaid','Invoice before delivery'),
132 ('manual','Shipping & Manual Invoice'),
133 ('postpaid','Automatic Invoice after delivery'),
134 ('picking','Invoice from the packings'),
135 ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]},
136 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
137 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
138 - 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.
139 - The 'Invoice after delivery' choice will generate the draft invoice after the packing list have been finished.
140 - The 'Invoice from the packings' choice is used to create an invoice during the packing process."""),
141 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}),
142 'project_id':fields.many2one('account.analytic.account', 'Analytic account', readonly=True, states={'draft':[('readonly', False)]}),
144 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}),
145 '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)."),
146 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Packing List', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
148 'shipped':fields.boolean('Picked', readonly=True),
149 'invoiced':fields.boolean('Paid', readonly=True),
151 'note': fields.text('Notes'),
153 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
154 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
155 'amount_total': fields.function(_amount_total, method=True, string='Total'),
156 '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."),
159 'picking_policy': lambda *a: 'direct',
160 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
161 'order_policy': lambda *a: 'manual',
162 'state': lambda *a: 'draft',
163 'user_id': lambda obj, cr, uid, context: uid,
164 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
165 'invoice_quantity': lambda *a: 'order',
166 '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'],
167 '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'],
168 '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'],
169 '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,
174 def onchange_shop_id(self, cr, uid, ids, shop_id):
177 shop=self.pool.get('sale.shop').browse(cr,uid,shop_id)
178 v['project_id']=shop.project_id.id
179 # Que faire si le client a une pricelist a lui ?
180 if shop.pricelist_id.id:
181 v['pricelist_id']=shop.pricelist_id.id
182 #v['payment_default_id']=shop.payment_default_id.id
185 def action_cancel_draft(self, cr, uid, ids, *args):
188 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
189 line_ids = map(lambda x: x[0], cr.fetchall())
190 self.write(cr, uid, ids, {'state':'draft', 'invoice_ids':[], 'shipped':0, 'invoiced':0})
191 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced':False, 'state':'draft', 'invoice_lines':[(6,0,[])]})
192 wf_service = netsvc.LocalService("workflow")
194 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
197 def onchange_partner_id(self, cr, uid, ids, part):
199 return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False}}
200 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact'])
201 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
202 return {'value':{'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'pricelist_id': pricelist}}
204 def button_dummy(self, cr, uid, ids, context={}):
207 #FIXME: the method should return the list of invoices created (invoice_ids)
208 # and not the id of the last invoice created (res). The problem is that we
209 # cannot change it directly since the method is called by the sale order
210 # workflow and I suppose it expects a single id...
211 def _inv_get(self, cr, uid, order, context={}):
214 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done']):
219 def make_invoice(order, lines):
220 a = order.partner_id.property_account_receivable.id
221 if order.partner_id and order.partner_id.property_payment_term.id:
222 pay_term = order.partner_id.property_payment_term.id
225 for preinv in order.invoice_ids:
226 if preinv.state in ('open','paid','proforma'):
227 for preline in preinv.invoice_line:
228 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id':False, 'price_unit':-preline.price_unit})
229 lines.append(inv_line_id)
232 'origin': order.name,
233 'type': 'out_invoice',
234 'reference': "P%dSO%d"%(order.partner_id.id,order.id),
236 'partner_id': order.partner_id.id,
237 'address_invoice_id': order.partner_invoice_id.id,
238 'address_contact_id': order.partner_invoice_id.id,
239 'invoice_line': [(6,0,lines)],
240 'currency_id' : order.pricelist_id.currency_id.id,
241 'comment': order.note,
242 'payment_term': pay_term,
244 inv.update(self._inv_get(cr, uid, order))
245 inv_obj = self.pool.get('account.invoice')
246 inv_id = inv_obj.create(cr, uid, inv)
247 inv_obj.button_compute(cr, uid, [inv_id])
250 for o in self.browse(cr,uid,ids):
252 for line in o.order_line:
253 if (line.state in states) and not line.invoiced:
254 lines.append(line.id)
255 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
257 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
260 for o in self.browse(cr, uid, ids):
261 for i in o.invoice_ids:
262 if i.state == 'draft':
265 for val in invoices.values():
267 res = make_invoice(val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []))
269 self.write(cr, uid, [o.id], {'state' : 'progress'})
270 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
272 for order, il in val:
273 res = make_invoice(order, il)
274 invoice_ids.append(res)
275 self.write(cr, uid, [order.id], {'state' : 'progress'})
276 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (order.id, res))
279 def action_invoice_cancel(self, cr, uid, ids, context={}):
280 for sale in self.browse(cr, uid, ids):
281 for line in sale.order_line:
283 for iline in line.invoice_lines:
284 if iline.invoice_id and iline.invoice_id.state == 'cancel':
288 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
289 self.write(cr, uid, ids, {'state':'invoice_except', 'invoice_id':False})
293 def action_cancel(self, cr, uid, ids, context={}):
295 sale_order_line_obj = self.pool.get('sale.order.line')
296 for sale in self.browse(cr, uid, ids):
297 for pick in sale.picking_ids:
298 if pick.state not in ('draft','cancel'):
299 raise osv.except_osv(
300 'Could not cancel sale order !',
301 'You must first cancel all packings attached to this sale order.')
302 for r in self.read(cr,uid,ids,['picking_ids']):
303 for pick in r['picking_ids']:
304 wf_service = netsvc.LocalService("workflow")
305 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
306 for inv in sale.invoice_ids:
307 if inv.state not in ('draft','cancel'):
308 raise osv.except_osv(
309 'Could not cancel this sale order !',
310 'You must first cancel all invoices attached to this sale order.')
311 for r in self.read(cr,uid,ids,['invoice_ids']):
312 for inv in r['invoice_ids']:
313 wf_service = netsvc.LocalService("workflow")
314 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
315 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
317 self.write(cr,uid,ids,{'state':'cancel'})
320 def action_wait(self, cr, uid, ids, *args):
321 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
322 event_obj = self.pool.get('res.partner.event')
323 for o in self.browse(cr, uid, ids):
325 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
326 'partner_id': o.partner_id.id,\
327 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
328 'user_id': (o.user_id and o.user_id.id) or uid,\
329 'partner_type': 'customer', 'probability': 1.0,\
330 'planned_revenue': o.amount_untaxed})
331 if (o.order_policy == 'manual') and (not o.invoice_ids):
332 self.write(cr, uid, [o.id], {'state': 'manual'})
334 self.write(cr, uid, [o.id], {'state': 'progress'})
335 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
337 def procurement_lines_get(self, cr, uid, ids, *args):
339 for order in self.browse(cr, uid, ids, context={}):
340 for line in order.order_line:
341 if line.procurement_id:
342 res.append(line.procurement_id.id)
345 # if mode == 'finished':
346 # returns True if all lines are done, False otherwise
347 # if mode == 'canceled':
348 # returns True if there is at least one canceled line, False otherwise
349 def test_state(self, cr, uid, ids, mode, *args):
350 assert mode in ('finished', 'canceled'), "invalid mode for test_state"
354 write_cancel_ids = []
355 for order in self.browse(cr, uid, ids, context={}):
356 for line in order.order_line:
357 if line.procurement_id and (line.procurement_id.state != 'done') and (line.state!='done'):
359 if line.procurement_id and line.procurement_id.state == 'cancel':
361 # if a line is finished (ie its procuremnt is done or it has not procuremernt and it
362 # is not already marked as done, mark it as being so...
363 if ((not line.procurement_id) or line.procurement_id.state == 'done') and line.state != 'done':
364 write_done_ids.append(line.id)
365 # ... same for canceled lines
366 if line.procurement_id and line.procurement_id.state == 'cancel' and line.state != 'cancel':
367 write_cancel_ids.append(line.id)
369 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
371 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'cancel'})
375 elif mode=='canceled':
378 def action_ship_create(self, cr, uid, ids, *args):
380 for order in self.browse(cr, uid, ids, context={}):
381 output_id = order.shop_id.warehouse_id.lot_output_id.id
383 for line in order.order_line:
385 date_planned = (DateTime.now() + DateTime.RelativeDateTime(days=line.delay or 0.0)).strftime('%Y-%m-%d')
386 if line.state == 'done':
388 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
389 location_id = order.shop_id.warehouse_id.lot_stock_id.id
391 loc_dest_id = order.partner_id.property_stock_customer.id
392 picking_id = self.pool.get('stock.picking').create(cr, uid, {
393 'origin': order.name,
396 'move_type': order.picking_policy,
397 'loc_move_id': loc_dest_id,
399 'address_id': order.partner_shipping_id.id,
401 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
405 move_id = self.pool.get('stock.move').create(cr, uid, {
407 'picking_id': picking_id,
408 'product_id': line.product_id.id,
409 'date_planned': date_planned,
410 'product_qty': line.product_uom_qty,
411 'product_uom': line.product_uom.id,
412 'product_uos_qty': line.product_uos_qty,
413 'product_uos': (line.product_uos and line.product_uos.id)\
414 or line.product_uom.id,
415 'product_packaging' : line.product_packaging.id,
416 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id,
417 'location_id': location_id,
418 'location_dest_id': output_id,
419 'sale_line_id': line.id,
420 'tracking_id': False,
424 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
426 'origin': order.name,
427 'date_planned': date_planned,
428 'product_id': line.product_id.id,
429 'product_qty': line.product_uom_qty,
430 'product_uom': line.product_uom.id,
431 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
432 'procure_method': line.type,
435 wf_service = netsvc.LocalService("workflow")
436 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
437 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
438 elif line.product_id and line.product_id.product_tmpl_id.type=='service':
439 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
441 'origin': order.name,
442 'date_planned': date_planned,
443 'product_id': line.product_id.id,
444 'product_qty': line.product_uom_qty,
445 'product_uom': line.product_uom.id,
446 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
447 'procure_method': line.type,
449 wf_service = netsvc.LocalService("workflow")
450 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
451 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
454 # No procurement because no product in the sale.order.line.
460 wf_service = netsvc.LocalService("workflow")
461 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
462 #val = {'picking_ids':[(6,0,[picking_id])]}
464 if order.state=='shipping_except':
465 val['state'] = 'progress'
466 if (order.order_policy == 'manual') and order.invoice_ids:
467 val['state'] = 'manual'
468 self.write(cr, uid, [order.id], val)
471 def action_ship_end(self, cr, uid, ids, context={}):
472 for order in self.browse(cr, uid, ids):
473 val = {'shipped':True}
474 if order.state=='shipping_except':
475 if (order.order_policy=='manual') and not order.invoice_ids:
476 val['state'] = 'manual'
478 val['state'] = 'progress'
479 self.write(cr, uid, [order.id], val)
482 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
483 invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed'])
485 part=inv['partner_id'] and inv['partner_id'][0]
486 pr = inv['amount_untaxed'] or 0.0
487 partnertype = 'customer'
489 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})
491 def has_stockable_products(self,cr, uid, ids, *args):
492 for order in self.browse(cr, uid, ids):
493 for order_line in order.order_line:
494 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
499 # TODO add a field price_unit_uos
500 # - update it on change product and unit price
501 # - use it in report if there is a uos
502 class sale_order_line(osv.osv):
503 def copy(self, cr, uid, id, default=None, context={}):
504 if not default: default = {}
505 default.update( {'invoice_lines':[]})
506 return super(sale_order_line, self).copy(cr, uid, id, default, context)
508 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
510 for line in self.browse(cr, uid, ids):
511 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
514 def _amount_line(self, cr, uid, ids, field_name, arg, context):
516 cur_obj=self.pool.get('res.currency')
517 for line in self.browse(cr, uid, ids):
518 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
519 cur = line.order_id.pricelist_id.currency_id
520 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
523 def _number_packages(self, cr, uid, ids, field_name, arg, context):
525 for line in self.browse(cr, uid, ids):
526 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
529 def _get_1st_packaging(self, cr, uid, context={}):
530 cr.execute('select id from product_packaging order by id asc limit 1')
536 _name = 'sale.order.line'
537 _description = 'Sale Order line'
539 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
540 'name': fields.char('Description', size=256, required=True, select=True),
541 'sequence': fields.integer('Sequence'),
542 'delay': fields.float('Delivery Delay', required=True),
543 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True),
544 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines', readonly=True),
545 'invoiced': fields.boolean('Invoiced', readonly=True),
546 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
547 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
548 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
549 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
550 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
551 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True),
552 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
553 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'),
554 'product_uom_qty': fields.float('Quantity (UOM)', digits=(16,2), required=True),
555 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
556 'product_uos_qty': fields.float('Quantity (UOS)'),
557 'product_uos': fields.many2one('product.uom', 'Product UOS'),
558 'product_packaging': fields.many2one('product.packaging', 'Packaging used'),
559 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
560 'discount': fields.float('Discount (%)', digits=(16,2)),
561 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number packages'),
562 'notes': fields.text('Notes'),
563 'th_weight' : fields.float('Weight'),
564 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'State', required=True, readonly=True),
568 'discount': lambda *a: 0.0,
569 'delay': lambda *a: 0.0,
570 'product_uom_qty': lambda *a: 1,
571 'product_uos_qty': lambda *a: 1,
572 'sequence': lambda *a: 10,
573 'invoiced': lambda *a: 0,
574 'state': lambda *a: 'draft',
575 'type': lambda *a: 'make_to_stock',
576 'product_packaging': _get_1st_packaging,
578 def invoice_line_create(self, cr, uid, ids, context={}):
579 def _get_line_qty(line):
580 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
581 return line.product_uos_qty or line.product_uom_qty
583 return self.pool.get('mrp.procurement').quantity_get(cr, uid, line.procurement_id.id, context)
585 for line in self.browse(cr, uid, ids, context):
586 if not line.invoiced:
588 a = line.product_id.product_tmpl_id.property_account_income.id
590 a = line.product_id.categ_id.property_account_income_categ.id
592 raise osv.except_osv('Error !', 'There is no income account defined for this product: "%s" (id:%d)' % (line.product_id.name, line.product_id.id,))
594 a = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
595 uosqty = _get_line_qty(line)
596 uos_id = (line.product_uos and line.product_uos.id) or line.product_uom.id
598 if line.product_uos_qty:
599 pu = round(pu * line.product_uom_qty / line.product_uos_qty, int(config['price_accuracy']))
600 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
605 'discount': line.discount,
607 'product_id': line.product_id.id or False,
608 'invoice_line_tax_id': [(6,0,[x.id for x in line.tax_id])],
610 'account_analytic_id': line.order_id.project_id.id,
612 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%d,%d)', (line.id, inv_id))
613 self.write(cr, uid, [line.id], {'invoiced':True})
614 create_ids.append(inv_id)
617 def button_confirm(self, cr, uid, ids, context={}):
618 return self.write(cr, uid, ids, {'state':'confirmed'})
620 def button_done(self, cr, uid, ids, context={}):
621 wf_service = netsvc.LocalService("workflow")
622 res = self.write(cr, uid, ids, {'state':'done'})
623 for line in self.browse(cr,uid,ids,context):
624 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
628 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
629 product_obj = self.pool.get('product.product')
631 return {'value': {'product_uom': product_uos,
632 'product_uom_qty': product_uos_qty}, 'domain':{}}
634 product = product_obj.browse(cr, uid, product_id)
636 'product_uom' : product.uom_id,
638 # FIXME must depend on uos/uom of the product and not only of the coeff.
641 'product_uom_qty' : product_uos_qty / product.uos_coeff,
642 'th_weight' : product_uos_qty / product.uos_coeff * product.weight
644 except ZeroDivisionError:
646 return {'value' : value}
648 def copy(self, cr, uid, id, default=None,context={}):
651 default.update({'state':'draft', 'move_ids':[], 'invoiced':False, 'invoice_lines':[]})
652 return super(sale_order_line, self).copy(cr, uid, id, default, context)
654 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
655 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
656 lang=False, update_tax=True):
657 product_uom_obj = self.pool.get('product.uom')
658 partner_obj = self.pool.get('res.partner')
659 product_obj = self.pool.get('product.product')
662 lang = partner_obj.browse(cr, uid, partner_id).lang
663 context = {'lang': lang}
666 return {'value': {'price_unit': 0.0, 'notes':'', 'th_weight' : 0,
667 'product_uos_qty': qty}, 'domain': {'product_uom': []}}
670 raise osv.except_osv('No Pricelist !',
671 'You have to select a pricelist in the sale form !\n'
672 'Please set one before choosing a product.')
674 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
675 product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
677 raise osv.except_osv('No valid pricelist line found !',
678 "Couldn't find a pricelist line matching this product and quantity.\n"
679 "You have to change either the product, the quantity or the pricelist.")
681 product = product_obj.browse(cr, uid, product, context=context)
684 uom2 = product_uom_obj.browse(cr, uid, uom)
685 if product.uom_id.category_id.id <> uom2.category_id.id:
690 uos2 = product_uom_obj.browse(cr, uid, uos)
691 if product.uos_id.category_id.id <> uos2.category_id.id:
696 result = {'price_unit': price, 'type': product.procure_method,
697 'notes': product.description_sale}
699 if update_tax: #The quantity only have changed
700 result['delay'] = (product.sale_delay or 0.0)
701 taxes = self.pool.get('account.tax').browse(cr, uid,
702 [x.id for x in product.taxes_id])
705 taxep = self.pool.get('res.partner').browse(cr, uid,
706 partner_id).property_account_tax
707 if not taxep or not taxep.id:
708 result['tax_id'] = [x.id for x in product.taxes_id]
712 if not t.tax_group==taxep.tax_group:
714 result['tax_id'] = res5
716 result['name'] = product.partner_ref
719 if not uom and not uos:
720 result['product_uom'] = product.uom_id.id
722 result['product_uos'] = product.uos_id.id
723 result['product_uos_qty'] = qty * product.uos_coeff
725 result['product_uos'] = False
726 result['product_uos_qty'] = qty
727 result['th_weight'] = qty * product.weight
728 domain = {'product_uom':
729 [('category_id', '=', product.uom_id.category_id.id)]}
730 elif uom: # whether uos is set or not
731 default_uom = product.uom_id and product.uom_id.id
732 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
734 result['product_uos'] = product.uos_id.id
735 result['product_uos_qty'] = q * product.uos_coeff
737 result['product_uos'] = False
738 result['product_uos_qty'] = q
739 result['th_weight'] = q * product.weight
740 elif uos: # only happens if uom is False
741 result['product_uom'] = product.uom_id and product.uom_id.id
742 result['product_uom_qty'] = qty_uos / product.uos_coeff
743 result['th_weight'] = result['product_uom_qty'] * product.weight
744 return {'value': result, 'domain': domain}