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 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, 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", 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', 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: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
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 new_quotation(self, cr, uid, ids, default=None, 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',
282 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
286 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
289 return {'value': value}
291 'title': _('Pricelist Warning!'),
292 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
294 return {'warning': warning, 'value': value}
296 def onchange_partner_id(self, cr, uid, ids, part):
298 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
300 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
301 part = self.pool.get('res.partner').browse(cr, uid, part)
302 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
303 payment_term = part.property_payment_term and part.property_payment_term.id or False
304 fiscal_position = part.property_account_position and part.property_account_position.id or False
305 dedicated_salesman = part.user_id and part.user_id.id or uid
307 'partner_invoice_id': addr['invoice'],
308 'partner_shipping_id': addr['delivery'],
309 'payment_term': payment_term,
310 'fiscal_position': fiscal_position,
311 'user_id': dedicated_salesman,
314 val['pricelist_id'] = pricelist
315 return {'value': val}
317 def create(self, cr, uid, vals, context=None):
318 order = super(sale_order, self).create(cr, uid, vals, context=context)
320 self.create_send_note(cr, uid, [order], context=context)
323 def button_dummy(self, cr, uid, ids, context=None):
326 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
327 # can be removed after 6.1.
328 def _inv_get(self, cr, uid, order, context=None):
331 def _prepare_invoice(self, cr, uid, order, lines, context=None):
332 """Prepare the dict of values to create the new invoice for a
333 sale order. This method may be overridden to implement custom
334 invoice generation (making sure to call super() to establish
335 a clean extension chain).
337 :param browse_record order: sale.order record to invoice
338 :param list(int) line: list of invoice line IDs that must be
339 attached to the invoice
340 :return: dict of value to create() the invoice
344 journal_ids = self.pool.get('account.journal').search(cr, uid,
345 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
348 raise osv.except_osv(_('Error!'),
349 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
351 'name': order.client_order_ref or '',
352 'origin': order.name,
353 'type': 'out_invoice',
354 'reference': order.client_order_ref or order.name,
355 'account_id': order.partner_id.property_account_receivable.id,
356 'partner_id': order.partner_id.id,
357 'journal_id': journal_ids[0],
358 'invoice_line': [(6, 0, lines)],
359 'currency_id': order.pricelist_id.currency_id.id,
360 'comment': order.note,
361 'payment_term': order.payment_term and order.payment_term.id or False,
362 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
363 'date_invoice': context.get('date_invoice', False),
364 'company_id': order.company_id.id,
365 'user_id': order.user_id and order.user_id.id or False
368 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
369 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
372 def _make_invoice(self, cr, uid, order, lines, context=None):
373 inv_obj = self.pool.get('account.invoice')
374 obj_invoice_line = self.pool.get('account.invoice.line')
377 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
378 from_line_invoice_ids = []
379 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
380 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
381 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
382 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
383 for preinv in order.invoice_ids:
384 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
385 for preline in preinv.invoice_line:
386 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
387 lines.append(inv_line_id)
388 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
389 inv_id = inv_obj.create(cr, uid, inv, context=context)
390 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
391 if data.get('value', False):
392 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
393 inv_obj.button_compute(cr, uid, [inv_id])
396 def print_quotation(self, cr, uid, ids, context=None):
398 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
400 assert len(ids) == 1, 'This option should only be used for a single id at a time'
401 wf_service = netsvc.LocalService("workflow")
402 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
404 'model': 'sale.order',
406 'form': self.read(cr, uid, ids[0], context=context),
408 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
410 def manual_invoice(self, cr, uid, ids, context=None):
411 """ create invoices for the given sale orders (ids), and open the form
412 view of one of the newly created invoices
414 mod_obj = self.pool.get('ir.model.data')
415 wf_service = netsvc.LocalService("workflow")
417 # create invoices through the sale orders' workflow
418 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
420 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
421 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
422 # determine newly created invoices
423 new_inv_ids = list(inv_ids1 - inv_ids0)
425 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
426 res_id = res and res[1] or False,
429 'name': _('Customer Invoices'),
433 'res_model': 'account.invoice',
434 'context': "{'type':'out_invoice'}",
435 'type': 'ir.actions.act_window',
438 'res_id': new_inv_ids and new_inv_ids[0] or False,
441 def action_view_invoice(self, cr, uid, ids, context=None):
443 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.
445 mod_obj = self.pool.get('ir.model.data')
446 act_obj = self.pool.get('ir.actions.act_window')
448 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
449 id = result and result[1] or False
450 result = act_obj.read(cr, uid, [id], context=context)[0]
451 #compute the number of invoices to display
453 for so in self.browse(cr, uid, ids, context=context):
454 inv_ids += [invoice.id for invoice in so.invoice_ids]
455 #choose the view_mode accordingly
457 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
459 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
460 result['views'] = [(res and res[1] or False, 'form')]
461 result['res_id'] = inv_ids and inv_ids[0] or False
464 def test_no_product(self, cr, uid, order, context):
465 for line in order.order_line:
466 if line.product_id and (line.product_id.type<>'service'):
470 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
472 states = ['confirmed', 'done', 'exception']
476 invoice = self.pool.get('account.invoice')
477 obj_sale_order_line = self.pool.get('sale.order.line')
478 partner_currency = {}
481 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
482 # last day of the last month as invoice date
484 context['date_inv'] = date_inv
485 for o in self.browse(cr, uid, ids, context=context):
486 currency_id = o.pricelist_id.currency_id.id
487 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
488 raise osv.except_osv(
490 _('You cannot group sales having different currencies for the same partner.'))
492 partner_currency[o.partner_id.id] = currency_id
494 for line in o.order_line:
497 elif (line.state in states):
498 lines.append(line.id)
499 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
501 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
503 for o in self.browse(cr, uid, ids, context=context):
504 for i in o.invoice_ids:
505 if i.state == 'draft':
507 for val in invoices.values():
509 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
512 invoice_ref += o.name + '|'
513 self.write(cr, uid, [o.id], {'state': 'progress'})
514 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
515 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
517 for order, il in val:
518 res = self._make_invoice(cr, uid, order, il, context=context)
519 invoice_ids.append(res)
520 self.write(cr, uid, [order.id], {'state': 'progress'})
521 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
523 self.invoice_send_note(cr, uid, ids, res, context)
526 def action_invoice_cancel(self, cr, uid, ids, context=None):
529 for sale in self.browse(cr, uid, ids, context=context):
530 for line in sale.order_line:
532 # Check if the line is invoiced (has asociated invoice
533 # lines from non-cancelled invoices).
536 for iline in line.invoice_lines:
537 if iline.invoice_id and iline.invoice_id.state != 'cancel':
540 # Update the line (only when needed)
541 if line.invoiced != invoiced:
542 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
543 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
546 def action_invoice_end(self, cr, uid, ids, context=None):
547 for order in self.browse(cr, uid, ids, context=context):
549 # Update the sale order lines state (and invoiced flag).
551 for line in order.order_line:
554 # Check if the line is invoiced (has asociated invoice
555 # lines from non-cancelled invoices).
558 for iline in line.invoice_lines:
559 if iline.invoice_id and iline.invoice_id.state != 'cancel':
562 if line.invoiced != invoiced:
563 vals['invoiced'] = invoiced
564 # If the line was in exception state, now it gets confirmed.
565 if line.state == 'exception':
566 vals['state'] = 'confirmed'
567 # Update the line (only when needed).
569 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
571 # Update the sales order state.
573 if order.state == 'invoice_except':
574 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
575 self.invoice_paid_send_note(cr, uid, [order.id], context=context)
578 def action_cancel(self, cr, uid, ids, context=None):
579 wf_service = netsvc.LocalService("workflow")
582 sale_order_line_obj = self.pool.get('sale.order.line')
583 for sale in self.browse(cr, uid, ids, context=context):
584 for inv in sale.invoice_ids:
585 if inv.state not in ('draft', 'cancel'):
586 raise osv.except_osv(
587 _('Cannot cancel this sales order!'),
588 _('First cancel all invoices attached to this sales order.'))
589 for r in self.read(cr, uid, ids, ['invoice_ids']):
590 for inv in r['invoice_ids']:
591 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
592 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
594 self.cancel_send_note(cr, uid, [sale.id], context=None)
595 self.write(cr, uid, ids, {'state': 'cancel'})
598 def action_button_confirm(self, cr, uid, ids, context=None):
599 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
600 wf_service = netsvc.LocalService('workflow')
601 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
603 # redisplay the record as a sale order
604 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
605 view_id = view_ref and view_ref[1] or False,
607 'type': 'ir.actions.act_window',
608 'name': _('Sales Order'),
609 'res_model': 'sale.order',
618 def action_wait(self, cr, uid, ids, context=None):
619 for o in self.browse(cr, uid, ids):
621 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
622 noprod = self.test_no_product(cr, uid, o, context)
623 if (o.order_policy == 'manual') or noprod:
624 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
626 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
627 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
628 self.confirm_send_note(cr, uid, ids, context)
631 def action_quotation_send(self, cr, uid, ids, context=None):
633 This function opens a window to compose an email, with the edi sale template message loaded by default
635 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
636 mod_obj = self.pool.get('ir.model.data')
637 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
638 template_id = template and template[1] or False
639 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
640 res_id = res and res[1] or False
643 'default_model': 'sale.order',
644 'default_res_id': ids[0],
645 'default_use_template': True,
646 'default_template_id': template_id,
647 'mark_so_as_sent': True
652 'res_model': 'mail.compose.message',
653 'views': [(res_id, 'form')],
655 'type': 'ir.actions.act_window',
661 def action_done(self, cr, uid, ids, context=None):
662 self.done_send_note(cr, uid, ids, context=context)
663 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
665 # ------------------------------------------------
666 # OpenChatter methods and notifications
667 # ------------------------------------------------
669 def needaction_domain_get(self, cr, uid, ids, context=None):
670 return [('state', '=', 'draft'), ('user_id','=',uid)]
672 def create_send_note(self, cr, uid, ids, context=None):
673 for obj in self.browse(cr, uid, ids, context=context):
674 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
676 def confirm_send_note(self, cr, uid, ids, context=None):
677 for obj in self.browse(cr, uid, ids, context=context):
678 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)
680 def cancel_send_note(self, cr, uid, ids, context=None):
681 for obj in self.browse(cr, uid, ids, context=context):
682 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
684 def done_send_note(self, cr, uid, ids, context=None):
685 for obj in self.browse(cr, uid, ids, context=context):
686 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)
688 def invoice_paid_send_note(self, cr, uid, ids, context=None):
689 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
691 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
692 for order in self.browse(cr, uid, ids, context=context):
693 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
694 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)
698 # TODO add a field price_unit_uos
699 # - update it on change product and unit price
700 # - use it in report if there is a uos
701 class sale_order_line(osv.osv):
703 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
704 tax_obj = self.pool.get('account.tax')
705 cur_obj = self.pool.get('res.currency')
709 for line in self.browse(cr, uid, ids, context=context):
710 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
711 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
712 cur = line.order_id.pricelist_id.currency_id
713 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
716 def _get_uom_id(self, cr, uid, *args):
718 proxy = self.pool.get('ir.model.data')
719 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
721 except Exception, ex:
724 _name = 'sale.order.line'
725 _description = 'Sales Order Line'
727 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
728 'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
729 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
730 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
731 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
732 'invoiced': fields.boolean('Invoiced', readonly=True),
733 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
734 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
735 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."),
736 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
737 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
738 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
739 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
740 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
741 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
742 'product_uos': fields.many2one('product.uom', 'Product UoS'),
743 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
744 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
745 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
746 help='* The \'Draft\' status is set when the related sales order in draft status. \
747 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
748 \n* The \'Exception\' status is set when the related sales order is set as exception. \
749 \n* The \'Done\' status is set when the sales order line has been picked. \
750 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
751 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
752 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
753 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
755 _order = 'sequence, id'
757 'product_uom' : _get_uom_id,
759 'product_uom_qty': 1,
760 'product_uos_qty': 1,
764 'type': 'make_to_stock',
768 def _get_line_qty(self, cr, uid, line, context=None):
769 if (line.order_id.invoice_quantity=='order'):
771 return line.product_uos_qty or 0.0
772 return line.product_uom_qty
774 def _get_line_uom(self, cr, uid, line, context=None):
775 if (line.order_id.invoice_quantity=='order'):
777 return line.product_uos.id
778 return line.product_uom.id
780 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
781 """Prepare the dict of values to create the new invoice line for a
782 sale order line. This method may be overridden to implement custom
783 invoice generation (making sure to call super() to establish
784 a clean extension chain).
786 :param browse_record line: sale.order.line record to invoice
787 :param int account_id: optional ID of a G/L account to force
788 (this is used for returning products including service)
789 :return: dict of values to create() the invoice line
792 if not line.invoiced:
795 account_id = line.product_id.product_tmpl_id.property_account_income.id
797 account_id = line.product_id.categ_id.property_account_income_categ.id
799 raise osv.except_osv(_('Error!'),
800 _('Please define income account for this product: "%s" (id:%d).') % \
801 (line.product_id.name, line.product_id.id,))
803 prop = self.pool.get('ir.property').get(cr, uid,
804 'property_account_income_categ', 'product.category',
806 account_id = prop and prop.id or False
807 uosqty = self._get_line_qty(cr, uid, line, context=context)
808 uos_id = self._get_line_uom(cr, uid, line, context=context)
811 pu = round(line.price_unit * line.product_uom_qty / uosqty,
812 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
813 fpos = line.order_id.fiscal_position or False
814 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
816 raise osv.except_osv(_('Error!'),
817 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
820 'sequence': line.sequence,
821 'origin': line.order_id.name,
822 'account_id': account_id,
825 'discount': line.discount,
827 'product_id': line.product_id.id or False,
828 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
829 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
834 def invoice_line_create(self, cr, uid, ids, context=None):
840 for line in self.browse(cr, uid, ids, context=context):
841 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
843 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
844 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
845 self.write(cr, uid, [line.id], {'invoiced': True})
846 sales.add(line.order_id.id)
847 create_ids.append(inv_id)
848 # Trigger workflow events
849 wf_service = netsvc.LocalService("workflow")
850 for sale_id in sales:
851 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
854 def button_cancel(self, cr, uid, ids, context=None):
855 for line in self.browse(cr, uid, ids, context=context):
857 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
858 return self.write(cr, uid, ids, {'state': 'cancel'})
860 def button_confirm(self, cr, uid, ids, context=None):
861 return self.write(cr, uid, ids, {'state': 'confirmed'})
863 def button_done(self, cr, uid, ids, context=None):
864 wf_service = netsvc.LocalService("workflow")
865 res = self.write(cr, uid, ids, {'state': 'done'})
866 for line in self.browse(cr, uid, ids, context=context):
867 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
870 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
871 product_obj = self.pool.get('product.product')
873 return {'value': {'product_uom': product_uos,
874 'product_uom_qty': product_uos_qty}, 'domain': {}}
876 product = product_obj.browse(cr, uid, product_id)
878 'product_uom': product.uom_id.id,
880 # FIXME must depend on uos/uom of the product and not only of the coeff.
883 'product_uom_qty': product_uos_qty / product.uos_coeff,
884 'th_weight': product_uos_qty / product.uos_coeff * product.weight
886 except ZeroDivisionError:
888 return {'value': value}
890 def copy_data(self, cr, uid, id, default=None, context=None):
893 default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []})
894 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
896 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
897 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
898 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
899 context = context or {}
900 lang = lang or context.get('lang',False)
902 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
904 product_uom_obj = self.pool.get('product.uom')
905 partner_obj = self.pool.get('res.partner')
906 product_obj = self.pool.get('product.product')
907 context = {'lang': lang, 'partner_id': partner_id}
909 lang = partner_obj.browse(cr, uid, partner_id).lang
910 context_partner = {'lang': lang, 'partner_id': partner_id}
913 return {'value': {'th_weight': 0,
914 'product_uos_qty': qty}, 'domain': {'product_uom': [],
917 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
921 product_obj = product_obj.browse(cr, uid, product, context=context)
925 uom2 = product_uom_obj.browse(cr, uid, uom)
926 if product_obj.uom_id.category_id.id != uom2.category_id.id:
929 if product_obj.uos_id:
930 uos2 = product_uom_obj.browse(cr, uid, uos)
931 if product_obj.uos_id.category_id.id != uos2.category_id.id:
935 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
936 if update_tax: #The quantity only have changed
937 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
940 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
941 if product_obj.description_sale:
942 result['name'] += '\n'+product_obj.description_sale
944 if (not uom) and (not uos):
945 result['product_uom'] = product_obj.uom_id.id
946 if product_obj.uos_id:
947 result['product_uos'] = product_obj.uos_id.id
948 result['product_uos_qty'] = qty * product_obj.uos_coeff
949 uos_category_id = product_obj.uos_id.category_id.id
951 result['product_uos'] = False
952 result['product_uos_qty'] = qty
953 uos_category_id = False
954 result['th_weight'] = qty * product_obj.weight
955 domain = {'product_uom':
956 [('category_id', '=', product_obj.uom_id.category_id.id)],
958 [('category_id', '=', uos_category_id)]}
959 elif uos and not uom: # only happens if uom is False
960 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
961 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
962 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
963 elif uom: # whether uos is set or not
964 default_uom = product_obj.uom_id and product_obj.uom_id.id
965 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
966 result['product_uom'] = default_uom
967 if product_obj.uos_id:
968 result['product_uos'] = product_obj.uos_id.id
969 result['product_uos_qty'] = qty * product_obj.uos_coeff
971 result['product_uos'] = False
972 result['product_uos_qty'] = qty
973 result['th_weight'] = q * product_obj.weight # Round the quantity up
976 uom2 = product_obj.uom_id
980 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
981 'Please set one before choosing a product.')
982 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
984 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
985 product, qty or 1.0, partner_id, {
986 'uom': uom or result.get('product_uom'),
990 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
991 "You have to change either the product, the quantity or the pricelist.")
993 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
995 result.update({'price_unit': price})
998 'title': _('Configuration Error!'),
999 'message' : warning_msgs
1001 return {'value': result, 'domain': domain, 'warning': warning}
1003 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1004 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1005 lang=False, update_tax=True, date_order=False, context=None):
1006 context = context or {}
1007 lang = lang or ('lang' in context and context['lang'])
1009 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1010 return self.product_id_change(cursor, user, ids, pricelist, product,
1011 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1012 partner_id=partner_id, lang=lang, update_tax=update_tax,
1013 date_order=date_order, context=context)
1015 def unlink(self, cr, uid, ids, context=None):
1018 """Allows to delete sales order lines in draft,cancel states"""
1019 for rec in self.browse(cr, uid, ids, context=context):
1020 if rec.state not in ['draft', 'cancel']:
1021 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1022 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1026 class mail_compose_message(osv.osv):
1027 _inherit = 'mail.compose.message'
1028 def send_mail(self, cr, uid, ids, context=None):
1029 context = context or {}
1030 if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1031 wf_service = netsvc.LocalService("workflow")
1032 wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1033 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1035 mail_compose_message()
1037 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: