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 netsvc
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):
48 'date_confirm': False,
49 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
51 return super(sale_order, self).copy(cr, uid, id, default, context=context)
53 def _amount_line_tax(self, cr, uid, line, context=None):
55 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']:
56 val += c.get('amount', 0.0)
59 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
60 cur_obj = self.pool.get('res.currency')
62 for order in self.browse(cr, uid, ids, context=context):
64 'amount_untaxed': 0.0,
69 cur = order.pricelist_id.currency_id
70 for line in order.order_line:
71 val1 += line.price_subtotal
72 val += self._amount_line_tax(cr, uid, line, context=context)
73 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
74 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
75 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
79 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
81 for sale in self.browse(cursor, user, ids, context=context):
86 for invoice in sale.invoice_ids:
87 if invoice.state not in ('draft', 'cancel'):
88 tot += invoice.amount_untaxed
90 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
95 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
97 for sale in self.browse(cursor, user, ids, context=context):
103 def _invoiced(self, cursor, user, ids, name, arg, context=None):
105 for sale in self.browse(cursor, user, ids, context=context):
107 invoice_existence = False
108 for invoice in sale.invoice_ids:
109 if invoice.state!='cancel':
110 invoice_existence = True
111 if invoice.state != 'paid':
114 if not invoice_existence or sale.state == 'manual':
118 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
127 clause += 'AND inv.state = \'paid\''
129 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
130 sale_clause = ', sale_order AS sale '
133 cursor.execute('SELECT rel.order_id ' \
134 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
135 'WHERE rel.invoice_id = inv.id ' + clause)
136 res = cursor.fetchall()
138 cursor.execute('SELECT sale.id ' \
139 'FROM sale_order AS sale ' \
140 'WHERE sale.id NOT IN ' \
141 '(SELECT rel.order_id ' \
142 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
143 res.extend(cursor.fetchall())
145 return [('id', '=', 0)]
146 return [('id', 'in', [x[0] for x in res])]
148 def _get_order(self, cr, uid, ids, context=None):
150 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
151 result[line.order_id.id] = True
154 def _get_default_company(self, cr, uid, context=None):
155 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
157 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
161 'name': fields.char('Order Reference', size=64, required=True,
162 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
163 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
164 'client_order_ref': fields.char('Customer Reference', size=64),
165 'state': fields.selection([
166 ('draft', 'Draft Quotation'),
167 ('sent', 'Quotation Sent'),
168 ('cancel', 'Cancelled'),
169 ('waiting_date', 'Waiting Schedule'),
170 ('progress', 'Sales Order'),
171 ('manual', 'Sale to Invoice'),
172 ('invoice_except', 'Invoice Exception'),
174 ], 'Status', readonly=True, track_visibility='onchange',
175 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),
176 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
177 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
178 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
179 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
180 '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'),
181 '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."),
182 '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."),
183 'order_policy': fields.selection([
184 ('manual', 'On Demand'),
185 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
186 help="""This field controls how invoice and delivery operations are synchronized."""),
187 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
188 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
189 '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."),
191 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 '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)."),
193 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
194 'invoiced': fields.function(_invoiced, string='Paid',
195 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
196 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
197 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
198 'note': fields.text('Terms and conditions'),
200 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
202 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
203 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
205 multi='sums', help="The amount without tax.", track_visibility='always'),
206 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
208 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
209 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
211 multi='sums', help="The tax amount."),
212 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
214 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
215 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
217 multi='sums', help="The total amount."),
219 '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)]}),
220 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
221 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
222 'company_id': fields.many2one('res.company', 'Company'),
225 'date_order': fields.date.context_today,
226 'order_policy': 'manual',
227 'company_id': _get_default_company,
229 'user_id': lambda obj, cr, uid, context: uid,
230 'name': lambda obj, cr, uid, context: '/',
231 'invoice_quantity': 'order',
232 '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'],
233 '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'],
234 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
237 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
242 def unlink(self, cr, uid, ids, context=None):
243 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
245 for s in sale_orders:
246 if s['state'] in ['draft', 'cancel']:
247 unlink_ids.append(s['id'])
249 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
251 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
253 def copy_quotation(self, cr, uid, ids, context=None):
254 id = self.copy(cr, uid, ids[0], context=None)
255 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
256 view_id = view_ref and view_ref[1] or False,
258 'type': 'ir.actions.act_window',
259 'name': _('Sales Order'),
260 'res_model': 'sale.order',
269 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
270 context = context or {}
274 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
277 return {'value': value}
279 'title': _('Pricelist Warning!'),
280 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
282 return {'warning': warning, 'value': value}
284 def get_salenote(self, cr, uid, ids, partner_id, context=None):
285 context_lang = context.copy()
287 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
288 context_lang.update({'lang': partner_lang})
289 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
291 def onchange_partner_id(self, cr, uid, ids, part, context=None):
293 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
295 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
296 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
297 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
298 payment_term = part.property_payment_term and part.property_payment_term.id or False
299 fiscal_position = part.property_account_position and part.property_account_position.id or False
300 dedicated_salesman = part.user_id and part.user_id.id or uid
302 'partner_invoice_id': addr['invoice'],
303 'partner_shipping_id': addr['delivery'],
304 'payment_term': payment_term,
305 'fiscal_position': fiscal_position,
306 'user_id': dedicated_salesman,
309 val['pricelist_id'] = pricelist
310 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
311 if sale_note: val.update({'note': sale_note})
312 return {'value': val}
314 def create(self, cr, uid, vals, context=None):
317 if vals.get('name', '/') == '/':
318 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
319 context.update({'mail_create_nolog': True})
320 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
321 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
324 def button_dummy(self, cr, uid, ids, context=None):
327 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
328 # can be removed after 6.1.
329 def _inv_get(self, cr, uid, order, context=None):
332 def _prepare_invoice(self, cr, uid, order, lines, context=None):
333 """Prepare the dict of values to create the new invoice for a
334 sales order. This method may be overridden to implement custom
335 invoice generation (making sure to call super() to establish
336 a clean extension chain).
338 :param browse_record order: sale.order record to invoice
339 :param list(int) line: list of invoice line IDs that must be
340 attached to the invoice
341 :return: dict of value to create() the invoice
345 journal_ids = self.pool.get('account.journal').search(cr, uid,
346 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
349 raise osv.except_osv(_('Error!'),
350 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
352 'name': order.client_order_ref or '',
353 'origin': order.name,
354 'type': 'out_invoice',
355 'reference': order.client_order_ref or order.name,
356 'account_id': order.partner_id.property_account_receivable.id,
357 'partner_id': order.partner_invoice_id.id,
358 'journal_id': journal_ids[0],
359 'invoice_line': [(6, 0, lines)],
360 'currency_id': order.pricelist_id.currency_id.id,
361 'comment': order.note,
362 'payment_term': order.payment_term and order.payment_term.id or False,
363 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
364 'date_invoice': context.get('date_invoice', False),
365 'company_id': order.company_id.id,
366 'user_id': order.user_id and order.user_id.id or False
369 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
370 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
373 def _make_invoice(self, cr, uid, order, lines, context=None):
374 inv_obj = self.pool.get('account.invoice')
375 obj_invoice_line = self.pool.get('account.invoice.line')
378 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
379 from_line_invoice_ids = []
380 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
381 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
382 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
383 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
384 for preinv in order.invoice_ids:
385 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
386 for preline in preinv.invoice_line:
387 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
388 lines.append(inv_line_id)
389 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
390 inv_id = inv_obj.create(cr, uid, inv, context=context)
391 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
392 if data.get('value', False):
393 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
394 inv_obj.button_compute(cr, uid, [inv_id])
397 def print_quotation(self, cr, uid, ids, context=None):
399 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
401 assert len(ids) == 1, 'This option should only be used for a single id at a time'
402 self.signal_quotation_sent(cr, uid, ids)
404 'model': 'sale.order',
406 'form': self.read(cr, uid, ids[0], context=context),
408 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
410 def manual_invoice(self, cr, uid, ids, context=None):
411 """ create invoices for the given sales orders (ids), and open the form
412 view of one of the newly created invoices
414 mod_obj = self.pool.get('ir.model.data')
416 # create invoices through the sales orders' workflow
417 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
418 self.signal_manual_invoice(cr, uid, ids)
419 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
420 # determine newly created invoices
421 new_inv_ids = list(inv_ids1 - inv_ids0)
423 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
424 res_id = res and res[1] or False,
427 'name': _('Customer Invoices'),
431 'res_model': 'account.invoice',
432 'context': "{'type':'out_invoice'}",
433 'type': 'ir.actions.act_window',
436 'res_id': new_inv_ids and new_inv_ids[0] or False,
439 def action_view_invoice(self, cr, uid, ids, context=None):
441 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.
443 mod_obj = self.pool.get('ir.model.data')
444 act_obj = self.pool.get('ir.actions.act_window')
446 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
447 id = result and result[1] or False
448 result = act_obj.read(cr, uid, [id], context=context)[0]
449 #compute the number of invoices to display
451 for so in self.browse(cr, uid, ids, context=context):
452 inv_ids += [invoice.id for invoice in so.invoice_ids]
453 #choose the view_mode accordingly
455 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
457 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
458 result['views'] = [(res and res[1] or False, 'form')]
459 result['res_id'] = inv_ids and inv_ids[0] or False
462 def test_no_product(self, cr, uid, order, context):
463 for line in order.order_line:
464 if line.product_id and (line.product_id.type<>'service'):
468 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
470 states = ['confirmed', 'done', 'exception']
474 invoice = self.pool.get('account.invoice')
475 obj_sale_order_line = self.pool.get('sale.order.line')
476 partner_currency = {}
479 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
480 # last day of the last month as invoice date
482 context['date_invoice'] = date_invoice
483 for o in self.browse(cr, uid, ids, context=context):
484 currency_id = o.pricelist_id.currency_id.id
485 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
486 raise osv.except_osv(
488 _('You cannot group sales having different currencies for the same partner.'))
490 partner_currency[o.partner_id.id] = currency_id
492 for line in o.order_line:
495 elif (line.state in states):
496 lines.append(line.id)
497 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
499 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
501 for o in self.browse(cr, uid, ids, context=context):
502 for i in o.invoice_ids:
503 if i.state == 'draft':
505 for val in invoices.values():
507 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
510 invoice_ref += o.name + '|'
511 self.write(cr, uid, [o.id], {'state': 'progress'})
512 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
513 #remove last '|' in invoice_ref
514 if len(invoice_ref) >= 1:
515 invoice_ref = invoice_ref[:-1]
516 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
518 for order, il in val:
519 res = self._make_invoice(cr, uid, order, il, context=context)
520 invoice_ids.append(res)
521 self.write(cr, uid, [order.id], {'state': 'progress'})
522 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
525 def action_invoice_cancel(self, cr, uid, ids, context=None):
526 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
529 def action_invoice_end(self, cr, uid, ids, context=None):
530 for this in self.browse(cr, uid, ids, context=context):
531 for line in this.order_line:
532 if line.state == 'exception':
533 line.write({'state': 'confirmed'})
534 if this.state == 'invoice_except':
535 this.write({'state': 'progress'})
538 def action_cancel(self, cr, uid, ids, context=None):
541 sale_order_line_obj = self.pool.get('sale.order.line')
542 account_invoice_obj = self.pool.get('account.invoice')
543 for sale in self.browse(cr, uid, ids, context=context):
544 for inv in sale.invoice_ids:
545 if inv.state not in ('draft', 'cancel'):
546 raise osv.except_osv(
547 _('Cannot cancel this sales order!'),
548 _('First cancel all invoices attached to this sales order.'))
549 for r in self.read(cr, uid, ids, ['invoice_ids']):
550 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
551 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
553 self.write(cr, uid, ids, {'state': 'cancel'})
556 def action_button_confirm(self, cr, uid, ids, context=None):
557 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
558 self.signal_order_confirm(cr, uid, ids)
560 # redisplay the record as a sales order
561 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
562 view_id = view_ref and view_ref[1] or False,
564 'type': 'ir.actions.act_window',
565 'name': _('Sales Order'),
566 'res_model': 'sale.order',
575 def action_wait(self, cr, uid, ids, context=None):
576 context = context or {}
577 for o in self.browse(cr, uid, ids):
579 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
580 noprod = self.test_no_product(cr, uid, o, context)
581 if (o.order_policy == 'manual') or noprod:
582 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
584 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
585 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
588 def action_quotation_send(self, cr, uid, ids, context=None):
590 This function opens a window to compose an email, with the edi sale template message loaded by default
592 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
593 ir_model_data = self.pool.get('ir.model.data')
595 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
599 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
601 compose_form_id = False
604 'default_model': 'sale.order',
605 'default_res_id': ids[0],
606 'default_use_template': bool(template_id),
607 'default_template_id': template_id,
608 'default_composition_mode': 'comment',
609 'mark_so_as_sent': True
612 'type': 'ir.actions.act_window',
615 'res_model': 'mail.compose.message',
616 'views': [(compose_form_id, 'form')],
617 'view_id': compose_form_id,
622 def action_done(self, cr, uid, ids, context=None):
623 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
627 # TODO add a field price_unit_uos
628 # - update it on change product and unit price
629 # - use it in report if there is a uos
630 class sale_order_line(osv.osv):
632 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
633 tax_obj = self.pool.get('account.tax')
634 cur_obj = self.pool.get('res.currency')
638 for line in self.browse(cr, uid, ids, context=context):
639 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
640 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
641 cur = line.order_id.pricelist_id.currency_id
642 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
645 def _get_uom_id(self, cr, uid, *args):
647 proxy = self.pool.get('ir.model.data')
648 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
650 except Exception, ex:
653 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
654 res = dict.fromkeys(ids, False)
655 for this in self.browse(cr, uid, ids, context=context):
656 res[this.id] = this.invoice_lines and \
657 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
660 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
661 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
662 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
663 sale_order_line sol ON (sol.order_id = rel.order_id)
664 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
665 return [i[0] for i in cr.fetchall()]
667 _name = 'sale.order.line'
668 _description = 'Sales Order Line'
670 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
671 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
672 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
673 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
674 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
675 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
677 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
678 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
679 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
680 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
681 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."),
682 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
683 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
684 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
685 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
686 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
687 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
688 'product_uos': fields.many2one('product.uom', 'Product UoS'),
689 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
690 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
691 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
692 help='* The \'Draft\' status is set when the related sales order in draft status. \
693 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
694 \n* The \'Exception\' status is set when the related sales order is set as exception. \
695 \n* The \'Done\' status is set when the sales order line has been picked. \
696 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
697 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
698 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
699 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
701 _order = 'order_id desc, sequence, id'
703 'product_uom' : _get_uom_id,
705 'product_uom_qty': 1,
706 'product_uos_qty': 1,
709 'type': 'make_to_stock',
713 def _get_line_qty(self, cr, uid, line, context=None):
714 if (line.order_id.invoice_quantity=='order'):
716 return line.product_uos_qty or 0.0
717 return line.product_uom_qty
719 def _get_line_uom(self, cr, uid, line, context=None):
720 if (line.order_id.invoice_quantity=='order'):
722 return line.product_uos.id
723 return line.product_uom.id
725 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
726 """Prepare the dict of values to create the new invoice line for a
727 sales order line. This method may be overridden to implement custom
728 invoice generation (making sure to call super() to establish
729 a clean extension chain).
731 :param browse_record line: sale.order.line record to invoice
732 :param int account_id: optional ID of a G/L account to force
733 (this is used for returning products including service)
734 :return: dict of values to create() the invoice line
737 if not line.invoiced:
740 account_id = line.product_id.property_account_income.id
742 account_id = line.product_id.categ_id.property_account_income_categ.id
744 raise osv.except_osv(_('Error!'),
745 _('Please define income account for this product: "%s" (id:%d).') % \
746 (line.product_id.name, line.product_id.id,))
748 prop = self.pool.get('ir.property').get(cr, uid,
749 'property_account_income_categ', 'product.category',
751 account_id = prop and prop.id or False
752 uosqty = self._get_line_qty(cr, uid, line, context=context)
753 uos_id = self._get_line_uom(cr, uid, line, context=context)
756 pu = round(line.price_unit * line.product_uom_qty / uosqty,
757 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
758 fpos = line.order_id.fiscal_position or False
759 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
761 raise osv.except_osv(_('Error!'),
762 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
765 'sequence': line.sequence,
766 'origin': line.order_id.name,
767 'account_id': account_id,
770 'discount': line.discount,
772 'product_id': line.product_id.id or False,
773 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
774 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
779 def invoice_line_create(self, cr, uid, ids, context=None):
785 for line in self.browse(cr, uid, ids, context=context):
786 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
788 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
789 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
790 sales.add(line.order_id.id)
791 create_ids.append(inv_id)
792 # Trigger workflow events
793 wf_service = netsvc.LocalService("workflow")
794 for sale_id in sales:
795 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
798 def button_cancel(self, cr, uid, ids, context=None):
799 for line in self.browse(cr, uid, ids, context=context):
801 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
802 return self.write(cr, uid, ids, {'state': 'cancel'})
804 def button_confirm(self, cr, uid, ids, context=None):
805 return self.write(cr, uid, ids, {'state': 'confirmed'})
807 def button_done(self, cr, uid, ids, context=None):
808 wf_service = netsvc.LocalService("workflow")
809 res = self.write(cr, uid, ids, {'state': 'done'})
810 for line in self.browse(cr, uid, ids, context=context):
811 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
814 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
815 product_obj = self.pool.get('product.product')
817 return {'value': {'product_uom': product_uos,
818 'product_uom_qty': product_uos_qty}, 'domain': {}}
820 product = product_obj.browse(cr, uid, product_id)
822 'product_uom': product.uom_id.id,
824 # FIXME must depend on uos/uom of the product and not only of the coeff.
827 'product_uom_qty': product_uos_qty / product.uos_coeff,
828 'th_weight': product_uos_qty / product.uos_coeff * product.weight
830 except ZeroDivisionError:
832 return {'value': value}
834 def copy_data(self, cr, uid, id, default=None, context=None):
837 default.update({'state': 'draft', 'invoice_lines': []})
838 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
840 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
841 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
842 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
843 context = context or {}
844 lang = lang or context.get('lang',False)
846 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
848 product_uom_obj = self.pool.get('product.uom')
849 partner_obj = self.pool.get('res.partner')
850 product_obj = self.pool.get('product.product')
851 context = {'lang': lang, 'partner_id': partner_id}
853 lang = partner_obj.browse(cr, uid, partner_id).lang
854 context_partner = {'lang': lang, 'partner_id': partner_id}
857 return {'value': {'th_weight': 0,
858 'product_uos_qty': qty}, 'domain': {'product_uom': [],
861 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
865 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
869 uom2 = product_uom_obj.browse(cr, uid, uom)
870 if product_obj.uom_id.category_id.id != uom2.category_id.id:
873 if product_obj.uos_id:
874 uos2 = product_uom_obj.browse(cr, uid, uos)
875 if product_obj.uos_id.category_id.id != uos2.category_id.id:
879 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
880 if update_tax: #The quantity only have changed
881 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
884 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
885 if product_obj.description_sale:
886 result['name'] += '\n'+product_obj.description_sale
888 if (not uom) and (not uos):
889 result['product_uom'] = product_obj.uom_id.id
890 if product_obj.uos_id:
891 result['product_uos'] = product_obj.uos_id.id
892 result['product_uos_qty'] = qty * product_obj.uos_coeff
893 uos_category_id = product_obj.uos_id.category_id.id
895 result['product_uos'] = False
896 result['product_uos_qty'] = qty
897 uos_category_id = False
898 result['th_weight'] = qty * product_obj.weight
899 domain = {'product_uom':
900 [('category_id', '=', product_obj.uom_id.category_id.id)],
902 [('category_id', '=', uos_category_id)]}
903 elif uos and not uom: # only happens if uom is False
904 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
905 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
906 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
907 elif uom: # whether uos is set or not
908 default_uom = product_obj.uom_id and product_obj.uom_id.id
909 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
910 if product_obj.uos_id:
911 result['product_uos'] = product_obj.uos_id.id
912 result['product_uos_qty'] = qty * product_obj.uos_coeff
914 result['product_uos'] = False
915 result['product_uos_qty'] = qty
916 result['th_weight'] = q * product_obj.weight # Round the quantity up
919 uom2 = product_obj.uom_id
923 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
924 'Please set one before choosing a product.')
925 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
927 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
928 product, qty or 1.0, partner_id, {
929 'uom': uom or result.get('product_uom'),
933 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
934 "You have to change either the product, the quantity or the pricelist.")
936 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
938 result.update({'price_unit': price})
941 'title': _('Configuration Error!'),
942 'message' : warning_msgs
944 return {'value': result, 'domain': domain, 'warning': warning}
946 def product_uom_change(self, cursor, user, 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, context=None):
949 context = context or {}
950 lang = lang or ('lang' in context and context['lang'])
952 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
953 return self.product_id_change(cursor, user, ids, pricelist, product,
954 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
955 partner_id=partner_id, lang=lang, update_tax=update_tax,
956 date_order=date_order, context=context)
958 def unlink(self, cr, uid, ids, context=None):
961 """Allows to delete sales order lines in draft,cancel states"""
962 for rec in self.browse(cr, uid, ids, context=context):
963 if rec.state not in ['draft', 'cancel']:
964 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
965 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
967 class res_company(osv.Model):
968 _inherit = "res.company"
970 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
974 class mail_compose_message(osv.Model):
975 _inherit = 'mail.compose.message'
977 def send_mail(self, cr, uid, ids, context=None):
978 context = context or {}
979 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
980 context = dict(context, mail_post_autofollow=True)
981 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
982 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
985 class account_invoice(osv.Model):
986 _inherit = 'account.invoice'
988 def confirm_paid(self, cr, uid, ids, context=None):
989 sale_order_obj = self.pool.get('sale.order')
990 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
991 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
993 sale_order_obj.message_post(cr, uid, so_ids, body=_("Invoice paid"), context=context)
996 def unlink(self, cr, uid, ids, context=None):
997 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
998 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
999 #if we can't cancel all invoices, do nothing
1000 if len(invoice_ids) == len(ids):
1001 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1002 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1003 wf_service = netsvc.LocalService("workflow")
1005 wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1006 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1008 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: