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):
71 id_set = ",".join(map(str, ids))
72 cr.execute("SELECT s.id,COALESCE(SUM(l.price_unit*l.product_uos_qty*(100-l.discount))/100.0,0) AS amount FROM sale_order s LEFT OUTER JOIN sale_order_line l ON (s.id=l.order_id) WHERE s.id IN ("+id_set+") GROUP BY s.id ")
73 res = dict(cr.fetchall())
74 cur_obj=self.pool.get('res.currency')
76 order=self.browse(cr, uid, [id])[0]
77 cur=order.pricelist_id.currency_id
78 res[id]=cur_obj.round(cr, uid, cur, res[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_uos_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 are automatically setted when a cancel operation occurs in the invoice validation (Invoice Exception) or in picking list process (Shipping Exception). The 'Waiting Schedule' state is set when the invoice is confirmed but waiting the 'Date Order' the schedule.", 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)]}, relate=True, select=True),
123 'partner_id':fields.many2one('res.partner', 'Partner', readonly=True, states={'draft':[('readonly',False)]}, change_default=True, relate=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')], 'Picking 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 pickings'),
135 ], 'Shipping Policy', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The Shipping Policy is used to synchronise invoive and delivery operations. The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice. The 'Shipping & Manual Invoice' will create the picking order directly and wait the user to manually click on the 'Invoice Button' to generate the draft invoice. The 'Invoice after delivery' choice will generate the draft invoice after the picking list have been finished"),
136 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft':[('readonly',False)]}),
137 'project_id':fields.many2one('account.analytic.account', 'Profit/Cost Center', readonly=True, states={'draft':[('readonly', False)]}),
139 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft':[('readonly',False)]}),
140 '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)."),
141 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Picking List', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
143 'shipped':fields.boolean('Picked', readonly=True),
144 'invoiced':fields.boolean('Paid', readonly=True),
146 'note': fields.text('Notes'),
148 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
149 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
150 'amount_total': fields.function(_amount_total, method=True, string='Total'),
151 '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."),
154 'picking_policy': lambda *a: 'direct',
155 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
156 'order_policy': lambda *a: 'manual',
157 'state': lambda *a: 'draft',
158 'user_id': lambda obj, cr, uid, context: uid,
159 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
160 'invoice_quantity': lambda *a: 'order'
165 def onchange_shop_id(self, cr, uid, ids, shop_id):
168 shop=self.pool.get('sale.shop').browse(cr,uid,shop_id)
169 v['project_id']=shop.project_id.id
170 # Que faire si le client a une pricelist a lui ?
171 if shop.pricelist_id.id:
172 v['pricelist_id']=shop.pricelist_id.id
173 #v['payment_default_id']=shop.payment_default_id.id
176 def action_cancel_draft(self, cr, uid, ids, *args):
179 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
180 line_ids = map(lambda x: x[0], cr.fetchall())
181 self.write(cr, uid, ids, {'state':'draft', 'invoice_ids':[], 'shipped':0, 'invoiced':0})
182 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced':False, 'state':'draft', 'invoice_lines':[(6,0,[])]})
183 wf_service = netsvc.LocalService("workflow")
185 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
188 def onchange_partner_id(self, cr, uid, ids, part):
190 return {'value':{'partner_invoice_id': False, 'partner_shipping_id':False, 'partner_order_id':False}}
191 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery','invoice','contact'])
192 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist[0]
193 return {'value':{'partner_invoice_id': addr['invoice'], 'partner_order_id':addr['contact'], 'partner_shipping_id':addr['delivery'], 'pricelist_id': pricelist}}
195 def button_dummy(self, cr, uid, ids, context={}):
198 #FIXME: the method should return the list of invoices created (invoice_ids)
199 # and not the id of the last invoice created (res). The problem is that we
200 # cannot change it directly since the method is called by the sale order
201 # workflow and I suppose it expects a single id...
202 def _inv_get(self, cr, uid, order, context={}):
205 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed','done']):
210 def make_invoice(order, lines):
211 a = order.partner_id.property_account_receivable[0]
212 if order.partner_id and order.partner_id.property_payment_term:
213 pay_term = order.partner_id.property_payment_term[0]
216 for preinv in order.invoice_ids:
217 if preinv.state in ('open','paid','proforma'):
218 for preline in preinv.invoice_line:
219 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id':False, 'price_unit':-preline.price_unit})
220 lines.append(inv_line_id)
223 'origin': order.name,
224 'type': 'out_invoice',
225 'reference': "P%dSO%d"%(order.partner_id.id,order.id),
227 'partner_id': order.partner_id.id,
228 'address_invoice_id': order.partner_invoice_id.id,
229 'address_contact_id': order.partner_invoice_id.id,
230 'invoice_line': [(6,0,lines)],
231 'currency_id' : order.pricelist_id.currency_id.id,
232 'comment': order.note,
233 'payment_term': pay_term,
235 inv.update(self._inv_get(cr, uid, order))
236 inv_obj = self.pool.get('account.invoice')
237 inv_id = inv_obj.create(cr, uid, inv)
238 inv_obj.button_compute(cr, uid, [inv_id])
241 for o in self.browse(cr,uid,ids):
243 for line in o.order_line:
244 if (line.state in states) and not line.invoiced:
245 lines.append(line.id)
246 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
248 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
251 for o in self.browse(cr, uid, ids):
252 for i in o.invoice_ids:
253 if i.state == 'draft':
256 for val in invoices.values():
258 res = make_invoice(val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []))
260 self.write(cr, uid, [o.id], {'state' : 'progress'})
261 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
263 for order, il in val:
264 res = make_invoice(order, il)
265 invoice_ids.append(res)
266 self.write(cr, uid, [order.id], {'state' : 'progress'})
267 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%d,%d)', (o.id, res))
270 def action_invoice_cancel(self, cr, uid, ids, context={}):
271 for sale in self.browse(cr, uid, ids):
272 for line in sale.order_line:
274 for iline in line.invoice_lines:
275 if iline.invoice_id and iline.invoice_id.state == 'cancel':
279 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
280 self.write(cr, uid, ids, {'state':'invoice_except', 'invoice_id':False})
284 def action_cancel(self, cr, uid, ids, context={}):
286 for sale in self.browse(cr, uid, ids):
287 for pick in sale.picking_ids:
288 if pick.state not in ('draft','cancel'):
289 raise osv.except_osv(
290 'Could not cancel sale order !',
291 'You must first cancel all pickings attached to this sale order.')
292 for r in self.read(cr,uid,ids,['picking_ids']):
293 for pick in r['picking_ids']:
294 wf_service = netsvc.LocalService("workflow")
295 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
296 for inv in sale.invoice_ids:
297 if inv.state not in ('draft','cancel'):
298 raise osv.except_osv(
299 'Could not cancel this sale order !',
300 'You must first cancel all invoices attached to this sale order.')
301 for r in self.read(cr,uid,ids,['invoice_ids']):
302 for inv in r['invoice_ids']:
303 wf_service = netsvc.LocalService("workflow")
304 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
305 self.write(cr,uid,ids,{'state':'cancel'})
308 def action_wait(self, cr, uid, ids, *args):
309 for r in self.read(cr,uid,ids,['order_policy','invoice_ids','name','amount_untaxed','partner_id','user_id','order_line']):
310 if self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open'):
311 self.pool.get('res.partner.event').create(cr, uid, {'name':'Sale Order: '+r['name'], 'partner_id':r['partner_id'][0], 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id':(r['user_id'] and r['user_id'][0]) or uid, 'partner_type':'customer', 'probability': 1.0, 'planned_revenue':r['amount_untaxed']})
312 if (r['order_policy']=='manual') and (not r['invoice_ids']):
313 self.write(cr,uid,[r['id']],{'state':'manual'})
315 self.write(cr,uid,[r['id']],{'state':'progress'})
316 self.pool.get('sale.order.line').button_confirm(cr, uid, r['order_line'])
318 def procurement_lines_get(self, cr, uid, ids, *args):
320 for order in self.browse(cr, uid, ids, context={}):
321 for line in order.order_line:
322 if line.procurement_id:
323 res.append(line.procurement_id.id)
326 # if mode == 'finished':
327 # returns True if all lines are done, False otherwise
328 # if mode == 'canceled':
329 # returns True if there is at least one canceled line, False otherwise
330 def test_state(self, cr, uid, ids, mode, *args):
331 assert mode in ('finished', 'canceled'), "invalid mode for test_state"
335 write_cancel_ids = []
336 for order in self.browse(cr, uid, ids, context={}):
337 for line in order.order_line:
338 if line.procurement_id and (line.procurement_id.state != 'done') and (line.state!='done'):
340 if line.procurement_id and line.procurement_id.state == 'cancel':
342 # if a line is finished (ie its procuremnt is done or it has not procuremernt and it
343 # is not already marked as done, mark it as being so...
344 if ((not line.procurement_id) or line.procurement_id.state == 'done') and line.state != 'done':
345 write_done_ids.append(line.id)
346 # ... same for canceled lines
347 if line.procurement_id and line.procurement_id.state == 'cancel' and line.state != 'cancel':
348 write_cancel_ids.append(line.id)
350 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
352 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'cancel'})
356 elif mode=='canceled':
359 def action_ship_create(self, cr, uid, ids, *args):
361 for order in self.browse(cr, uid, ids, context={}):
362 output_id = order.shop_id.warehouse_id.lot_output_id.id
364 for line in order.order_line:
366 date_planned = (DateTime.now() + DateTime.RelativeDateTime(days=line.delay or 0.0)).strftime('%Y-%m-%d')
367 if line.state == 'done':
369 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
370 location_id = order.shop_id.warehouse_id.lot_stock_id.id
372 loc_dest_id = order.partner_id.property_stock_customer[0]
373 picking_id = self.pool.get('stock.picking').create(cr, uid, {
374 'origin': order.name,
377 'move_type': order.picking_policy,
378 'loc_move_id': loc_dest_id,
380 'address_id': order.partner_shipping_id.id,
382 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
386 move_id = self.pool.get('stock.move').create(cr, uid, {
388 'picking_id': picking_id,
389 'product_id': line.product_id.id,
390 'date_planned': date_planned,
391 'product_qty': line.product_uom_qty,
392 'product_uom': line.product_uom.id,
393 'product_uos_qty': line.product_uos_qty,
394 'product_uos': line.product_uos.id,
395 'product_packaging' : line.product_packaging.id,
396 'address_id' : line.address_allotment_id.id or order.partner_shipping_id.id,
397 'location_id': location_id,
398 'location_dest_id': output_id,
399 'sale_line_id': line.id,
400 'tracking_id': False,
404 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
406 'origin': order.name,
407 'date_planned': date_planned,
408 'product_id': line.product_id.id,
409 'product_qty': line.product_uom_qty,
410 'product_uom': line.product_uom.id,
411 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
412 'procure_method': line.type,
415 wf_service = netsvc.LocalService("workflow")
416 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
417 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
418 elif line.product_id and line.product_id.product_tmpl_id.type=='service':
419 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
421 'origin': order.name,
422 'date_planned': date_planned,
423 'product_id': line.product_id.id,
424 'product_qty': line.product_uom_qty,
425 'product_uom': line.product_uom.id,
426 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
427 'procure_method': line.type,
429 wf_service = netsvc.LocalService("workflow")
430 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
431 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
434 # No procurement because no product in the sale.order.line.
440 wf_service = netsvc.LocalService("workflow")
441 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
442 #val = {'picking_ids':[(6,0,[picking_id])]}
444 if order.state=='shipping_except':
445 val['state'] = 'progress'
446 if (order.order_policy == 'manual') and order.invoice_ids:
447 val['state'] = 'manual'
448 self.write(cr, uid, [order.id], val)
451 def action_ship_end(self, cr, uid, ids, context={}):
452 for order in self.browse(cr, uid, ids):
453 val = {'shipped':True}
454 if order.state=='shipping_except':
455 if (order.order_policy=='manual') and not order.invoice_ids:
456 val['state'] = 'manual'
458 val['state'] = 'progress'
459 self.write(cr, uid, [order.id], val)
462 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
463 invs = self.read(cr, uid, ids, ['date_order','partner_id','amount_untaxed'])
465 part=inv['partner_id'] and inv['partner_id'][0]
466 pr = inv['amount_untaxed'] or 0.0
467 partnertype = 'customer'
469 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})
471 def has_stockable_products(self,cr, uid, ids, *args):
472 for order in self.browse(cr, uid, ids):
473 for order_line in order.order_line:
474 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
479 class sale_order_line(osv.osv):
480 def copy(self, cr, uid, id, default=None, context={}):
481 if not default: default = {}
482 default.update( {'invoice_lines':[]})
483 return super(sale_order_line, self).copy(cr, uid, id, default, context)
485 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
487 for line in self.browse(cr, uid, ids):
488 if line.product_uos.id:
489 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) /100.0)
491 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
494 def _amount_line(self, cr, uid, ids, field_name, arg, context):
496 cur_obj=self.pool.get('res.currency')
497 for line in self.browse(cr, uid, ids):
498 if line.product_uos.id:
499 res[line.id] = line.price_unit * line.product_uos_qty * (1 - (line.discount or 0.0) /100.0)
501 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
502 cur = line.order_id.pricelist_id.currency_id
503 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
506 def _number_packages(self, cr, uid, ids, field_name, arg, context):
508 for line in self.browse(cr, uid, ids):
509 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
512 def _get_1st_packaging(self, cr, uid, context={}):
513 cr.execute('select id from product_packaging order by id asc limit 1')
519 _name = 'sale.order.line'
520 _description = 'Sale Order line'
522 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
523 'name': fields.char('Description', size=256, required=True, select=True),
524 'sequence': fields.integer('Sequence'),
525 'delay': fields.float('Delivery Delay', required=True),
526 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], change_default=True, relate=True),
527 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id','invoice_id', 'Invoice Lines', readonly=True),
528 'invoiced': fields.boolean('Invoiced', readonly=True, select=True),
529 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
530 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
531 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
532 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
533 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
534 'type': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')],'Procure Method', required=True),
535 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
536 'address_allotment_id' : fields.many2one('res.partner.address', 'Allotment Partner'),
537 'product_uom_qty': fields.float('Quantity (UOM)', digits=(16,2), required=True),
538 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
539 'product_uos_qty': fields.float('Quantity (UOS)'),
540 'product_uos': fields.many2one('product.uom', 'Product UOS'),
541 'product_packaging': fields.many2one('product.packaging', 'Packaging used'),
542 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
543 'discount': fields.float('Discount (%)', digits=(16,2)),
544 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number packages'),
545 'notes': fields.text('Notes'),
546 'th_weight' : fields.float('Weight'),
547 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'State', required=True, readonly=True),
548 'price_unit_customer': fields.float('Customer Unit Price', digits=(16, int(config['price_accuracy']))),
552 'discount': lambda *a: 0.0,
553 'delay': lambda *a: 0.0,
554 'product_uom_qty': lambda *a: 1,
555 'product_uos_qty': lambda *a: 1,
556 'sequence': lambda *a: 10,
557 'invoiced': lambda *a: 0,
558 'state': lambda *a: 'draft',
559 'type': lambda *a: 'make_to_stock',
560 'product_packaging': _get_1st_packaging,
562 def invoice_line_create(self, cr, uid, ids, context={}):
563 def _get_line_qty(line):
564 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
565 return line.product_uos_qty or line.product_uom_qty
567 return self.pool.get('mrp.procurement').quantity_get(cr, uid, line.procurement_id.id, context)
569 for line in self.browse(cr, uid, ids, context):
570 if not line.invoiced:
572 a = line.product_id.product_tmpl_id.property_account_income
574 a = line.product_id.categ_id.property_account_income_categ
576 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,))
579 a = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
580 uosqty = _get_line_qty(line)
581 uos_id = (line.product_uos and line.product_uos.id) or False
582 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
585 'price_unit': line.price_unit,
587 'discount': line.discount,
589 'product_id': line.product_id.id or False,
590 'invoice_line_tax_id': [(6,0,[x.id for x in line.tax_id])],
592 'account_analytic_id': line.order_id.project_id.id,
594 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%d,%d)', (line.id, inv_id))
595 self.write(cr, uid, [line.id], {'invoiced':True})
596 create_ids.append(inv_id)
599 def button_confirm(self, cr, uid, ids, context={}):
600 return self.write(cr, uid, ids, {'state':'confirmed'})
602 def button_done(self, cr, uid, ids, context={}):
603 wf_service = netsvc.LocalService("workflow")
604 res = self.write(cr, uid, ids, {'state':'done'})
605 for line in self.browse(cr,uid,ids,context):
606 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
610 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
612 return {'value': {'product_uom': product_uos, 'product_uom_qty': product_uos_qty}, 'domain':{}}
613 res = self.pool.get('product.product').read(cr, uid, [product_id], ['uom_id', 'uos_id', 'uos_coeff', 'weight'])[0]
615 'product_uom' : res['uom_id'],
619 'product_uom_qty' : product_uos_qty / res['uos_coeff'],
620 'weight' : product_uos_qty / res['uos_coeff'] * res['weight']
622 except ZeroDivisionError:
624 return {'value' : value}
626 def copy(self, cr, uid, id, default=None,context={}):
629 default.update({'state':'draft', 'move_ids':[], 'invoiced':False, 'invoice_lines':[]})
630 return super(sale_order_line, self).copy(cr, uid, id, default, context)
632 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False):
634 lang=self.pool.get('res.partner').read(cr, uid, [partner_id])[0]['lang']
635 context = {'lang':lang}
637 return {'value': {'price_unit': 0.0, 'notes':'', 'weight' : 0}, 'domain':{'product_uom':[]}}
639 raise osv.except_osv('No Pricelist !', 'You have to select a pricelist in the sale form !\nPlease set one before choosing a product.')
640 price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], product, qty or 1.0, partner_id, {'uom': uom})[pricelist]
642 raise osv.except_osv('No valid pricelist line found !', "Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist.")
643 res = self.pool.get('product.product').read(cr, uid, [product], context=context)[0]
644 # dt = (DateTime.now() + DateTime.RelativeDateTime(days=res['sale_delay'] or 0.0)).strftime('%Y-%m-%d')
646 result = {'price_unit': price, 'type':res['procure_method'], 'delay':(res['sale_delay'] or 0.0), 'notes':res['description_sale']}
648 taxes = self.pool.get('account.tax').browse(cr, uid, res['taxes_id'])
649 taxep = self.pool.get('res.partner').browse(cr, uid, partner_id).property_account_tax
651 result['tax_id'] = res['taxes_id']
654 tp = self.pool.get('account.tax').browse(cr, uid, taxep[0])
656 if not t.tax_group==tp.tax_group:
658 result['tax_id'] = res5
660 result['name'] = res['partner_ref']
662 if not uom and not uos:
663 result['product_uom'] = res['uom_id'] and res['uom_id'][0]
664 if result['product_uom']:
665 result['product_uos'] = res['uos_id']
666 result['product_uos_qty'] = qty * res['uos_coeff']
667 result['weight'] = qty * res['weight']
668 res2 = self.pool.get('product.uom').read(cr, uid, [result['product_uom']], ['category_id'])
669 if res2 and res2[0]['category_id']:
670 domain = {'product_uom':[('category_id','=',res2[0]['category_id'][0])]}
671 elif uom: # whether uos is set or not
672 default_uom = res['uom_id'] and res['uom_id'][0]
673 q = self.pool.get('product.uom')._compute_qty(cr, uid, uom, qty, default_uom)
674 result['product_uos'] = res['uos_id']
675 result['product_uos_qty'] = q * res['uos_coeff']
676 result['weight'] = q * res['weight']
677 elif uos: # only happens if uom is False
678 result['product_uom'] = res['uom_id'] and res['uom_id'][0]
679 result['product_uom_qty'] = qty_uos / res['uos_coeff']
680 result['weight'] = result['product_uom_qty'] * res['weight']
681 return {'value':result, 'domain':domain}