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
26 from mx import DateTime
27 from tools import config
28 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'),
45 def _incoterm_get(self, cr, uid, context={}):
46 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
50 class sale_order(osv.osv):
52 _description = "Sale Order"
54 def copy(self, cr, uid, id, default=None, context={}):
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context)
66 def _amount_line_tax(self, cr, uid, line, context={}):
68 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):
72 def _amount_all(self, cr, uid, ids, field_name, arg, context):
74 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids):
77 'amount_untaxed': 0.0,
82 cur = order.pricelist_id.currency_id
83 for line in order.order_line:
84 val1 += line.price_subtotal
85 val += self._amount_line_tax(cr, uid, line, context)
86 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
91 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 p.sale_id,sum(m.product_qty), m.state
102 stock_picking p on (p.id=m.picking_id)
104 p.sale_id in ('''+','.join(map(str, ids))+''')
105 GROUP BY m.state, p.sale_id''')
106 for oid, nbr, state in cr.fetchall():
107 if state == 'cancel':
110 res[oid][0] += nbr or 0.0
111 res[oid][1] += nbr or 0.0
113 res[oid][1] += nbr or 0.0
118 res[r] = 100.0 * res[r][0] / res[r][1]
119 for order in self.browse(cr, uid, ids, context):
121 res[order.id] = 100.0
124 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
131 for invoice in sale.invoice_ids:
132 if invoice.state not in ('draft', 'cancel'):
133 tot += invoice.amount_untaxed
136 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
141 def _invoiced(self, cursor, user, ids, name, arg, context=None):
143 for sale in self.browse(cursor, user, ids, context=context):
145 for invoice in sale.invoice_ids:
146 if invoice.state != 'paid':
149 if not sale.invoice_ids:
153 def _invoiced_search(self, cursor, user, obj, name, args):
161 clause += 'AND inv.state = \'paid\''
163 clause += 'AND inv.state <> \'paid\''
165 cursor.execute('SELECT rel.order_id ' \
166 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
167 'WHERE rel.invoice_id = inv.id ' + clause)
168 res = cursor.fetchall()
170 cursor.execute('SELECT sale.id ' \
171 'FROM sale_order AS sale ' \
172 'WHERE sale.id NOT IN ' \
173 '(SELECT rel.order_id ' \
174 'FROM sale_order_invoice_rel AS rel)')
175 res.extend(cursor.fetchall())
177 return [('id', '=', 0)]
178 return [('id', 'in', [x[0] for x in res])]
180 def _get_order(self, cr, uid, ids, context={}):
182 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
183 result[line.order_id.id] = True
187 'name': fields.char('Order Reference', size=64, required=True, select=True),
188 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
189 'origin': fields.char('Origin', size=64),
190 'client_order_ref': fields.char('Customer Ref', size=64),
192 'state': fields.selection([
193 ('draft', 'Quotation'),
194 ('waiting_date', 'Waiting Schedule'),
195 ('manual', 'Manual In Progress'),
196 ('progress', 'In Progress'),
197 ('shipping_except', 'Shipping Exception'),
198 ('invoice_except', 'Invoice Exception'),
200 ('cancel', 'Cancelled')
201 ], '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),
202 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
204 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
205 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
206 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
207 '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."),
208 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
210 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
211 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
212 '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?"""),
213 'order_policy': fields.selection([
214 ('prepaid', 'Payment Before Delivery'),
215 ('manual', 'Shipping & Manual Invoice'),
216 ('postpaid', 'Invoice on Order After Delivery'),
217 ('picking', 'Invoice from the Packing'),
218 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
219 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
220 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
221 - 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.
222 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all packing lists have been finished.
223 - The 'Invoice from the packing' choice is used to create an invoice during the packing process."""),
224 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
225 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
227 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
228 '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)."),
229 '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"),
230 'shipped': fields.boolean('Picked', readonly=True),
231 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
232 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
233 'invoiced': fields.function(_invoiced, method=True, string='Paid',
234 fnct_search=_invoiced_search, type='boolean'),
235 'note': fields.text('Notes'),
237 'amount_untaxed': fields.function(_amount_all, method=True, string='Untaxed Amount',
239 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
240 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
243 'amount_tax': fields.function(_amount_all, method=True, string='Taxes',
245 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
246 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
249 'amount_total': fields.function(_amount_all, method=True, string='Total',
251 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
252 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
256 '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),
257 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
258 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
261 'picking_policy': lambda *a: 'direct',
262 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
263 'order_policy': lambda *a: 'manual',
264 'state': lambda *a: 'draft',
265 'user_id': lambda obj, cr, uid, context: uid,
266 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
267 'invoice_quantity': lambda *a: 'order',
268 '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'],
269 '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'],
270 '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'],
271 '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,
276 def unlink(self, cr, uid, ids, context=None):
277 sale_orders = self.read(cr, uid, ids, ['state'])
279 for s in sale_orders:
280 if s['state'] in ['draft', 'cancel']:
281 unlink_ids.append(s['id'])
283 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
284 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
286 def onchange_shop_id(self, cr, uid, ids, shop_id):
289 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
290 v['project_id'] = shop.project_id.id
291 # Que faire si le client a une pricelist a lui ?
292 if shop.pricelist_id.id:
293 v['pricelist_id'] = shop.pricelist_id.id
294 #v['payment_default_id']=shop.payment_default_id.id
297 def action_cancel_draft(self, cr, uid, ids, *args):
300 cr.execute('select id from sale_order_line where order_id in ('+','.join(map(str, ids))+')', ('draft',))
301 line_ids = map(lambda x: x[0], cr.fetchall())
302 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
303 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
304 wf_service = netsvc.LocalService("workflow")
306 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
309 def onchange_partner_id(self, cr, uid, ids, part):
311 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
312 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
313 part = self.pool.get('res.partner').browse(cr, uid, part)
314 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
315 payment_term = part.property_payment_term and part.property_payment_term.id or False
316 fiscal_position = part.property_account_position and part.property_account_position.id or False
317 val = {'partner_invoice_id': addr['invoice'], 'partner_order_id': addr['contact'], 'partner_shipping_id': addr['delivery'], 'payment_term': payment_term, 'fiscal_position': fiscal_position}
319 val['pricelist_id'] = pricelist
320 return {'value': val}
322 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
326 if policy == 'prepaid':
328 elif policy == 'picking':
329 inv_qty = 'procurement'
330 return {'value': {'invoice_quantity': inv_qty}}
332 def write(self, cr, uid, ids, vals, context=None):
333 if 'order_policy' in vals:
334 if vals['order_policy'] == 'prepaid':
335 vals.update({'invoice_quantity': 'order'})
336 elif vals['order_policy'] == 'picking':
337 vals.update({'invoice_quantity': 'procurement'})
338 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
340 def create(self, cr, uid, vals, context={}):
341 if 'order_policy' in vals:
342 if vals['order_policy'] == 'prepaid':
343 vals.update({'invoice_quantity': 'order'})
344 if vals['order_policy'] == 'picking':
345 vals.update({'invoice_quantity': 'procurement'})
346 return super(sale_order, self).create(cr, uid, vals, context=context)
348 def button_dummy(self, cr, uid, ids, context={}):
351 #FIXME: the method should return the list of invoices created (invoice_ids)
352 # and not the id of the last invoice created (res). The problem is that we
353 # cannot change it directly since the method is called by the sale order
354 # workflow and I suppose it expects a single id...
355 def _inv_get(self, cr, uid, order, context={}):
358 def _make_invoice(self, cr, uid, order, lines, context={}):
359 a = order.partner_id.property_account_receivable.id
360 if order.payment_term:
361 pay_term = order.payment_term.id
364 for preinv in order.invoice_ids:
365 if preinv.state not in ('cancel',):
366 for preline in preinv.invoice_line:
367 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
368 lines.append(inv_line_id)
370 'name': order.client_order_ref or order.name,
371 'origin': order.name,
372 'type': 'out_invoice',
373 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
375 'partner_id': order.partner_id.id,
376 'address_invoice_id': order.partner_invoice_id.id,
377 'address_contact_id': order.partner_order_id.id,
378 'invoice_line': [(6, 0, lines)],
379 'currency_id': order.pricelist_id.currency_id.id,
380 'comment': order.note,
381 'payment_term': pay_term,
382 'fiscal_position': order.partner_id.property_account_position.id
384 inv_obj = self.pool.get('account.invoice')
385 inv.update(self._inv_get(cr, uid, order))
386 inv_id = inv_obj.create(cr, uid, inv)
387 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
388 if data.get('value', False):
389 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
390 inv_obj.button_compute(cr, uid, [inv_id])
393 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception']):
398 for o in self.browse(cr, uid, ids):
400 for line in o.order_line:
401 if (line.state in states) and not line.invoiced:
402 lines.append(line.id)
403 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
405 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
408 for o in self.browse(cr, uid, ids):
409 for i in o.invoice_ids:
410 if i.state == 'draft':
412 picking_obj = self.pool.get('stock.picking')
413 for val in invoices.values():
415 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []))
417 self.write(cr, uid, [o.id], {'state': 'progress'})
418 if o.order_policy == 'picking':
419 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
420 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
422 for order, il in val:
423 res = self._make_invoice(cr, uid, order, il)
424 invoice_ids.append(res)
425 self.write(cr, uid, [order.id], {'state': 'progress'})
426 if order.order_policy == 'picking':
427 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
428 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
431 def action_invoice_cancel(self, cr, uid, ids, context={}):
432 for sale in self.browse(cr, uid, ids):
433 for line in sale.order_line:
435 for iline in line.invoice_lines:
436 if iline.invoice_id and iline.invoice_id.state == 'cancel':
440 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
441 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 stock_move_obj = self.pool.get('stock.move')
509 for order in self.browse(cr, uid, ids, context={}):
511 #check for pending deliveries
512 pending_deliveries = False
514 for line in order.order_line:
515 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
516 for move in stock_move_obj.browse( cr, uid, move_ids ):
517 #if one of the related order lines is in state draft, auto or confirmed
518 #this order line is not yet delivered
519 if move.state in ('draft', 'auto', 'confirmed'):
520 pending_deliveries = True
522 if ((not line.procurement_id) or (line.procurement_id.state=='done')) and not pending_deliveries:
524 if line.state != 'done':
525 write_done_ids.append(line.id)
528 if line.procurement_id:
529 if (line.procurement_id.state == 'cancel'):
531 if line.state != 'exception':
532 write_cancel_ids.append(line.id)
537 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
539 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
541 if mode == 'finished':
543 elif mode == 'canceled':
549 def action_ship_create(self, cr, uid, ids, *args):
551 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
552 for order in self.browse(cr, uid, ids, context={}):
553 output_id = order.shop_id.warehouse_id.lot_output_id.id
555 for line in order.order_line:
557 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
558 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
559 if line.state == 'done':
561 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
562 location_id = order.shop_id.warehouse_id.lot_stock_id.id
564 loc_dest_id = order.partner_id.property_stock_customer.id
565 picking_id = self.pool.get('stock.picking').create(cr, uid, {
566 'origin': order.name,
569 'move_type': order.picking_policy,
571 'address_id': order.partner_shipping_id.id,
573 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
577 move_id = self.pool.get('stock.move').create(cr, uid, {
578 'name': line.name[:64],
579 'picking_id': picking_id,
580 'product_id': line.product_id.id,
581 'date_planned': date_planned,
582 'product_qty': line.product_uom_qty,
583 'product_uom': line.product_uom.id,
584 'product_uos_qty': line.product_uos_qty,
585 'product_uos': (line.product_uos and line.product_uos.id)\
586 or line.product_uom.id,
587 'product_packaging': line.product_packaging.id,
588 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
589 'location_id': location_id,
590 'location_dest_id': output_id,
591 'sale_line_id': line.id,
592 'tracking_id': False,
597 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
599 'origin': order.name,
600 'date_planned': date_planned,
601 'product_id': line.product_id.id,
602 'product_qty': line.product_uom_qty,
603 'product_uom': line.product_uom.id,
604 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
605 or line.product_uom_qty,
606 'product_uos': (line.product_uos and line.product_uos.id)\
607 or line.product_uom.id,
608 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
609 'procure_method': line.type,
611 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
613 wf_service = netsvc.LocalService("workflow")
614 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
615 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
616 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
617 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
619 'origin': order.name,
620 'date_planned': date_planned,
621 'product_id': line.product_id.id,
622 'product_qty': line.product_uom_qty,
623 'product_uom': line.product_uom.id,
624 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
625 'procure_method': line.type,
626 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
628 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
629 wf_service = netsvc.LocalService("workflow")
630 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
633 # No procurement because no product in the sale.order.line.
639 wf_service = netsvc.LocalService("workflow")
640 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
642 if order.state == 'shipping_except':
643 val['state'] = 'progress'
645 if (order.order_policy == 'manual'):
646 for line in order.order_line:
647 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
648 val['state'] = 'manual'
650 self.write(cr, uid, [order.id], val)
654 def action_ship_end(self, cr, uid, ids, context={}):
655 for order in self.browse(cr, uid, ids):
656 val = {'shipped': True}
657 if order.state == 'shipping_except':
658 val['state'] = 'progress'
659 if (order.order_policy == 'manual'):
660 for line in order.order_line:
661 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
662 val['state'] = 'manual'
664 for line in order.order_line:
666 if line.state == 'exception':
667 towrite.append(line.id)
669 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
670 self.write(cr, uid, [order.id], val)
673 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
674 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
676 part = inv['partner_id'] and inv['partner_id'][0]
677 pr = inv['amount_untaxed'] or 0.0
678 partnertype = 'customer'
681 'name': 'Order: '+name,
683 'description': 'Order '+str(inv['id']),
686 'date': time.strftime('%Y-%m-%d'),
689 'partner_type': partnertype,
691 'planned_revenue': pr,
695 self.pool.get('res.partner.event').create(cr, uid, event)
697 def has_stockable_products(self, cr, uid, ids, *args):
698 for order in self.browse(cr, uid, ids):
699 for order_line in order.order_line:
700 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
705 # TODO add a field price_unit_uos
706 # - update it on change product and unit price
707 # - use it in report if there is a uos
708 class sale_order_line(osv.osv):
709 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
711 for line in self.browse(cr, uid, ids):
712 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
715 def _amount_line(self, cr, uid, ids, field_name, arg, context):
717 cur_obj = self.pool.get('res.currency')
718 for line in self.browse(cr, uid, ids):
719 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
720 cur = line.order_id.pricelist_id.currency_id
721 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
724 def _number_packages(self, cr, uid, ids, field_name, arg, context):
726 for line in self.browse(cr, uid, ids):
728 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
733 _name = 'sale.order.line'
734 _description = 'Sale Order line'
736 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
737 'name': fields.char('Description', size=256, required=True, select=True),
738 'sequence': fields.integer('Sequence'),
739 'delay': fields.float('Delivery Delay', required=True),
740 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
741 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
742 'invoiced': fields.boolean('Invoiced', readonly=True),
743 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
744 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
745 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
746 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
747 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
748 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True),
749 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
750 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
751 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
752 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
753 'product_uos_qty': fields.float('Quantity (UoS)'),
754 'product_uos': fields.many2one('product.uom', 'Product UoS'),
755 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
756 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
757 'discount': fields.float('Discount (%)', digits=(16, 2)),
758 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
759 'notes': fields.text('Notes'),
760 'th_weight': fields.float('Weight'),
761 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
762 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
764 _order = 'sequence, id'
766 'discount': lambda *a: 0.0,
767 'delay': lambda *a: 0.0,
768 'product_uom_qty': lambda *a: 1,
769 'product_uos_qty': lambda *a: 1,
770 'sequence': lambda *a: 10,
771 'invoiced': lambda *a: 0,
772 'state': lambda *a: 'draft',
773 'type': lambda *a: 'make_to_stock',
774 'product_packaging': lambda *a: False
777 def invoice_line_create(self, cr, uid, ids, context={}):
778 def _get_line_qty(line):
779 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
781 return line.product_uos_qty or 0.0
782 return line.product_uom_qty
784 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
785 line.procurement_id.id, context)
787 def _get_line_uom(line):
788 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
790 return line.product_uos.id
791 return line.product_uom.id
793 return self.pool.get('mrp.procurement').uom_get(cr, uid,
794 line.procurement_id.id, context)
798 for line in self.browse(cr, uid, ids, context):
799 if not line.invoiced:
801 a = line.product_id.product_tmpl_id.property_account_income.id
803 a = line.product_id.categ_id.property_account_income_categ.id
805 raise osv.except_osv(_('Error !'),
806 _('There is no income account defined ' \
807 'for this product: "%s" (id:%d)') % \
808 (line.product_id.name, line.product_id.id,))
810 a = self.pool.get('ir.property').get(cr, uid,
811 'property_account_income_categ', 'product.category',
813 uosqty = _get_line_qty(line)
814 uos_id = _get_line_uom(line)
817 pu = round(line.price_unit * line.product_uom_qty / uosqty,
818 int(config['price_accuracy']))
819 fpos = line.order_id.fiscal_position or False
820 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
821 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
823 'origin': line.order_id.name,
827 'discount': line.discount,
829 'product_id': line.product_id.id or False,
830 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
832 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
834 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
835 self.write(cr, uid, [line.id], {'invoiced': True})
837 sales[line.order_id.id] = True
838 create_ids.append(inv_id)
840 # Trigger workflow events
841 wf_service = netsvc.LocalService("workflow")
842 for sid in sales.keys():
843 wf_service.trg_write(uid, 'sale.order', sid, cr)
846 def button_cancel(self, cr, uid, ids, context={}):
847 for line in self.browse(cr, uid, ids, context=context):
849 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
850 return self.write(cr, uid, ids, {'state': 'cancel'})
852 def button_confirm(self, cr, uid, ids, context={}):
853 return self.write(cr, uid, ids, {'state': 'confirmed'})
855 def button_done(self, cr, uid, ids, context={}):
856 wf_service = netsvc.LocalService("workflow")
857 res = self.write(cr, uid, ids, {'state': 'done'})
858 for line in self.browse(cr, uid, ids, context):
859 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
863 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
864 product_obj = self.pool.get('product.product')
866 return {'value': {'product_uom': product_uos,
867 'product_uom_qty': product_uos_qty}, 'domain': {}}
869 product = product_obj.browse(cr, uid, product_id)
871 'product_uom': product.uom_id.id,
873 # FIXME must depend on uos/uom of the product and not only of the coeff.
876 'product_uom_qty': product_uos_qty / product.uos_coeff,
877 'th_weight': product_uos_qty / product.uos_coeff * product.weight
879 except ZeroDivisionError:
881 return {'value': value}
883 def copy_data(self, cr, uid, id, default=None, context={}):
886 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
887 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
889 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
890 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
891 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
893 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.'))
895 product_uom_obj = self.pool.get('product.uom')
896 partner_obj = self.pool.get('res.partner')
897 product_obj = self.pool.get('product.product')
899 lang = partner_obj.browse(cr, uid, partner_id).lang
900 context = {'lang': lang, 'partner_id': partner_id}
903 return {'value': {'th_weight': 0, 'product_packaging': False,
904 'product_uos_qty': qty}, 'domain': {'product_uom': [],
908 date_order = time.strftime('%Y-%m-%d')
911 product_obj = product_obj.browse(cr, uid, product, context=context)
912 if not packaging and product_obj.packaging:
913 packaging = product_obj.packaging[0].id
914 result['product_packaging'] = packaging
917 default_uom = product_obj.uom_id and product_obj.uom_id.id
918 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
919 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
920 # qty = qty - qty % q + q
921 if qty and (q and not (qty % q) == 0):
925 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)
926 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
928 'title': _('Packing Information !'),
931 result['product_uom_qty'] = qty
934 uom2 = product_uom_obj.browse(cr, uid, uom)
935 if product_obj.uom_id.category_id.id != uom2.category_id.id:
939 if product_obj.uos_id:
940 uos2 = product_uom_obj.browse(cr, uid, uos)
941 if product_obj.uos_id.category_id.id != uos2.category_id.id:
945 result.update({'type': product_obj.procure_method})
946 if product_obj.description_sale:
947 result['notes'] = product_obj.description_sale
948 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
949 if update_tax: #The quantity only have changed
950 result['delay'] = (product_obj.sale_delay or 0.0)
951 partner = partner_obj.browse(cr, uid, partner_id)
952 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
954 result['name'] = product_obj.partner_ref
956 if (not uom) and (not uos):
957 result['product_uom'] = product_obj.uom_id.id
958 if product_obj.uos_id:
959 result['product_uos'] = product_obj.uos_id.id
960 result['product_uos_qty'] = qty * product_obj.uos_coeff
961 uos_category_id = product_obj.uos_id.category_id.id
963 result['product_uos'] = False
964 result['product_uos_qty'] = qty
965 uos_category_id = False
966 result['th_weight'] = qty * product_obj.weight
967 domain = {'product_uom':
968 [('category_id', '=', product_obj.uom_id.category_id.id)],
970 [('category_id', '=', uos_category_id)]}
972 elif uos: # only happens if uom is False
973 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
974 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
975 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
976 elif uom: # whether uos is set or not
977 default_uom = product_obj.uom_id and product_obj.uom_id.id
978 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
979 if product_obj.uos_id:
980 result['product_uos'] = product_obj.uos_id.id
981 result['product_uos_qty'] = qty * product_obj.uos_coeff
983 result['product_uos'] = False
984 result['product_uos_qty'] = qty
985 result['th_weight'] = q * product_obj.weight # Round the quantity up
991 'title': 'No Pricelist !',
993 'You have to select a pricelist in the sale form !\n'
994 'Please set one before choosing a product.'
997 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
998 product, qty or 1.0, partner_id, {
1004 'title': 'No valid pricelist line found !',
1006 "Couldn't find a pricelist line matching this product and quantity.\n"
1007 "You have to change either the product, the quantity or the pricelist."
1010 result.update({'price_unit': price})
1011 return {'value': result, 'domain': domain, 'warning': warning}
1013 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1014 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1015 lang=False, update_tax=True, date_order=False):
1016 res = self.product_id_change(cursor, user, ids, pricelist, product,
1017 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1018 partner_id=partner_id, lang=lang, update_tax=update_tax,
1019 date_order=date_order)
1020 if 'product_uom' in res['value']:
1021 del res['value']['product_uom']
1023 res['value']['price_unit'] = 0.0
1026 def unlink(self, cr, uid, ids, context={}):
1027 """Allows to delete sale order lines in draft,cancel states"""
1028 for rec in self.browse(cr, uid, ids, context=context):
1029 if rec.state not in ['draft', 'cancel']:
1030 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1031 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1036 class sale_config_picking_policy(osv.osv_memory):
1037 _name = 'sale.config.picking_policy'
1039 'name': fields.char('Name', size=64),
1040 'picking_policy': fields.selection([
1041 ('direct', 'Direct Delivery'),
1042 ('one', 'All at Once')
1043 ], 'Packing Default Policy', required=True),
1044 'order_policy': fields.selection([
1045 ('manual', 'Invoice Based on Sales Orders'),
1046 ('picking', 'Invoice Based on Deliveries'),
1047 ], 'Shipping Default Policy', required=True),
1048 'step': fields.selection([
1049 ('one', 'Delivery Order Only'),
1050 ('two', 'Packing List & Delivery Order')
1051 ], 'Steps To Deliver a Sale Order', required=True,
1052 help="By default, Open ERP is able to manage complex routing and paths "\
1053 "of products in your warehouse and partner locations. This will configure "\
1054 "the most common and simple methods to deliver products to the customer "\
1055 "in one or two operations by the worker.")
1058 'picking_policy': lambda *a: 'direct',
1059 'order_policy': lambda *a: 'picking',
1060 'step': lambda *a: 'one'
1063 def set_default(self, cr, uid, ids, context=None):
1064 for o in self.browse(cr, uid, ids, context=context):
1065 ir_values_obj = self.pool.get('ir.values')
1066 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1067 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1070 md = self.pool.get('ir.model.data')
1071 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1072 group_id = md.browse(cr, uid, group_id, context).res_id
1073 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1074 menu_id = md.browse(cr, uid, menu_id, context).res_id
1075 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1077 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1078 location_id = md.browse(cr, uid, location_id, context).res_id
1079 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1082 'view_type': 'form',
1083 "view_mode": 'form',
1084 'res_model': 'ir.actions.configuration.wizard',
1085 'type': 'ir.actions.act_window',
1089 def action_cancel(self, cr, uid, ids, context=None):
1091 'view_type': 'form',
1092 "view_mode": 'form',
1093 'res_model': 'ir.actions.configuration.wizard',
1094 'type': 'ir.actions.act_window',
1098 sale_config_picking_policy()