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"
53 def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
56 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
57 if shop.project_id.id:
58 v['project_id'] = shop.project_id.id
59 if shop.pricelist_id.id:
60 v['pricelist_id'] = shop.pricelist_id.id
63 def copy(self, cr, uid, id, default=None, context=None):
69 'date_confirm': False,
70 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
72 return super(sale_order, self).copy(cr, uid, id, default, context=context)
74 def _amount_line_tax(self, cr, uid, line, context=None):
76 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']:
77 val += c.get('amount', 0.0)
80 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
81 cur_obj = self.pool.get('res.currency')
83 for order in self.browse(cr, uid, ids, context=context):
85 'amount_untaxed': 0.0,
90 cur = order.pricelist_id.currency_id
91 for line in order.order_line:
92 val1 += line.price_subtotal
93 val += self._amount_line_tax(cr, uid, line, context=context)
94 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
95 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
96 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
100 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
102 for sale in self.browse(cursor, user, ids, context=context):
107 for invoice in sale.invoice_ids:
108 if invoice.state not in ('draft', 'cancel'):
109 tot += invoice.amount_untaxed
111 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
116 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
118 for sale in self.browse(cursor, user, ids, context=context):
124 def _invoiced(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
128 invoice_existence = False
129 for invoice in sale.invoice_ids:
130 if invoice.state!='cancel':
131 invoice_existence = True
132 if invoice.state != 'paid':
135 if not invoice_existence or sale.state == 'manual':
139 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
148 clause += 'AND inv.state = \'paid\''
150 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
151 sale_clause = ', sale_order AS sale '
154 cursor.execute('SELECT rel.order_id ' \
155 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
156 'WHERE rel.invoice_id = inv.id ' + clause)
157 res = cursor.fetchall()
159 cursor.execute('SELECT sale.id ' \
160 'FROM sale_order AS sale ' \
161 'WHERE sale.id NOT IN ' \
162 '(SELECT rel.order_id ' \
163 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
164 res.extend(cursor.fetchall())
166 return [('id', '=', 0)]
167 return [('id', 'in', [x[0] for x in res])]
169 def _get_order(self, cr, uid, ids, context=None):
171 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
172 result[line.order_id.id] = True
176 'name': fields.char('Order Reference', size=64, required=True,
177 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
178 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
180 'client_order_ref': fields.char('Customer Reference', size=64),
181 'state': fields.selection([
182 ('draft', 'Draft Quotation'),
183 ('sent', 'Quotation Sent'),
184 ('cancel', 'Cancelled'),
185 ('waiting_date', 'Waiting Schedule'),
186 ('progress', 'Sale Order'),
187 ('manual', 'Sale to Invoice'),
188 ('invoice_except', 'Invoice Exception'),
190 ], 'Status', readonly=True, tracked=True, 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 sale 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),
191 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
193 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
194 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
195 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
196 '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."),
197 '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."),
198 'order_policy': fields.selection([
199 ('manual', 'On Demand'),
200 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
201 help="""This field controls how invoice and delivery operations are synchronized.
202 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
203 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
204 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
205 '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."),
207 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
208 '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)."),
209 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
210 'invoiced': fields.function(_invoiced, string='Paid',
211 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
212 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
213 fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."),
214 'note': fields.text('Terms and conditions'),
216 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
218 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
219 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
221 multi='sums', help="The amount without tax."),
222 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
224 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
227 multi='sums', help="The tax amount."),
228 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
230 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
231 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
233 multi='sums', help="The total amount."),
235 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
236 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
237 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
238 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
241 'date_order': fields.date.context_today,
242 'order_policy': 'manual',
244 'user_id': lambda obj, cr, uid, context: uid,
245 'name': lambda obj, cr, uid, context: '/',
246 'invoice_quantity': 'order',
247 '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'],
248 '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'],
251 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
256 def unlink(self, cr, uid, ids, context=None):
257 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
259 for s in sale_orders:
260 if s['state'] in ['draft', 'cancel']:
261 unlink_ids.append(s['id'])
263 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
265 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
267 def copy_quotation(self, cr, uid, ids, context=None):
268 id = self.copy(cr, uid, ids[0], context=None)
269 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
270 view_id = view_ref and view_ref[1] or False,
272 'type': 'ir.actions.act_window',
273 'name': _('Sales Order'),
274 'res_model': 'sale.order',
283 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
284 context = context or {}
288 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
291 return {'value': value}
293 'title': _('Pricelist Warning!'),
294 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
296 return {'warning': warning, 'value': value}
298 def onchange_partner_id(self, cr, uid, ids, part, context=None):
300 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
302 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
303 #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the
304 #invoicing addresses and all the fields related to the partner.
305 if part.parent_id and not part.is_company:
306 part = part.parent_id
307 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
308 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
309 payment_term = part.property_payment_term and part.property_payment_term.id or False
310 fiscal_position = part.property_account_position and part.property_account_position.id or False
311 dedicated_salesman = part.user_id and part.user_id.id or uid
313 'partner_invoice_id': addr['invoice'],
314 'partner_shipping_id': addr['delivery'],
315 'payment_term': payment_term,
316 'fiscal_position': fiscal_position,
317 'user_id': dedicated_salesman,
320 val['pricelist_id'] = pricelist
321 return {'value': val}
323 def create(self, cr, uid, vals, context=None):
324 if vals.get('name','/')=='/':
325 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
326 order = super(sale_order, self).create(cr, uid, vals, context=context)
328 self.create_send_note(cr, uid, [order], context=context)
331 def button_dummy(self, cr, uid, ids, context=None):
334 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
335 # can be removed after 6.1.
336 def _inv_get(self, cr, uid, order, context=None):
339 def _prepare_invoice(self, cr, uid, order, lines, context=None):
340 """Prepare the dict of values to create the new invoice for a
341 sale order. This method may be overridden to implement custom
342 invoice generation (making sure to call super() to establish
343 a clean extension chain).
345 :param browse_record order: sale.order record to invoice
346 :param list(int) line: list of invoice line IDs that must be
347 attached to the invoice
348 :return: dict of value to create() the invoice
352 journal_ids = self.pool.get('account.journal').search(cr, uid,
353 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
356 raise osv.except_osv(_('Error!'),
357 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
359 'name': order.client_order_ref or '',
360 'origin': order.name,
361 'type': 'out_invoice',
362 'reference': order.client_order_ref or order.name,
363 'account_id': order.partner_id.property_account_receivable.id,
364 'partner_id': order.partner_invoice_id.id,
365 'journal_id': journal_ids[0],
366 'invoice_line': [(6, 0, lines)],
367 'currency_id': order.pricelist_id.currency_id.id,
368 'comment': order.note,
369 'payment_term': order.payment_term and order.payment_term.id or False,
370 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
371 'date_invoice': context.get('date_invoice', False),
372 'company_id': order.company_id.id,
373 'user_id': order.user_id and order.user_id.id or False
376 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
377 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
380 def _make_invoice(self, cr, uid, order, lines, context=None):
381 inv_obj = self.pool.get('account.invoice')
382 obj_invoice_line = self.pool.get('account.invoice.line')
385 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
386 from_line_invoice_ids = []
387 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
388 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
389 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
390 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
391 for preinv in order.invoice_ids:
392 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
393 for preline in preinv.invoice_line:
394 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
395 lines.append(inv_line_id)
396 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
397 inv_id = inv_obj.create(cr, uid, inv, context=context)
398 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
399 if data.get('value', False):
400 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
401 inv_obj.button_compute(cr, uid, [inv_id])
404 def print_quotation(self, cr, uid, ids, context=None):
406 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
408 assert len(ids) == 1, 'This option should only be used for a single id at a time'
409 wf_service = netsvc.LocalService("workflow")
410 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
412 'model': 'sale.order',
414 'form': self.read(cr, uid, ids[0], context=context),
416 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
418 def manual_invoice(self, cr, uid, ids, context=None):
419 """ create invoices for the given sale orders (ids), and open the form
420 view of one of the newly created invoices
422 mod_obj = self.pool.get('ir.model.data')
423 wf_service = netsvc.LocalService("workflow")
425 # create invoices through the sale orders' workflow
426 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
428 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
429 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
430 # determine newly created invoices
431 new_inv_ids = list(inv_ids1 - inv_ids0)
433 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
434 res_id = res and res[1] or False,
437 'name': _('Customer Invoices'),
441 'res_model': 'account.invoice',
442 'context': "{'type':'out_invoice'}",
443 'type': 'ir.actions.act_window',
446 'res_id': new_inv_ids and new_inv_ids[0] or False,
449 def action_view_invoice(self, cr, uid, ids, context=None):
451 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
453 mod_obj = self.pool.get('ir.model.data')
454 act_obj = self.pool.get('ir.actions.act_window')
456 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
457 id = result and result[1] or False
458 result = act_obj.read(cr, uid, [id], context=context)[0]
459 #compute the number of invoices to display
461 for so in self.browse(cr, uid, ids, context=context):
462 inv_ids += [invoice.id for invoice in so.invoice_ids]
463 #choose the view_mode accordingly
465 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
467 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
468 result['views'] = [(res and res[1] or False, 'form')]
469 result['res_id'] = inv_ids and inv_ids[0] or False
472 def test_no_product(self, cr, uid, order, context):
473 for line in order.order_line:
474 if line.product_id and (line.product_id.type<>'service'):
478 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
480 states = ['confirmed', 'done', 'exception']
484 invoice = self.pool.get('account.invoice')
485 obj_sale_order_line = self.pool.get('sale.order.line')
486 partner_currency = {}
489 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
490 # last day of the last month as invoice date
492 context['date_invoice'] = date_invoice
493 for o in self.browse(cr, uid, ids, context=context):
494 currency_id = o.pricelist_id.currency_id.id
495 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
496 raise osv.except_osv(
498 _('You cannot group sales having different currencies for the same partner.'))
500 partner_currency[o.partner_id.id] = currency_id
502 for line in o.order_line:
505 elif (line.state in states):
506 lines.append(line.id)
507 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
509 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
511 for o in self.browse(cr, uid, ids, context=context):
512 for i in o.invoice_ids:
513 if i.state == 'draft':
515 for val in invoices.values():
517 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
520 invoice_ref += o.name + '|'
521 self.write(cr, uid, [o.id], {'state': 'progress'})
522 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
523 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
525 for order, il in val:
526 res = self._make_invoice(cr, uid, order, il, context=context)
527 invoice_ids.append(res)
528 self.write(cr, uid, [order.id], {'state': 'progress'})
529 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
531 self.invoice_send_note(cr, uid, ids, res, context)
534 def action_invoice_cancel(self, cr, uid, ids, context=None):
535 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
538 def action_invoice_end(self, cr, uid, ids, context=None):
539 for this in self.browse(cr, uid, ids, context=context):
540 for line in this.order_line:
541 if line.state == 'exception':
542 line.write({'state': 'confirmed'})
543 if this.state == 'invoice_except':
544 this.write({'state': 'progress'})
545 this.invoice_paid_send_note()
548 def action_cancel(self, cr, uid, ids, context=None):
549 wf_service = netsvc.LocalService("workflow")
552 sale_order_line_obj = self.pool.get('sale.order.line')
553 for sale in self.browse(cr, uid, ids, context=context):
554 for inv in sale.invoice_ids:
555 if inv.state not in ('draft', 'cancel'):
556 raise osv.except_osv(
557 _('Cannot cancel this sales order!'),
558 _('First cancel all invoices attached to this sales order.'))
559 for r in self.read(cr, uid, ids, ['invoice_ids']):
560 for inv in r['invoice_ids']:
561 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
562 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
564 self.cancel_send_note(cr, uid, [sale.id], context=None)
565 self.write(cr, uid, ids, {'state': 'cancel'})
568 def action_button_confirm(self, cr, uid, ids, context=None):
569 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
570 wf_service = netsvc.LocalService('workflow')
571 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
573 # redisplay the record as a sale order
574 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
575 view_id = view_ref and view_ref[1] or False,
577 'type': 'ir.actions.act_window',
578 'name': _('Sales Order'),
579 'res_model': 'sale.order',
588 def action_wait(self, cr, uid, ids, context=None):
589 context = context or {}
590 for o in self.browse(cr, uid, ids):
592 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
593 noprod = self.test_no_product(cr, uid, o, context)
594 if (o.order_policy == 'manual') or noprod:
595 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
597 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
598 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
599 self.confirm_send_note(cr, uid, ids, context)
602 def action_quotation_send(self, cr, uid, ids, context=None):
604 This function opens a window to compose an email, with the edi sale template message loaded by default
606 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
607 ir_model_data = self.pool.get('ir.model.data')
609 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
613 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
615 compose_form_id = False
618 'default_model': 'sale.order',
619 'default_res_id': ids[0],
620 'default_use_template': bool(template_id),
621 'default_template_id': template_id,
622 'default_composition_mode': 'comment',
623 'mark_so_as_sent': True
626 'type': 'ir.actions.act_window',
629 'res_model': 'mail.compose.message',
630 'views': [(compose_form_id, 'form')],
631 'view_id': compose_form_id,
636 def action_done(self, cr, uid, ids, context=None):
637 self.done_send_note(cr, uid, ids, context=context)
638 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
640 # ------------------------------------------------
641 # OpenChatter methods and notifications
642 # ------------------------------------------------
644 def needaction_domain_get(self, cr, uid, ids, context=None):
645 return [('state', '=', 'draft'), ('user_id','=',uid)]
647 def create_send_note(self, cr, uid, ids, context=None):
648 for obj in self.browse(cr, uid, ids, context=context):
649 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
651 def confirm_send_note(self, cr, uid, ids, context=None):
652 for obj in self.browse(cr, uid, ids, context=context):
653 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
655 def cancel_send_note(self, cr, uid, ids, context=None):
656 for obj in self.browse(cr, uid, ids, context=context):
657 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
659 def done_send_note(self, cr, uid, ids, context=None):
660 for obj in self.browse(cr, uid, ids, context=context):
661 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> set to <b>Done</b>") % (obj.partner_id.name), context=context)
663 def invoice_paid_send_note(self, cr, uid, ids, context=None):
664 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
666 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
667 for order in self.browse(cr, uid, ids, context=context):
668 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
669 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
673 # TODO add a field price_unit_uos
674 # - update it on change product and unit price
675 # - use it in report if there is a uos
676 class sale_order_line(osv.osv):
678 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
679 tax_obj = self.pool.get('account.tax')
680 cur_obj = self.pool.get('res.currency')
684 for line in self.browse(cr, uid, ids, context=context):
685 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
686 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
687 cur = line.order_id.pricelist_id.currency_id
688 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
691 def _get_uom_id(self, cr, uid, *args):
693 proxy = self.pool.get('ir.model.data')
694 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
696 except Exception, ex:
699 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
700 res = dict.fromkeys(ids, False)
701 for this in self.browse(cr, uid, ids, context=context):
702 res[this.id] = this.invoice_lines and \
703 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
706 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
707 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
708 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
709 sale_order_line sol ON (sol.order_id = rel.order_id)
710 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
711 return [i[0] for i in cr.fetchall()]
713 _name = 'sale.order.line'
714 _description = 'Sales Order Line'
716 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
717 'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
718 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
719 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
720 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
721 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
722 store={'account.invoice': (_order_lines_from_invoice, ['state'], 10)}),
723 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
724 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
725 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."),
726 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
727 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
728 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
729 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
730 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
731 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
732 'product_uos': fields.many2one('product.uom', 'Product UoS'),
733 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
734 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
735 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
736 help='* The \'Draft\' status is set when the related sales order in draft status. \
737 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
738 \n* The \'Exception\' status is set when the related sales order is set as exception. \
739 \n* The \'Done\' status is set when the sales order line has been picked. \
740 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
741 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
742 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
743 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
745 _order = 'order_id desc, sequence'
747 'product_uom' : _get_uom_id,
749 'product_uom_qty': 1,
750 'product_uos_qty': 1,
753 'type': 'make_to_stock',
757 def _get_line_qty(self, cr, uid, line, context=None):
758 if (line.order_id.invoice_quantity=='order'):
760 return line.product_uos_qty or 0.0
761 return line.product_uom_qty
763 def _get_line_uom(self, cr, uid, line, context=None):
764 if (line.order_id.invoice_quantity=='order'):
766 return line.product_uos.id
767 return line.product_uom.id
769 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
770 """Prepare the dict of values to create the new invoice line for a
771 sale order line. This method may be overridden to implement custom
772 invoice generation (making sure to call super() to establish
773 a clean extension chain).
775 :param browse_record line: sale.order.line record to invoice
776 :param int account_id: optional ID of a G/L account to force
777 (this is used for returning products including service)
778 :return: dict of values to create() the invoice line
781 if not line.invoiced:
784 account_id = line.product_id.product_tmpl_id.property_account_income.id
786 account_id = line.product_id.categ_id.property_account_income_categ.id
788 raise osv.except_osv(_('Error!'),
789 _('Please define income account for this product: "%s" (id:%d).') % \
790 (line.product_id.name, line.product_id.id,))
792 prop = self.pool.get('ir.property').get(cr, uid,
793 'property_account_income_categ', 'product.category',
795 account_id = prop and prop.id or False
796 uosqty = self._get_line_qty(cr, uid, line, context=context)
797 uos_id = self._get_line_uom(cr, uid, line, context=context)
800 pu = round(line.price_unit * line.product_uom_qty / uosqty,
801 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
802 fpos = line.order_id.fiscal_position or False
803 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
805 raise osv.except_osv(_('Error!'),
806 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
809 'sequence': line.sequence,
810 'origin': line.order_id.name,
811 'account_id': account_id,
814 'discount': line.discount,
816 'product_id': line.product_id.id or False,
817 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
818 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
823 def invoice_line_create(self, cr, uid, ids, context=None):
829 for line in self.browse(cr, uid, ids, context=context):
830 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
832 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
833 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
834 sales.add(line.order_id.id)
835 create_ids.append(inv_id)
836 # Trigger workflow events
837 wf_service = netsvc.LocalService("workflow")
838 for sale_id in sales:
839 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
842 def button_cancel(self, cr, uid, ids, context=None):
843 for line in self.browse(cr, uid, ids, context=context):
845 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
846 return self.write(cr, uid, ids, {'state': 'cancel'})
848 def button_confirm(self, cr, uid, ids, context=None):
849 return self.write(cr, uid, ids, {'state': 'confirmed'})
851 def button_done(self, cr, uid, ids, context=None):
852 wf_service = netsvc.LocalService("workflow")
853 res = self.write(cr, uid, ids, {'state': 'done'})
854 for line in self.browse(cr, uid, ids, context=context):
855 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
858 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
859 product_obj = self.pool.get('product.product')
861 return {'value': {'product_uom': product_uos,
862 'product_uom_qty': product_uos_qty}, 'domain': {}}
864 product = product_obj.browse(cr, uid, product_id)
866 'product_uom': product.uom_id.id,
868 # FIXME must depend on uos/uom of the product and not only of the coeff.
871 'product_uom_qty': product_uos_qty / product.uos_coeff,
872 'th_weight': product_uos_qty / product.uos_coeff * product.weight
874 except ZeroDivisionError:
876 return {'value': value}
878 def copy_data(self, cr, uid, id, default=None, context=None):
881 default.update({'state': 'draft', 'invoice_lines': []})
882 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
884 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
885 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
886 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
887 context = context or {}
888 lang = lang or context.get('lang',False)
890 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
892 product_uom_obj = self.pool.get('product.uom')
893 partner_obj = self.pool.get('res.partner')
894 product_obj = self.pool.get('product.product')
895 context = {'lang': lang, 'partner_id': partner_id}
897 lang = partner_obj.browse(cr, uid, partner_id).lang
898 context_partner = {'lang': lang, 'partner_id': partner_id}
901 return {'value': {'th_weight': 0,
902 'product_uos_qty': qty}, 'domain': {'product_uom': [],
905 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
909 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
913 uom2 = product_uom_obj.browse(cr, uid, uom)
914 if product_obj.uom_id.category_id.id != uom2.category_id.id:
917 if product_obj.uos_id:
918 uos2 = product_uom_obj.browse(cr, uid, uos)
919 if product_obj.uos_id.category_id.id != uos2.category_id.id:
923 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
924 if update_tax: #The quantity only have changed
925 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
928 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
929 if product_obj.description_sale:
930 result['name'] += '\n'+product_obj.description_sale
932 if (not uom) and (not uos):
933 result['product_uom'] = product_obj.uom_id.id
934 if product_obj.uos_id:
935 result['product_uos'] = product_obj.uos_id.id
936 result['product_uos_qty'] = qty * product_obj.uos_coeff
937 uos_category_id = product_obj.uos_id.category_id.id
939 result['product_uos'] = False
940 result['product_uos_qty'] = qty
941 uos_category_id = False
942 result['th_weight'] = qty * product_obj.weight
943 domain = {'product_uom':
944 [('category_id', '=', product_obj.uom_id.category_id.id)],
946 [('category_id', '=', uos_category_id)]}
947 elif uos and not uom: # only happens if uom is False
948 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
949 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
950 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
951 elif uom: # whether uos is set or not
952 default_uom = product_obj.uom_id and product_obj.uom_id.id
953 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
954 result['product_uom'] = default_uom
955 if product_obj.uos_id:
956 result['product_uos'] = product_obj.uos_id.id
957 result['product_uos_qty'] = qty * product_obj.uos_coeff
959 result['product_uos'] = False
960 result['product_uos_qty'] = qty
961 result['th_weight'] = q * product_obj.weight # Round the quantity up
964 uom2 = product_obj.uom_id
968 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
969 'Please set one before choosing a product.')
970 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
972 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
973 product, qty or 1.0, partner_id, {
974 'uom': uom or result.get('product_uom'),
978 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
979 "You have to change either the product, the quantity or the pricelist.")
981 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
983 result.update({'price_unit': price})
986 'title': _('Configuration Error!'),
987 'message' : warning_msgs
989 return {'value': result, 'domain': domain, 'warning': warning}
991 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
992 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
993 lang=False, update_tax=True, date_order=False, context=None):
994 context = context or {}
995 lang = lang or ('lang' in context and context['lang'])
997 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
998 return self.product_id_change(cursor, user, ids, pricelist, product,
999 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1000 partner_id=partner_id, lang=lang, update_tax=update_tax,
1001 date_order=date_order, context=context)
1003 def unlink(self, cr, uid, ids, context=None):
1006 """Allows to delete sales order lines in draft,cancel states"""
1007 for rec in self.browse(cr, uid, ids, context=context):
1008 if rec.state not in ['draft', 'cancel']:
1009 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1010 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1013 class mail_compose_message(osv.osv):
1014 _inherit = 'mail.compose.message'
1015 def send_mail(self, cr, uid, ids, context=None):
1016 context = context or {}
1017 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1018 wf_service = netsvc.LocalService("workflow")
1019 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1020 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1022 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: