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', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping 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 Account', 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 onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
271 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
274 return {'value': value}
276 'title': _('Pricelist Warning!'),
277 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
279 return {'warning': warning, 'value': value}
281 def onchange_partner_id(self, cr, uid, ids, part):
283 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
285 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
286 part = self.pool.get('res.partner').browse(cr, uid, part)
287 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
288 payment_term = part.property_payment_term and part.property_payment_term.id or False
289 fiscal_position = part.property_account_position and part.property_account_position.id or False
290 dedicated_salesman = part.user_id and part.user_id.id or uid
292 'partner_invoice_id': addr['invoice'],
293 'partner_shipping_id': addr['delivery'],
294 'payment_term': payment_term,
295 'fiscal_position': fiscal_position,
296 'user_id': dedicated_salesman,
299 val['pricelist_id'] = pricelist
300 return {'value': val}
302 def create(self, cr, uid, vals, context=None):
303 order = super(sale_order, self).create(cr, uid, vals, context=context)
305 self.create_send_note(cr, uid, [order], context=context)
308 def button_dummy(self, cr, uid, ids, context=None):
311 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
312 # can be removed after 6.1.
313 def _inv_get(self, cr, uid, order, context=None):
316 def _prepare_invoice(self, cr, uid, order, lines, context=None):
317 """Prepare the dict of values to create the new invoice for a
318 sale order. This method may be overridden to implement custom
319 invoice generation (making sure to call super() to establish
320 a clean extension chain).
322 :param browse_record order: sale.order record to invoice
323 :param list(int) line: list of invoice line IDs that must be
324 attached to the invoice
325 :return: dict of value to create() the invoice
329 journal_ids = self.pool.get('account.journal').search(cr, uid,
330 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
333 raise osv.except_osv(_('Error!'),
334 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
336 'name': order.client_order_ref or '',
337 'origin': order.name,
338 'type': 'out_invoice',
339 'reference': order.client_order_ref or order.name,
340 'account_id': order.partner_id.property_account_receivable.id,
341 'partner_id': order.partner_id.id,
342 'journal_id': journal_ids[0],
343 'invoice_line': [(6, 0, lines)],
344 'currency_id': order.pricelist_id.currency_id.id,
345 'comment': order.note,
346 'payment_term': order.payment_term and order.payment_term.id or False,
347 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
348 'date_invoice': context.get('date_invoice', False),
349 'company_id': order.company_id.id,
350 'user_id': order.user_id and order.user_id.id or False
353 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
354 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
357 def _make_invoice(self, cr, uid, order, lines, context=None):
358 inv_obj = self.pool.get('account.invoice')
359 obj_invoice_line = self.pool.get('account.invoice.line')
362 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
363 from_line_invoice_ids = []
364 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
365 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
366 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
367 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
368 for preinv in order.invoice_ids:
369 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
370 for preline in preinv.invoice_line:
371 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
372 lines.append(inv_line_id)
373 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
374 inv_id = inv_obj.create(cr, uid, inv, context=context)
375 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
376 if data.get('value', False):
377 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
378 inv_obj.button_compute(cr, uid, [inv_id])
381 def print_quotation(self, cr, uid, ids, context=None):
383 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
385 assert len(ids) == 1, 'This option should only be used for a single id at a time'
386 wf_service = netsvc.LocalService("workflow")
387 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
389 'model': 'sale.order',
391 'form': self.read(cr, uid, ids[0], context=context),
393 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
395 def manual_invoice(self, cr, uid, ids, context=None):
396 """ create invoices for the given sale orders (ids), and open the form
397 view of one of the newly created invoices
399 mod_obj = self.pool.get('ir.model.data')
400 wf_service = netsvc.LocalService("workflow")
402 # create invoices through the sale orders' workflow
403 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
405 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
406 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
407 # determine newly created invoices
408 new_inv_ids = list(inv_ids1 - inv_ids0)
410 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
411 res_id = res and res[1] or False,
414 'name': _('Customer Invoices'),
418 'res_model': 'account.invoice',
419 'context': "{'type':'out_invoice'}",
420 'type': 'ir.actions.act_window',
423 'res_id': new_inv_ids and new_inv_ids[0] or False,
426 def action_view_invoice(self, cr, uid, ids, context=None):
428 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.
430 mod_obj = self.pool.get('ir.model.data')
431 act_obj = self.pool.get('ir.actions.act_window')
433 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
434 id = result and result[1] or False
435 result = act_obj.read(cr, uid, [id], context=context)[0]
436 #compute the number of invoices to display
438 for so in self.browse(cr, uid, ids, context=context):
439 inv_ids += [invoice.id for invoice in so.invoice_ids]
440 #choose the view_mode accordingly
442 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
444 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
445 result['views'] = [(res and res[1] or False, 'form')]
446 result['res_id'] = inv_ids and inv_ids[0] or False
449 def test_no_product(self, cr, uid, order, context):
450 for line in order.order_line:
451 if line.product_id and (line.product_id.type<>'service'):
455 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
457 states = ['confirmed', 'done', 'exception']
461 invoice = self.pool.get('account.invoice')
462 obj_sale_order_line = self.pool.get('sale.order.line')
463 partner_currency = {}
466 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
467 # last day of the last month as invoice date
469 context['date_inv'] = date_inv
470 for o in self.browse(cr, uid, ids, context=context):
471 currency_id = o.pricelist_id.currency_id.id
472 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
473 raise osv.except_osv(
475 _('You cannot group sales having different currencies for the same partner.'))
477 partner_currency[o.partner_id.id] = currency_id
479 for line in o.order_line:
482 elif (line.state in states):
483 lines.append(line.id)
484 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
486 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
488 for o in self.browse(cr, uid, ids, context=context):
489 for i in o.invoice_ids:
490 if i.state == 'draft':
492 for val in invoices.values():
494 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
497 invoice_ref += o.name + '|'
498 self.write(cr, uid, [o.id], {'state': 'progress'})
499 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
500 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
502 for order, il in val:
503 res = self._make_invoice(cr, uid, order, il, context=context)
504 invoice_ids.append(res)
505 self.write(cr, uid, [order.id], {'state': 'progress'})
506 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
508 self.invoice_send_note(cr, uid, ids, res, context)
511 def action_invoice_cancel(self, cr, uid, ids, context=None):
514 for sale in self.browse(cr, uid, ids, context=context):
515 for line in sale.order_line:
517 # Check if the line is invoiced (has asociated invoice
518 # lines from non-cancelled invoices).
521 for iline in line.invoice_lines:
522 if iline.invoice_id and iline.invoice_id.state != 'cancel':
525 # Update the line (only when needed)
526 if line.invoiced != invoiced:
527 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
528 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
531 def action_invoice_end(self, cr, uid, ids, context=None):
532 for order in self.browse(cr, uid, ids, context=context):
534 # Update the sale order lines state (and invoiced flag).
536 for line in order.order_line:
539 # Check if the line is invoiced (has asociated invoice
540 # lines from non-cancelled invoices).
543 for iline in line.invoice_lines:
544 if iline.invoice_id and iline.invoice_id.state != 'cancel':
547 if line.invoiced != invoiced:
548 vals['invoiced'] = invoiced
549 # If the line was in exception state, now it gets confirmed.
550 if line.state == 'exception':
551 vals['state'] = 'confirmed'
552 # Update the line (only when needed).
554 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
556 # Update the sales order state.
558 if order.state == 'invoice_except':
559 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
560 self.invoice_paid_send_note(cr, uid, [order.id], context=context)
563 def action_cancel(self, cr, uid, ids, context=None):
564 wf_service = netsvc.LocalService("workflow")
567 sale_order_line_obj = self.pool.get('sale.order.line')
568 for sale in self.browse(cr, uid, ids, context=context):
569 for inv in sale.invoice_ids:
570 if inv.state not in ('draft', 'cancel'):
571 raise osv.except_osv(
572 _('Cannot cancel this sales order!'),
573 _('First cancel all invoices attached to this sales order.'))
574 for r in self.read(cr, uid, ids, ['invoice_ids']):
575 for inv in r['invoice_ids']:
576 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
577 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
579 self.cancel_send_note(cr, uid, [sale.id], context=None)
580 self.write(cr, uid, ids, {'state': 'cancel'})
583 def action_button_confirm(self, cr, uid, ids, context=None):
584 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
585 wf_service = netsvc.LocalService('workflow')
586 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
588 # redisplay the record as a sale order
589 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
590 view_id = view_ref and view_ref[1] or False,
592 'type': 'ir.actions.act_window',
593 'name': _('Sales Order'),
594 'res_model': 'sale.order',
603 def action_wait(self, cr, uid, ids, context=None):
604 for o in self.browse(cr, uid, ids):
606 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
607 noprod = self.test_no_product(cr, uid, o, context)
608 if (o.order_policy == 'manual') or noprod:
609 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
611 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
612 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
613 self.confirm_send_note(cr, uid, ids, context)
616 def action_quotation_send(self, cr, uid, ids, context=None):
618 This function opens a window to compose an email, with the edi sale template message loaded by default
620 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
621 mod_obj = self.pool.get('ir.model.data')
622 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
623 template_id = template and template[1] or False
624 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
625 res_id = res and res[1] or False
628 'default_model': 'sale.order',
629 'default_res_id': ids[0],
630 'default_use_template': True,
631 'default_template_id': template_id,
632 'mark_so_as_sent': True
637 'res_model': 'mail.compose.message',
638 'views': [(res_id, 'form')],
640 'type': 'ir.actions.act_window',
646 def action_done(self, cr, uid, ids, context=None):
647 self.done_send_note(cr, uid, ids, context=context)
648 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
650 # ------------------------------------------------
651 # OpenChatter methods and notifications
652 # ------------------------------------------------
654 def needaction_domain_get(self, cr, uid, ids, context=None):
655 return [('state', '=', 'draft'), ('user_id','=',uid)]
657 def create_send_note(self, cr, uid, ids, context=None):
658 for obj in self.browse(cr, uid, ids, context=context):
659 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
661 def confirm_send_note(self, cr, uid, ids, context=None):
662 for obj in self.browse(cr, uid, ids, context=context):
663 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)
665 def cancel_send_note(self, cr, uid, ids, context=None):
666 for obj in self.browse(cr, uid, ids, context=context):
667 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
669 def done_send_note(self, cr, uid, ids, context=None):
670 for obj in self.browse(cr, uid, ids, context=context):
671 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)
673 def invoice_paid_send_note(self, cr, uid, ids, context=None):
674 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
676 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
677 for order in self.browse(cr, uid, ids, context=context):
678 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
679 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)
683 # TODO add a field price_unit_uos
684 # - update it on change product and unit price
685 # - use it in report if there is a uos
686 class sale_order_line(osv.osv):
688 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
689 tax_obj = self.pool.get('account.tax')
690 cur_obj = self.pool.get('res.currency')
694 for line in self.browse(cr, uid, ids, context=context):
695 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
696 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
697 cur = line.order_id.pricelist_id.currency_id
698 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
701 def _get_uom_id(self, cr, uid, *args):
703 proxy = self.pool.get('ir.model.data')
704 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
706 except Exception, ex:
709 _name = 'sale.order.line'
710 _description = 'Sales Order Line'
712 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
713 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
714 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
715 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
716 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
717 'invoiced': fields.boolean('Invoiced', readonly=True),
718 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
719 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
720 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
721 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
722 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
723 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
724 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
725 'product_uos': fields.many2one('product.uom', 'Product UoS'),
726 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
727 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
728 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
729 help='* The \'Draft\' status is set when the related sales order in draft status. \
730 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
731 \n* The \'Exception\' status is set when the related sales order is set as exception. \
732 \n* The \'Done\' status is set when the sales order line has been picked. \
733 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
734 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
735 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
736 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
738 _order = 'sequence, id'
740 'product_uom' : _get_uom_id,
742 'product_uom_qty': 1,
743 'product_uos_qty': 1,
750 def _get_line_qty(self, cr, uid, line, context=None):
751 if (line.order_id.invoice_quantity=='order'):
753 return line.product_uos_qty or 0.0
754 return line.product_uom_qty
756 def _get_line_uom(self, cr, uid, line, context=None):
757 if (line.order_id.invoice_quantity=='order'):
759 return line.product_uos.id
760 return line.product_uom.id
762 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
763 """Prepare the dict of values to create the new invoice line for a
764 sale order line. This method may be overridden to implement custom
765 invoice generation (making sure to call super() to establish
766 a clean extension chain).
768 :param browse_record line: sale.order.line record to invoice
769 :param int account_id: optional ID of a G/L account to force
770 (this is used for returning products including service)
771 :return: dict of values to create() the invoice line
774 if not line.invoiced:
777 account_id = line.product_id.product_tmpl_id.property_account_income.id
779 account_id = line.product_id.categ_id.property_account_income_categ.id
781 raise osv.except_osv(_('Error!'),
782 _('Please define income account for this product: "%s" (id:%d).') % \
783 (line.product_id.name, line.product_id.id,))
785 prop = self.pool.get('ir.property').get(cr, uid,
786 'property_account_income_categ', 'product.category',
788 account_id = prop and prop.id or False
789 uosqty = self._get_line_qty(cr, uid, line, context=context)
790 uos_id = self._get_line_uom(cr, uid, line, context=context)
793 pu = round(line.price_unit * line.product_uom_qty / uosqty,
794 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
795 fpos = line.order_id.fiscal_position or False
796 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
798 raise osv.except_osv(_('Error!'),
799 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
802 'sequence': line.sequence,
803 'origin': line.order_id.name,
804 'account_id': account_id,
807 'discount': line.discount,
809 'product_id': line.product_id.id or False,
810 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
811 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
816 def invoice_line_create(self, cr, uid, ids, context=None):
822 for line in self.browse(cr, uid, ids, context=context):
823 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
825 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
826 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
827 self.write(cr, uid, [line.id], {'invoiced': True})
828 sales.add(line.order_id.id)
829 create_ids.append(inv_id)
830 # Trigger workflow events
831 wf_service = netsvc.LocalService("workflow")
832 for sale_id in sales:
833 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
836 def button_cancel(self, cr, uid, ids, context=None):
837 for line in self.browse(cr, uid, ids, context=context):
839 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
840 return self.write(cr, uid, ids, {'state': 'cancel'})
842 def button_confirm(self, cr, uid, ids, context=None):
843 return self.write(cr, uid, ids, {'state': 'confirmed'})
845 def button_done(self, cr, uid, ids, context=None):
846 wf_service = netsvc.LocalService("workflow")
847 res = self.write(cr, uid, ids, {'state': 'done'})
848 for line in self.browse(cr, uid, ids, context=context):
849 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
852 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
853 product_obj = self.pool.get('product.product')
855 return {'value': {'product_uom': product_uos,
856 'product_uom_qty': product_uos_qty}, 'domain': {}}
858 product = product_obj.browse(cr, uid, product_id)
860 'product_uom': product.uom_id.id,
862 # FIXME must depend on uos/uom of the product and not only of the coeff.
865 'product_uom_qty': product_uos_qty / product.uos_coeff,
866 'th_weight': product_uos_qty / product.uos_coeff * product.weight
868 except ZeroDivisionError:
870 return {'value': value}
872 def copy_data(self, cr, uid, id, default=None, context=None):
875 default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []})
876 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
878 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
879 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
880 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
881 context = context or {}
882 lang = lang or context.get('lang',False)
884 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
886 product_uom_obj = self.pool.get('product.uom')
887 partner_obj = self.pool.get('res.partner')
888 product_obj = self.pool.get('product.product')
889 context = {'lang': lang, 'partner_id': partner_id}
891 lang = partner_obj.browse(cr, uid, partner_id).lang
892 context_partner = {'lang': lang, 'partner_id': partner_id}
895 return {'value': {'th_weight': 0,
896 'product_uos_qty': qty}, 'domain': {'product_uom': [],
899 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
903 product_obj = product_obj.browse(cr, uid, product, context=context)
907 uom2 = product_uom_obj.browse(cr, uid, uom)
908 if product_obj.uom_id.category_id.id != uom2.category_id.id:
911 if product_obj.uos_id:
912 uos2 = product_uom_obj.browse(cr, uid, uos)
913 if product_obj.uos_id.category_id.id != uos2.category_id.id:
917 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
918 if update_tax: #The quantity only have changed
919 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
922 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
923 if product_obj.description_sale:
924 result['name'] += '\n'+product_obj.description_sale
926 if (not uom) and (not uos):
927 result['product_uom'] = product_obj.uom_id.id
928 if product_obj.uos_id:
929 result['product_uos'] = product_obj.uos_id.id
930 result['product_uos_qty'] = qty * product_obj.uos_coeff
931 uos_category_id = product_obj.uos_id.category_id.id
933 result['product_uos'] = False
934 result['product_uos_qty'] = qty
935 uos_category_id = False
936 result['th_weight'] = qty * product_obj.weight
937 domain = {'product_uom':
938 [('category_id', '=', product_obj.uom_id.category_id.id)],
940 [('category_id', '=', uos_category_id)]}
941 elif uos and not uom: # only happens if uom is False
942 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
943 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
944 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
945 elif uom: # whether uos is set or not
946 default_uom = product_obj.uom_id and product_obj.uom_id.id
947 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
948 if product_obj.uos_id:
949 result['product_uos'] = product_obj.uos_id.id
950 result['product_uos_qty'] = qty * product_obj.uos_coeff
952 result['product_uos'] = False
953 result['product_uos_qty'] = qty
954 result['th_weight'] = q * product_obj.weight # Round the quantity up
957 uom2 = product_obj.uom_id
961 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
962 'Please set one before choosing a product.')
963 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
965 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
966 product, qty or 1.0, partner_id, {
967 'uom': uom or result.get('product_uom'),
971 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
972 "You have to change either the product, the quantity or the pricelist.")
974 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
976 result.update({'price_unit': price})
979 'title': _('Configuration Error!'),
980 'message' : warning_msgs
982 return {'value': result, 'domain': domain, 'warning': warning}
984 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
985 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
986 lang=False, update_tax=True, date_order=False, context=None):
987 context = context or {}
988 lang = lang or ('lang' in context and context['lang'])
990 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
991 return self.product_id_change(cursor, user, ids, pricelist, product,
992 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
993 partner_id=partner_id, lang=lang, update_tax=update_tax,
994 date_order=date_order, context=context)
996 def unlink(self, cr, uid, ids, context=None):
999 """Allows to delete sales order lines in draft,cancel states"""
1000 for rec in self.browse(cr, uid, ids, context=context):
1001 if rec.state not in ['draft', 'cancel']:
1002 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1003 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1007 class mail_compose_message(osv.osv):
1008 _inherit = 'mail.compose.message'
1009 def send_mail(self, cr, uid, ids, context=None):
1010 context = context or {}
1011 if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1012 wf_service = netsvc.LocalService("workflow")
1013 wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1014 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1016 mail_compose_message()
1018 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: