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 _
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 'payment_account_id': fields.many2many('account.account', 'sale_shop_account', 'shop_id', 'account_id', 'Payment Accounts'),
37 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
38 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account'),
44 def _incoterm_get(self, cr, uid, context={}):
45 cr.execute('select code, code||\', \'||name from stock_incoterms where active')
49 class sale_order(osv.osv):
51 _description = "Sale Order"
53 def copy(self, cr, uid, id, default=None, context={}):
61 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
63 return super(sale_order, self).copy(cr, uid, id, default, context)
65 def _amount_line_tax(self, cr, uid, line, context={}):
67 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):
71 def _amount_all(self, cr, uid, ids, field_name, arg, context):
73 cur_obj = self.pool.get('res.currency')
74 for order in self.browse(cr, uid, ids):
76 'amount_untaxed': 0.0,
81 cur = order.pricelist_id.currency_id
82 for line in order.order_line:
83 val1 += line.price_subtotal
84 val += self._amount_line_tax(cr, uid, line, context)
85 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
86 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
87 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
90 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
97 p.sale_id,sum(m.product_qty), mp.state as mp_state, m.state as state, p.type as tp
101 stock_picking p on (p.id=m.picking_id)
103 mrp_procurement mp on (mp.move_id=m.id)
106 GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
108 for oid, nbr, state, move_state, type_pick in cr.fetchall():
109 if state == 'cancel':
111 res[oid][1] += nbr or 0.0
112 if state == 'done' or move_state == 'done':
113 res[oid][0] += nbr or 0.0
115 if type_pick == 'in':#which clearly means that this is a returned picking
116 res[oid][1] -= 2*nbr or 0.0 # Deducting the return picking qty
117 if state == 'done' or move_state == 'done':
119 res[oid][0] -= nbr or 0.0
125 res[r] = 100.0 * res[r][0] / res[r][1]
126 for order in self.browse(cr, uid, ids, context):
128 res[order.id] = 100.0
131 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
133 for sale in self.browse(cursor, user, ids, context=context):
138 for invoice in sale.invoice_ids:
139 if invoice.state not in ('draft', 'cancel'):
140 tot += invoice.amount_untaxed
143 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
148 def _invoiced(self, cursor, user, ids, name, arg, context=None):
150 for sale in self.browse(cursor, user, ids, context=context):
152 for invoice in sale.invoice_ids:
153 if invoice.state != 'paid':
156 if not sale.invoice_ids:
160 def _invoiced_search(self, cursor, user, obj, name, args, context):
168 clause += 'AND inv.state = \'paid\''
170 clause += 'AND inv.state <> \'paid\''
172 cursor.execute('SELECT rel.order_id ' \
173 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv ' \
174 'WHERE rel.invoice_id = inv.id ' + clause)
175 res = cursor.fetchall()
177 cursor.execute('SELECT sale.id ' \
178 'FROM sale_order AS sale ' \
179 'WHERE sale.id NOT IN ' \
180 '(SELECT rel.order_id ' \
181 'FROM sale_order_invoice_rel AS rel)')
182 res.extend(cursor.fetchall())
184 return [('id', '=', 0)]
185 return [('id', 'in', [x[0] for x in res])]
187 def _get_order(self, cr, uid, ids, context={}):
189 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
190 result[line.order_id.id] = True
194 'name': fields.char('Order Reference', size=64, required=True, select=True),
195 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
196 'origin': fields.char('Origin', size=64),
197 'client_order_ref': fields.char('Customer Ref', size=64),
199 'state': fields.selection([
200 ('draft', 'Quotation'),
201 ('waiting_date', 'Waiting Schedule'),
202 ('manual', 'Manual In Progress'),
203 ('progress', 'In Progress'),
204 ('shipping_except', 'Shipping Exception'),
205 ('invoice_except', 'Invoice Exception'),
207 ('cancel', 'Cancelled')
208 ], '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),
209 'date_order': fields.date('Date Ordered', required=True, readonly=True, states={'draft': [('readonly', False)]}),
211 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
212 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
213 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
214 '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."),
215 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}),
217 'incoterm': fields.selection(_incoterm_get, 'Incoterm', size=3),
218 'picking_policy': fields.selection([('direct', 'Partial Delivery'), ('one', 'Complete Delivery')],
219 '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?"""),
220 'order_policy': fields.selection([
221 ('prepaid', 'Payment Before Delivery'),
222 ('manual', 'Shipping & Manual Invoice'),
223 ('postpaid', 'Invoice on Order After Delivery'),
224 ('picking', 'Invoice from the Packing'),
225 ], 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
226 help="""The Shipping Policy is used to synchronise invoice and delivery operations.
227 - The 'Pay before delivery' choice will first generate the invoice and then generate the packing order after the payment of this invoice.
228 - 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.
229 - The 'Invoice on Order After Delivery' choice will generate the draft invoice based on sale order after all packing lists have been finished.
230 - The 'Invoice from the packing' choice is used to create an invoice during the packing process."""),
231 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}),
232 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}),
234 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
235 '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)."),
236 '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"),
237 'shipped': fields.boolean('Picked', readonly=True),
238 'picked_rate': fields.function(_picked_rate, method=True, string='Picked', type='float'),
239 'invoiced_rate': fields.function(_invoiced_rate, method=True, string='Invoiced', type='float'),
240 'invoiced': fields.function(_invoiced, method=True, string='Paid',
241 fnct_search=_invoiced_search, type='boolean'),
242 'note': fields.text('Notes'),
244 'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Untaxed Amount',
246 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
247 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
250 'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Taxes',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
256 'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
263 '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),
264 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
265 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
268 'picking_policy': lambda *a: 'direct',
269 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
270 'order_policy': lambda *a: 'manual',
271 'state': lambda *a: 'draft',
272 'user_id': lambda obj, cr, uid, context: uid,
273 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
274 'invoice_quantity': lambda *a: 'order',
275 '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'],
276 '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'],
277 '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'],
278 # '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,
283 def unlink(self, cr, uid, ids, context=None):
284 sale_orders = self.read(cr, uid, ids, ['state'])
286 for s in sale_orders:
287 if s['state'] in ['draft', 'cancel']:
288 unlink_ids.append(s['id'])
290 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sale Order(s) which are already confirmed !'))
291 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
293 def onchange_shop_id(self, cr, uid, ids, shop_id):
296 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
297 v['project_id'] = shop.project_id.id
298 # Que faire si le client a une pricelist a lui ?
299 if shop.pricelist_id.id:
300 v['pricelist_id'] = shop.pricelist_id.id
301 #v['payment_default_id']=shop.payment_default_id.id
304 def action_cancel_draft(self, cr, uid, ids, *args):
307 cr.execute('select id from sale_order_line where order_id in %s', (tuple(ids),))
308 line_ids = map(lambda x: x[0], cr.fetchall())
309 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
310 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
311 wf_service = netsvc.LocalService("workflow")
313 # Deleting the existing instance of workflow for SO
314 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
315 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
318 def onchange_partner_id(self, cr, uid, ids, part):
320 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
322 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
323 part = self.pool.get('res.partner').browse(cr, uid, part)
324 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
325 payment_term = part.property_payment_term and part.property_payment_term.id or False
326 fiscal_position = part.property_account_position and part.property_account_position.id or False
327 dedicated_salesman = part.user_id and part.user_id.id or uid
330 'partner_invoice_id': addr['invoice'],
331 'partner_order_id': addr['contact'],
332 'partner_shipping_id': addr['delivery'],
333 'payment_term': payment_term,
334 'fiscal_position': fiscal_position,
335 'user_id': dedicated_salesman,
339 val['pricelist_id'] = pricelist
341 return {'value': val}
343 def shipping_policy_change(self, cr, uid, ids, policy, context={}):
347 if policy == 'prepaid':
349 elif policy == 'picking':
350 inv_qty = 'procurement'
351 return {'value': {'invoice_quantity': inv_qty}}
353 def write(self, cr, uid, ids, vals, context=None):
354 if 'order_policy' in vals:
355 if vals['order_policy'] == 'prepaid':
356 vals.update({'invoice_quantity': 'order'})
357 elif vals['order_policy'] == 'picking':
358 vals.update({'invoice_quantity': 'procurement'})
359 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
361 def create(self, cr, uid, vals, context={}):
362 if 'order_policy' in vals:
363 if vals['order_policy'] == 'prepaid':
364 vals.update({'invoice_quantity': 'order'})
365 if vals['order_policy'] == 'picking':
366 vals.update({'invoice_quantity': 'procurement'})
367 return super(sale_order, self).create(cr, uid, vals, context=context)
369 def button_dummy(self, cr, uid, ids, context={}):
372 #FIXME: the method should return the list of invoices created (invoice_ids)
373 # and not the id of the last invoice created (res). The problem is that we
374 # cannot change it directly since the method is called by the sale order
375 # workflow and I suppose it expects a single id...
376 def _inv_get(self, cr, uid, order, context={}):
379 def _make_invoice(self, cr, uid, order, lines, context={}):
380 a = order.partner_id.property_account_receivable.id
381 if order.payment_term:
382 pay_term = order.payment_term.id
385 for preinv in order.invoice_ids:
386 if preinv.state not in ('cancel',):
387 for preline in preinv.invoice_line:
388 inv_line_id = self.pool.get('account.invoice.line').copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
389 lines.append(inv_line_id)
391 'name': order.client_order_ref or order.name,
392 'origin': order.name,
393 'type': 'out_invoice',
394 'reference': "P%dSO%d" % (order.partner_id.id, order.id),
396 'partner_id': order.partner_id.id,
397 'address_invoice_id': order.partner_invoice_id.id,
398 'address_contact_id': order.partner_order_id.id,
399 'invoice_line': [(6, 0, lines)],
400 'currency_id': order.pricelist_id.currency_id.id,
401 'comment': order.note,
402 'payment_term': pay_term,
403 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id
405 inv_obj = self.pool.get('account.invoice')
406 inv.update(self._inv_get(cr, uid, order))
407 inv_id = inv_obj.create(cr, uid, inv)
408 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
409 if data.get('value', False):
410 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
411 inv_obj.button_compute(cr, uid, [inv_id])
414 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception']):
419 for o in self.browse(cr, uid, ids):
421 for line in o.order_line:
422 if (line.state in states) and not line.invoiced:
423 lines.append(line.id)
424 created_lines = self.pool.get('sale.order.line').invoice_line_create(cr, uid, lines)
426 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
429 for o in self.browse(cr, uid, ids):
430 for i in o.invoice_ids:
431 if i.state == 'draft':
433 picking_obj = self.pool.get('stock.picking')
434 for val in invoices.values():
436 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []))
438 self.write(cr, uid, [o.id], {'state': 'progress'})
439 if o.order_policy == 'picking':
440 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
441 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
443 for order, il in val:
444 res = self._make_invoice(cr, uid, order, il)
445 invoice_ids.append(res)
446 self.write(cr, uid, [order.id], {'state': 'progress'})
447 if order.order_policy == 'picking':
448 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
449 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
452 def action_invoice_cancel(self, cr, uid, ids, context=None):
453 for sale in self.browse(cr, uid, ids):
454 for line in sale.order_line:
456 # Check if the line is invoiced (has asociated invoice
457 # lines from non-cancelled invoices).
460 for iline in line.invoice_lines:
461 if iline.invoice_id and iline.invoice_id.state != 'cancel':
464 # Update the line (only when needed)
465 if line.invoiced != invoiced:
466 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
467 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
470 def action_invoice_end(self, cr, uid, ids, context=None):
471 for order in self.browse(cr, uid, ids, context=context):
473 # Update the sale order lines state (and invoiced flag).
475 for line in order.order_line:
478 # Check if the line is invoiced (has asociated invoice
479 # lines from non-cancelled invoices).
482 for iline in line.invoice_lines:
483 if iline.invoice_id and iline.invoice_id.state != 'cancel':
486 if line.invoiced != invoiced:
487 vals['invoiced'] = invoiced
488 # If the line was in exception state, now it gets confirmed.
489 if line.state == 'exception':
490 vals['state'] = 'confirmed'
491 # Update the line (only when needed).
493 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
495 # Update the sale order state.
497 if order.state == 'invoice_except':
498 self.write(cr, uid, [order.id], {'state' : 'progress'}, context=context)
502 def action_cancel(self, cr, uid, ids, context={}):
504 wf_service = netsvc.LocalService("workflow")
505 sale_order_line_obj = self.pool.get('sale.order.line')
506 for sale in self.browse(cr, uid, ids):
507 for pick in sale.picking_ids:
508 if pick.state not in ('draft', 'cancel'):
509 raise osv.except_osv(
510 _('Could not cancel sale order !'),
511 _('You must first cancel all packing attached to this sale order.'))
512 if pick.state == 'cancel':
513 for mov in pick.move_lines:
514 proc_ids = self.pool.get('mrp.procurement').search(cr, uid, [('move_id', '=', mov.id)])
516 for proc in proc_ids:
517 wf_service.trg_validate(uid, 'mrp.procurement', proc, 'button_check', cr)
518 for r in self.read(cr, uid, ids, ['picking_ids']):
519 for pick in r['picking_ids']:
520 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
521 for inv in sale.invoice_ids:
522 if inv.state not in ('draft', 'cancel'):
523 raise osv.except_osv(
524 _('Could not cancel this sale order !'),
525 _('You must first cancel all invoices attached to this sale order.'))
526 for r in self.read(cr, uid, ids, ['invoice_ids']):
527 for inv in r['invoice_ids']:
528 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
529 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
531 self.write(cr, uid, ids, {'state': 'cancel'})
534 def action_wait(self, cr, uid, ids, *args):
535 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
536 event_obj = self.pool.get('res.partner.event')
537 for o in self.browse(cr, uid, ids):
539 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
540 'partner_id': o.partner_id.id,\
541 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
542 'user_id': (o.user_id and o.user_id.id) or uid,\
543 'partner_type': 'customer', 'probability': 1.0,\
544 'planned_revenue': o.amount_untaxed})
545 if (o.order_policy == 'manual'):
546 self.write(cr, uid, [o.id], {'state': 'manual'})
548 self.write(cr, uid, [o.id], {'state': 'progress'})
549 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
551 def procurement_lines_get(self, cr, uid, ids, *args):
553 for order in self.browse(cr, uid, ids, context={}):
554 for line in order.order_line:
555 if line.procurement_id:
556 res.append(line.procurement_id.id)
559 # if mode == 'finished':
560 # returns True if all lines are done, False otherwise
561 # if mode == 'canceled':
562 # returns True if there is at least one canceled line, False otherwise
563 def test_state(self, cr, uid, ids, mode, *args):
564 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
569 write_cancel_ids = []
570 for order in self.browse(cr, uid, ids, context={}):
571 for line in order.order_line:
572 if (not line.procurement_id) or (line.procurement_id.state=='done'):
573 if line.state != 'done':
574 write_done_ids.append(line.id)
577 if line.procurement_id:
578 if (line.procurement_id.state == 'cancel'):
580 if line.state != 'exception':
581 write_cancel_ids.append(line.id)
585 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
587 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
589 if mode == 'finished':
591 elif mode == 'canceled':
597 def action_ship_create(self, cr, uid, ids, *args):
599 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
600 picking_obj = self.pool.get('stock.picking')
601 sale_order_line_obj = self.pool.get('sale.order.line')
602 move_obj = self.pool.get('stock.move')
603 proc_obj = self.pool.get('mrp.procurement')
604 for order in self.browse(cr, uid, ids, context={}):
605 output_id = order.shop_id.warehouse_id.lot_output_id.id
607 for line in order.order_line:
609 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
610 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
611 if line.state == 'done':
613 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
614 location_id = order.shop_id.warehouse_id.lot_stock_id.id
616 loc_dest_id = order.partner_id.property_stock_customer.id
617 picking_id = picking_obj.create(cr, uid, {
618 'origin': order.name,
621 'move_type': order.picking_policy,
623 'address_id': order.partner_shipping_id.id,
625 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
629 move_id = move_obj.create(cr, uid, {
630 'name': line.name[:64],
631 'picking_id': picking_id,
632 'product_id': line.product_id.id,
633 'date_planned': date_planned,
634 'product_qty': line.product_uom_qty,
635 'product_uom': line.product_uom.id,
636 'product_uos_qty': line.product_uos_qty,
637 'product_uos': (line.product_uos and line.product_uos.id)\
638 or line.product_uom.id,
639 'product_packaging': line.product_packaging.id,
640 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
641 'location_id': location_id,
642 'location_dest_id': output_id,
643 'sale_line_id': line.id,
644 'tracking_id': False,
649 proc_id = proc_obj.create(cr, uid, {
651 'origin': order.name,
652 'date_planned': date_planned,
653 'product_id': line.product_id.id,
654 'product_qty': line.product_uom_qty,
655 'product_uom': line.product_uom.id,
656 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
657 or line.product_uom_qty,
658 'product_uos': (line.product_uos and line.product_uos.id)\
659 or line.product_uom.id,
660 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
661 'procure_method': line.type,
663 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
665 wf_service = netsvc.LocalService("workflow")
666 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
667 sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
668 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
669 proc_id = proc_obj.create(cr, uid, {
671 'origin': order.name,
672 'date_planned': date_planned,
673 'product_id': line.product_id.id,
674 'product_qty': line.product_uom_qty,
675 'product_uom': line.product_uom.id,
676 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
677 'procure_method': line.type,
678 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
680 sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
681 wf_service = netsvc.LocalService("workflow")
682 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
685 # No procurement because no product in the sale.order.line.
691 wf_service = netsvc.LocalService("workflow")
692 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
694 if order.state == 'shipping_except':
695 val['state'] = 'progress'
697 if (order.order_policy == 'manual'):
698 for line in order.order_line:
699 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
700 val['state'] = 'manual'
702 self.write(cr, uid, [order.id], val)
706 def action_ship_end(self, cr, uid, ids, context={}):
707 for order in self.browse(cr, uid, ids):
708 val = {'shipped': True}
709 if order.state == 'shipping_except':
710 val['state'] = 'progress'
711 if (order.order_policy == 'manual'):
712 for line in order.order_line:
713 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
714 val['state'] = 'manual'
716 for line in order.order_line:
718 if line.state == 'exception':
719 towrite.append(line.id)
721 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
722 self.write(cr, uid, [order.id], val)
725 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
726 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
728 part = inv['partner_id'] and inv['partner_id'][0]
729 pr = inv['amount_untaxed'] or 0.0
730 partnertype = 'customer'
733 'name': 'Order: '+name,
735 'description': 'Order '+str(inv['id']),
738 'date': time.strftime('%Y-%m-%d'),
741 'partner_type': partnertype,
743 'planned_revenue': pr,
747 self.pool.get('res.partner.event').create(cr, uid, event)
749 def has_stockable_products(self, cr, uid, ids, *args):
750 for order in self.browse(cr, uid, ids):
751 for order_line in order.order_line:
752 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
757 # TODO add a field price_unit_uos
758 # - update it on change product and unit price
759 # - use it in report if there is a uos
760 class sale_order_line(osv.osv):
761 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
763 for line in self.browse(cr, uid, ids):
764 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
767 def _amount_line(self, cr, uid, ids, field_name, arg, context):
769 cur_obj = self.pool.get('res.currency')
770 for line in self.browse(cr, uid, ids):
771 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
772 cur = line.order_id.pricelist_id.currency_id
773 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
776 def _number_packages(self, cr, uid, ids, field_name, arg, context):
778 for line in self.browse(cr, uid, ids):
780 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
785 _name = 'sale.order.line'
786 _description = 'Sale Order line'
788 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
789 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
790 'sequence': fields.integer('Sequence'),
791 '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", readonly=True, states={'draft':[('readonly',False)]}),
792 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
793 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
794 'invoiced': fields.boolean('Invoiced', readonly=True),
795 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
796 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy'])), readonly=True, states={'draft':[('readonly',False)]}),
797 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
798 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
799 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
800 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
801 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
802 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
803 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
804 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}),
805 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
806 'product_uos': fields.many2one('product.uom', 'Product UoS'),
807 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
808 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
809 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
810 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
811 'notes': fields.text('Notes'),
812 'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
813 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
814 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
816 _order = 'sequence, id'
818 'discount': lambda *a: 0.0,
819 'delay': lambda *a: 0.0,
820 'product_uom_qty': lambda *a: 1,
821 'product_uos_qty': lambda *a: 1,
822 'sequence': lambda *a: 10,
823 'invoiced': lambda *a: 0,
824 'state': lambda *a: 'draft',
825 'type': lambda *a: 'make_to_stock',
826 'product_packaging': lambda *a: False
829 def invoice_line_create(self, cr, uid, ids, context={}):
830 def _get_line_qty(line):
831 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
833 return line.product_uos_qty or 0.0
834 return line.product_uom_qty
836 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
837 line.procurement_id.id, context)
839 def _get_line_uom(line):
840 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
842 return line.product_uos.id
843 return line.product_uom.id
845 return self.pool.get('mrp.procurement').uom_get(cr, uid,
846 line.procurement_id.id, context)
850 for line in self.browse(cr, uid, ids, context):
851 if not line.invoiced:
853 a = line.product_id.product_tmpl_id.property_account_income.id
855 a = line.product_id.categ_id.property_account_income_categ.id
857 raise osv.except_osv(_('Error !'),
858 _('There is no income account defined ' \
859 'for this product: "%s" (id:%d)') % \
860 (line.product_id.name, line.product_id.id,))
862 a = self.pool.get('ir.property').get(cr, uid,
863 'property_account_income_categ', 'product.category',
865 uosqty = _get_line_qty(line)
866 uos_id = _get_line_uom(line)
869 pu = round(line.price_unit * line.product_uom_qty / uosqty,
870 int(config['price_accuracy']))
871 fpos = line.order_id.fiscal_position or False
872 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
874 raise osv.except_osv(_('Error !'),
875 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
876 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
878 'origin': line.order_id.name,
882 'discount': line.discount,
884 'product_id': line.product_id.id or False,
885 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
887 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
889 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
890 self.write(cr, uid, [line.id], {'invoiced': True})
892 sales[line.order_id.id] = True
893 create_ids.append(inv_id)
895 # Trigger workflow events
896 wf_service = netsvc.LocalService("workflow")
897 for sid in sales.keys():
898 wf_service.trg_write(uid, 'sale.order', sid, cr)
901 def button_cancel(self, cr, uid, ids, context={}):
902 for line in self.browse(cr, uid, ids, context=context):
904 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
905 return self.write(cr, uid, ids, {'state': 'cancel'})
907 def button_confirm(self, cr, uid, ids, context={}):
908 return self.write(cr, uid, ids, {'state': 'confirmed'})
910 def button_done(self, cr, uid, ids, context={}):
911 wf_service = netsvc.LocalService("workflow")
912 res = self.write(cr, uid, ids, {'state': 'done'})
913 for line in self.browse(cr, uid, ids, context):
914 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
918 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
919 product_obj = self.pool.get('product.product')
921 return {'value': {'product_uom': product_uos,
922 'product_uom_qty': product_uos_qty}, 'domain': {}}
924 product = product_obj.browse(cr, uid, product_id)
926 'product_uom': product.uom_id.id,
928 # FIXME must depend on uos/uom of the product and not only of the coeff.
931 'product_uom_qty': product_uos_qty / product.uos_coeff,
932 'th_weight': product_uos_qty / product.uos_coeff * product.weight
934 except ZeroDivisionError:
936 return {'value': value}
938 def copy_data(self, cr, uid, id, default=None, context={}):
941 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
942 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
944 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
945 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
946 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
948 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.'))
950 product_uom_obj = self.pool.get('product.uom')
951 partner_obj = self.pool.get('res.partner')
952 product_obj = self.pool.get('product.product')
954 lang = partner_obj.browse(cr, uid, partner_id).lang
955 context = {'lang': lang, 'partner_id': partner_id}
957 return {'value': {'th_weight': 0, 'product_packaging': False,
958 'product_uos_qty': qty}, 'domain': {'product_uom': [],
962 date_order = time.strftime('%Y-%m-%d')
965 product_obj = product_obj.browse(cr, uid, product, context=context)
966 if not packaging and product_obj.packaging:
967 packaging = product_obj.packaging[0].id
968 result['product_packaging'] = packaging
971 default_uom = product_obj.uom_id and product_obj.uom_id.id
972 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
973 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
974 # qty = qty - qty % q + q
975 if qty and (q and not (qty % q) == 0):
979 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)
980 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
982 'title': _('Packing Information !'),
985 result['product_uom_qty'] = qty
988 uom2 = product_uom_obj.browse(cr, uid, uom)
989 if product_obj.uom_id.category_id.id != uom2.category_id.id:
993 if product_obj.uos_id:
994 uos2 = product_uom_obj.browse(cr, uid, uos)
995 if product_obj.uos_id.category_id.id != uos2.category_id.id:
999 result.update({'type': product_obj.procure_method})
1000 if product_obj.description_sale:
1001 result['notes'] = product_obj.description_sale
1002 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1003 if update_tax: #The quantity only have changed
1004 result['delay'] = (product_obj.sale_delay or 0.0)
1005 partner = partner_obj.browse(cr, uid, partner_id)
1006 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1008 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1010 if (not uom) and (not uos):
1011 result['product_uom'] = product_obj.uom_id.id
1012 if product_obj.uos_id:
1013 result['product_uos'] = product_obj.uos_id.id
1014 result['product_uos_qty'] = qty * product_obj.uos_coeff
1015 uos_category_id = product_obj.uos_id.category_id.id
1017 result['product_uos'] = False
1018 result['product_uos_qty'] = qty
1019 uos_category_id = False
1020 result['th_weight'] = qty * product_obj.weight
1021 domain = {'product_uom':
1022 [('category_id', '=', product_obj.uom_id.category_id.id)],
1024 [('category_id', '=', uos_category_id)]}
1026 elif uos and not uom: # only happens if uom is False
1027 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1028 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1029 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1030 elif uom: # whether uos is set or not
1031 default_uom = product_obj.uom_id and product_obj.uom_id.id
1032 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1033 if product_obj.uos_id:
1034 result['product_uos'] = product_obj.uos_id.id
1035 result['product_uos_qty'] = qty * product_obj.uos_coeff
1037 result['product_uos'] = False
1038 result['product_uos_qty'] = qty
1039 result['th_weight'] = q * product_obj.weight # Round the quantity up
1045 'title': 'No Pricelist !',
1047 'You have to select a pricelist in the sale form !\n'
1048 'Please set one before choosing a product.'
1051 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1052 product, qty or 1.0, partner_id, {
1058 'title': 'No valid pricelist line found !',
1060 "Couldn't find a pricelist line matching this product and quantity.\n"
1061 "You have to change either the product, the quantity or the pricelist."
1064 result.update({'price_unit': price})
1065 return {'value': result, 'domain': domain, 'warning': warning}
1067 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1068 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1069 lang=False, update_tax=True, date_order=False):
1070 res = self.product_id_change(cursor, user, ids, pricelist, product,
1071 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1072 partner_id=partner_id, lang=lang, update_tax=update_tax,
1073 date_order=date_order)
1074 if 'product_uom' in res['value']:
1075 del res['value']['product_uom']
1077 res['value']['price_unit'] = 0.0
1080 def unlink(self, cr, uid, ids, context={}):
1081 """Allows to delete sale order lines in draft,cancel states"""
1082 for rec in self.browse(cr, uid, ids, context=context):
1083 if rec.state not in ['draft', 'cancel']:
1084 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1085 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1090 class sale_config_picking_policy(osv.osv_memory):
1091 _name = 'sale.config.picking_policy'
1093 'name': fields.char('Name', size=64),
1094 'picking_policy': fields.selection([
1095 ('direct', 'Direct Delivery'),
1096 ('one', 'All at Once')
1097 ], 'Packing Default Policy', required=True),
1098 'order_policy': fields.selection([
1099 ('manual', 'Invoice Based on Sales Orders'),
1100 ('picking', 'Invoice Based on Deliveries'),
1101 ], 'Shipping Default Policy', required=True),
1102 'step': fields.selection([
1103 ('one', 'Delivery Order Only'),
1104 ('two', 'Packing List & Delivery Order')
1105 ], 'Steps To Deliver a Sale Order', required=True,
1106 help="By default, Open ERP is able to manage complex routing and paths "\
1107 "of products in your warehouse and partner locations. This will configure "\
1108 "the most common and simple methods to deliver products to the customer "\
1109 "in one or two operations by the worker.")
1112 'picking_policy': lambda *a: 'direct',
1113 'order_policy': lambda *a: 'picking',
1114 'step': lambda *a: 'one'
1117 def set_default(self, cr, uid, ids, context=None):
1118 for o in self.browse(cr, uid, ids, context=context):
1119 ir_values_obj = self.pool.get('ir.values')
1120 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1121 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1124 md = self.pool.get('ir.model.data')
1125 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1126 group_id = md.browse(cr, uid, group_id, context).res_id
1127 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1128 menu_id = md.browse(cr, uid, menu_id, context).res_id
1129 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1131 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1132 location_id = md.browse(cr, uid, location_id, context).res_id
1133 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1136 'view_type': 'form',
1137 "view_mode": 'form',
1138 'res_model': 'ir.actions.configuration.wizard',
1139 'type': 'ir.actions.act_window',
1143 def action_cancel(self, cr, uid, ids, context=None):
1145 'view_type': 'form',
1146 "view_mode": 'form',
1147 'res_model': 'ir.actions.configuration.wizard',
1148 'type': 'ir.actions.act_window',
1152 sale_config_picking_policy()