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 = netsvc.LocalService("workflow")
521 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
522 for inv in sale.invoice_ids:
523 if inv.state not in ('draft', 'cancel'):
524 raise osv.except_osv(
525 _('Could not cancel this sale order !'),
526 _('You must first cancel all invoices attached to this sale order.'))
527 for r in self.read(cr, uid, ids, ['invoice_ids']):
528 for inv in r['invoice_ids']:
529 wf_service = netsvc.LocalService("workflow")
530 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
531 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
533 self.write(cr, uid, ids, {'state': 'cancel'})
536 def action_wait(self, cr, uid, ids, *args):
537 event_p = self.pool.get('res.partner.event.type').check(cr, uid, 'sale_open')
538 event_obj = self.pool.get('res.partner.event')
539 for o in self.browse(cr, uid, ids):
541 event_obj.create(cr, uid, {'name': 'Sale Order: '+ o.name,\
542 'partner_id': o.partner_id.id,\
543 'date': time.strftime('%Y-%m-%d %H:%M:%S'),\
544 'user_id': (o.user_id and o.user_id.id) or uid,\
545 'partner_type': 'customer', 'probability': 1.0,\
546 'planned_revenue': o.amount_untaxed})
547 if (o.order_policy == 'manual'):
548 self.write(cr, uid, [o.id], {'state': 'manual'})
550 self.write(cr, uid, [o.id], {'state': 'progress'})
551 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
553 def procurement_lines_get(self, cr, uid, ids, *args):
555 for order in self.browse(cr, uid, ids, context={}):
556 for line in order.order_line:
557 if line.procurement_id:
558 res.append(line.procurement_id.id)
561 # if mode == 'finished':
562 # returns True if all lines are done, False otherwise
563 # if mode == 'canceled':
564 # returns True if there is at least one canceled line, False otherwise
565 def test_state(self, cr, uid, ids, mode, *args):
566 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
571 write_cancel_ids = []
572 for order in self.browse(cr, uid, ids, context={}):
573 for line in order.order_line:
574 if (not line.procurement_id) or (line.procurement_id.state=='done'):
575 if line.state != 'done':
576 write_done_ids.append(line.id)
579 if line.procurement_id:
580 if (line.procurement_id.state == 'cancel'):
582 if line.state != 'exception':
583 write_cancel_ids.append(line.id)
587 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
589 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
591 if mode == 'finished':
593 elif mode == 'canceled':
599 def action_ship_create(self, cr, uid, ids, *args):
601 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
602 picking_obj = self.pool.get('stock.picking')
603 sale_order_line_obj = self.pool.get('sale.order.line')
604 move_obj = self.pool.get('stock.move')
605 proc_obj = self.pool.get('mrp.procurement')
606 for order in self.browse(cr, uid, ids, context={}):
607 output_id = order.shop_id.warehouse_id.lot_output_id.id
609 for line in order.order_line:
611 date_planned = DateTime.now() + DateTime.DateTimeDeltaFromDays(line.delay or 0.0)
612 date_planned = (date_planned - DateTime.DateTimeDeltaFromDays(company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
613 if line.state == 'done':
615 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
616 location_id = order.shop_id.warehouse_id.lot_stock_id.id
618 loc_dest_id = order.partner_id.property_stock_customer.id
619 picking_id = picking_obj.create(cr, uid, {
620 'origin': order.name,
623 'move_type': order.picking_policy,
625 'address_id': order.partner_shipping_id.id,
627 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
631 move_id = move_obj.create(cr, uid, {
632 'name': line.name[:64],
633 'picking_id': picking_id,
634 'product_id': line.product_id.id,
635 'date_planned': date_planned,
636 'product_qty': line.product_uom_qty,
637 'product_uom': line.product_uom.id,
638 'product_uos_qty': line.product_uos_qty,
639 'product_uos': (line.product_uos and line.product_uos.id)\
640 or line.product_uom.id,
641 'product_packaging': line.product_packaging.id,
642 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
643 'location_id': location_id,
644 'location_dest_id': output_id,
645 'sale_line_id': line.id,
646 'tracking_id': False,
651 proc_id = proc_obj.create(cr, uid, {
653 'origin': order.name,
654 'date_planned': date_planned,
655 'product_id': line.product_id.id,
656 'product_qty': line.product_uom_qty,
657 'product_uom': line.product_uom.id,
658 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
659 or line.product_uom_qty,
660 'product_uos': (line.product_uos and line.product_uos.id)\
661 or line.product_uom.id,
662 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
663 'procure_method': line.type,
665 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
667 wf_service = netsvc.LocalService("workflow")
668 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
669 sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
670 elif line.product_id and line.product_id.product_tmpl_id.type == 'service':
671 proc_id = proc_obj.create(cr, uid, {
673 'origin': order.name,
674 'date_planned': date_planned,
675 'product_id': line.product_id.id,
676 'product_qty': line.product_uom_qty,
677 'product_uom': line.product_uom.id,
678 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
679 'procure_method': line.type,
680 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
682 sale_order_line_obj.write(cr, uid, [line.id], {'procurement_id': proc_id})
683 wf_service = netsvc.LocalService("workflow")
684 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
687 # No procurement because no product in the sale.order.line.
693 wf_service = netsvc.LocalService("workflow")
694 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
696 if order.state == 'shipping_except':
697 val['state'] = 'progress'
699 if (order.order_policy == 'manual'):
700 for line in order.order_line:
701 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
702 val['state'] = 'manual'
704 self.write(cr, uid, [order.id], val)
708 def action_ship_end(self, cr, uid, ids, context={}):
709 for order in self.browse(cr, uid, ids):
710 val = {'shipped': True}
711 if order.state == 'shipping_except':
712 val['state'] = 'progress'
713 if (order.order_policy == 'manual'):
714 for line in order.order_line:
715 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
716 val['state'] = 'manual'
718 for line in order.order_line:
720 if line.state == 'exception':
721 towrite.append(line.id)
723 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
724 self.write(cr, uid, [order.id], val)
727 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
728 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
730 part = inv['partner_id'] and inv['partner_id'][0]
731 pr = inv['amount_untaxed'] or 0.0
732 partnertype = 'customer'
735 'name': 'Order: '+name,
737 'description': 'Order '+str(inv['id']),
740 'date': time.strftime('%Y-%m-%d'),
743 'partner_type': partnertype,
745 'planned_revenue': pr,
749 self.pool.get('res.partner.event').create(cr, uid, event)
751 def has_stockable_products(self, cr, uid, ids, *args):
752 for order in self.browse(cr, uid, ids):
753 for order_line in order.order_line:
754 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
759 # TODO add a field price_unit_uos
760 # - update it on change product and unit price
761 # - use it in report if there is a uos
762 class sale_order_line(osv.osv):
763 def _amount_line_net(self, cr, uid, ids, field_name, arg, context):
765 for line in self.browse(cr, uid, ids):
766 res[line.id] = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
769 def _amount_line(self, cr, uid, ids, field_name, arg, context):
771 cur_obj = self.pool.get('res.currency')
772 for line in self.browse(cr, uid, ids):
773 res[line.id] = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0)
774 cur = line.order_id.pricelist_id.currency_id
775 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
778 def _number_packages(self, cr, uid, ids, field_name, arg, context):
780 for line in self.browse(cr, uid, ids):
782 res[line.id] = int(line.product_uom_qty / line.product_packaging.qty)
787 _name = 'sale.order.line'
788 _description = 'Sale Order line'
790 'order_id': fields.many2one('sale.order', 'Order Ref', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
791 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
792 'sequence': fields.integer('Sequence'),
793 '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)]}),
794 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
795 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
796 'invoiced': fields.boolean('Invoiced', readonly=True),
797 'procurement_id': fields.many2one('mrp.procurement', 'Procurement'),
798 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy'])), readonly=True, states={'draft':[('readonly',False)]}),
799 'price_net': fields.function(_amount_line_net, method=True, string='Net Price', digits=(16, int(config['price_accuracy']))),
800 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal', digits=(16, int(config['price_accuracy']))),
801 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft':[('readonly',False)]}),
802 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procure Method', required=True, readonly=True, states={'draft':[('readonly',False)]}),
803 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft':[('readonly',False)]}),
804 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
805 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft':[('readonly',False)]}),
806 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, readonly=True, states={'draft':[('readonly',False)]}),
807 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft':[('readonly',False)]}),
808 'product_uos': fields.many2one('product.uom', 'Product UoS'),
809 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
810 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
811 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft':[('readonly',False)]}),
812 'number_packages': fields.function(_number_packages, method=True, type='integer', string='Number Packages'),
813 'notes': fields.text('Notes'),
814 'th_weight': fields.float('Weight', readonly=True, states={'draft':[('readonly',False)]}),
815 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled'), ('exception', 'Exception')], 'Status', required=True, readonly=True),
816 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', string='Customer')
818 _order = 'sequence, id'
820 'discount': lambda *a: 0.0,
821 'delay': lambda *a: 0.0,
822 'product_uom_qty': lambda *a: 1,
823 'product_uos_qty': lambda *a: 1,
824 'sequence': lambda *a: 10,
825 'invoiced': lambda *a: 0,
826 'state': lambda *a: 'draft',
827 'type': lambda *a: 'make_to_stock',
828 'product_packaging': lambda *a: False
831 def invoice_line_create(self, cr, uid, ids, context={}):
832 def _get_line_qty(line):
833 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
835 return line.product_uos_qty or 0.0
836 return line.product_uom_qty
838 return self.pool.get('mrp.procurement').quantity_get(cr, uid,
839 line.procurement_id.id, context)
841 def _get_line_uom(line):
842 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
844 return line.product_uos.id
845 return line.product_uom.id
847 return self.pool.get('mrp.procurement').uom_get(cr, uid,
848 line.procurement_id.id, context)
852 for line in self.browse(cr, uid, ids, context):
853 if not line.invoiced:
855 a = line.product_id.product_tmpl_id.property_account_income.id
857 a = line.product_id.categ_id.property_account_income_categ.id
859 raise osv.except_osv(_('Error !'),
860 _('There is no income account defined ' \
861 'for this product: "%s" (id:%d)') % \
862 (line.product_id.name, line.product_id.id,))
864 a = self.pool.get('ir.property').get(cr, uid,
865 'property_account_income_categ', 'product.category',
867 uosqty = _get_line_qty(line)
868 uos_id = _get_line_uom(line)
871 pu = round(line.price_unit * line.product_uom_qty / uosqty,
872 int(config['price_accuracy']))
873 fpos = line.order_id.fiscal_position or False
874 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
876 raise osv.except_osv(_('Error !'),
877 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
878 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
880 'origin': line.order_id.name,
884 'discount': line.discount,
886 'product_id': line.product_id.id or False,
887 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
889 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
891 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
892 self.write(cr, uid, [line.id], {'invoiced': True})
894 sales[line.order_id.id] = True
895 create_ids.append(inv_id)
897 # Trigger workflow events
898 wf_service = netsvc.LocalService("workflow")
899 for sid in sales.keys():
900 wf_service.trg_write(uid, 'sale.order', sid, cr)
903 def button_cancel(self, cr, uid, ids, context={}):
904 for line in self.browse(cr, uid, ids, context=context):
906 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced !'))
907 return self.write(cr, uid, ids, {'state': 'cancel'})
909 def button_confirm(self, cr, uid, ids, context={}):
910 return self.write(cr, uid, ids, {'state': 'confirmed'})
912 def button_done(self, cr, uid, ids, context={}):
913 wf_service = netsvc.LocalService("workflow")
914 res = self.write(cr, uid, ids, {'state': 'done'})
915 for line in self.browse(cr, uid, ids, context):
916 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
920 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
921 product_obj = self.pool.get('product.product')
923 return {'value': {'product_uom': product_uos,
924 'product_uom_qty': product_uos_qty}, 'domain': {}}
926 product = product_obj.browse(cr, uid, product_id)
928 'product_uom': product.uom_id.id,
930 # FIXME must depend on uos/uom of the product and not only of the coeff.
933 'product_uom_qty': product_uos_qty / product.uos_coeff,
934 'th_weight': product_uos_qty / product.uos_coeff * product.weight
936 except ZeroDivisionError:
938 return {'value': value}
940 def copy_data(self, cr, uid, id, default=None, context={}):
943 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
944 return super(sale_order_line, self).copy_data(cr, uid, id, default, context)
946 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
947 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
948 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False):
950 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.'))
952 product_uom_obj = self.pool.get('product.uom')
953 partner_obj = self.pool.get('res.partner')
954 product_obj = self.pool.get('product.product')
956 lang = partner_obj.browse(cr, uid, partner_id).lang
957 context = {'lang': lang, 'partner_id': partner_id}
959 return {'value': {'th_weight': 0, 'product_packaging': False,
960 'product_uos_qty': qty}, 'domain': {'product_uom': [],
964 date_order = time.strftime('%Y-%m-%d')
967 product_obj = product_obj.browse(cr, uid, product, context=context)
968 if not packaging and product_obj.packaging:
969 packaging = product_obj.packaging[0].id
970 result['product_packaging'] = packaging
973 default_uom = product_obj.uom_id and product_obj.uom_id.id
974 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context)
975 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
976 # qty = qty - qty % q + q
977 if qty and (q and not (qty % q) == 0):
981 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)
982 warn_msg = warn_msg + "\n\n" + _("EAN: ") + str(ean) + _(" Quantity: ") + str(qty_pack) + _(" Type of ul: ") + str(type_ul.name)
984 'title': _('Packing Information !'),
987 result['product_uom_qty'] = qty
990 uom2 = product_uom_obj.browse(cr, uid, uom)
991 if product_obj.uom_id.category_id.id != uom2.category_id.id:
995 if product_obj.uos_id:
996 uos2 = product_uom_obj.browse(cr, uid, uos)
997 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1001 result.update({'type': product_obj.procure_method})
1002 if product_obj.description_sale:
1003 result['notes'] = product_obj.description_sale
1004 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1005 if update_tax: #The quantity only have changed
1006 result['delay'] = (product_obj.sale_delay or 0.0)
1007 partner = partner_obj.browse(cr, uid, partner_id)
1008 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1010 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context)[0][1]
1012 if (not uom) and (not uos):
1013 result['product_uom'] = product_obj.uom_id.id
1014 if product_obj.uos_id:
1015 result['product_uos'] = product_obj.uos_id.id
1016 result['product_uos_qty'] = qty * product_obj.uos_coeff
1017 uos_category_id = product_obj.uos_id.category_id.id
1019 result['product_uos'] = False
1020 result['product_uos_qty'] = qty
1021 uos_category_id = False
1022 result['th_weight'] = qty * product_obj.weight
1023 domain = {'product_uom':
1024 [('category_id', '=', product_obj.uom_id.category_id.id)],
1026 [('category_id', '=', uos_category_id)]}
1028 elif uos and not uom: # only happens if uom is False
1029 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1030 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1031 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1032 elif uom: # whether uos is set or not
1033 default_uom = product_obj.uom_id and product_obj.uom_id.id
1034 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1035 if product_obj.uos_id:
1036 result['product_uos'] = product_obj.uos_id.id
1037 result['product_uos_qty'] = qty * product_obj.uos_coeff
1039 result['product_uos'] = False
1040 result['product_uos_qty'] = qty
1041 result['th_weight'] = q * product_obj.weight # Round the quantity up
1047 'title': 'No Pricelist !',
1049 'You have to select a pricelist in the sale form !\n'
1050 'Please set one before choosing a product.'
1053 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1054 product, qty or 1.0, partner_id, {
1060 'title': 'No valid pricelist line found !',
1062 "Couldn't find a pricelist line matching this product and quantity.\n"
1063 "You have to change either the product, the quantity or the pricelist."
1066 result.update({'price_unit': price})
1067 return {'value': result, 'domain': domain, 'warning': warning}
1069 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1070 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1071 lang=False, update_tax=True, date_order=False):
1072 res = self.product_id_change(cursor, user, ids, pricelist, product,
1073 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1074 partner_id=partner_id, lang=lang, update_tax=update_tax,
1075 date_order=date_order)
1076 if 'product_uom' in res['value']:
1077 del res['value']['product_uom']
1079 res['value']['price_unit'] = 0.0
1082 def unlink(self, cr, uid, ids, context={}):
1083 """Allows to delete sale order lines in draft,cancel states"""
1084 for rec in self.browse(cr, uid, ids, context=context):
1085 if rec.state not in ['draft', 'cancel']:
1086 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sale order line which is %s !')%(rec.state,))
1087 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1092 class sale_config_picking_policy(osv.osv_memory):
1093 _name = 'sale.config.picking_policy'
1095 'name': fields.char('Name', size=64),
1096 'picking_policy': fields.selection([
1097 ('direct', 'Direct Delivery'),
1098 ('one', 'All at Once')
1099 ], 'Packing Default Policy', required=True),
1100 'order_policy': fields.selection([
1101 ('manual', 'Invoice Based on Sales Orders'),
1102 ('picking', 'Invoice Based on Deliveries'),
1103 ], 'Shipping Default Policy', required=True),
1104 'step': fields.selection([
1105 ('one', 'Delivery Order Only'),
1106 ('two', 'Packing List & Delivery Order')
1107 ], 'Steps To Deliver a Sale Order', required=True,
1108 help="By default, Open ERP is able to manage complex routing and paths "\
1109 "of products in your warehouse and partner locations. This will configure "\
1110 "the most common and simple methods to deliver products to the customer "\
1111 "in one or two operations by the worker.")
1114 'picking_policy': lambda *a: 'direct',
1115 'order_policy': lambda *a: 'picking',
1116 'step': lambda *a: 'one'
1119 def set_default(self, cr, uid, ids, context=None):
1120 for o in self.browse(cr, uid, ids, context=context):
1121 ir_values_obj = self.pool.get('ir.values')
1122 ir_values_obj.set(cr, uid, 'default', False, 'picking_policy', ['sale.order'], o.picking_policy)
1123 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], o.order_policy)
1126 md = self.pool.get('ir.model.data')
1127 group_id = md._get_id(cr, uid, 'base', 'group_no_one')
1128 group_id = md.browse(cr, uid, group_id, context).res_id
1129 menu_id = md._get_id(cr, uid, 'stock', 'menu_action_picking_tree_delivery')
1130 menu_id = md.browse(cr, uid, menu_id, context).res_id
1131 self.pool.get('ir.ui.menu').write(cr, uid, [menu_id], {'groups_id': [(6, 0, [group_id])]})
1133 location_id = md._get_id(cr, uid, 'stock', 'stock_location_output')
1134 location_id = md.browse(cr, uid, location_id, context).res_id
1135 self.pool.get('stock.location').write(cr, uid, [location_id], {'chained_auto_packing': 'transparent'})
1138 'view_type': 'form',
1139 "view_mode": 'form',
1140 'res_model': 'ir.actions.configuration.wizard',
1141 'type': 'ir.actions.act_window',
1145 def action_cancel(self, cr, uid, ids, context=None):
1147 'view_type': 'form',
1148 "view_mode": 'form',
1149 'res_model': 'ir.actions.configuration.wizard',
1150 'type': 'ir.actions.act_window',
1154 sale_config_picking_policy()