1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 from osv import fields, osv
25 from mx import DateTime
26 from tools import config
27 from tools.translate import _
30 class sale_shop(osv.osv):
32 _description = "Sale Shop"
34 'name': fields.char('Shop Name', size=64, required=True),
35 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
36 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
37 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
38 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
43 def _incoterm_get(self, cr, uid, context={}):
44 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
48 class sale_order(osv.osv):
50 _description = "Sale Order"
52 def copy(self, cr, uid, id, default=None, context={}):
60 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
62 return super(sale_order, self).copy(cr, uid, id, default, context)
64 def _amount_line_tax(self, cr, uid, line, context={}):
66 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):
70 def _amount_all(self, cr, uid, ids, field_name, arg, context):
72 cur_obj = self.pool.get('res.currency')
73 for order in self.browse(cr, uid, ids):
75 'amount_untaxed': 0.0,
80 cur = order.pricelist_id.currency_id
81 for line in order.order_line:
82 val1 += line.price_subtotal
83 val += self._amount_line_tax(cr, uid, line, context)
84 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
85 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
86 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
89 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
96 p.sale_id,sum(m.product_qty), m.state
100 stock_picking p on (p.id=m.picking_id)
102 p.sale_id in ('''+','.join(map(str, ids))+''')
103 GROUP BY m.state, p.sale_id''')
104 for oid, nbr, state in cr.fetchall():
105 if state == 'cancel':
108 res[oid][0] += nbr or 0.0
109 res[oid][1] += nbr or 0.0
111 res[oid][1] += nbr or 0.0
116 res[r] = 100.0 * res[r][0] / res[r][1]
117 for order in self.browse(cr, uid, ids, context):
119 res[order.id] = 100.0
122 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
124 for sale in self.browse(cursor, user, ids, context=context):
129 for invoice in sale.invoice_ids:
130 if invoice.state not in ('draft', 'cancel'):
131 tot += invoice.amount_untaxed
134 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
139 def _invoiced(self, cursor, user, ids, name, arg, context=None):
141 for sale in self.browse(cursor, user, ids, context=context):
143 for invoice in sale.invoice_ids:
144 if invoice.state != 'paid':
147 if not sale.invoice_ids:
151 def _invoiced_search(self, cursor, user, obj, name, args):
159 clause += 'AND inv.state = \'paid\''
161 clause += 'AND inv.state <> \'paid\''
163 cursor.execute('SELECT rel.order_id ' \
164 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
165 'WHERE rel.invoice_id = inv.id ' + clause)
166 res = cursor.fetchall()
168 cursor.execute('SELECT sale.id ' \
169 'FROM sale_order AS sale ' \
170 'WHERE sale.id NOT IN ' \
171 '(SELECT rel.order_id ' \
172 'FROM sale_order_invoice_rel AS rel)')
173 res.extend(cursor.fetchall())
175 return [('id', '=', 0)]
176 return [('id', 'in', [x[0] for x in res])]
178 def _get_order(self, cr, uid, ids, context={}):
180 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
181 result[line.order_id.id] = True
185 'name': fields.char('Order Reference', size=64, required=True, select=True),
186 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
187 'origin': fields.char('Origin', size=64),
188 'client_order_ref': fields.char('Customer Ref', size=64),
190 'state': fields.selection([
191 ('draft', 'Quotation'),
192 ('waiting_date', 'Waiting Schedule'),
193 ('manual', 'Manual In Progress'),
194 ('progress', 'In Progress'),
195 ('shipping_except', 'Shipping Exception'),
196 ('invoice_except', 'Invoice Exception'),
198 ('cancel', 'Cancelled')
199 ], '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 picking 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),
200 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
202 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
203 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
204 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
205 '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."),
206 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
208 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
209 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
210 'Picking Policy', required=True, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
211 'order_policy': fields.selection([
212 ('prepaid', 'Payment Before Delivery'),
213 ('manual', 'Shipping & Manual Invoice'),
214 ('postpaid', 'Invoice on Order After Delivery'),
215 ('picking', 'Invoice from Picking'),
216 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
217 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
218 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
219 - 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.
220 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all picking lists have been finished.
221 - The 'Invoice from the picking' choice is used to create an invoice during the packing process."""),
222 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
225 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
226 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', help="This is the list of invoices that have been generated for this sale order. The same sale order may have been invoiced in several times (by line for example)."),
227 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is the list of picking list that have been generated for this invoice"),
228 'shipped': fields.boolean('Picked', readonly=True),
229 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
230 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
231 'invoiced': fields.function(_invoiced, method=True, string='Paid',
232 fnct_search=_invoiced_search, type='boolean'),
233 'note': fields.text('Notes'),
235 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
241 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
243 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
244 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
247 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
249 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
250 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254 '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),
255 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
256 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
257 'company_id': fields.many2one('res.company','Company'),
260 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.order', c),
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)
369 journal_obj = self.pool.get('account.journal')
370 journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', order.company_id.id)], limit=1)
372 raise osv.except_osv(_('Error !'),
373 _('There is no sale journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
375 'name': order.client_order_ref or order.name,
376 'origin': order.name,
377 'type': 'out_invoice',
378 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
380 'partner_id': order.partner_id.id,
381 'journal_id': journal_ids[0],
382 'address_invoice_id': order.partner_invoice_id.id,
383 'address_contact_id': order.partner_order_id.id,
384 'invoice_line': [(6, 0, lines)],
385 'currency_id': order.pricelist_id.currency_id.id,
386 'comment': order.note,
387 'payment_term': pay_term,
388 'fiscal_position': order.partner_id.property_account_position.id,
389 'date_invoice' : context.get('date_invoice',False),
390 'company_id' : order.company_id.id,
392 inv_obj = self.pool.get('account.invoice')
393 inv.update(self._inv_get(cr, uid, order))
394 inv_id = inv_obj.create(cr, uid, inv, context)
395 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
396 if data.get('value', False):
397 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
398 inv_obj.button_compute(cr, uid, [inv_id])
401 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False):
407 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
408 # last day of the last month as invoice date
410 context['date_inv'] = date_inv
411 for o in self.browse(cr, uid, ids):
413 for line in o.order_line:
414 if (line.state in states) and not line.invoiced:
415 lines.append(line.id)
416 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
418 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
421 for o in self.browse(cr, uid, ids):
422 for i in o.invoice_ids:
423 if i.state == 'draft':
425 picking_obj = self.pool.get('stock.picking')
426 for val in invoices.values():
428 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x,y: x + y, [l for o,l in val], []), context=context)
430 self.write(cr, uid, [o.id], {'state': 'progress'})
431 if o.order_policy == 'picking':
432 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
433 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
435 for order, il in val:
436 res = self._make_invoice(cr, uid, order, il, context=context)
437 invoice_ids.append(res)
438 self.write(cr, uid, [order.id], {'state': 'progress'})
439 if order.order_policy == 'picking':
440 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
441 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
444 def action_invoice_cancel(self, cr, uid, ids, context={}):
445 for sale in self.browse(cr, uid, ids):
446 for line in sale.order_line:
448 for iline in line.invoice_lines:
449 if iline.invoice_id and iline.invoice_id.state == 'cancel':
453 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced})
454 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False})
457 def action_cancel(self, cr, uid, ids, context={}):
459 sale_order_line_obj = self.pool.get('sale.order.line')
460 for sale in self.browse(cr, uid, ids):
461 for pick in sale.picking_ids:
462 if pick.state not in ('draft', 'cancel'):
463 raise osv.except_osv(
464 _('Could not cancel sale order !'),
465 _('You must first cancel all packing attached to this sale order.'))
466 for r in self.read(cr, uid, ids, ['picking_ids']):
467 for pick in r['picking_ids']:
468 wf_service = netsvc.LocalService("workflow")
469 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
470 for inv in sale.invoice_ids:
471 if inv.state not in ('draft', 'cancel'):
472 raise osv.except_osv(
473 _('Could not cancel this sale order !'),
474 _('You must first cancel all invoices attached to this sale order.'))
475 for r in self.read(cr, uid, ids, ['invoice_ids']):
476 for inv in r['invoice_ids']:
477 wf_service = netsvc.LocalService("workflow")
478 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
479 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
481 self.write(cr, uid, ids, {'state': 'cancel'})
484 def action_wait(self, cr, uid, ids, *args):
485 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
486 event_obj = self.pool.get('res.partner.event')
487 for o in self.browse(cr, uid, ids):
489 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
490 'partner_id': o.partner_id.id,\
491 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
492 'user_id': (o.user_id and o.user_id.id) or uid,\
493 'partner_type': 'customer', 'probability': 1.0,\
494 'planned_revenue': o.amount_untaxed})
495 if (o.order_policy == 'manual'):
496 self.write(cr, uid, [o.id], {'state': 'manual'})
498 self.write(cr, uid, [o.id], {'state': 'progress'})
499 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
501 def procurement_lines_get(self, cr, uid, ids, *args):
503 for order in self.browse(cr, uid, ids, context={}):
504 for line in order.order_line:
505 if line.procurement_id:
506 res.append(line.procurement_id.id)
509 # if mode == 'finished':
510 # returns True if all lines are done, False otherwise
511 # if mode == 'canceled':
512 # returns True if there is at least one canceled line, False otherwise
513 def test_state(self, cr, uid, ids, mode, *args):
514 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
519 write_cancel_ids = []
520 stock_move_obj = self.pool.get('stock.move')
521 for order in self.browse(cr, uid, ids, context={}):
523 #check for pending deliveries
524 pending_deliveries = False
525 # check => if order_lines do not exist,don't proceed for any mode.
526 if not order.order_line:
528 for line in order.order_line:
529 move_ids = stock_move_obj.search(cr, uid, [('sale_line_id','=', line.id)])
530 for move in stock_move_obj.browse( cr, uid, move_ids ):
531 #if one of the related order lines is in state draft, auto or confirmed
532 #this order line is not yet delivered
533 if move.state in ('draft', 'waiting', 'confirmed'):
534 pending_deliveries = True
535 # Reason => if there are no move lines,the following condition will always set to be true,and will set SO to 'DONE'.
536 # Added move_ids check to SOLVE.
537 if move_ids and ((not line.procurement_id) or (line.procurement_id.state=='done')) and not pending_deliveries:
539 if line.state != 'done':
540 write_done_ids.append(line.id)
543 if line.procurement_id:
544 if (line.procurement_id.state == 'cancel'):
546 if line.state != 'exception':
547 write_cancel_ids.append(line.id)
551 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
553 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
555 if mode == 'finished':
557 elif mode == 'canceled':
563 def action_ship_create(self, cr, uid, ids, *args):
565 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
566 for order in self.browse(cr, uid, ids, context={}):
567 output_id = order.shop_id.warehouse_id.lot_output_id.id
569 for line in order.order_line:
571 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
572 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
573 if line.state == 'done':
575 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
576 location_id = order.shop_id.warehouse_id.lot_stock_id.id
578 loc_dest_id = order.partner_id.property_stock_customer.id
579 picking_id = self.pool.get('stock.picking').create(cr, uid, {
580 'origin': order.name,
583 'move_type': order.picking_policy,
585 'address_id': order.partner_shipping_id.id,
587 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
588 'company_id': order.company_id.id,
591 move_id = self.pool.get('stock.move').create(cr, uid, {
592 'name': line.name[:64],
593 'picking_id': picking_id,
594 'product_id': line.product_id.id,
595 'date_planned': date_planned,
596 'product_qty': line.product_uom_qty,
597 'product_uom': line.product_uom.id,
598 'product_uos_qty': line.product_uos_qty,
599 'product_uos': (line.product_uos and line.product_uos.id)\
600 or line.product_uom.id,
601 'product_packaging': line.product_packaging.id,
602 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
603 'location_id': location_id,
604 'location_dest_id': output_id,
605 'sale_line_id': line.id,
606 'tracking_id': False,
610 'company_id': order.company_id.id,
612 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
614 'origin': order.name,
615 'date_planned': date_planned,
616 'product_id': line.product_id.id,
617 'product_qty': line.product_uom_qty,
618 'product_uom': line.product_uom.id,
619 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
620 or line.product_uom_qty,
621 'product_uos': (line.product_uos and line.product_uos.id)\
622 or line.product_uom.id,
623 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
624 'procure_method': line.type,
626 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
627 'company_id': order.company_id.id,
629 wf_service = netsvc.LocalService("workflow")
630 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
631 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
632 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
633 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
635 'origin': order.name,
636 'date_planned': date_planned,
637 'product_id': line.product_id.id,
638 'product_qty': line.product_uom_qty,
639 'product_uom': line.product_uom.id,
640 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
641 'procure_method': line.type,
642 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
643 'company_id': order.company_id.id,
645 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
646 wf_service = netsvc.LocalService("workflow")
647 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
650 # No procurement because no product in the sale.order.line.
656 wf_service = netsvc.LocalService("workflow")
657 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
659 if order.state == 'shipping_except':
660 val['state'] = 'progress'
662 if (order.order_policy == 'manual'):
663 for line in order.order_line:
664 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
665 val['state'] = 'manual'
667 self.write(cr, uid, [order.id], val)
671 def action_ship_end(self, cr, uid, ids, context={}):
672 for order in self.browse(cr, uid, ids):
673 val = {'shipped': True}
674 if order.state == 'shipping_except':
675 val['state'] = 'progress'
676 if (order.order_policy == 'manual'):
677 for line in order.order_line:
678 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
679 val['state'] = 'manual'
681 for line in order.order_line:
683 if line.state == 'exception':
684 towrite.append(line.id)
686 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
687 self.write(cr, uid, [order.id], val)
690 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
691 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
693 part = inv['partner_id'] and inv['partner_id'][0]
694 pr = inv['amount_untaxed'] or 0.0
695 partnertype = 'customer'
698 'name': 'Order: '+name,
700 'description': 'Order '+str(inv['id']),
703 'date': time.strftime('%Y-%m-%d'),
706 'partner_type': partnertype,
708 'planned_revenue': pr,
712 self.pool.get('res.partner.event').create(cr, uid, event)
714 def has_stockable_products(self, cr, uid, ids, *args):
715 for order in self.browse(cr, uid, ids):
716 for order_line in order.order_line:
717 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
722 # TODO add a field price_unit_uos
723 # - update it on change product and unit price
724 # - use it in report if there is a uos
725 class sale_order_line(osv.osv):
726 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
728 for line in self.browse(cr, uid, ids):
729 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
732 def _amount_line(self, cr, uid, ids, field_name, arg, context):
734 cur_obj = self.pool.get('res.currency')
735 for line in self.browse(cr, uid, ids):
736 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
737 cur = line.order_id.pricelist_id.currency_id
738 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
741 def _number_packages(self, cr, uid, ids, field_name, arg, context):
743 for line in self.browse(cr, uid, ids):
745 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
750 _name = 'sale.order.line'
751 _description = 'Sale Order line'
753 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True),
754 'name': fields.char('Description', size=256, required=True, select=True),
755 'sequence': fields.integer('Sequence'),
756 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the the shipping of the products to the customer"),
757 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
758 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
759 'invoiced': fields.boolean('Invoiced', readonly=True),
760 'procurement_id': fields.many2one('mrp.procurement', 'Requisition'),
761 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
762 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
763 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal'),
764 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes'),
765 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Requisition Method', required=True),
766 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties'),
767 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
768 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True),
769 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
770 'product_uos_qty': fields.float('Quantity (UoS)'),
771 'product_uos': fields.many2one('product.uom', 'Product UoS'),
772 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
773 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
774 'discount': fields.float('Discount (%)', digits=(16, 2)),
775 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
776 'notes': fields.text('Notes'),
777 'th_weight': fields.float('Weight'),
778 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'State', required=True, readonly=True),
779 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer'),
780 'salesman_id':fields.related('order_id','user_id',type='many2one',relation='res.users',string='Salesman'),
781 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company'),
783 _order = 'sequence, id'
785 'discount': lambda *a: 0.0,
786 'delay': lambda *a: 0.0,
787 'product_uom_qty': lambda *a: 1,
788 'product_uos_qty': lambda *a: 1,
789 'sequence': lambda *a: 10,
790 'invoiced': lambda *a: 0,
791 'state': lambda *a: 'draft',
792 'type': lambda *a: 'make_to_stock',
793 'product_packaging': lambda *a: False
796 def invoice_line_create(self, cr, uid, ids, context={}):
797 def _get_line_qty(line):
798 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
800 return line.product_uos_qty or 0.0
801 return line.product_uom_qty
803 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
804 line.procurement_id.id, context)
806 def _get_line_uom(line):
807 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
809 return line.product_uos.id
810 return line.product_uom.id
812 return self.pool.get('mrp.procurement').uom_get(cr, uid,
813 line.procurement_id.id, context)
817 for line in self.browse(cr, uid, ids, context):
818 if not line.invoiced:
820 a = line.product_id.product_tmpl_id.property_account_income.id
822 a = line.product_id.categ_id.property_account_income_categ.id
824 raise osv.except_osv(_('Error !'),
825 _('There is no income account defined ' \
826 'for this product: "%s" (id:%d)') % \
827 (line.product_id.name, line.product_id.id,))
829 a = self.pool.get('ir.property').get(cr, uid,
830 'property_account_income_categ', 'product.category',
832 uosqty = _get_line_qty(line)
833 uos_id = _get_line_uom(line)
836 pu = round(line.price_unit * line.product_uom_qty / uosqty,
837 int(config['price_accuracy']))
838 fpos = line.order_id.fiscal_position or False
839 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
841 raise osv.except_osv(_('Error !'),
842 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
843 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
845 'origin': line.order_id.name,
849 'discount': line.discount,
851 'product_id': line.product_id.id or False,
852 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
854 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
856 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
857 self.write(cr, uid, [line.id], {'invoiced': True})
859 sales[line.order_id.id] = True
860 create_ids.append(inv_id)
862 # Trigger workflow events
863 wf_service = netsvc.LocalService("workflow")
864 for sid in sales.keys():
865 wf_service.trg_write(uid, 'sale.order', sid, cr)
868 def button_cancel(self, cr, uid, ids, context={}):
869 for line in self.browse(cr, uid, ids, context=context):
871 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
872 return self.write(cr, uid, ids, {'state': 'cancel'})
874 def button_confirm(self, cr, uid, ids, context={}):
875 return self.write(cr, uid, ids, {'state': 'confirmed'})
877 def button_done(self, cr, uid, ids, context={}):
878 wf_service = netsvc.LocalService("workflow")
879 res = self.write(cr, uid, ids, {'state': 'done'})
880 for line in self.browse(cr, uid, ids, context):
881 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
885 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
886 product_obj = self.pool.get('product.product')
888 return {'value': {'product_uom': product_uos,
889 'product_uom_qty': product_uos_qty}, 'domain': {}}
891 product = product_obj.browse(cr, uid, product_id)
893 'product_uom': product.uom_id.id,
895 # FIXME must depend on uos/uom of the product and not only of the coeff.
898 'product_uom_qty': product_uos_qty / product.uos_coeff,
899 'th_weight': product_uos_qty / product.uos_coeff * product.weight
901 except ZeroDivisionError:
903 return {'value': value}
905 def copy_data(self, cr, uid, id, default=None, context={}):
908 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
909 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
911 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
912 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
913 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
915 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.'))
917 product_uom_obj = self.pool.get('product.uom')
918 partner_obj = self.pool.get('res.partner')
919 product_obj = self.pool.get('product.product')
921 lang = partner_obj.browse(cr, uid, partner_id).lang
922 context = {'lang': lang, 'partner_id': partner_id}
925 return {'value': {'th_weight': 0, 'product_packaging': False,
926 'product_uos_qty': qty}, 'domain': {'product_uom': [],
930 date_order = time.strftime('%Y-%m-%d')
933 product_obj = product_obj.browse(cr, uid, product, context=context)
934 if not packaging and product_obj.packaging:
935 packaging = product_obj.packaging[0].id
936 result['product_packaging'] = packaging
939 default_uom = product_obj.uom_id and product_obj.uom_id.id
940 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
941 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
942 # qty = qty - qty % q + q
943 if qty and (q and not (qty % q) == 0):
947 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)
948 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
950 'title': _('Picking Information !'),
953 result['product_uom_qty'] = qty
956 uom2 = product_uom_obj.browse(cr, uid, uom)
957 if product_obj.uom_id.category_id.id != uom2.category_id.id:
961 if product_obj.uos_id:
962 uos2 = product_uom_obj.browse(cr, uid, uos)
963 if product_obj.uos_id.category_id.id != uos2.category_id.id:
967 result.update({'type': product_obj.procure_method})
968 if product_obj.description_sale:
969 result['notes'] = product_obj.description_sale
970 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
971 if update_tax: #The quantity only have changed
972 result['delay'] = (product_obj.sale_delay or 0.0)
973 partner = partner_obj.browse(cr, uid, partner_id)
974 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
976 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
978 if (not uom) and (not uos):
979 result['product_uom'] = product_obj.uom_id.id
980 if product_obj.uos_id:
981 result['product_uos'] = product_obj.uos_id.id
982 result['product_uos_qty'] = qty * product_obj.uos_coeff
983 uos_category_id = product_obj.uos_id.category_id.id
985 result['product_uos'] = False
986 result['product_uos_qty'] = qty
987 uos_category_id = False
988 result['th_weight'] = qty * product_obj.weight
989 domain = {'product_uom':
990 [('category_id', '=', product_obj.uom_id.category_id.id)],
992 [('category_id', '=', uos_category_id)]}
994 elif uos: # only happens if uom is False
995 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
996 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
997 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
998 elif uom: # whether uos is set or not
999 default_uom = product_obj.uom_id and product_obj.uom_id.id
1000 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1001 if product_obj.uos_id:
1002 result['product_uos'] = product_obj.uos_id.id
1003 result['product_uos_qty'] = qty * product_obj.uos_coeff
1005 result['product_uos'] = False
1006 result['product_uos_qty'] = qty
1007 result['th_weight'] = q * product_obj.weight # Round the quantity up
1013 'title': 'No Pricelist !',
1015 'You have to select a pricelist in the sale form !\n'
1016 'Please set one before choosing a product.'
1019 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1020 product, qty or 1.0, partner_id, {
1026 'title': 'No valid pricelist line found !',
1028 "Couldn't find a pricelist line matching this product and quantity.\n"
1029 "You have to change either the product, the quantity or the pricelist."
1032 result.update({'price_unit': price})
1033 return {'value': result, 'domain': domain, 'warning': warning}
1035 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1036 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1037 lang=False, update_tax=True, date_order=False):
1038 res = self.product_id_change(cursor, user, ids, pricelist, product,
1039 qty=0, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1040 partner_id=partner_id, lang=lang, update_tax=update_tax,
1041 date_order=date_order)
1042 if 'product_uom' in res['value']:
1043 del res['value']['product_uom']
1045 res['value']['price_unit'] = 0.0
1048 def unlink(self, cr, uid, ids, context={}):
1049 """Allows to delete sale order lines in draft,cancel states"""
1050 for rec in self.browse(cr, uid, ids, context=context):
1051 if rec.state not in ['draft', 'cancel']:
1052 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1053 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1058 class sale_config_picking_policy(osv.osv_memory):
1059 _name = 'sale.config.picking_policy'
1061 'name': fields.char('Name', size=64),
1062 'picking_policy': fields.selection([
1063 ('direct', 'Direct Delivery'),
1064 ('one', 'All at Once')
1065 ], 'Picking Default Policy', required=True),
1066 'order_policy': fields.selection([
1067 ('manual', 'Invoice Based on Sales Orders'),
1068 ('picking', 'Invoice Based on Deliveries'),
1069 ], 'Shipping Default Policy', required=True),
1070 'step': fields.selection([
1071 ('one', 'Delivery Order Only'),
1072 ('two', 'Picking List & Delivery Order')
1073 ], 'Steps To Deliver a Sale Order', required=True,
1074 help="By default, Open ERP is able to manage complex routing and paths "\
1075 "of products in your warehouse and partner locations. This will configure "\
1076 "the most common and simple methods to deliver products to the customer "\
1077 "in one or two operations by the worker.")
1080 'picking_policy': lambda *a: 'direct',
1081 'order_policy': lambda *a: 'picking',
1082 'step': lambda *a: 'one'
1085 def set_default(self, cr, uid, ids, context=None):
1086 for o in self.browse(cr, uid, ids, context=context):
1087 ir_values_obj = self.pool.get('ir.values')
1088 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1089 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1092 md = self.pool.get('ir.model.data')
1093 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1094 group_id = md.browse(cr, uid, group_id, context).res_id
1095 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1096 menu_id = md.browse(cr, uid, menu_id, context).res_id
1097 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1099 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1100 location_id = md.browse(cr, uid, location_id, context).res_id
1101 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1104 'view_type': 'form',
1105 "view_mode": 'form',
1106 'res_model': 'ir.actions.configuration.wizard',
1107 'type': 'ir.actions.act_window',
1111 def action_cancel(self, cr, uid, ids, context=None):
1113 'view_type': 'form',
1114 "view_mode": 'form',
1115 'res_model': 'ir.actions.configuration.wizard',
1116 'type': 'ir.actions.act_window',
1120 sale_config_picking_policy()