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, partner_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
67 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
68 sale_note = self.get_sale_note(cr, uid, ids, partner, shop_id, context=context)
69 v.update({'note': sale_note})
72 def copy(self, cr, uid, id, default=None, context=None):
78 'date_confirm': False,
79 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
81 return super(sale_order, self).copy(cr, uid, id, default, context=context)
83 def _amount_line_tax(self, cr, uid, line, context=None):
85 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']:
86 val += c.get('amount', 0.0)
89 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
90 cur_obj = self.pool.get('res.currency')
92 for order in self.browse(cr, uid, ids, context=context):
94 'amount_untaxed': 0.0,
99 cur = order.pricelist_id.currency_id
100 for line in order.order_line:
101 val1 += line.price_subtotal
102 val += self._amount_line_tax(cr, uid, line, context=context)
103 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
104 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
105 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
109 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
111 for sale in self.browse(cursor, user, ids, context=context):
116 for invoice in sale.invoice_ids:
117 if invoice.state not in ('draft', 'cancel'):
118 tot += invoice.amount_untaxed
120 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
125 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
127 for sale in self.browse(cursor, user, ids, context=context):
133 def _invoiced(self, cursor, user, ids, name, arg, context=None):
135 for sale in self.browse(cursor, user, ids, context=context):
137 invoice_existence = False
138 for invoice in sale.invoice_ids:
139 if invoice.state!='cancel':
140 invoice_existence = True
141 if invoice.state != 'paid':
144 if not invoice_existence or sale.state == 'manual':
148 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
157 clause += 'AND inv.state = \'paid\''
159 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
160 sale_clause = ', sale_order AS sale '
163 cursor.execute('SELECT rel.order_id ' \
164 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
165 'WHERE rel.invoice_id = inv.id ' + clause)
166 res = cursor.fetchall()
168 cursor.execute('SELECT sale.id ' \
169 'FROM sale_order AS sale ' \
170 'WHERE sale.id NOT IN ' \
171 '(SELECT rel.order_id ' \
172 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
173 res.extend(cursor.fetchall())
175 return [('id', '=', 0)]
176 return [('id', 'in', [x[0] for x in res])]
178 def _get_order(self, cr, uid, ids, context=None):
180 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
181 result[line.order_id.id] = True
184 def _get_default_shop(self, cr, uid, context=None):
185 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
186 shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context)
188 raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!'))
192 'name': fields.char('Order Reference', size=64, required=True,
193 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
194 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
195 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
196 'client_order_ref': fields.char('Customer Reference', size=64),
197 'state': fields.selection([
198 ('draft', 'Draft Quotation'),
199 ('sent', 'Quotation Sent'),
200 ('cancel', 'Cancelled'),
201 ('waiting_date', 'Waiting Schedule'),
202 ('progress', 'Sales Order'),
203 ('manual', 'Sale to Invoice'),
204 ('invoice_except', 'Invoice Exception'),
206 ], 'Status', readonly=True, track_visibility='onchange',
207 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),
208 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
209 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
210 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
211 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
212 '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'),
213 '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."),
214 '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."),
215 'order_policy': fields.selection([
216 ('manual', 'On Demand'),
217 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
218 help="""This field controls how invoice and delivery operations are synchronized.
219 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
220 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
221 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
222 '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."),
224 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
225 '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)."),
226 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
227 'invoiced': fields.function(_invoiced, string='Paid',
228 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
229 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
230 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
231 'note': fields.text('Terms and conditions'),
233 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
235 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
236 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
238 multi='sums', help="The amount without tax.", track_visibility='always'),
239 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
241 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
242 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
244 multi='sums', help="The tax amount."),
245 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
247 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
248 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
250 multi='sums', help="The total amount."),
252 '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)]}),
253 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
254 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
255 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
258 'date_order': fields.date.context_today,
259 'order_policy': 'manual',
261 'user_id': lambda obj, cr, uid, context: uid,
262 'name': lambda obj, cr, uid, context: '/',
263 'invoice_quantity': 'order',
264 'shop_id': _get_default_shop,
265 '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'],
266 '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'],
267 'note': lambda self, cr, uid, context: self.pool.get('res.company').browse(cr, uid, uid, context=context).sale_note
270 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
275 def unlink(self, cr, uid, ids, context=None):
276 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
278 for s in sale_orders:
279 if s['state'] in ['draft', 'cancel']:
280 unlink_ids.append(s['id'])
282 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before !'))
284 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
286 def copy_quotation(self, cr, uid, ids, context=None):
287 id = self.copy(cr, uid, ids[0], context=None)
288 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
289 view_id = view_ref and view_ref[1] or False,
291 'type': 'ir.actions.act_window',
292 'name': _('Sales Order'),
293 'res_model': 'sale.order',
302 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
303 context = context or {}
307 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
310 return {'value': value}
312 'title': _('Pricelist Warning!'),
313 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
315 return {'warning': warning, 'value': value}
317 def get_sale_note(self, cr, uid, ids, part, shop_id, context=None):
318 context_lang = context.copy()
319 context_lang.update({'lang': part.lang})
320 sale_note = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context_lang).company_id.sale_note
323 def onchange_partner_id(self, cr, uid, ids, part, shop_id, context=None):
325 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
327 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
328 #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the
329 #invoicing addresses and all the fields related to the partner.
330 if part.parent_id and not part.is_company:
331 part = part.parent_id
332 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
333 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
334 payment_term = part.property_payment_term and part.property_payment_term.id or False
335 fiscal_position = part.property_account_position and part.property_account_position.id or False
336 dedicated_salesman = part.user_id and part.user_id.id or uid
338 'partner_invoice_id': addr['invoice'],
339 'partner_shipping_id': addr['delivery'],
340 'payment_term': payment_term,
341 'fiscal_position': fiscal_position,
342 'user_id': dedicated_salesman,
345 val['pricelist_id'] = pricelist
346 sale_note = self.get_sale_note(cr, uid, ids, part, shop_id, context=context)
347 val.update({'note': sale_note})
348 return {'value': val}
350 def create(self, cr, uid, vals, context=None):
351 if vals.get('name','/')=='/':
352 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
353 return super(sale_order, self).create(cr, uid, vals, context=context)
355 def button_dummy(self, cr, uid, ids, context=None):
358 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
359 # can be removed after 6.1.
360 def _inv_get(self, cr, uid, order, context=None):
363 def _prepare_invoice(self, cr, uid, order, lines, context=None):
364 """Prepare the dict of values to create the new invoice for a
365 sales order. This method may be overridden to implement custom
366 invoice generation (making sure to call super() to establish
367 a clean extension chain).
369 :param browse_record order: sale.order record to invoice
370 :param list(int) line: list of invoice line IDs that must be
371 attached to the invoice
372 :return: dict of value to create() the invoice
376 journal_ids = self.pool.get('account.journal').search(cr, uid,
377 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
380 raise osv.except_osv(_('Error!'),
381 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
383 'name': order.client_order_ref or '',
384 'origin': order.name,
385 'type': 'out_invoice',
386 'reference': order.client_order_ref or order.name,
387 'account_id': order.partner_id.property_account_receivable.id,
388 'partner_id': order.partner_invoice_id.id,
389 'journal_id': journal_ids[0],
390 'invoice_line': [(6, 0, lines)],
391 'currency_id': order.pricelist_id.currency_id.id,
392 'comment': order.note,
393 'payment_term': order.payment_term and order.payment_term.id or False,
394 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
395 'date_invoice': context.get('date_invoice', False),
396 'company_id': order.company_id.id,
397 'user_id': order.user_id and order.user_id.id or False
400 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
401 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
404 def _make_invoice(self, cr, uid, order, lines, context=None):
405 inv_obj = self.pool.get('account.invoice')
406 obj_invoice_line = self.pool.get('account.invoice.line')
409 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
410 from_line_invoice_ids = []
411 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
412 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
413 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
414 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
415 for preinv in order.invoice_ids:
416 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
417 for preline in preinv.invoice_line:
418 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
419 lines.append(inv_line_id)
420 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
421 inv_id = inv_obj.create(cr, uid, inv, context=context)
422 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
423 if data.get('value', False):
424 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
425 inv_obj.button_compute(cr, uid, [inv_id])
428 def print_quotation(self, cr, uid, ids, context=None):
430 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
432 assert len(ids) == 1, 'This option should only be used for a single id at a time'
433 self.signal_quotation_sent(cr, uid, ids)
435 'model': 'sale.order',
437 'form': self.read(cr, uid, ids[0], context=context),
439 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
441 def manual_invoice(self, cr, uid, ids, context=None):
442 """ create invoices for the given sales orders (ids), and open the form
443 view of one of the newly created invoices
445 mod_obj = self.pool.get('ir.model.data')
447 # create invoices through the sales orders' workflow
448 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
449 self.signal_manual_invoice(cr, uid, ids)
450 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
451 # determine newly created invoices
452 new_inv_ids = list(inv_ids1 - inv_ids0)
454 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
455 res_id = res and res[1] or False,
458 'name': _('Customer Invoices'),
462 'res_model': 'account.invoice',
463 'context': "{'type':'out_invoice'}",
464 'type': 'ir.actions.act_window',
467 'res_id': new_inv_ids and new_inv_ids[0] or False,
470 def action_view_invoice(self, cr, uid, ids, context=None):
472 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.
474 mod_obj = self.pool.get('ir.model.data')
475 act_obj = self.pool.get('ir.actions.act_window')
477 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
478 id = result and result[1] or False
479 result = act_obj.read(cr, uid, [id], context=context)[0]
480 #compute the number of invoices to display
482 for so in self.browse(cr, uid, ids, context=context):
483 inv_ids += [invoice.id for invoice in so.invoice_ids]
484 #choose the view_mode accordingly
486 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
488 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
489 result['views'] = [(res and res[1] or False, 'form')]
490 result['res_id'] = inv_ids and inv_ids[0] or False
493 def test_no_product(self, cr, uid, order, context):
494 for line in order.order_line:
495 if line.product_id and (line.product_id.type<>'service'):
499 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
501 states = ['confirmed', 'done', 'exception']
505 invoice = self.pool.get('account.invoice')
506 obj_sale_order_line = self.pool.get('sale.order.line')
507 partner_currency = {}
510 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
511 # last day of the last month as invoice date
513 context['date_invoice'] = date_invoice
514 for o in self.browse(cr, uid, ids, context=context):
515 currency_id = o.pricelist_id.currency_id.id
516 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
517 raise osv.except_osv(
519 _('You cannot group sales having different currencies for the same partner.'))
521 partner_currency[o.partner_id.id] = currency_id
523 for line in o.order_line:
526 elif (line.state in states):
527 lines.append(line.id)
528 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
530 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
532 for o in self.browse(cr, uid, ids, context=context):
533 for i in o.invoice_ids:
534 if i.state == 'draft':
536 for val in invoices.values():
538 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
541 invoice_ref += o.name + '|'
542 self.write(cr, uid, [o.id], {'state': 'progress'})
543 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
544 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
546 for order, il in val:
547 res = self._make_invoice(cr, uid, order, il, context=context)
548 invoice_ids.append(res)
549 self.write(cr, uid, [order.id], {'state': 'progress'})
550 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
553 def action_invoice_cancel(self, cr, uid, ids, context=None):
554 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
557 def action_invoice_end(self, cr, uid, ids, context=None):
558 for this in self.browse(cr, uid, ids, context=context):
559 for line in this.order_line:
560 if line.state == 'exception':
561 line.write({'state': 'confirmed'})
562 if this.state == 'invoice_except':
563 this.write({'state': 'progress'})
566 def action_cancel(self, cr, uid, ids, context=None):
569 sale_order_line_obj = self.pool.get('sale.order.line')
570 account_invoice_obj = self.pool.get('account.invoice')
571 for sale in self.browse(cr, uid, ids, context=context):
572 for inv in sale.invoice_ids:
573 if inv.state not in ('draft', 'cancel'):
574 raise osv.except_osv(
575 _('Cannot cancel this sales order!'),
576 _('First cancel all invoices attached to this sales order.'))
577 for r in self.read(cr, uid, ids, ['invoice_ids']):
578 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
579 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
581 self.write(cr, uid, ids, {'state': 'cancel'})
584 def action_button_confirm(self, cr, uid, ids, context=None):
585 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
586 self.signal_order_confirm(cr, uid, ids)
588 # redisplay the record as a sales order
589 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
590 view_id = view_ref and view_ref[1] or False,
592 'type': 'ir.actions.act_window',
593 'name': _('Sales Order'),
594 'res_model': 'sale.order',
603 def action_wait(self, cr, uid, ids, context=None):
604 context = context or {}
605 for o in self.browse(cr, uid, ids):
607 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
608 noprod = self.test_no_product(cr, uid, o, context)
609 if (o.order_policy == 'manual') or noprod:
610 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
612 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
613 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
616 def action_quotation_send(self, cr, uid, ids, context=None):
618 This function opens a window to compose an email, with the edi sale template message loaded by default
620 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
621 ir_model_data = self.pool.get('ir.model.data')
623 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
627 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
629 compose_form_id = False
632 'default_model': 'sale.order',
633 'default_res_id': ids[0],
634 'default_use_template': bool(template_id),
635 'default_template_id': template_id,
636 'default_composition_mode': 'comment',
637 'mark_so_as_sent': True
640 'type': 'ir.actions.act_window',
643 'res_model': 'mail.compose.message',
644 'views': [(compose_form_id, 'form')],
645 'view_id': compose_form_id,
650 def action_done(self, cr, uid, ids, context=None):
651 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
655 # TODO add a field price_unit_uos
656 # - update it on change product and unit price
657 # - use it in report if there is a uos
658 class sale_order_line(osv.osv):
660 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
661 tax_obj = self.pool.get('account.tax')
662 cur_obj = self.pool.get('res.currency')
666 for line in self.browse(cr, uid, ids, context=context):
667 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
668 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
669 cur = line.order_id.pricelist_id.currency_id
670 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
673 def _get_uom_id(self, cr, uid, *args):
675 proxy = self.pool.get('ir.model.data')
676 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
678 except Exception, ex:
681 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
682 res = dict.fromkeys(ids, False)
683 for this in self.browse(cr, uid, ids, context=context):
684 res[this.id] = this.invoice_lines and \
685 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
688 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
689 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
690 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
691 sale_order_line sol ON (sol.order_id = rel.order_id)
692 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
693 return [i[0] for i in cr.fetchall()]
695 _name = 'sale.order.line'
696 _description = 'Sales Order Line'
698 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
699 'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
700 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
701 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
702 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
703 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
704 store={'account.invoice': (_order_lines_from_invoice, ['state'], 10)}),
705 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
706 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
707 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."),
708 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
709 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
710 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
711 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
712 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
713 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
714 'product_uos': fields.many2one('product.uom', 'Product UoS'),
715 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
716 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
717 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
718 help='* The \'Draft\' status is set when the related sales order in draft status. \
719 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
720 \n* The \'Exception\' status is set when the related sales order is set as exception. \
721 \n* The \'Done\' status is set when the sales order line has been picked. \
722 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
723 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
724 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
725 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
727 _order = 'order_id desc, sequence'
729 'product_uom' : _get_uom_id,
731 'product_uom_qty': 1,
732 'product_uos_qty': 1,
735 'type': 'make_to_stock',
739 def _get_line_qty(self, cr, uid, line, context=None):
740 if (line.order_id.invoice_quantity=='order'):
742 return line.product_uos_qty or 0.0
743 return line.product_uom_qty
745 def _get_line_uom(self, cr, uid, line, context=None):
746 if (line.order_id.invoice_quantity=='order'):
748 return line.product_uos.id
749 return line.product_uom.id
751 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
752 """Prepare the dict of values to create the new invoice line for a
753 sales order line. This method may be overridden to implement custom
754 invoice generation (making sure to call super() to establish
755 a clean extension chain).
757 :param browse_record line: sale.order.line record to invoice
758 :param int account_id: optional ID of a G/L account to force
759 (this is used for returning products including service)
760 :return: dict of values to create() the invoice line
763 if not line.invoiced:
766 account_id = line.product_id.property_account_income.id
768 account_id = line.product_id.categ_id.property_account_income_categ.id
770 raise osv.except_osv(_('Error!'),
771 _('Please define income account for this product: "%s" (id:%d).') % \
772 (line.product_id.name, line.product_id.id,))
774 prop = self.pool.get('ir.property').get(cr, uid,
775 'property_account_income_categ', 'product.category',
777 account_id = prop and prop.id or False
778 uosqty = self._get_line_qty(cr, uid, line, context=context)
779 uos_id = self._get_line_uom(cr, uid, line, context=context)
782 pu = round(line.price_unit * line.product_uom_qty / uosqty,
783 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
784 fpos = line.order_id.fiscal_position or False
785 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
787 raise osv.except_osv(_('Error!'),
788 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
791 'sequence': line.sequence,
792 'origin': line.order_id.name,
793 'account_id': account_id,
796 'discount': line.discount,
798 'product_id': line.product_id.id or False,
799 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
800 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
805 def invoice_line_create(self, cr, uid, ids, context=None):
811 for line in self.browse(cr, uid, ids, context=context):
812 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
814 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
815 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
816 sales.add(line.order_id.id)
817 create_ids.append(inv_id)
818 # Trigger workflow events
819 wf_service = netsvc.LocalService("workflow")
820 for sale_id in sales:
821 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
824 def button_cancel(self, cr, uid, ids, context=None):
825 for line in self.browse(cr, uid, ids, context=context):
827 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
828 return self.write(cr, uid, ids, {'state': 'cancel'})
830 def button_confirm(self, cr, uid, ids, context=None):
831 return self.write(cr, uid, ids, {'state': 'confirmed'})
833 def button_done(self, cr, uid, ids, context=None):
834 wf_service = netsvc.LocalService("workflow")
835 res = self.write(cr, uid, ids, {'state': 'done'})
836 for line in self.browse(cr, uid, ids, context=context):
837 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
840 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
841 product_obj = self.pool.get('product.product')
843 return {'value': {'product_uom': product_uos,
844 'product_uom_qty': product_uos_qty}, 'domain': {}}
846 product = product_obj.browse(cr, uid, product_id)
848 'product_uom': product.uom_id.id,
850 # FIXME must depend on uos/uom of the product and not only of the coeff.
853 'product_uom_qty': product_uos_qty / product.uos_coeff,
854 'th_weight': product_uos_qty / product.uos_coeff * product.weight
856 except ZeroDivisionError:
858 return {'value': value}
860 def copy_data(self, cr, uid, id, default=None, context=None):
863 default.update({'state': 'draft', 'invoice_lines': []})
864 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
866 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
867 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
868 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
869 context = context or {}
870 lang = lang or context.get('lang',False)
872 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
874 product_uom_obj = self.pool.get('product.uom')
875 partner_obj = self.pool.get('res.partner')
876 product_obj = self.pool.get('product.product')
877 context = {'lang': lang, 'partner_id': partner_id}
879 lang = partner_obj.browse(cr, uid, partner_id).lang
880 context_partner = {'lang': lang, 'partner_id': partner_id}
883 return {'value': {'th_weight': 0,
884 'product_uos_qty': qty}, 'domain': {'product_uom': [],
887 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
891 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
895 uom2 = product_uom_obj.browse(cr, uid, uom)
896 if product_obj.uom_id.category_id.id != uom2.category_id.id:
899 if product_obj.uos_id:
900 uos2 = product_uom_obj.browse(cr, uid, uos)
901 if product_obj.uos_id.category_id.id != uos2.category_id.id:
905 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
906 if update_tax: #The quantity only have changed
907 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
910 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
911 if product_obj.description_sale:
912 result['name'] += '\n'+product_obj.description_sale
914 if (not uom) and (not uos):
915 result['product_uom'] = product_obj.uom_id.id
916 if product_obj.uos_id:
917 result['product_uos'] = product_obj.uos_id.id
918 result['product_uos_qty'] = qty * product_obj.uos_coeff
919 uos_category_id = product_obj.uos_id.category_id.id
921 result['product_uos'] = False
922 result['product_uos_qty'] = qty
923 uos_category_id = False
924 result['th_weight'] = qty * product_obj.weight
925 domain = {'product_uom':
926 [('category_id', '=', product_obj.uom_id.category_id.id)],
928 [('category_id', '=', uos_category_id)]}
929 elif uos and not uom: # only happens if uom is False
930 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
931 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
932 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
933 elif uom: # whether uos is set or not
934 default_uom = product_obj.uom_id and product_obj.uom_id.id
935 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
936 if product_obj.uos_id:
937 result['product_uos'] = product_obj.uos_id.id
938 result['product_uos_qty'] = qty * product_obj.uos_coeff
940 result['product_uos'] = False
941 result['product_uos_qty'] = qty
942 result['th_weight'] = q * product_obj.weight # Round the quantity up
945 uom2 = product_obj.uom_id
949 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
950 'Please set one before choosing a product.')
951 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
953 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
954 product, qty or 1.0, partner_id, {
955 'uom': uom or result.get('product_uom'),
959 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
960 "You have to change either the product, the quantity or the pricelist.")
962 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
964 result.update({'price_unit': price})
967 'title': _('Configuration Error!'),
968 'message' : warning_msgs
970 return {'value': result, 'domain': domain, 'warning': warning}
972 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
973 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
974 lang=False, update_tax=True, date_order=False, context=None):
975 context = context or {}
976 lang = lang or ('lang' in context and context['lang'])
978 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
979 return self.product_id_change(cursor, user, ids, pricelist, product,
980 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
981 partner_id=partner_id, lang=lang, update_tax=update_tax,
982 date_order=date_order, context=context)
984 def unlink(self, cr, uid, ids, context=None):
987 """Allows to delete sales order lines in draft,cancel states"""
988 for rec in self.browse(cr, uid, ids, context=context):
989 if rec.state not in ['draft', 'cancel']:
990 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
991 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
993 class res_company(osv.Model):
994 _inherit = "res.company"
996 'sale_note': fields.text('sales_note', translate=True),
999 class mail_compose_message(osv.Model):
1000 _inherit = 'mail.compose.message'
1002 def send_mail(self, cr, uid, ids, context=None):
1003 context = context or {}
1004 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1005 context = dict(context, mail_post_autofollow=True)
1006 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1007 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1009 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: