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_wrapper(self, cr, uid, ids, field_name, arg, context=None):
62 """ Wrapper because of direct method passing as parameter for function fields """
63 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
65 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
66 cur_obj = self.pool.get('res.currency')
68 for order in self.browse(cr, uid, ids, context=context):
70 'amount_untaxed': 0.0,
75 cur = order.pricelist_id.currency_id
76 for line in order.order_line:
77 val1 += line.price_subtotal
78 val += self._amount_line_tax(cr, uid, line, context=context)
79 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
80 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
81 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
85 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
87 for sale in self.browse(cursor, user, ids, context=context):
92 for invoice in sale.invoice_ids:
93 if invoice.state not in ('draft', 'cancel'):
94 tot += invoice.amount_untaxed
96 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
101 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
103 for sale in self.browse(cursor, user, ids, context=context):
109 def _invoiced(self, cursor, user, ids, name, arg, context=None):
111 for sale in self.browse(cursor, user, ids, context=context):
113 invoice_existence = False
114 for invoice in sale.invoice_ids:
115 if invoice.state!='cancel':
116 invoice_existence = True
117 if invoice.state != 'paid':
120 if not invoice_existence or sale.state == 'manual':
124 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
133 clause += 'AND inv.state = \'paid\''
135 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
136 sale_clause = ', sale_order AS sale '
139 cursor.execute('SELECT rel.order_id ' \
140 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
141 'WHERE rel.invoice_id = inv.id ' + clause)
142 res = cursor.fetchall()
144 cursor.execute('SELECT sale.id ' \
145 'FROM sale_order AS sale ' \
146 'WHERE sale.id NOT IN ' \
147 '(SELECT rel.order_id ' \
148 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
149 res.extend(cursor.fetchall())
151 return [('id', '=', 0)]
152 return [('id', 'in', [x[0] for x in res])]
154 def _get_order(self, cr, uid, ids, context=None):
156 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
157 result[line.order_id.id] = True
160 def _get_default_company(self, cr, uid, context=None):
161 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
163 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
167 'name': fields.char('Order Reference', size=64, required=True,
168 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
169 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
170 'client_order_ref': fields.char('Reference/Description', size=64),
171 'state': fields.selection([
172 ('draft', 'Draft Quotation'),
173 ('sent', 'Quotation Sent'),
174 ('cancel', 'Cancelled'),
175 ('waiting_date', 'Waiting Schedule'),
176 ('progress', 'Sales Order'),
177 ('manual', 'Sale to Invoice'),
178 ('invoice_except', 'Invoice Exception'),
180 ], 'Status', readonly=True, track_visibility='onchange',
181 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),
182 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
183 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
184 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
185 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
186 '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'),
187 '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."),
188 '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."),
189 'order_policy': fields.selection([
190 ('manual', 'On Demand'),
191 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
192 help="""This field controls how invoice and delivery operations are synchronized."""),
193 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
194 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
195 '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."),
197 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
198 '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)."),
199 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
200 'invoiced': fields.function(_invoiced, string='Paid',
201 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
202 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
203 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
204 'note': fields.text('Terms and conditions'),
206 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
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 amount without tax.", track_visibility='always'),
212 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
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 tax amount."),
218 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
220 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
221 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
223 multi='sums', help="The total amount."),
225 '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)]}),
226 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
227 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
228 'company_id': fields.many2one('res.company', 'Company'),
231 'date_order': fields.date.context_today,
232 'order_policy': 'manual',
233 'company_id': _get_default_company,
235 'user_id': lambda obj, cr, uid, context: uid,
236 'name': lambda obj, cr, uid, context: '/',
237 'invoice_quantity': 'order',
238 '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'],
239 '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'],
240 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
243 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
245 _order = 'date_order desc, id desc'
248 def unlink(self, cr, uid, ids, context=None):
249 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
251 for s in sale_orders:
252 if s['state'] in ['draft', 'cancel']:
253 unlink_ids.append(s['id'])
255 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
257 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
259 def copy_quotation(self, cr, uid, ids, context=None):
260 id = self.copy(cr, uid, ids[0], context=None)
261 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
262 view_id = view_ref and view_ref[1] or False,
264 'type': 'ir.actions.act_window',
265 'name': _('Sales Order'),
266 'res_model': 'sale.order',
275 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
276 context = context or {}
280 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
283 return {'value': value}
285 'title': _('Pricelist Warning!'),
286 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
288 return {'warning': warning, 'value': value}
290 def get_salenote(self, cr, uid, ids, partner_id, context=None):
291 context_lang = context.copy()
293 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
294 context_lang.update({'lang': partner_lang})
295 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
297 def onchange_partner_id(self, cr, uid, ids, part, context=None):
299 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
301 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
302 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
303 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
304 payment_term = part.property_payment_term and part.property_payment_term.id or False
305 fiscal_position = part.property_account_position and part.property_account_position.id or False
306 dedicated_salesman = part.user_id and part.user_id.id or uid
308 'partner_invoice_id': addr['invoice'],
309 'partner_shipping_id': addr['delivery'],
310 'payment_term': payment_term,
311 'fiscal_position': fiscal_position,
312 'user_id': dedicated_salesman,
315 val['pricelist_id'] = pricelist
316 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
317 if sale_note: val.update({'note': sale_note})
318 return {'value': val}
320 def create(self, cr, uid, vals, context=None):
323 if vals.get('name', '/') == '/':
324 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
325 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
326 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
327 vals = dict(defaults, **vals)
328 context.update({'mail_create_nolog': True})
329 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
330 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
333 def button_dummy(self, cr, uid, ids, context=None):
336 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
337 # can be removed after 6.1.
338 def _inv_get(self, cr, uid, order, context=None):
341 def _prepare_invoice(self, cr, uid, order, lines, context=None):
342 """Prepare the dict of values to create the new invoice for a
343 sales order. This method may be overridden to implement custom
344 invoice generation (making sure to call super() to establish
345 a clean extension chain).
347 :param browse_record order: sale.order record to invoice
348 :param list(int) line: list of invoice line IDs that must be
349 attached to the invoice
350 :return: dict of value to create() the invoice
354 journal_ids = self.pool.get('account.journal').search(cr, uid,
355 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
358 raise osv.except_osv(_('Error!'),
359 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
361 'name': order.client_order_ref or '',
362 'origin': order.name,
363 'type': 'out_invoice',
364 'reference': order.client_order_ref or order.name,
365 'account_id': order.partner_id.property_account_receivable.id,
366 'partner_id': order.partner_invoice_id.id,
367 'journal_id': journal_ids[0],
368 'invoice_line': [(6, 0, lines)],
369 'currency_id': order.pricelist_id.currency_id.id,
370 'comment': order.note,
371 'payment_term': order.payment_term and order.payment_term.id or False,
372 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
373 'date_invoice': context.get('date_invoice', False),
374 'company_id': order.company_id.id,
375 'user_id': order.user_id and order.user_id.id or False
378 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
379 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
382 def _make_invoice(self, cr, uid, order, lines, context=None):
383 inv_obj = self.pool.get('account.invoice')
384 obj_invoice_line = self.pool.get('account.invoice.line')
387 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
388 from_line_invoice_ids = []
389 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
390 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
391 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
392 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
393 for preinv in order.invoice_ids:
394 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
395 for preline in preinv.invoice_line:
396 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
397 lines.append(inv_line_id)
398 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
399 inv_id = inv_obj.create(cr, uid, inv, context=context)
400 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
401 if data.get('value', False):
402 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
403 inv_obj.button_compute(cr, uid, [inv_id])
406 def print_quotation(self, cr, uid, ids, context=None):
408 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
410 assert len(ids) == 1, 'This option should only be used for a single id at a time'
411 self.signal_quotation_sent(cr, uid, ids)
412 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
414 def manual_invoice(self, cr, uid, ids, context=None):
415 """ create invoices for the given sales orders (ids), and open the form
416 view of one of the newly created invoices
418 mod_obj = self.pool.get('ir.model.data')
420 # create invoices through the sales orders' workflow
421 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
422 self.signal_manual_invoice(cr, uid, ids)
423 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
424 # determine newly created invoices
425 new_inv_ids = list(inv_ids1 - inv_ids0)
427 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
428 res_id = res and res[1] or False,
431 'name': _('Customer Invoices'),
435 'res_model': 'account.invoice',
436 'context': "{'type':'out_invoice'}",
437 'type': 'ir.actions.act_window',
440 'res_id': new_inv_ids and new_inv_ids[0] or False,
443 def action_view_invoice(self, cr, uid, ids, context=None):
445 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.
447 mod_obj = self.pool.get('ir.model.data')
448 act_obj = self.pool.get('ir.actions.act_window')
450 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
451 id = result and result[1] or False
452 result = act_obj.read(cr, uid, [id], context=context)[0]
453 #compute the number of invoices to display
455 for so in self.browse(cr, uid, ids, context=context):
456 inv_ids += [invoice.id for invoice in so.invoice_ids]
457 #choose the view_mode accordingly
459 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
461 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
462 result['views'] = [(res and res[1] or False, 'form')]
463 result['res_id'] = inv_ids and inv_ids[0] or False
466 def test_no_product(self, cr, uid, order, context):
467 for line in order.order_line:
468 if line.product_id and (line.product_id.type<>'service'):
472 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
474 states = ['confirmed', 'done', 'exception']
478 invoice = self.pool.get('account.invoice')
479 obj_sale_order_line = self.pool.get('sale.order.line')
480 partner_currency = {}
483 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
484 # last day of the last month as invoice date
486 context['date_invoice'] = date_invoice
487 for o in self.browse(cr, uid, ids, context=context):
488 currency_id = o.pricelist_id.currency_id.id
489 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
490 raise osv.except_osv(
492 _('You cannot group sales having different currencies for the same partner.'))
494 partner_currency[o.partner_id.id] = currency_id
496 for line in o.order_line:
499 elif (line.state in states):
500 lines.append(line.id)
501 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
503 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
505 for o in self.browse(cr, uid, ids, context=context):
506 for i in o.invoice_ids:
507 if i.state == 'draft':
509 for val in invoices.values():
511 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
514 invoice_ref += o.name + '|'
515 self.write(cr, uid, [o.id], {'state': 'progress'})
516 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
517 #remove last '|' in invoice_ref
518 if len(invoice_ref) >= 1:
519 invoice_ref = invoice_ref[:-1]
520 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
522 for order, il in val:
523 res = self._make_invoice(cr, uid, order, il, context=context)
524 invoice_ids.append(res)
525 self.write(cr, uid, [order.id], {'state': 'progress'})
526 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
529 def action_invoice_cancel(self, cr, uid, ids, context=None):
530 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
533 def action_invoice_end(self, cr, uid, ids, context=None):
534 for this in self.browse(cr, uid, ids, context=context):
535 for line in this.order_line:
536 if line.state == 'exception':
537 line.write({'state': 'confirmed'})
538 if this.state == 'invoice_except':
539 this.write({'state': 'progress'})
542 def action_cancel(self, cr, uid, ids, context=None):
545 sale_order_line_obj = self.pool.get('sale.order.line')
546 account_invoice_obj = self.pool.get('account.invoice')
547 for sale in self.browse(cr, uid, ids, context=context):
548 for inv in sale.invoice_ids:
549 if inv.state not in ('draft', 'cancel'):
550 raise osv.except_osv(
551 _('Cannot cancel this sales order!'),
552 _('First cancel all invoices attached to this sales order.'))
553 for r in self.read(cr, uid, ids, ['invoice_ids']):
554 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
555 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
557 self.write(cr, uid, ids, {'state': 'cancel'})
560 def action_button_confirm(self, cr, uid, ids, context=None):
561 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
562 self.signal_order_confirm(cr, uid, ids)
564 # redisplay the record as a sales order
565 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
566 view_id = view_ref and view_ref[1] or False,
568 'type': 'ir.actions.act_window',
569 'name': _('Sales Order'),
570 'res_model': 'sale.order',
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)
629 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
631 fiscal_obj = self.pool.get('account.fiscal.position')
632 product_obj = self.pool.get('product.product')
633 line_obj = self.pool.get('sale.order.line')
637 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
639 for line in order_lines:
640 # create (0, 0, { fields })
641 # update (1, ID, { fields })
642 if line[0] in [0, 1]:
644 if line[2].get('product_id'):
645 taxes_id = product_obj.browse(cr, uid, line[2]['product_id'], context=context).taxes_id
647 # don't change taxes if they are no product defined
648 taxes_id = line_obj.browse(cr, uid, line[1], context=context).product_id.taxes_id
650 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, taxes_id)]]
651 order_line.append(line)
654 # link all (6, 0, IDS)
655 elif line[0] in [4, 6]:
656 line_ids = line[0] == 4 and [line[1]] or line[2]
657 for line_id in line_ids:
658 taxes_id = line_obj.browse(cr, uid, line_id, context=context).product_id.taxes_id
659 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, taxes_id)]]}])
662 order_line.append(line)
663 return {'value': {'order_line': order_line}}
666 # TODO add a field price_unit_uos
667 # - update it on change product and unit price
668 # - use it in report if there is a uos
669 class sale_order_line(osv.osv):
671 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
672 tax_obj = self.pool.get('account.tax')
673 cur_obj = self.pool.get('res.currency')
677 for line in self.browse(cr, uid, ids, context=context):
678 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
679 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
680 cur = line.order_id.pricelist_id.currency_id
681 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
684 def _get_uom_id(self, cr, uid, *args):
686 proxy = self.pool.get('ir.model.data')
687 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
689 except Exception, ex:
692 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
693 res = dict.fromkeys(ids, False)
694 for this in self.browse(cr, uid, ids, context=context):
695 res[this.id] = this.invoice_lines and \
696 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
699 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
700 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
701 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
702 sale_order_line sol ON (sol.order_id = rel.order_id)
703 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
704 return [i[0] for i in cr.fetchall()]
706 _name = 'sale.order.line'
707 _description = 'Sales Order Line'
709 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
710 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
711 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
712 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
713 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
714 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
716 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
717 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
718 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
719 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
720 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."),
721 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
722 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
723 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
724 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
725 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
726 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
727 'product_uos': fields.many2one('product.uom', 'Product UoS'),
728 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
729 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
730 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
731 help='* The \'Draft\' status is set when the related sales order in draft status. \
732 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
733 \n* The \'Exception\' status is set when the related sales order is set as exception. \
734 \n* The \'Done\' status is set when the sales order line has been picked. \
735 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
736 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
737 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
738 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
740 _order = 'order_id desc, sequence, id'
742 'product_uom' : _get_uom_id,
744 'product_uom_qty': 1,
745 'product_uos_qty': 1,
748 'type': 'make_to_stock',
752 def _get_line_qty(self, cr, uid, line, context=None):
753 if (line.order_id.invoice_quantity=='order'):
755 return line.product_uos_qty or 0.0
756 return line.product_uom_qty
758 def _get_line_uom(self, cr, uid, line, context=None):
759 if (line.order_id.invoice_quantity=='order'):
761 return line.product_uos.id
762 return line.product_uom.id
764 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
765 """Prepare the dict of values to create the new invoice line for a
766 sales order line. This method may be overridden to implement custom
767 invoice generation (making sure to call super() to establish
768 a clean extension chain).
770 :param browse_record line: sale.order.line record to invoice
771 :param int account_id: optional ID of a G/L account to force
772 (this is used for returning products including service)
773 :return: dict of values to create() the invoice line
776 if not line.invoiced:
779 account_id = line.product_id.property_account_income.id
781 account_id = line.product_id.categ_id.property_account_income_categ.id
783 raise osv.except_osv(_('Error!'),
784 _('Please define income account for this product: "%s" (id:%d).') % \
785 (line.product_id.name, line.product_id.id,))
787 prop = self.pool.get('ir.property').get(cr, uid,
788 'property_account_income_categ', 'product.category',
790 account_id = prop and prop.id or False
791 uosqty = self._get_line_qty(cr, uid, line, context=context)
792 uos_id = self._get_line_uom(cr, uid, line, context=context)
795 pu = round(line.price_unit * line.product_uom_qty / uosqty,
796 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
797 fpos = line.order_id.fiscal_position or False
798 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
800 raise osv.except_osv(_('Error!'),
801 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
804 'sequence': line.sequence,
805 'origin': line.order_id.name,
806 'account_id': account_id,
809 'discount': line.discount,
811 'product_id': line.product_id.id or False,
812 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
813 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
818 def invoice_line_create(self, cr, uid, ids, context=None):
824 for line in self.browse(cr, uid, ids, context=context):
825 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
827 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
828 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
829 sales.add(line.order_id.id)
830 create_ids.append(inv_id)
831 # Trigger workflow events
832 for sale_id in sales:
833 workflow.trg_write(uid, 'sale.order', sale_id, cr)
836 def button_cancel(self, cr, uid, ids, context=None):
837 for line in self.browse(cr, uid, ids, context=context):
839 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
840 return self.write(cr, uid, ids, {'state': 'cancel'})
842 def button_confirm(self, cr, uid, ids, context=None):
843 return self.write(cr, uid, ids, {'state': 'confirmed'})
845 def button_done(self, cr, uid, ids, context=None):
846 res = self.write(cr, uid, ids, {'state': 'done'})
847 for line in self.browse(cr, uid, ids, context=context):
848 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
851 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
852 product_obj = self.pool.get('product.product')
854 return {'value': {'product_uom': product_uos,
855 'product_uom_qty': product_uos_qty}, 'domain': {}}
857 product = product_obj.browse(cr, uid, product_id)
859 'product_uom': product.uom_id.id,
861 # FIXME must depend on uos/uom of the product and not only of the coeff.
864 'product_uom_qty': product_uos_qty / product.uos_coeff,
865 'th_weight': product_uos_qty / product.uos_coeff * product.weight
867 except ZeroDivisionError:
869 return {'value': value}
871 def create(self, cr, uid, values, context=None):
872 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']):
873 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
874 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
875 qty=float(values.get('product_uom_qty', False)),
876 uom=values.get('product_uom', False),
877 qty_uos=float(values.get('product_uos_qty', False)),
878 uos=values.get('product_uos', False),
879 name=values.get('name', False),
880 partner_id=order['partner_id'][0],
881 date_order=order['date_order'],
882 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
883 flag=False, # Force name update
886 values = dict(defaults, **values)
887 return super(sale_order_line, self).create(cr, uid, values, context=context)
889 def copy_data(self, cr, uid, id, default=None, context=None):
892 default.update({'state': 'draft', 'invoice_lines': []})
893 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
895 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
896 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
897 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
898 context = context or {}
899 lang = lang or context.get('lang',False)
901 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
903 product_uom_obj = self.pool.get('product.uom')
904 partner_obj = self.pool.get('res.partner')
905 product_obj = self.pool.get('product.product')
906 context = {'lang': lang, 'partner_id': partner_id}
907 partner = partner_obj.browse(cr, uid, partner_id)
909 context_partner = {'lang': lang, 'partner_id': partner_id}
912 return {'value': {'th_weight': 0,
913 'product_uos_qty': qty}, 'domain': {'product_uom': [],
916 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
920 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
924 uom2 = product_uom_obj.browse(cr, uid, uom)
925 if product_obj.uom_id.category_id.id != uom2.category_id.id:
928 if product_obj.uos_id:
929 uos2 = product_uom_obj.browse(cr, uid, uos)
930 if product_obj.uos_id.category_id.id != uos2.category_id.id:
936 if not fiscal_position:
937 fpos = partner.property_account_position or False
939 fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position)
940 if update_tax: #The quantity only have changed
941 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
944 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
945 if product_obj.description_sale:
946 result['name'] += '\n'+product_obj.description_sale
948 if (not uom) and (not uos):
949 result['product_uom'] = product_obj.uom_id.id
950 if product_obj.uos_id:
951 result['product_uos'] = product_obj.uos_id.id
952 result['product_uos_qty'] = qty * product_obj.uos_coeff
953 uos_category_id = product_obj.uos_id.category_id.id
955 result['product_uos'] = False
956 result['product_uos_qty'] = qty
957 uos_category_id = False
958 result['th_weight'] = qty * product_obj.weight
959 domain = {'product_uom':
960 [('category_id', '=', product_obj.uom_id.category_id.id)],
962 [('category_id', '=', uos_category_id)]}
963 elif uos and not uom: # only happens if uom is False
964 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
965 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
966 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
967 elif uom: # whether uos is set or not
968 default_uom = product_obj.uom_id and product_obj.uom_id.id
969 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
970 if product_obj.uos_id:
971 result['product_uos'] = product_obj.uos_id.id
972 result['product_uos_qty'] = qty * product_obj.uos_coeff
974 result['product_uos'] = False
975 result['product_uos_qty'] = qty
976 result['th_weight'] = q * product_obj.weight # Round the quantity up
979 uom2 = product_obj.uom_id
983 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
984 'Please set one before choosing a product.')
985 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
987 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
988 product, qty or 1.0, partner_id, {
989 'uom': uom or result.get('product_uom'),
993 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
994 "You have to change either the product, the quantity or the pricelist.")
996 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
998 result.update({'price_unit': price})
1001 'title': _('Configuration Error!'),
1002 'message' : warning_msgs
1004 return {'value': result, 'domain': domain, 'warning': warning}
1006 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1007 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1008 lang=False, update_tax=True, date_order=False, context=None):
1009 context = context or {}
1010 lang = lang or ('lang' in context and context['lang'])
1012 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1013 return self.product_id_change(cursor, user, ids, pricelist, product,
1014 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1015 partner_id=partner_id, lang=lang, update_tax=update_tax,
1016 date_order=date_order, context=context)
1018 def unlink(self, cr, uid, ids, context=None):
1021 """Allows to delete sales order lines in draft,cancel states"""
1022 for rec in self.browse(cr, uid, ids, context=context):
1023 if rec.state not in ['draft', 'cancel']:
1024 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1025 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1027 class res_company(osv.Model):
1028 _inherit = "res.company"
1030 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
1034 class mail_compose_message(osv.Model):
1035 _inherit = 'mail.compose.message'
1037 def send_mail(self, cr, uid, ids, context=None):
1038 context = context or {}
1039 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1040 context = dict(context, mail_post_autofollow=True)
1041 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1042 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1045 class account_invoice(osv.Model):
1046 _inherit = 'account.invoice'
1048 def confirm_paid(self, cr, uid, ids, context=None):
1049 sale_order_obj = self.pool.get('sale.order')
1050 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1051 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1052 for so_id in so_ids:
1053 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1056 def unlink(self, cr, uid, ids, context=None):
1057 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1058 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1059 #if we can't cancel all invoices, do nothing
1060 if len(invoice_ids) == len(ids):
1061 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1062 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1064 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1065 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1067 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: