1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 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 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
25 from openerp.osv import fields, osv
26 from openerp.tools.translate import _
27 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
28 import openerp.addons.decimal_precision as dp
29 from openerp import workflow
31 class sale_order(osv.osv):
33 _inherit = ['mail.thread', 'ir.needaction_mixin']
34 _description = "Sales Order"
37 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
38 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
42 def copy(self, cr, uid, id, default=None, context=None):
46 'date_order': fields.date.context_today(self, cr, uid, context=context),
49 'date_confirm': False,
50 'client_order_ref': '',
51 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
53 return super(sale_order, self).copy(cr, uid, id, default, context=context)
55 def _amount_line_tax(self, cr, uid, line, context=None):
57 for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.product_id, line.order_id.partner_id)['taxes']:
58 val += c.get('amount', 0.0)
61 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
62 cur_obj = self.pool.get('res.currency')
64 for order in self.browse(cr, uid, ids, context=context):
66 'amount_untaxed': 0.0,
71 cur = order.pricelist_id.currency_id
72 for line in order.order_line:
73 val1 += line.price_subtotal
74 val += self._amount_line_tax(cr, uid, line, context=context)
75 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
76 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
77 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
81 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
83 for sale in self.browse(cursor, user, ids, context=context):
88 for invoice in sale.invoice_ids:
89 if invoice.state not in ('draft', 'cancel'):
90 tot += invoice.amount_untaxed
92 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
97 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
99 for sale in self.browse(cursor, user, ids, context=context):
105 def _invoiced(self, cursor, user, ids, name, arg, context=None):
107 for sale in self.browse(cursor, user, ids, context=context):
109 invoice_existence = False
110 for invoice in sale.invoice_ids:
111 if invoice.state!='cancel':
112 invoice_existence = True
113 if invoice.state != 'paid':
116 if not invoice_existence or sale.state == 'manual':
120 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
129 clause += 'AND inv.state = \'paid\''
131 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
132 sale_clause = ', sale_order AS sale '
135 cursor.execute('SELECT rel.order_id ' \
136 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
137 'WHERE rel.invoice_id = inv.id ' + clause)
138 res = cursor.fetchall()
140 cursor.execute('SELECT sale.id ' \
141 'FROM sale_order AS sale ' \
142 'WHERE sale.id NOT IN ' \
143 '(SELECT rel.order_id ' \
144 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
145 res.extend(cursor.fetchall())
147 return [('id', '=', 0)]
148 return [('id', 'in', [x[0] for x in res])]
150 def _get_order(self, cr, uid, ids, context=None):
152 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
153 result[line.order_id.id] = True
156 def _get_default_company(self, cr, uid, context=None):
157 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
159 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
163 'name': fields.char('Order Reference', size=64, required=True,
164 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
165 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
166 'client_order_ref': fields.char('Customer Reference', size=64),
167 'state': fields.selection([
168 ('draft', 'Draft Quotation'),
169 ('sent', 'Quotation Sent'),
170 ('cancel', 'Cancelled'),
171 ('waiting_date', 'Waiting Schedule'),
172 ('progress', 'Sales Order'),
173 ('manual', 'Sale to Invoice'),
174 ('invoice_except', 'Invoice Exception'),
176 ], 'Status', readonly=True, track_visibility='onchange',
177 help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
178 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
180 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
181 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
182 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
183 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
184 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
185 'order_policy': fields.selection([
186 ('manual', 'On Demand'),
187 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
188 help="""This field controls how invoice and delivery operations are synchronized."""),
189 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
190 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
191 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
193 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
194 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
195 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
196 'invoiced': fields.function(_invoiced, string='Paid',
197 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
198 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
199 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
200 'note': fields.text('Terms and conditions'),
202 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
204 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
205 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
207 multi='sums', help="The amount without tax.", track_visibility='always'),
208 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
210 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
211 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
213 multi='sums', help="The tax amount."),
214 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
216 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
217 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
219 multi='sums', help="The total amount."),
221 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
222 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
223 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
224 'company_id': fields.many2one('res.company', 'Company'),
227 'date_order': fields.date.context_today,
228 'order_policy': 'manual',
229 'company_id': _get_default_company,
231 'user_id': lambda obj, cr, uid, context: uid,
232 'name': lambda obj, cr, uid, context: '/',
233 'invoice_quantity': 'order',
234 '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'],
235 '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'],
236 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
239 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
241 _order = 'date_order desc, id desc'
244 def unlink(self, cr, uid, ids, context=None):
245 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
247 for s in sale_orders:
248 if s['state'] in ['draft', 'cancel']:
249 unlink_ids.append(s['id'])
251 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
253 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
255 def copy_quotation(self, cr, uid, ids, context=None):
256 id = self.copy(cr, uid, ids[0], context=None)
257 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
258 view_id = view_ref and view_ref[1] or False,
260 'type': 'ir.actions.act_window',
261 'name': _('Sales Order'),
262 'res_model': 'sale.order',
271 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
272 context = context or {}
276 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
279 return {'value': value}
281 'title': _('Pricelist Warning!'),
282 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
284 return {'warning': warning, 'value': value}
286 def get_salenote(self, cr, uid, ids, partner_id, context=None):
287 context_lang = context.copy()
289 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
290 context_lang.update({'lang': partner_lang})
291 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
293 def onchange_partner_id(self, cr, uid, ids, part, context=None):
295 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
297 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
298 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
299 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
300 payment_term = part.property_payment_term and part.property_payment_term.id or False
301 fiscal_position = part.property_account_position and part.property_account_position.id or False
302 dedicated_salesman = part.user_id and part.user_id.id or uid
304 'partner_invoice_id': addr['invoice'],
305 'partner_shipping_id': addr['delivery'],
306 'payment_term': payment_term,
307 'fiscal_position': fiscal_position,
308 'user_id': dedicated_salesman,
311 val['pricelist_id'] = pricelist
312 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
313 if sale_note: val.update({'note': sale_note})
314 return {'value': val}
316 def create(self, cr, uid, vals, context=None):
319 if vals.get('name', '/') == '/':
320 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
321 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
322 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
323 vals = dict(defaults, **vals)
324 context.update({'mail_create_nolog': True})
325 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
326 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
329 def button_dummy(self, cr, uid, ids, context=None):
332 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
333 # can be removed after 6.1.
334 def _inv_get(self, cr, uid, order, context=None):
337 def _prepare_invoice(self, cr, uid, order, lines, context=None):
338 """Prepare the dict of values to create the new invoice for a
339 sales order. This method may be overridden to implement custom
340 invoice generation (making sure to call super() to establish
341 a clean extension chain).
343 :param browse_record order: sale.order record to invoice
344 :param list(int) line: list of invoice line IDs that must be
345 attached to the invoice
346 :return: dict of value to create() the invoice
350 journal_ids = self.pool.get('account.journal').search(cr, uid,
351 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
354 raise osv.except_osv(_('Error!'),
355 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
357 'name': order.client_order_ref or '',
358 'origin': order.name,
359 'type': 'out_invoice',
360 'reference': order.client_order_ref or order.name,
361 'account_id': order.partner_id.property_account_receivable.id,
362 'partner_id': order.partner_invoice_id.id,
363 'journal_id': journal_ids[0],
364 'invoice_line': [(6, 0, lines)],
365 'currency_id': order.pricelist_id.currency_id.id,
366 'comment': order.note,
367 'payment_term': order.payment_term and order.payment_term.id or False,
368 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
369 'date_invoice': context.get('date_invoice', False),
370 'company_id': order.company_id.id,
371 'user_id': order.user_id and order.user_id.id or False
374 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
375 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
378 def _make_invoice(self, cr, uid, order, lines, context=None):
379 inv_obj = self.pool.get('account.invoice')
380 obj_invoice_line = self.pool.get('account.invoice.line')
383 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
384 from_line_invoice_ids = []
385 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
386 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
387 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
388 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
389 for preinv in order.invoice_ids:
390 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
391 for preline in preinv.invoice_line:
392 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
393 lines.append(inv_line_id)
394 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
395 inv_id = inv_obj.create(cr, uid, inv, context=context)
396 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
397 if data.get('value', False):
398 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
399 inv_obj.button_compute(cr, uid, [inv_id])
402 def print_quotation(self, cr, uid, ids, context=None):
404 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
406 assert len(ids) == 1, 'This option should only be used for a single id at a time'
407 self.signal_quotation_sent(cr, uid, ids)
409 'model': 'sale.order',
411 'form': self.read(cr, uid, ids[0], context=context),
413 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
415 def manual_invoice(self, cr, uid, ids, context=None):
416 """ create invoices for the given sales orders (ids), and open the form
417 view of one of the newly created invoices
419 mod_obj = self.pool.get('ir.model.data')
421 # create invoices through the sales orders' workflow
422 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
423 self.signal_manual_invoice(cr, uid, ids)
424 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
425 # determine newly created invoices
426 new_inv_ids = list(inv_ids1 - inv_ids0)
428 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
429 res_id = res and res[1] or False,
432 'name': _('Customer Invoices'),
436 'res_model': 'account.invoice',
437 'context': "{'type':'out_invoice'}",
438 'type': 'ir.actions.act_window',
441 'res_id': new_inv_ids and new_inv_ids[0] or False,
444 def action_view_invoice(self, cr, uid, ids, context=None):
446 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
448 mod_obj = self.pool.get('ir.model.data')
449 act_obj = self.pool.get('ir.actions.act_window')
451 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
452 id = result and result[1] or False
453 result = act_obj.read(cr, uid, [id], context=context)[0]
454 #compute the number of invoices to display
456 for so in self.browse(cr, uid, ids, context=context):
457 inv_ids += [invoice.id for invoice in so.invoice_ids]
458 #choose the view_mode accordingly
460 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
462 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
463 result['views'] = [(res and res[1] or False, 'form')]
464 result['res_id'] = inv_ids and inv_ids[0] or False
467 def test_no_product(self, cr, uid, order, context):
468 for line in order.order_line:
469 if line.product_id and (line.product_id.type<>'service'):
473 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
475 states = ['confirmed', 'done', 'exception']
479 invoice = self.pool.get('account.invoice')
480 obj_sale_order_line = self.pool.get('sale.order.line')
481 partner_currency = {}
484 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
485 # last day of the last month as invoice date
487 context['date_invoice'] = date_invoice
488 for o in self.browse(cr, uid, ids, context=context):
489 currency_id = o.pricelist_id.currency_id.id
490 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
491 raise osv.except_osv(
493 _('You cannot group sales having different currencies for the same partner.'))
495 partner_currency[o.partner_id.id] = currency_id
497 for line in o.order_line:
500 elif (line.state in states):
501 lines.append(line.id)
502 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
504 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
506 for o in self.browse(cr, uid, ids, context=context):
507 for i in o.invoice_ids:
508 if i.state == 'draft':
510 for val in invoices.values():
512 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
515 invoice_ref += o.name + '|'
516 self.write(cr, uid, [o.id], {'state': 'progress'})
517 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
518 #remove last '|' in invoice_ref
519 if len(invoice_ref) >= 1:
520 invoice_ref = invoice_ref[:-1]
521 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
523 for order, il in val:
524 res = self._make_invoice(cr, uid, order, il, context=context)
525 invoice_ids.append(res)
526 self.write(cr, uid, [order.id], {'state': 'progress'})
527 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
530 def action_invoice_cancel(self, cr, uid, ids, context=None):
531 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
534 def action_invoice_end(self, cr, uid, ids, context=None):
535 for this in self.browse(cr, uid, ids, context=context):
536 for line in this.order_line:
537 if line.state == 'exception':
538 line.write({'state': 'confirmed'})
539 if this.state == 'invoice_except':
540 this.write({'state': 'progress'})
543 def action_cancel(self, cr, uid, ids, context=None):
546 sale_order_line_obj = self.pool.get('sale.order.line')
547 account_invoice_obj = self.pool.get('account.invoice')
548 for sale in self.browse(cr, uid, ids, context=context):
549 for inv in sale.invoice_ids:
550 if inv.state not in ('draft', 'cancel'):
551 raise osv.except_osv(
552 _('Cannot cancel this sales order!'),
553 _('First cancel all invoices attached to this sales order.'))
554 for r in self.read(cr, uid, ids, ['invoice_ids']):
555 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
556 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
558 self.write(cr, uid, ids, {'state': 'cancel'})
561 def action_button_confirm(self, cr, uid, ids, context=None):
562 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
563 self.signal_order_confirm(cr, uid, ids)
565 # redisplay the record as a sales order
566 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
567 view_id = view_ref and view_ref[1] or False,
569 'type': 'ir.actions.act_window',
570 'name': _('Sales Order'),
571 'res_model': 'sale.order',
580 def action_wait(self, cr, uid, ids, context=None):
581 context = context or {}
582 for o in self.browse(cr, uid, ids):
584 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
585 noprod = self.test_no_product(cr, uid, o, context)
586 if (o.order_policy == 'manual') or noprod:
587 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
589 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
590 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
593 def action_quotation_send(self, cr, uid, ids, context=None):
595 This function opens a window to compose an email, with the edi sale template message loaded by default
597 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
598 ir_model_data = self.pool.get('ir.model.data')
600 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
604 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
606 compose_form_id = False
609 'default_model': 'sale.order',
610 'default_res_id': ids[0],
611 'default_use_template': bool(template_id),
612 'default_template_id': template_id,
613 'default_composition_mode': 'comment',
614 'mark_so_as_sent': True
617 'type': 'ir.actions.act_window',
620 'res_model': 'mail.compose.message',
621 'views': [(compose_form_id, 'form')],
622 'view_id': compose_form_id,
627 def action_done(self, cr, uid, ids, context=None):
628 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
632 # TODO add a field price_unit_uos
633 # - update it on change product and unit price
634 # - use it in report if there is a uos
635 class sale_order_line(osv.osv):
637 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
638 tax_obj = self.pool.get('account.tax')
639 cur_obj = self.pool.get('res.currency')
643 for line in self.browse(cr, uid, ids, context=context):
644 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
645 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
646 cur = line.order_id.pricelist_id.currency_id
647 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
650 def _get_uom_id(self, cr, uid, *args):
652 proxy = self.pool.get('ir.model.data')
653 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
655 except Exception, ex:
658 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
659 res = dict.fromkeys(ids, False)
660 for this in self.browse(cr, uid, ids, context=context):
661 res[this.id] = this.invoice_lines and \
662 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
665 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
666 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
667 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
668 sale_order_line sol ON (sol.order_id = rel.order_id)
669 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
670 return [i[0] for i in cr.fetchall()]
672 _name = 'sale.order.line'
673 _description = 'Sales Order Line'
675 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
676 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
677 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
678 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
679 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
680 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
682 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
683 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
684 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
685 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
686 help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."),
687 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
688 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
689 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
690 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
691 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
692 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
693 'product_uos': fields.many2one('product.uom', 'Product UoS'),
694 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
695 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
696 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
697 help='* The \'Draft\' status is set when the related sales order in draft status. \
698 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
699 \n* The \'Exception\' status is set when the related sales order is set as exception. \
700 \n* The \'Done\' status is set when the sales order line has been picked. \
701 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
702 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
703 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
704 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
706 _order = 'order_id desc, sequence, id'
708 'product_uom' : _get_uom_id,
710 'product_uom_qty': 1,
711 'product_uos_qty': 1,
714 'type': 'make_to_stock',
718 def _get_line_qty(self, cr, uid, line, context=None):
719 if (line.order_id.invoice_quantity=='order'):
721 return line.product_uos_qty or 0.0
722 return line.product_uom_qty
724 def _get_line_uom(self, cr, uid, line, context=None):
725 if (line.order_id.invoice_quantity=='order'):
727 return line.product_uos.id
728 return line.product_uom.id
730 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
731 """Prepare the dict of values to create the new invoice line for a
732 sales order line. This method may be overridden to implement custom
733 invoice generation (making sure to call super() to establish
734 a clean extension chain).
736 :param browse_record line: sale.order.line record to invoice
737 :param int account_id: optional ID of a G/L account to force
738 (this is used for returning products including service)
739 :return: dict of values to create() the invoice line
742 if not line.invoiced:
745 account_id = line.product_id.property_account_income.id
747 account_id = line.product_id.categ_id.property_account_income_categ.id
749 raise osv.except_osv(_('Error!'),
750 _('Please define income account for this product: "%s" (id:%d).') % \
751 (line.product_id.name, line.product_id.id,))
753 prop = self.pool.get('ir.property').get(cr, uid,
754 'property_account_income_categ', 'product.category',
756 account_id = prop and prop.id or False
757 uosqty = self._get_line_qty(cr, uid, line, context=context)
758 uos_id = self._get_line_uom(cr, uid, line, context=context)
761 pu = round(line.price_unit * line.product_uom_qty / uosqty,
762 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
763 fpos = line.order_id.fiscal_position or False
764 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
766 raise osv.except_osv(_('Error!'),
767 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
770 'sequence': line.sequence,
771 'origin': line.order_id.name,
772 'account_id': account_id,
775 'discount': line.discount,
777 'product_id': line.product_id.id or False,
778 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
779 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
784 def invoice_line_create(self, cr, uid, ids, context=None):
790 for line in self.browse(cr, uid, ids, context=context):
791 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
793 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
794 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
795 sales.add(line.order_id.id)
796 create_ids.append(inv_id)
797 # Trigger workflow events
798 for sale_id in sales:
799 workflow.trg_write(uid, 'sale.order', sale_id, cr)
802 def button_cancel(self, cr, uid, ids, context=None):
803 for line in self.browse(cr, uid, ids, context=context):
805 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
806 return self.write(cr, uid, ids, {'state': 'cancel'})
808 def button_confirm(self, cr, uid, ids, context=None):
809 return self.write(cr, uid, ids, {'state': 'confirmed'})
811 def button_done(self, cr, uid, ids, context=None):
812 res = self.write(cr, uid, ids, {'state': 'done'})
813 for line in self.browse(cr, uid, ids, context=context):
814 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
817 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
818 product_obj = self.pool.get('product.product')
820 return {'value': {'product_uom': product_uos,
821 'product_uom_qty': product_uos_qty}, 'domain': {}}
823 product = product_obj.browse(cr, uid, product_id)
825 'product_uom': product.uom_id.id,
827 # FIXME must depend on uos/uom of the product and not only of the coeff.
830 'product_uom_qty': product_uos_qty / product.uos_coeff,
831 'th_weight': product_uos_qty / product.uos_coeff * product.weight
833 except ZeroDivisionError:
835 return {'value': value}
837 def create(self, cr, uid, values, context=None):
838 if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
839 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
840 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
841 qty=float(values.get('product_uom_qty', False)),
842 uom=values.get('product_uom', False),
843 qty_uos=float(values.get('product_uos_qty', False)),
844 uos=values.get('product_uos', False),
845 name=values.get('name', False),
846 partner_id=order['partner_id'][0],
847 date_order=order['date_order'],
848 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
849 flag=False, # Force name update
852 values = dict(defaults, **values)
853 return super(sale_order_line, self).create(cr, uid, values, context=context)
855 def copy_data(self, cr, uid, id, default=None, context=None):
858 default.update({'state': 'draft', 'invoice_lines': []})
859 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
861 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
862 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
863 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
864 context = context or {}
865 lang = lang or context.get('lang',False)
867 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
869 product_uom_obj = self.pool.get('product.uom')
870 partner_obj = self.pool.get('res.partner')
871 product_obj = self.pool.get('product.product')
872 context = {'lang': lang, 'partner_id': partner_id}
874 lang = partner_obj.browse(cr, uid, partner_id).lang
875 context_partner = {'lang': lang, 'partner_id': partner_id}
878 return {'value': {'th_weight': 0,
879 'product_uos_qty': qty}, 'domain': {'product_uom': [],
882 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
886 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
890 uom2 = product_uom_obj.browse(cr, uid, uom)
891 if product_obj.uom_id.category_id.id != uom2.category_id.id:
894 if product_obj.uos_id:
895 uos2 = product_uom_obj.browse(cr, uid, uos)
896 if product_obj.uos_id.category_id.id != uos2.category_id.id:
900 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
901 if update_tax: #The quantity only have changed
902 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
905 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
906 if product_obj.description_sale:
907 result['name'] += '\n'+product_obj.description_sale
909 if (not uom) and (not uos):
910 result['product_uom'] = product_obj.uom_id.id
911 if product_obj.uos_id:
912 result['product_uos'] = product_obj.uos_id.id
913 result['product_uos_qty'] = qty * product_obj.uos_coeff
914 uos_category_id = product_obj.uos_id.category_id.id
916 result['product_uos'] = False
917 result['product_uos_qty'] = qty
918 uos_category_id = False
919 result['th_weight'] = qty * product_obj.weight
920 domain = {'product_uom':
921 [('category_id', '=', product_obj.uom_id.category_id.id)],
923 [('category_id', '=', uos_category_id)]}
924 elif uos and not uom: # only happens if uom is False
925 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
926 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
927 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
928 elif uom: # whether uos is set or not
929 default_uom = product_obj.uom_id and product_obj.uom_id.id
930 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
931 if product_obj.uos_id:
932 result['product_uos'] = product_obj.uos_id.id
933 result['product_uos_qty'] = qty * product_obj.uos_coeff
935 result['product_uos'] = False
936 result['product_uos_qty'] = qty
937 result['th_weight'] = q * product_obj.weight # Round the quantity up
940 uom2 = product_obj.uom_id
944 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
945 'Please set one before choosing a product.')
946 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
948 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
949 product, qty or 1.0, partner_id, {
950 'uom': uom or result.get('product_uom'),
954 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
955 "You have to change either the product, the quantity or the pricelist.")
957 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
959 result.update({'price_unit': price})
962 'title': _('Configuration Error!'),
963 'message' : warning_msgs
965 return {'value': result, 'domain': domain, 'warning': warning}
967 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
968 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
969 lang=False, update_tax=True, date_order=False, context=None):
970 context = context or {}
971 lang = lang or ('lang' in context and context['lang'])
973 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
974 return self.product_id_change(cursor, user, ids, pricelist, product,
975 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
976 partner_id=partner_id, lang=lang, update_tax=update_tax,
977 date_order=date_order, context=context)
979 def unlink(self, cr, uid, ids, context=None):
982 """Allows to delete sales order lines in draft,cancel states"""
983 for rec in self.browse(cr, uid, ids, context=context):
984 if rec.state not in ['draft', 'cancel']:
985 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
986 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
988 class res_company(osv.Model):
989 _inherit = "res.company"
991 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
995 class mail_compose_message(osv.Model):
996 _inherit = 'mail.compose.message'
998 def send_mail(self, cr, uid, ids, context=None):
999 context = context or {}
1000 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1001 context = dict(context, mail_post_autofollow=True)
1002 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1003 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1006 class account_invoice(osv.Model):
1007 _inherit = 'account.invoice'
1009 def confirm_paid(self, cr, uid, ids, context=None):
1010 sale_order_obj = self.pool.get('sale.order')
1011 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1012 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1013 for so_id in so_ids:
1014 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1017 def unlink(self, cr, uid, ids, context=None):
1018 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1019 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1020 #if we can't cancel all invoices, do nothing
1021 if len(invoice_ids) == len(ids):
1022 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1023 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1025 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1026 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1028 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: