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 res_company(osv.Model):
32 _inherit = "res.company"
34 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
37 class sale_order(osv.osv):
39 _inherit = ['mail.thread', 'ir.needaction_mixin']
40 _description = "Sales Order"
43 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
44 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
48 def copy(self, cr, uid, id, default=None, context=None):
52 'date_order': fields.date.context_today(self, cr, uid, context=context),
55 'date_confirm': False,
56 'client_order_ref': '',
57 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
59 return super(sale_order, self).copy(cr, uid, id, default, context=context)
61 def _amount_line_tax(self, cr, uid, line, context=None):
63 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']:
64 val += c.get('amount', 0.0)
67 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
68 """ Wrapper because of direct method passing as parameter for function fields """
69 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
71 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
72 cur_obj = self.pool.get('res.currency')
74 for order in self.browse(cr, uid, ids, context=context):
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=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']
91 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
93 for sale in self.browse(cursor, user, ids, context=context):
98 for invoice in sale.invoice_ids:
99 if invoice.state not in ('draft', 'cancel'):
100 tot += invoice.amount_untaxed
102 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
107 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
109 for sale in self.browse(cursor, user, ids, context=context):
115 def _invoiced(self, cursor, user, ids, name, arg, context=None):
117 for sale in self.browse(cursor, user, ids, context=context):
119 invoice_existence = False
120 for invoice in sale.invoice_ids:
121 if invoice.state!='cancel':
122 invoice_existence = True
123 if invoice.state != 'paid':
126 if not invoice_existence or sale.state == 'manual':
130 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
137 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
138 clause += 'AND inv.state = \'paid\''
140 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
141 sale_clause = ', sale_order AS sale '
144 cursor.execute('SELECT rel.order_id ' \
145 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
146 'WHERE rel.invoice_id = inv.id ' + clause)
147 res = cursor.fetchall()
149 cursor.execute('SELECT sale.id ' \
150 'FROM sale_order AS sale ' \
151 'WHERE sale.id NOT IN ' \
152 '(SELECT rel.order_id ' \
153 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
154 res.extend(cursor.fetchall())
156 return [('id', '=', 0)]
157 return [('id', 'in', [x[0] for x in res])]
159 def _get_order(self, cr, uid, ids, context=None):
161 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
162 result[line.order_id.id] = True
165 def _get_default_company(self, cr, uid, context=None):
166 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
168 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
172 'name': fields.char('Order Reference', size=64, required=True,
173 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
174 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
175 'client_order_ref': fields.char('Customer Reference', size=64),
176 'state': fields.selection([
177 ('draft', 'Draft Quotation'),
178 ('sent', 'Quotation Sent'),
179 ('cancel', 'Cancelled'),
180 ('waiting_date', 'Waiting Schedule'),
181 ('progress', 'Sales Order'),
182 ('manual', 'Sale to Invoice'),
183 ('invoice_except', 'Invoice Exception'),
185 ], 'Status', readonly=True, track_visibility='onchange',
186 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),
187 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
188 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
189 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
190 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
191 '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'),
192 '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."),
193 '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."),
194 'order_policy': fields.selection([
195 ('manual', 'On Demand'),
196 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
197 help="""This field controls how invoice and delivery operations are synchronized."""),
198 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
199 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
200 '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."),
202 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
203 '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)."),
204 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
205 'invoiced': fields.function(_invoiced, string='Paid',
206 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
207 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
208 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
209 'note': fields.text('Terms and conditions'),
211 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
213 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
214 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
216 multi='sums', help="The amount without tax.", track_visibility='always'),
217 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
219 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
220 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
222 multi='sums', help="The tax amount."),
223 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
225 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
226 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
228 multi='sums', help="The total amount."),
230 '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)]}),
231 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
232 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
233 'company_id': fields.many2one('res.company', 'Company'),
236 'date_order': fields.date.context_today,
237 'order_policy': 'manual',
238 'company_id': _get_default_company,
240 'user_id': lambda obj, cr, uid, context: uid,
241 'name': lambda obj, cr, uid, context: '/',
242 'invoice_quantity': 'order',
243 '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'],
244 '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'],
245 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
248 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
250 _order = 'date_order desc, id desc'
253 def unlink(self, cr, uid, ids, context=None):
254 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
256 for s in sale_orders:
257 if s['state'] in ['draft', 'cancel']:
258 unlink_ids.append(s['id'])
260 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
262 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
264 def copy_quotation(self, cr, uid, ids, context=None):
265 id = self.copy(cr, uid, ids[0], context=context)
266 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
267 view_id = view_ref and view_ref[1] or False,
269 'type': 'ir.actions.act_window',
270 'name': _('Sales Order'),
271 'res_model': 'sale.order',
280 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
281 context = context or {}
285 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
288 return {'value': value}
290 'title': _('Pricelist Warning!'),
291 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
293 return {'warning': warning, 'value': value}
295 def get_salenote(self, cr, uid, ids, partner_id, context=None):
296 context_lang = context.copy()
298 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
299 context_lang.update({'lang': partner_lang})
300 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
302 def onchange_partner_id(self, cr, uid, ids, part, context=None):
304 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
306 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
307 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
308 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
309 payment_term = part.property_payment_term and part.property_payment_term.id or False
310 fiscal_position = part.property_account_position and part.property_account_position.id or False
311 dedicated_salesman = part.user_id and part.user_id.id or uid
313 'partner_invoice_id': addr['invoice'],
314 'partner_shipping_id': addr['delivery'],
315 'payment_term': payment_term,
316 'fiscal_position': fiscal_position,
317 'user_id': dedicated_salesman,
320 val['pricelist_id'] = pricelist
321 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
322 if sale_note: val.update({'note': sale_note})
323 return {'value': val}
325 def create(self, cr, uid, vals, context=None):
328 if vals.get('name', '/') == '/':
329 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
330 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
331 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
332 vals = dict(defaults, **vals)
333 context.update({'mail_create_nolog': True})
334 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
335 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
338 def button_dummy(self, cr, uid, ids, context=None):
341 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
342 # can be removed after 6.1.
343 def _inv_get(self, cr, uid, order, context=None):
346 def _prepare_invoice(self, cr, uid, order, lines, context=None):
347 """Prepare the dict of values to create the new invoice for a
348 sales order. This method may be overridden to implement custom
349 invoice generation (making sure to call super() to establish
350 a clean extension chain).
352 :param browse_record order: sale.order record to invoice
353 :param list(int) line: list of invoice line IDs that must be
354 attached to the invoice
355 :return: dict of value to create() the invoice
359 journal_ids = self.pool.get('account.journal').search(cr, uid,
360 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
363 raise osv.except_osv(_('Error!'),
364 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
366 'name': order.client_order_ref or '',
367 'origin': order.name,
368 'type': 'out_invoice',
369 'reference': order.client_order_ref or order.name,
370 'account_id': order.partner_id.property_account_receivable.id,
371 'partner_id': order.partner_invoice_id.id,
372 'journal_id': journal_ids[0],
373 'invoice_line': [(6, 0, lines)],
374 'currency_id': order.pricelist_id.currency_id.id,
375 'comment': order.note,
376 'payment_term': order.payment_term and order.payment_term.id or False,
377 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
378 'date_invoice': context.get('date_invoice', False),
379 'company_id': order.company_id.id,
380 'user_id': order.user_id and order.user_id.id or False
383 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
384 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
387 def _make_invoice(self, cr, uid, order, lines, context=None):
388 inv_obj = self.pool.get('account.invoice')
389 obj_invoice_line = self.pool.get('account.invoice.line')
392 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
393 from_line_invoice_ids = []
394 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
395 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
396 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
397 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
398 for preinv in order.invoice_ids:
399 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
400 for preline in preinv.invoice_line:
401 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
402 lines.append(inv_line_id)
403 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
404 inv_id = inv_obj.create(cr, uid, inv, context=context)
405 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
406 if data.get('value', False):
407 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
408 inv_obj.button_compute(cr, uid, [inv_id])
411 def print_quotation(self, cr, uid, ids, context=None):
413 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
415 assert len(ids) == 1, 'This option should only be used for a single id at a time'
416 self.signal_quotation_sent(cr, uid, ids)
418 'model': 'sale.order',
420 'form': self.read(cr, uid, ids[0], context=context),
422 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
424 def manual_invoice(self, cr, uid, ids, context=None):
425 """ create invoices for the given sales orders (ids), and open the form
426 view of one of the newly created invoices
428 mod_obj = self.pool.get('ir.model.data')
430 # create invoices through the sales orders' workflow
431 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
432 self.signal_manual_invoice(cr, uid, ids)
433 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
434 # determine newly created invoices
435 new_inv_ids = list(inv_ids1 - inv_ids0)
437 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
438 res_id = res and res[1] or False,
441 'name': _('Customer Invoices'),
445 'res_model': 'account.invoice',
446 'context': "{'type':'out_invoice'}",
447 'type': 'ir.actions.act_window',
450 'res_id': new_inv_ids and new_inv_ids[0] or False,
453 def action_view_invoice(self, cr, uid, ids, context=None):
455 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.
457 mod_obj = self.pool.get('ir.model.data')
458 act_obj = self.pool.get('ir.actions.act_window')
460 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
461 id = result and result[1] or False
462 result = act_obj.read(cr, uid, [id], context=context)[0]
463 #compute the number of invoices to display
465 for so in self.browse(cr, uid, ids, context=context):
466 inv_ids += [invoice.id for invoice in so.invoice_ids]
467 #choose the view_mode accordingly
469 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
471 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
472 result['views'] = [(res and res[1] or False, 'form')]
473 result['res_id'] = inv_ids and inv_ids[0] or False
476 def test_no_product(self, cr, uid, order, context):
477 for line in order.order_line:
478 if line.product_id and (line.product_id.type<>'service'):
482 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
484 states = ['confirmed', 'done', 'exception']
488 invoice = self.pool.get('account.invoice')
489 obj_sale_order_line = self.pool.get('sale.order.line')
490 partner_currency = {}
493 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
494 # last day of the last month as invoice date
496 context['date_invoice'] = date_invoice
497 for o in self.browse(cr, uid, ids, context=context):
498 currency_id = o.pricelist_id.currency_id.id
499 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
500 raise osv.except_osv(
502 _('You cannot group sales having different currencies for the same partner.'))
504 partner_currency[o.partner_id.id] = currency_id
506 for line in o.order_line:
509 elif (line.state in states):
510 lines.append(line.id)
511 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
513 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
515 for o in self.browse(cr, uid, ids, context=context):
516 for i in o.invoice_ids:
517 if i.state == 'draft':
519 for val in invoices.values():
521 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
525 invoice_ref += (o.client_order_ref or o.name) + '|'
526 origin_ref += (o.origin or o.name) + '|'
527 self.write(cr, uid, [o.id], {'state': 'progress'})
528 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
529 #remove last '|' in invoice_ref
530 if len(invoice_ref) >= 1:
531 invoice_ref = invoice_ref[:-1]
532 if len(origin_ref) >= 1:
533 origin_ref = origin_ref[:-1]
534 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
536 for order, il in val:
537 res = self._make_invoice(cr, uid, order, il, context=context)
538 invoice_ids.append(res)
539 self.write(cr, uid, [order.id], {'state': 'progress'})
540 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
543 def action_invoice_cancel(self, cr, uid, ids, context=None):
544 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
547 def action_invoice_end(self, cr, uid, ids, context=None):
548 for this in self.browse(cr, uid, ids, context=context):
549 for line in this.order_line:
550 if line.state == 'exception':
551 line.write({'state': 'confirmed'})
552 if this.state == 'invoice_except':
553 this.write({'state': 'progress'})
556 def action_cancel(self, cr, uid, ids, context=None):
559 sale_order_line_obj = self.pool.get('sale.order.line')
560 account_invoice_obj = self.pool.get('account.invoice')
561 for sale in self.browse(cr, uid, ids, context=context):
562 for inv in sale.invoice_ids:
563 if inv.state not in ('draft', 'cancel'):
564 raise osv.except_osv(
565 _('Cannot cancel this sales order!'),
566 _('First cancel all invoices attached to this sales order.'))
567 for r in self.read(cr, uid, ids, ['invoice_ids']):
568 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
569 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
571 self.write(cr, uid, ids, {'state': 'cancel'})
574 def action_button_confirm(self, cr, uid, ids, context=None):
575 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
576 self.signal_order_confirm(cr, uid, ids)
579 def action_wait(self, cr, uid, ids, context=None):
580 context = context or {}
581 for o in self.browse(cr, uid, ids):
583 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
584 noprod = self.test_no_product(cr, uid, o, context)
585 if (o.order_policy == 'manual') or noprod:
586 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
588 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
589 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
592 def action_quotation_send(self, cr, uid, ids, context=None):
594 This function opens a window to compose an email, with the edi sale template message loaded by default
596 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
597 ir_model_data = self.pool.get('ir.model.data')
599 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
603 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
605 compose_form_id = False
608 'default_model': 'sale.order',
609 'default_res_id': ids[0],
610 'default_use_template': bool(template_id),
611 'default_template_id': template_id,
612 'default_composition_mode': 'comment',
613 'mark_so_as_sent': True
616 'type': 'ir.actions.act_window',
619 'res_model': 'mail.compose.message',
620 'views': [(compose_form_id, 'form')],
621 'view_id': compose_form_id,
626 def action_done(self, cr, uid, ids, context=None):
627 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
631 # TODO add a field price_unit_uos
632 # - update it on change product and unit price
633 # - use it in report if there is a uos
634 class sale_order_line(osv.osv):
636 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
637 tax_obj = self.pool.get('account.tax')
638 cur_obj = self.pool.get('res.currency')
642 for line in self.browse(cr, uid, ids, context=context):
643 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
644 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
645 cur = line.order_id.pricelist_id.currency_id
646 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
649 def _get_uom_id(self, cr, uid, *args):
651 proxy = self.pool.get('ir.model.data')
652 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
654 except Exception, ex:
657 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
658 res = dict.fromkeys(ids, False)
659 for this in self.browse(cr, uid, ids, context=context):
660 res[this.id] = this.invoice_lines and \
661 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
664 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
665 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
666 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
667 sale_order_line sol ON (sol.order_id = rel.order_id)
668 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
669 return [i[0] for i in cr.fetchall()]
671 _name = 'sale.order.line'
672 _description = 'Sales Order Line'
674 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
675 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
676 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
677 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
678 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
679 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
681 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
682 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
683 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
684 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
685 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."),
686 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
687 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
688 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
689 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
690 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
691 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
692 'product_uos': fields.many2one('product.uom', 'Product UoS'),
693 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
694 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
695 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
696 help='* The \'Draft\' status is set when the related sales order in draft status. \
697 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
698 \n* The \'Exception\' status is set when the related sales order is set as exception. \
699 \n* The \'Done\' status is set when the sales order line has been picked. \
700 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
701 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
702 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
703 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
705 _order = 'order_id desc, sequence, id'
707 'product_uom' : _get_uom_id,
709 'product_uom_qty': 1,
710 'product_uos_qty': 1,
713 'type': 'make_to_stock',
717 def _get_line_qty(self, cr, uid, line, context=None):
718 if (line.order_id.invoice_quantity=='order'):
720 return line.product_uos_qty or 0.0
721 return line.product_uom_qty
723 def _get_line_uom(self, cr, uid, line, context=None):
724 if (line.order_id.invoice_quantity=='order'):
726 return line.product_uos.id
727 return line.product_uom.id
729 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
730 """Prepare the dict of values to create the new invoice line for a
731 sales order line. This method may be overridden to implement custom
732 invoice generation (making sure to call super() to establish
733 a clean extension chain).
735 :param browse_record line: sale.order.line record to invoice
736 :param int account_id: optional ID of a G/L account to force
737 (this is used for returning products including service)
738 :return: dict of values to create() the invoice line
741 if not line.invoiced:
744 account_id = line.product_id.property_account_income.id
746 account_id = line.product_id.categ_id.property_account_income_categ.id
748 raise osv.except_osv(_('Error!'),
749 _('Please define income account for this product: "%s" (id:%d).') % \
750 (line.product_id.name, line.product_id.id,))
752 prop = self.pool.get('ir.property').get(cr, uid,
753 'property_account_income_categ', 'product.category',
755 account_id = prop and prop.id or False
756 uosqty = self._get_line_qty(cr, uid, line, context=context)
757 uos_id = self._get_line_uom(cr, uid, line, context=context)
760 pu = round(line.price_unit * line.product_uom_qty / uosqty,
761 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
762 fpos = line.order_id.fiscal_position or False
763 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
765 raise osv.except_osv(_('Error!'),
766 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
769 'sequence': line.sequence,
770 'origin': line.order_id.name,
771 'account_id': account_id,
774 'discount': line.discount,
776 'product_id': line.product_id.id or False,
777 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
778 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
783 def invoice_line_create(self, cr, uid, ids, context=None):
789 for line in self.browse(cr, uid, ids, context=context):
790 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
792 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
793 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
794 sales.add(line.order_id.id)
795 create_ids.append(inv_id)
796 # Trigger workflow events
797 for sale_id in sales:
798 workflow.trg_write(uid, 'sale.order', sale_id, cr)
801 def button_cancel(self, cr, uid, ids, context=None):
802 for line in self.browse(cr, uid, ids, context=context):
804 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
805 return self.write(cr, uid, ids, {'state': 'cancel'})
807 def button_confirm(self, cr, uid, ids, context=None):
808 return self.write(cr, uid, ids, {'state': 'confirmed'})
810 def button_done(self, cr, uid, ids, context=None):
811 res = self.write(cr, uid, ids, {'state': 'done'})
812 for line in self.browse(cr, uid, ids, context=context):
813 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
816 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
817 product_obj = self.pool.get('product.product')
819 return {'value': {'product_uom': product_uos,
820 'product_uom_qty': product_uos_qty}, 'domain': {}}
822 product = product_obj.browse(cr, uid, product_id)
824 'product_uom': product.uom_id.id,
826 # FIXME must depend on uos/uom of the product and not only of the coeff.
829 'product_uom_qty': product_uos_qty / product.uos_coeff,
830 'th_weight': product_uos_qty / product.uos_coeff * product.weight
832 except ZeroDivisionError:
834 return {'value': value}
836 def create(self, cr, uid, values, context=None):
837 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']):
838 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
839 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
840 qty=float(values.get('product_uom_qty', False)),
841 uom=values.get('product_uom', False),
842 qty_uos=float(values.get('product_uos_qty', False)),
843 uos=values.get('product_uos', False),
844 name=values.get('name', False),
845 partner_id=order['partner_id'][0],
846 date_order=order['date_order'],
847 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
848 flag=False, # Force name update
851 if defaults.get('tax_id'):
852 defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
853 values = dict(defaults, **values)
854 return super(sale_order_line, self).create(cr, uid, values, context=context)
856 def copy_data(self, cr, uid, id, default=None, context=None):
859 default.update({'state': 'draft', 'invoice_lines': []})
860 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
862 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
863 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
864 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
865 context = context or {}
866 lang = lang or context.get('lang',False)
868 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
870 product_uom_obj = self.pool.get('product.uom')
871 partner_obj = self.pool.get('res.partner')
872 product_obj = self.pool.get('product.product')
873 context = {'lang': lang, 'partner_id': partner_id}
875 lang = partner_obj.browse(cr, uid, partner_id).lang
876 context_partner = {'lang': lang, 'partner_id': partner_id}
879 return {'value': {'th_weight': 0,
880 'product_uos_qty': qty}, 'domain': {'product_uom': [],
883 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
887 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
891 uom2 = product_uom_obj.browse(cr, uid, uom)
892 if product_obj.uom_id.category_id.id != uom2.category_id.id:
895 if product_obj.uos_id:
896 uos2 = product_uom_obj.browse(cr, uid, uos)
897 if product_obj.uos_id.category_id.id != uos2.category_id.id:
901 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
902 if update_tax: #The quantity only have changed
903 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
906 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
907 if product_obj.description_sale:
908 result['name'] += '\n'+product_obj.description_sale
910 if (not uom) and (not uos):
911 result['product_uom'] = product_obj.uom_id.id
912 if product_obj.uos_id:
913 result['product_uos'] = product_obj.uos_id.id
914 result['product_uos_qty'] = qty * product_obj.uos_coeff
915 uos_category_id = product_obj.uos_id.category_id.id
917 result['product_uos'] = False
918 result['product_uos_qty'] = qty
919 uos_category_id = False
920 result['th_weight'] = qty * product_obj.weight
921 domain = {'product_uom':
922 [('category_id', '=', product_obj.uom_id.category_id.id)],
924 [('category_id', '=', uos_category_id)]}
925 elif uos and not uom: # only happens if uom is False
926 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
927 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
928 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
929 elif uom: # whether uos is set or not
930 default_uom = product_obj.uom_id and product_obj.uom_id.id
931 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
932 if product_obj.uos_id:
933 result['product_uos'] = product_obj.uos_id.id
934 result['product_uos_qty'] = qty * product_obj.uos_coeff
936 result['product_uos'] = False
937 result['product_uos_qty'] = qty
938 result['th_weight'] = q * product_obj.weight # Round the quantity up
941 uom2 = product_obj.uom_id
945 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
946 'Please set one before choosing a product.')
947 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
949 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
950 product, qty or 1.0, partner_id, {
951 'uom': uom or result.get('product_uom'),
955 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
956 "You have to change either the product, the quantity or the pricelist.")
958 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
960 result.update({'price_unit': price})
963 'title': _('Configuration Error!'),
964 'message' : warning_msgs
966 return {'value': result, 'domain': domain, 'warning': warning}
968 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
969 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
970 lang=False, update_tax=True, date_order=False, context=None):
971 context = context or {}
972 lang = lang or ('lang' in context and context['lang'])
974 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
975 return self.product_id_change(cursor, user, ids, pricelist, product,
976 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
977 partner_id=partner_id, lang=lang, update_tax=update_tax,
978 date_order=date_order, context=context)
980 def unlink(self, cr, uid, ids, context=None):
983 """Allows to delete sales order lines in draft,cancel states"""
984 for rec in self.browse(cr, uid, ids, context=context):
985 if rec.state not in ['draft', 'cancel']:
986 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
987 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
990 class mail_compose_message(osv.Model):
991 _inherit = 'mail.compose.message'
993 def send_mail(self, cr, uid, ids, context=None):
994 context = context or {}
995 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
996 context = dict(context, mail_post_autofollow=True)
997 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
998 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1001 class account_invoice(osv.Model):
1002 _inherit = 'account.invoice'
1004 def confirm_paid(self, cr, uid, ids, context=None):
1005 sale_order_obj = self.pool.get('sale.order')
1006 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1007 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1008 for so_id in so_ids:
1009 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1012 def unlink(self, cr, uid, ids, context=None):
1013 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1014 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1015 #if we can't cancel all invoices, do nothing
1016 if len(invoice_ids) == len(ids):
1017 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1018 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1020 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1021 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1023 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: