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
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
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 copy(self, cr, uid, id, default=None, context=None):
59 'date_confirm': False,
60 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
62 return super(sale_order, self).copy(cr, uid, id, default, context=context)
64 def _amount_line_tax(self, cr, uid, line, context=None):
66 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']:
67 val += c.get('amount', 0.0)
70 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
71 cur_obj = self.pool.get('res.currency')
73 for order in self.browse(cr, uid, ids, context=context):
75 'amount_untaxed': 0.0,
80 cur = order.pricelist_id.currency_id
81 for line in order.order_line:
82 val1 += line.price_subtotal
83 val += self._amount_line_tax(cr, uid, line, context=context)
84 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
85 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
86 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
90 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
92 for sale in self.browse(cursor, user, ids, context=context):
97 for invoice in sale.invoice_ids:
98 if invoice.state not in ('draft', 'cancel'):
99 tot += invoice.amount_untaxed
101 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
106 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
108 for sale in self.browse(cursor, user, ids, context=context):
114 def _invoiced(self, cursor, user, ids, name, arg, context=None):
116 for sale in self.browse(cursor, user, ids, context=context):
118 invoice_existence = False
119 for invoice in sale.invoice_ids:
120 if invoice.state!='cancel':
121 invoice_existence = True
122 if invoice.state != 'paid':
125 if not invoice_existence or sale.state == 'manual':
129 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
138 clause += 'AND inv.state = \'paid\''
140 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
141 sale_clause = ', sale_order AS sale '
144 cursor.execute('SELECT rel.order_id ' \
145 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
146 'WHERE rel.invoice_id = inv.id ' + clause)
147 res = cursor.fetchall()
149 cursor.execute('SELECT sale.id ' \
150 'FROM sale_order AS sale ' \
151 'WHERE sale.id NOT IN ' \
152 '(SELECT rel.order_id ' \
153 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
154 res.extend(cursor.fetchall())
156 return [('id', '=', 0)]
157 return [('id', 'in', [x[0] for x in res])]
159 def _get_order(self, cr, uid, ids, context=None):
161 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
162 result[line.order_id.id] = True
166 'name': fields.char('Order Reference', size=64, required=True,
167 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
168 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
169 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
170 'client_order_ref': fields.char('Customer Reference', size=64),
171 'state': fields.selection([
172 ('draft', 'Draft Quotation'),
173 ('sent', 'Quotation Sent'),
174 ('cancel', 'Cancelled'),
175 ('waiting_date', 'Waiting Schedule'),
176 ('progress', 'Sale Order'),
177 ('manual', 'Sale to Invoice'),
178 ('invoice_except', 'Invoice Exception'),
180 ], 'Status', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
181 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
182 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
183 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
184 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
185 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
186 '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."),
187 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."),
188 'order_policy': fields.selection([
189 ('manual', 'On Demand'),
190 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
191 help="""This field controls how invoice and delivery operations are synchronized.
192 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
193 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
194 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
196 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
197 '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)."),
198 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
199 'invoiced': fields.function(_invoiced, string='Paid',
200 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
201 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
202 fnct_search=_invoiced_search, type='boolean', help="It indicates that sale order has at least one invoice."),
203 'note': fields.text('Terms and conditions'),
205 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Untaxed Amount',
207 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
208 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
210 multi='sums', help="The amount without tax."),
211 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Taxes',
213 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
214 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
216 multi='sums', help="The tax amount."),
217 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Account'), string='Total',
219 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
220 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
222 multi='sums', help="The total amount."),
224 '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)]}),
225 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
226 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
227 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
230 'date_order': fields.date.context_today,
231 'order_policy': 'manual',
233 'user_id': lambda obj, cr, uid, context: uid,
234 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
235 'invoice_quantity': 'order',
236 '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'],
237 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
240 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
245 def unlink(self, cr, uid, ids, context=None):
246 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
248 for s in sale_orders:
249 if s['state'] in ['draft', 'cancel']:
250 unlink_ids.append(s['id'])
252 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
254 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
256 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
257 if (not pricelist_id) or (not order_lines):
260 'title': _('Pricelist Warning!'),
261 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
263 return {'warning': warning}
266 def onchange_partner_id(self, cr, uid, ids, part):
268 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
270 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
271 part = self.pool.get('res.partner').browse(cr, uid, part)
272 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
273 payment_term = part.property_payment_term and part.property_payment_term.id or False
274 fiscal_position = part.property_account_position and part.property_account_position.id or False
275 dedicated_salesman = part.user_id and part.user_id.id or uid
277 'partner_invoice_id': addr['invoice'],
278 'partner_shipping_id': addr['delivery'],
279 'payment_term': payment_term,
280 'fiscal_position': fiscal_position,
281 'user_id': dedicated_salesman,
284 val['pricelist_id'] = pricelist
285 return {'value': val}
287 def create(self, cr, uid, vals, context=None):
288 order = super(sale_order, self).create(cr, uid, vals, context=context)
289 record = self.browse(cr, uid, order, context=context)
290 if record.section_id:
291 followers = [follow.id for follow in record.section_id.message_follower_ids]
292 self.message_subscribe(cr, uid, [record.id],followers, context=context)
294 self.create_send_note(cr, uid, [order], context=context)
297 def button_dummy(self, cr, uid, ids, context=None):
300 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
301 # can be removed after 6.1.
302 def _inv_get(self, cr, uid, order, context=None):
305 def _prepare_invoice(self, cr, uid, order, lines, context=None):
306 """Prepare the dict of values to create the new invoice for a
307 sale order. This method may be overridden to implement custom
308 invoice generation (making sure to call super() to establish
309 a clean extension chain).
311 :param browse_record order: sale.order record to invoice
312 :param list(int) line: list of invoice line IDs that must be
313 attached to the invoice
314 :return: dict of value to create() the invoice
318 journal_ids = self.pool.get('account.journal').search(cr, uid,
319 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
322 raise osv.except_osv(_('Error!'),
323 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
325 'name': order.client_order_ref or '',
326 'origin': order.name,
327 'type': 'out_invoice',
328 'reference': order.client_order_ref or order.name,
329 'account_id': order.partner_id.property_account_receivable.id,
330 'partner_id': order.partner_id.id,
331 'journal_id': journal_ids[0],
332 'invoice_line': [(6, 0, lines)],
333 'currency_id': order.pricelist_id.currency_id.id,
334 'comment': order.note,
335 'payment_term': order.payment_term and order.payment_term.id or False,
336 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
337 'date_invoice': context.get('date_invoice', False),
338 'company_id': order.company_id.id,
339 'user_id': order.user_id and order.user_id.id or False
342 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
343 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
346 def _make_invoice(self, cr, uid, order, lines, context=None):
347 inv_obj = self.pool.get('account.invoice')
348 obj_invoice_line = self.pool.get('account.invoice.line')
351 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
352 from_line_invoice_ids = []
353 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
354 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
355 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
356 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
357 for preinv in order.invoice_ids:
358 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
359 for preline in preinv.invoice_line:
360 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
361 lines.append(inv_line_id)
362 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
363 inv_id = inv_obj.create(cr, uid, inv, context=context)
364 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
365 if data.get('value', False):
366 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
367 inv_obj.button_compute(cr, uid, [inv_id])
370 def print_quotation(self, cr, uid, ids, context=None):
372 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
374 assert len(ids) == 1, 'This option should only be used for a single id at a time'
375 wf_service = netsvc.LocalService("workflow")
376 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
378 'model': 'sale.order',
380 'form': self.read(cr, uid, ids[0], context=context),
382 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
384 def manual_invoice(self, cr, uid, ids, context=None):
385 """ create invoices for the given sale orders (ids), and open the form
386 view of one of the newly created invoices
388 mod_obj = self.pool.get('ir.model.data')
389 wf_service = netsvc.LocalService("workflow")
391 # create invoices through the sale orders' workflow
392 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
394 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
395 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
396 # determine newly created invoices
397 new_inv_ids = list(inv_ids1 - inv_ids0)
399 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
400 res_id = res and res[1] or False,
403 'name': _('Customer Invoices'),
407 'res_model': 'account.invoice',
408 'context': "{'type':'out_invoice'}",
409 'type': 'ir.actions.act_window',
412 'res_id': new_inv_ids and new_inv_ids[0] or False,
415 def action_view_invoice(self, cr, uid, ids, context=None):
417 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.
419 mod_obj = self.pool.get('ir.model.data')
420 act_obj = self.pool.get('ir.actions.act_window')
422 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
423 id = result and result[1] or False
424 result = act_obj.read(cr, uid, [id], context=context)[0]
425 #compute the number of invoices to display
427 for so in self.browse(cr, uid, ids, context=context):
428 inv_ids += [invoice.id for invoice in so.invoice_ids]
429 #choose the view_mode accordingly
431 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
433 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
434 result['views'] = [(res and res[1] or False, 'form')]
435 result['res_id'] = inv_ids and inv_ids[0] or False
438 def test_no_product(self, cr, uid, order, context):
439 for line in order.order_line:
440 if line.product_id and (line.product_id.type<>'service'):
444 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
448 invoice = self.pool.get('account.invoice')
449 obj_sale_order_line = self.pool.get('sale.order.line')
450 partner_currency = {}
453 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
454 # last day of the last month as invoice date
456 context['date_inv'] = date_inv
457 for o in self.browse(cr, uid, ids, context=context):
458 currency_id = o.pricelist_id.currency_id.id
459 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
460 raise osv.except_osv(
462 _('You cannot group sales having different currencies for the same partner.'))
464 partner_currency[o.partner_id.id] = currency_id
466 for line in o.order_line:
469 elif (line.state in states):
470 lines.append(line.id)
471 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
473 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
475 for o in self.browse(cr, uid, ids, context=context):
476 for i in o.invoice_ids:
477 if i.state == 'draft':
479 for val in invoices.values():
481 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
484 invoice_ref += o.name + '|'
485 self.write(cr, uid, [o.id], {'state': 'progress'})
486 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
487 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
489 for order, il in val:
490 res = self._make_invoice(cr, uid, order, il, context=context)
491 invoice_ids.append(res)
492 self.write(cr, uid, [order.id], {'state': 'progress'})
493 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
495 self.invoice_send_note(cr, uid, ids, res, context)
498 def action_invoice_cancel(self, cr, uid, ids, context=None):
501 for sale in self.browse(cr, uid, ids, context=context):
502 for line in sale.order_line:
504 # Check if the line is invoiced (has asociated invoice
505 # lines from non-cancelled invoices).
508 for iline in line.invoice_lines:
509 if iline.invoice_id and iline.invoice_id.state != 'cancel':
512 # Update the line (only when needed)
513 if line.invoiced != invoiced:
514 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
515 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
518 def action_invoice_end(self, cr, uid, ids, context=None):
519 for order in self.browse(cr, uid, ids, context=context):
521 # Update the sale order lines state (and invoiced flag).
523 for line in order.order_line:
526 # Check if the line is invoiced (has asociated invoice
527 # lines from non-cancelled invoices).
530 for iline in line.invoice_lines:
531 if iline.invoice_id and iline.invoice_id.state != 'cancel':
534 if line.invoiced != invoiced:
535 vals['invoiced'] = invoiced
536 # If the line was in exception state, now it gets confirmed.
537 if line.state == 'exception':
538 vals['state'] = 'confirmed'
539 # Update the line (only when needed).
541 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
543 # Update the sales order state.
545 if order.state == 'invoice_except':
546 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
547 self.invoice_paid_send_note(cr, uid, [order.id], context=context)
550 def action_cancel(self, cr, uid, ids, context=None):
551 wf_service = netsvc.LocalService("workflow")
554 sale_order_line_obj = self.pool.get('sale.order.line')
555 for sale in self.browse(cr, uid, ids, context=context):
556 for inv in sale.invoice_ids:
557 if inv.state not in ('draft', 'cancel'):
558 raise osv.except_osv(
559 _('Cannot cancel this sales order!'),
560 _('First cancel all invoices attached to this sales order.'))
561 for r in self.read(cr, uid, ids, ['invoice_ids']):
562 for inv in r['invoice_ids']:
563 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
564 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
566 self.cancel_send_note(cr, uid, [sale.id], context=None)
567 self.write(cr, uid, ids, {'state': 'cancel'})
570 def action_button_confirm(self, cr, uid, ids, context=None):
571 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
572 wf_service = netsvc.LocalService('workflow')
573 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
575 # redisplay the record as a sale order
576 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
577 view_id = view_ref and view_ref[1] or False,
579 'type': 'ir.actions.act_window',
580 'name': _('Sales Order'),
581 'res_model': 'sale.order',
590 def action_wait(self, cr, uid, ids, context=None):
591 for o in self.browse(cr, uid, ids):
593 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
594 noprod = self.test_no_product(cr, uid, o, context)
595 if (o.order_policy == 'manual') or noprod:
596 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
598 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
599 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
600 self.confirm_send_note(cr, uid, ids, context)
603 def action_quotation_send(self, cr, uid, ids, context=None):
605 This function opens a window to compose an email, with the edi sale template message loaded by default
607 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
608 mod_obj = self.pool.get('ir.model.data')
609 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
610 template_id = template and template[1] or False
611 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
612 res_id = res and res[1] or False
615 'default_model': 'sale.order',
616 'default_res_id': ids[0],
617 'default_use_template': True,
618 'default_template_id': template_id,
619 'mark_so_as_sent': True
624 'res_model': 'mail.compose.message',
625 'views': [(res_id, 'form')],
627 'type': 'ir.actions.act_window',
633 def action_done(self, cr, uid, ids, context=None):
634 self.done_send_note(cr, uid, ids, context=context)
635 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
637 # ------------------------------------------------
638 # OpenChatter methods and notifications
639 # ------------------------------------------------
641 def needaction_domain_get(self, cr, uid, ids, context=None):
642 return [('state', '=', 'draft'), ('user_id','=',uid)]
644 def create_send_note(self, cr, uid, ids, context=None):
645 for obj in self.browse(cr, uid, ids, context=context):
646 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
648 def confirm_send_note(self, cr, uid, ids, context=None):
649 for obj in self.browse(cr, uid, ids, context=context):
650 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)
652 def cancel_send_note(self, cr, uid, ids, context=None):
653 for obj in self.browse(cr, uid, ids, context=context):
654 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
656 def done_send_note(self, cr, uid, ids, context=None):
657 for obj in self.browse(cr, uid, ids, context=context):
658 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)
660 def invoice_paid_send_note(self, cr, uid, ids, context=None):
661 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
663 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
664 for order in self.browse(cr, uid, ids, context=context):
665 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
666 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)
670 # TODO add a field price_unit_uos
671 # - update it on change product and unit price
672 # - use it in report if there is a uos
673 class sale_order_line(osv.osv):
675 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
676 tax_obj = self.pool.get('account.tax')
677 cur_obj = self.pool.get('res.currency')
681 for line in self.browse(cr, uid, ids, context=context):
682 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
683 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
684 cur = line.order_id.pricelist_id.currency_id
685 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
688 def _get_uom_id(self, cr, uid, *args):
690 proxy = self.pool.get('ir.model.data')
691 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
693 except Exception, ex:
696 _name = 'sale.order.line'
697 _description = 'Sales Order Line'
699 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
700 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
701 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
702 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
703 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
704 'invoiced': fields.boolean('Invoiced', readonly=True),
705 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
706 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
707 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
708 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
709 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
710 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
711 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
712 'product_uos': fields.many2one('product.uom', 'Product UoS'),
713 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
714 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
715 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
716 help='* The \'Draft\' state is set when the related sales order in draft state. \
717 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
718 \n* The \'Exception\' state is set when the related sales order is set as exception. \
719 \n* The \'Done\' state is set when the sales order line has been picked. \
720 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
721 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
722 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
723 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
725 _order = 'sequence, id'
727 'product_uom' : _get_uom_id,
729 'product_uom_qty': 1,
730 'product_uos_qty': 1,
737 def _get_line_qty(self, cr, uid, line, context=None):
738 if (line.order_id.invoice_quantity=='order'):
740 return line.product_uos_qty or 0.0
741 return line.product_uom_qty
743 def _get_line_uom(self, cr, uid, line, context=None):
744 if (line.order_id.invoice_quantity=='order'):
746 return line.product_uos.id
747 return line.product_uom.id
749 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
750 """Prepare the dict of values to create the new invoice line for a
751 sale order line. This method may be overridden to implement custom
752 invoice generation (making sure to call super() to establish
753 a clean extension chain).
755 :param browse_record line: sale.order.line record to invoice
756 :param int account_id: optional ID of a G/L account to force
757 (this is used for returning products including service)
758 :return: dict of values to create() the invoice line
761 if not line.invoiced:
764 account_id = line.product_id.product_tmpl_id.property_account_income.id
766 account_id = line.product_id.categ_id.property_account_income_categ.id
768 raise osv.except_osv(_('Error!'),
769 _('Please define income account for this product: "%s" (id:%d).') % \
770 (line.product_id.name, line.product_id.id,))
772 prop = self.pool.get('ir.property').get(cr, uid,
773 'property_account_income_categ', 'product.category',
775 account_id = prop and prop.id or False
776 uosqty = self._get_line_qty(cr, uid, line, context=context)
777 uos_id = self._get_line_uom(cr, uid, line, context=context)
780 pu = round(line.price_unit * line.product_uom_qty / uosqty,
781 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
782 fpos = line.order_id.fiscal_position or False
783 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
785 raise osv.except_osv(_('Error!'),
786 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
789 'origin': line.order_id.name,
790 'account_id': account_id,
793 'discount': line.discount,
795 'product_id': line.product_id.id or False,
796 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
797 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
802 def invoice_line_create(self, cr, uid, ids, context=None):
808 for line in self.browse(cr, uid, ids, context=context):
809 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
811 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
812 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
813 self.write(cr, uid, [line.id], {'invoiced': True})
814 sales.add(line.order_id.id)
815 create_ids.append(inv_id)
816 # Trigger workflow events
817 wf_service = netsvc.LocalService("workflow")
818 for sale_id in sales:
819 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
822 def button_cancel(self, cr, uid, ids, context=None):
823 for line in self.browse(cr, uid, ids, context=context):
825 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
826 return self.write(cr, uid, ids, {'state': 'cancel'})
828 def button_confirm(self, cr, uid, ids, context=None):
829 return self.write(cr, uid, ids, {'state': 'confirmed'})
831 def button_done(self, cr, uid, ids, context=None):
832 wf_service = netsvc.LocalService("workflow")
833 res = self.write(cr, uid, ids, {'state': 'done'})
834 for line in self.browse(cr, uid, ids, context=context):
835 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
838 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
839 product_obj = self.pool.get('product.product')
841 return {'value': {'product_uom': product_uos,
842 'product_uom_qty': product_uos_qty}, 'domain': {}}
844 product = product_obj.browse(cr, uid, product_id)
846 'product_uom': product.uom_id.id,
848 # FIXME must depend on uos/uom of the product and not only of the coeff.
851 'product_uom_qty': product_uos_qty / product.uos_coeff,
852 'th_weight': product_uos_qty / product.uos_coeff * product.weight
854 except ZeroDivisionError:
856 return {'value': value}
858 def copy_data(self, cr, uid, id, default=None, context=None):
861 default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []})
862 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
864 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
865 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
866 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
867 context = context or {}
868 lang = lang or context.get('lang',False)
870 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
872 product_uom_obj = self.pool.get('product.uom')
873 partner_obj = self.pool.get('res.partner')
874 product_obj = self.pool.get('product.product')
875 context = {'lang': lang, 'partner_id': partner_id}
877 lang = partner_obj.browse(cr, uid, partner_id).lang
878 context_partner = {'lang': lang, 'partner_id': partner_id}
881 return {'value': {'th_weight': 0,
882 'product_uos_qty': qty}, 'domain': {'product_uom': [],
885 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
889 product_obj = product_obj.browse(cr, uid, product, context=context)
893 uom2 = product_uom_obj.browse(cr, uid, uom)
894 if product_obj.uom_id.category_id.id != uom2.category_id.id:
897 if product_obj.uos_id:
898 uos2 = product_uom_obj.browse(cr, uid, uos)
899 if product_obj.uos_id.category_id.id != uos2.category_id.id:
903 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
904 if update_tax: #The quantity only have changed
905 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
906 result.update({'type': product_obj.procure_method})
909 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
910 if product_obj.description_sale:
911 result['name'] += '\n'+product_obj.description_sale
913 if (not uom) and (not uos):
914 result['product_uom'] = product_obj.uom_id.id
915 if product_obj.uos_id:
916 result['product_uos'] = product_obj.uos_id.id
917 result['product_uos_qty'] = qty * product_obj.uos_coeff
918 uos_category_id = product_obj.uos_id.category_id.id
920 result['product_uos'] = False
921 result['product_uos_qty'] = qty
922 uos_category_id = False
923 result['th_weight'] = qty * product_obj.weight
924 domain = {'product_uom':
925 [('category_id', '=', product_obj.uom_id.category_id.id)],
927 [('category_id', '=', uos_category_id)]}
928 elif uos and not uom: # only happens if uom is False
929 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
930 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
931 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
932 elif uom: # whether uos is set or not
933 default_uom = product_obj.uom_id and product_obj.uom_id.id
934 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
935 if product_obj.uos_id:
936 result['product_uos'] = product_obj.uos_id.id
937 result['product_uos_qty'] = qty * product_obj.uos_coeff
939 result['product_uos'] = False
940 result['product_uos_qty'] = qty
941 result['th_weight'] = q * product_obj.weight # Round the quantity up
944 uom2 = product_obj.uom_id
948 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
949 'Please set one before choosing a product.')
950 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
952 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
953 product, qty or 1.0, partner_id, {
954 'uom': uom or result.get('product_uom'),
958 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
959 "You have to change either the product, the quantity or the pricelist.")
961 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
963 result.update({'price_unit': price})
966 'title': _('Configuration Error!'),
967 'message' : warning_msgs
969 return {'value': result, 'domain': domain, 'warning': warning}
971 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
972 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
973 lang=False, update_tax=True, date_order=False, context=None):
974 context = context or {}
975 lang = lang or ('lang' in context and context['lang'])
977 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
978 return self.product_id_change(cursor, user, ids, pricelist, product,
979 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
980 partner_id=partner_id, lang=lang, update_tax=update_tax,
981 date_order=date_order, context=context)
983 def unlink(self, cr, uid, ids, context=None):
986 """Allows to delete sales order lines in draft,cancel states"""
987 for rec in self.browse(cr, uid, ids, context=context):
988 if rec.state not in ['draft', 'cancel']:
989 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
990 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
994 class mail_compose_message(osv.osv):
995 _inherit = 'mail.compose.message'
996 def send_mail(self, cr, uid, ids, context=None):
997 context = context or {}
998 if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
999 wf_service = netsvc.LocalService("workflow")
1000 wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1001 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1002 mail_compose_message()
1004 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: