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 import pooler
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import openerp.addons.decimal_precision as dp
30 from openerp import netsvc
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40 'company_id': fields.many2one('res.company', 'Company', required=False),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 class sale_order(osv.osv):
50 _inherit = ['mail.thread', 'ir.needaction_mixin']
51 _description = "Sales Order"
54 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['manual', 'progress'],
55 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['sent']
59 def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
62 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
63 if shop.project_id.id:
64 v['project_id'] = shop.project_id.id
65 if shop.pricelist_id.id:
66 v['pricelist_id'] = shop.pricelist_id.id
69 def copy(self, cr, uid, id, default=None, context=None):
75 'date_confirm': False,
76 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
78 return super(sale_order, self).copy(cr, uid, id, default, context=context)
80 def _amount_line_tax(self, cr, uid, line, context=None):
82 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']:
83 val += c.get('amount', 0.0)
86 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
87 cur_obj = self.pool.get('res.currency')
89 for order in self.browse(cr, uid, ids, context=context):
91 'amount_untaxed': 0.0,
96 cur = order.pricelist_id.currency_id
97 for line in order.order_line:
98 val1 += line.price_subtotal
99 val += self._amount_line_tax(cr, uid, line, context=context)
100 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
101 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
102 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
106 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
108 for sale in self.browse(cursor, user, ids, context=context):
113 for invoice in sale.invoice_ids:
114 if invoice.state not in ('draft', 'cancel'):
115 tot += invoice.amount_untaxed
117 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
122 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
124 for sale in self.browse(cursor, user, ids, context=context):
130 def _invoiced(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
134 invoice_existence = False
135 for invoice in sale.invoice_ids:
136 if invoice.state!='cancel':
137 invoice_existence = True
138 if invoice.state != 'paid':
141 if not invoice_existence or sale.state == 'manual':
145 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
154 clause += 'AND inv.state = \'paid\''
156 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
157 sale_clause = ', sale_order AS sale '
160 cursor.execute('SELECT rel.order_id ' \
161 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
162 'WHERE rel.invoice_id = inv.id ' + clause)
163 res = cursor.fetchall()
165 cursor.execute('SELECT sale.id ' \
166 'FROM sale_order AS sale ' \
167 'WHERE sale.id NOT IN ' \
168 '(SELECT rel.order_id ' \
169 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
170 res.extend(cursor.fetchall())
172 return [('id', '=', 0)]
173 return [('id', 'in', [x[0] for x in res])]
175 def _get_order(self, cr, uid, ids, context=None):
177 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
178 result[line.order_id.id] = True
181 def _get_default_shop(self, cr, uid, context=None):
182 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
183 shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context)
185 raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!'))
189 'name': fields.char('Order Reference', size=64, required=True,
190 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
191 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
193 'client_order_ref': fields.char('Customer Reference', size=64),
194 'state': fields.selection([
195 ('draft', 'Draft Quotation'),
196 ('sent', 'Quotation Sent'),
197 ('cancel', 'Cancelled'),
198 ('waiting_date', 'Waiting Schedule'),
199 ('progress', 'Sales Order'),
200 ('manual', 'Sale to Invoice'),
201 ('invoice_except', 'Invoice Exception'),
203 ], 'Status', readonly=True, track_visibility='onchange',
204 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),
205 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
206 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
207 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
208 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
209 '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'),
210 '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."),
211 '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."),
212 'order_policy': fields.selection([
213 ('manual', 'On Demand'),
214 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
215 help="""This field controls how invoice and delivery operations are synchronized."""),
216 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
217 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
218 '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."),
220 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
221 '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)."),
222 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
223 'invoiced': fields.function(_invoiced, string='Paid',
224 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
225 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
226 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
227 'note': fields.text('Terms and conditions'),
229 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
231 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
232 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234 multi='sums', help="The amount without tax.", track_visibility='always'),
235 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240 multi='sums', help="The tax amount."),
241 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
243 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
244 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
246 multi='sums', help="The total amount."),
248 '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)]}),
249 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
250 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
251 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
254 'date_order': fields.date.context_today,
255 'order_policy': 'manual',
257 'user_id': lambda obj, cr, uid, context: uid,
258 'name': lambda obj, cr, uid, context: '/',
259 'invoice_quantity': 'order',
260 'shop_id': _get_default_shop,
261 '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'],
262 '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'],
265 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
270 def unlink(self, cr, uid, ids, context=None):
271 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
273 for s in sale_orders:
274 if s['state'] in ['draft', 'cancel']:
275 unlink_ids.append(s['id'])
277 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
279 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
281 def copy_quotation(self, cr, uid, ids, context=None):
282 id = self.copy(cr, uid, ids[0], context=None)
283 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
284 view_id = view_ref and view_ref[1] or False,
286 'type': 'ir.actions.act_window',
287 'name': _('Sales Order'),
288 'res_model': 'sale.order',
297 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
298 context = context or {}
302 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
305 return {'value': value}
307 'title': _('Pricelist Warning!'),
308 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
310 return {'warning': warning, 'value': value}
312 def onchange_partner_id(self, cr, uid, ids, part, context=None):
314 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
316 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
317 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
318 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
319 payment_term = part.property_payment_term and part.property_payment_term.id or False
320 fiscal_position = part.property_account_position and part.property_account_position.id or False
321 dedicated_salesman = part.user_id and part.user_id.id or uid
323 'partner_invoice_id': addr['invoice'],
324 'partner_shipping_id': addr['delivery'],
325 'payment_term': payment_term,
326 'fiscal_position': fiscal_position,
327 'user_id': dedicated_salesman,
330 val['pricelist_id'] = pricelist
331 return {'value': val}
333 def create(self, cr, uid, vals, context=None):
334 if vals.get('name','/')=='/':
335 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
336 return super(sale_order, self).create(cr, uid, vals, 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 wf_service = netsvc.LocalService("workflow")
417 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
419 'model': 'sale.order',
421 'form': self.read(cr, uid, ids[0], context=context),
423 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
425 def manual_invoice(self, cr, uid, ids, context=None):
426 """ create invoices for the given sales orders (ids), and open the form
427 view of one of the newly created invoices
429 mod_obj = self.pool.get('ir.model.data')
430 wf_service = netsvc.LocalService("workflow")
432 # create invoices through the sales orders' workflow
433 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
435 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
436 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
437 # determine newly created invoices
438 new_inv_ids = list(inv_ids1 - inv_ids0)
440 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
441 res_id = res and res[1] or False,
444 'name': _('Customer Invoices'),
448 'res_model': 'account.invoice',
449 'context': "{'type':'out_invoice'}",
450 'type': 'ir.actions.act_window',
453 'res_id': new_inv_ids and new_inv_ids[0] or False,
456 def action_view_invoice(self, cr, uid, ids, context=None):
458 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.
460 mod_obj = self.pool.get('ir.model.data')
461 act_obj = self.pool.get('ir.actions.act_window')
463 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
464 id = result and result[1] or False
465 result = act_obj.read(cr, uid, [id], context=context)[0]
466 #compute the number of invoices to display
468 for so in self.browse(cr, uid, ids, context=context):
469 inv_ids += [invoice.id for invoice in so.invoice_ids]
470 #choose the view_mode accordingly
472 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
474 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
475 result['views'] = [(res and res[1] or False, 'form')]
476 result['res_id'] = inv_ids and inv_ids[0] or False
479 def test_no_product(self, cr, uid, order, context):
480 for line in order.order_line:
481 if line.product_id and (line.product_id.type<>'service'):
485 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
487 states = ['confirmed', 'done', 'exception']
491 invoice = self.pool.get('account.invoice')
492 obj_sale_order_line = self.pool.get('sale.order.line')
493 partner_currency = {}
496 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
497 # last day of the last month as invoice date
499 context['date_invoice'] = date_invoice
500 for o in self.browse(cr, uid, ids, context=context):
501 currency_id = o.pricelist_id.currency_id.id
502 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
503 raise osv.except_osv(
505 _('You cannot group sales having different currencies for the same partner.'))
507 partner_currency[o.partner_id.id] = currency_id
509 for line in o.order_line:
512 elif (line.state in states):
513 lines.append(line.id)
514 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
516 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
518 for o in self.browse(cr, uid, ids, context=context):
519 for i in o.invoice_ids:
520 if i.state == 'draft':
522 for val in invoices.values():
524 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
527 invoice_ref += o.name + '|'
528 self.write(cr, uid, [o.id], {'state': 'progress'})
529 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
530 #remove last '|' in invoice_ref
531 if len(invoice_ref) >= 1:
532 invoice_ref = invoice_ref[:-1]
533 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
535 for order, il in val:
536 res = self._make_invoice(cr, uid, order, il, context=context)
537 invoice_ids.append(res)
538 self.write(cr, uid, [order.id], {'state': 'progress'})
539 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
542 def action_invoice_cancel(self, cr, uid, ids, context=None):
543 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
546 def action_invoice_end(self, cr, uid, ids, context=None):
547 for this in self.browse(cr, uid, ids, context=context):
548 for line in this.order_line:
549 if line.state == 'exception':
550 line.write({'state': 'confirmed'})
551 if this.state == 'invoice_except':
552 this.write({'state': 'progress'})
555 def action_cancel(self, cr, uid, ids, context=None):
556 wf_service = netsvc.LocalService("workflow")
559 sale_order_line_obj = self.pool.get('sale.order.line')
560 for sale in self.browse(cr, uid, ids, context=context):
561 for inv in sale.invoice_ids:
562 if inv.state not in ('draft', 'cancel'):
563 raise osv.except_osv(
564 _('Cannot cancel this sales order!'),
565 _('First cancel all invoices attached to this sales order.'))
566 for r in self.read(cr, uid, ids, ['invoice_ids']):
567 for inv in r['invoice_ids']:
568 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
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 wf_service = netsvc.LocalService('workflow')
577 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
579 # redisplay the record as a sales order
580 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
581 view_id = view_ref and view_ref[1] or False,
583 'type': 'ir.actions.act_window',
584 'name': _('Sales Order'),
585 'res_model': 'sale.order',
594 def action_wait(self, cr, uid, ids, context=None):
595 context = context or {}
596 for o in self.browse(cr, uid, ids):
598 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
599 noprod = self.test_no_product(cr, uid, o, context)
600 if (o.order_policy == 'manual') or noprod:
601 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
603 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
604 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
607 def action_quotation_send(self, cr, uid, ids, context=None):
609 This function opens a window to compose an email, with the edi sale template message loaded by default
611 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
612 ir_model_data = self.pool.get('ir.model.data')
614 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
618 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
620 compose_form_id = False
623 'default_model': 'sale.order',
624 'default_res_id': ids[0],
625 'default_use_template': bool(template_id),
626 'default_template_id': template_id,
627 'default_composition_mode': 'comment',
628 'mark_so_as_sent': True
631 'type': 'ir.actions.act_window',
634 'res_model': 'mail.compose.message',
635 'views': [(compose_form_id, 'form')],
636 'view_id': compose_form_id,
641 def action_done(self, cr, uid, ids, context=None):
642 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
646 # TODO add a field price_unit_uos
647 # - update it on change product and unit price
648 # - use it in report if there is a uos
649 class sale_order_line(osv.osv):
651 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
652 tax_obj = self.pool.get('account.tax')
653 cur_obj = self.pool.get('res.currency')
657 for line in self.browse(cr, uid, ids, context=context):
658 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
659 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
660 cur = line.order_id.pricelist_id.currency_id
661 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
664 def _get_uom_id(self, cr, uid, *args):
666 proxy = self.pool.get('ir.model.data')
667 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
669 except Exception, ex:
672 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
673 res = dict.fromkeys(ids, False)
674 for this in self.browse(cr, uid, ids, context=context):
675 res[this.id] = this.invoice_lines and \
676 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
679 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
680 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
681 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
682 sale_order_line sol ON (sol.order_id = rel.order_id)
683 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
684 return [i[0] for i in cr.fetchall()]
686 _name = 'sale.order.line'
687 _description = 'Sales Order Line'
689 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
690 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
691 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
692 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
693 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
694 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
696 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
697 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
698 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
699 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
700 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."),
701 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
702 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
703 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
704 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
705 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
706 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
707 'product_uos': fields.many2one('product.uom', 'Product UoS'),
708 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
709 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
710 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
711 help='* The \'Draft\' status is set when the related sales order in draft status. \
712 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
713 \n* The \'Exception\' status is set when the related sales order is set as exception. \
714 \n* The \'Done\' status is set when the sales order line has been picked. \
715 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
716 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
717 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
718 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
720 _order = 'order_id desc, sequence, id'
722 'product_uom' : _get_uom_id,
724 'product_uom_qty': 1,
725 'product_uos_qty': 1,
728 'type': 'make_to_stock',
732 def _get_line_qty(self, cr, uid, line, context=None):
733 if (line.order_id.invoice_quantity=='order'):
735 return line.product_uos_qty or 0.0
736 return line.product_uom_qty
738 def _get_line_uom(self, cr, uid, line, context=None):
739 if (line.order_id.invoice_quantity=='order'):
741 return line.product_uos.id
742 return line.product_uom.id
744 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
745 """Prepare the dict of values to create the new invoice line for a
746 sales order line. This method may be overridden to implement custom
747 invoice generation (making sure to call super() to establish
748 a clean extension chain).
750 :param browse_record line: sale.order.line record to invoice
751 :param int account_id: optional ID of a G/L account to force
752 (this is used for returning products including service)
753 :return: dict of values to create() the invoice line
756 if not line.invoiced:
759 account_id = line.product_id.property_account_income.id
761 account_id = line.product_id.categ_id.property_account_income_categ.id
763 raise osv.except_osv(_('Error!'),
764 _('Please define income account for this product: "%s" (id:%d).') % \
765 (line.product_id.name, line.product_id.id,))
767 prop = self.pool.get('ir.property').get(cr, uid,
768 'property_account_income_categ', 'product.category',
770 account_id = prop and prop.id or False
771 uosqty = self._get_line_qty(cr, uid, line, context=context)
772 uos_id = self._get_line_uom(cr, uid, line, context=context)
775 pu = round(line.price_unit * line.product_uom_qty / uosqty,
776 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
777 fpos = line.order_id.fiscal_position or False
778 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
780 raise osv.except_osv(_('Error!'),
781 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
784 'sequence': line.sequence,
785 'origin': line.order_id.name,
786 'account_id': account_id,
789 'discount': line.discount,
791 'product_id': line.product_id.id or False,
792 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
793 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
798 def invoice_line_create(self, cr, uid, ids, context=None):
804 for line in self.browse(cr, uid, ids, context=context):
805 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
807 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
808 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
809 sales.add(line.order_id.id)
810 create_ids.append(inv_id)
811 # Trigger workflow events
812 wf_service = netsvc.LocalService("workflow")
813 for sale_id in sales:
814 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
817 def button_cancel(self, cr, uid, ids, context=None):
818 for line in self.browse(cr, uid, ids, context=context):
820 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
821 return self.write(cr, uid, ids, {'state': 'cancel'})
823 def button_confirm(self, cr, uid, ids, context=None):
824 return self.write(cr, uid, ids, {'state': 'confirmed'})
826 def button_done(self, cr, uid, ids, context=None):
827 wf_service = netsvc.LocalService("workflow")
828 res = self.write(cr, uid, ids, {'state': 'done'})
829 for line in self.browse(cr, uid, ids, context=context):
830 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
833 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
834 product_obj = self.pool.get('product.product')
836 return {'value': {'product_uom': product_uos,
837 'product_uom_qty': product_uos_qty}, 'domain': {}}
839 product = product_obj.browse(cr, uid, product_id)
841 'product_uom': product.uom_id.id,
843 # FIXME must depend on uos/uom of the product and not only of the coeff.
846 'product_uom_qty': product_uos_qty / product.uos_coeff,
847 'th_weight': product_uos_qty / product.uos_coeff * product.weight
849 except ZeroDivisionError:
851 return {'value': value}
853 def copy_data(self, cr, uid, id, default=None, context=None):
856 default.update({'state': 'draft', 'invoice_lines': []})
857 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
859 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
860 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
861 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
862 context = context or {}
863 lang = lang or context.get('lang',False)
865 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
867 product_uom_obj = self.pool.get('product.uom')
868 partner_obj = self.pool.get('res.partner')
869 product_obj = self.pool.get('product.product')
870 context = {'lang': lang, 'partner_id': partner_id}
872 lang = partner_obj.browse(cr, uid, partner_id).lang
873 context_partner = {'lang': lang, 'partner_id': partner_id}
876 return {'value': {'th_weight': 0,
877 'product_uos_qty': qty}, 'domain': {'product_uom': [],
880 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
884 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
888 uom2 = product_uom_obj.browse(cr, uid, uom)
889 if product_obj.uom_id.category_id.id != uom2.category_id.id:
892 if product_obj.uos_id:
893 uos2 = product_uom_obj.browse(cr, uid, uos)
894 if product_obj.uos_id.category_id.id != uos2.category_id.id:
898 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
899 if update_tax: #The quantity only have changed
900 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
903 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
904 if product_obj.description_sale:
905 result['name'] += '\n'+product_obj.description_sale
907 if (not uom) and (not uos):
908 result['product_uom'] = product_obj.uom_id.id
909 if product_obj.uos_id:
910 result['product_uos'] = product_obj.uos_id.id
911 result['product_uos_qty'] = qty * product_obj.uos_coeff
912 uos_category_id = product_obj.uos_id.category_id.id
914 result['product_uos'] = False
915 result['product_uos_qty'] = qty
916 uos_category_id = False
917 result['th_weight'] = qty * product_obj.weight
918 domain = {'product_uom':
919 [('category_id', '=', product_obj.uom_id.category_id.id)],
921 [('category_id', '=', uos_category_id)]}
922 elif uos and not uom: # only happens if uom is False
923 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
924 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
925 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
926 elif uom: # whether uos is set or not
927 default_uom = product_obj.uom_id and product_obj.uom_id.id
928 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
929 if product_obj.uos_id:
930 result['product_uos'] = product_obj.uos_id.id
931 result['product_uos_qty'] = qty * product_obj.uos_coeff
933 result['product_uos'] = False
934 result['product_uos_qty'] = qty
935 result['th_weight'] = q * product_obj.weight # Round the quantity up
938 uom2 = product_obj.uom_id
942 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
943 'Please set one before choosing a product.')
944 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
946 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
947 product, qty or 1.0, partner_id, {
948 'uom': uom or result.get('product_uom'),
952 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
953 "You have to change either the product, the quantity or the pricelist.")
955 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
957 result.update({'price_unit': price})
960 'title': _('Configuration Error!'),
961 'message' : warning_msgs
963 return {'value': result, 'domain': domain, 'warning': warning}
965 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
966 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
967 lang=False, update_tax=True, date_order=False, context=None):
968 context = context or {}
969 lang = lang or ('lang' in context and context['lang'])
971 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
972 return self.product_id_change(cursor, user, ids, pricelist, product,
973 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
974 partner_id=partner_id, lang=lang, update_tax=update_tax,
975 date_order=date_order, context=context)
977 def unlink(self, cr, uid, ids, context=None):
980 """Allows to delete sales order lines in draft,cancel states"""
981 for rec in self.browse(cr, uid, ids, context=context):
982 if rec.state not in ['draft', 'cancel']:
983 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
984 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
987 class mail_compose_message(osv.Model):
988 _inherit = 'mail.compose.message'
990 def send_mail(self, cr, uid, ids, context=None):
991 context = context or {}
992 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
993 context = dict(context, mail_post_autofollow=True)
994 wf_service = netsvc.LocalService("workflow")
995 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
996 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
999 class account_invoice(osv.Model):
1000 _inherit = 'account.invoice'
1002 def unlink(self, cr, uid, ids, context=None):
1003 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1004 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1005 #if we can't cancel all invoices, do nothing
1006 if len(invoice_ids) == len(ids):
1007 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1008 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1009 wf_service = netsvc.LocalService("workflow")
1011 wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1012 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1014 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: