1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
25 from openerp import pooler
26 from openerp.osv import fields, osv
27 from openerp.tools.translate import _
28 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import openerp.addons.decimal_precision as dp
30 from openerp import netsvc
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40 'company_id': fields.many2one('res.company', 'Company', required=False),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 class sale_order(osv.osv):
50 _inherit = ['mail.thread', 'ir.needaction_mixin']
51 _description = "Sales Order"
54 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['manual', 'progress'],
55 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['sent']
59 def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
62 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
63 if shop.project_id.id:
64 v['project_id'] = shop.project_id.id
65 if shop.pricelist_id.id:
66 v['pricelist_id'] = shop.pricelist_id.id
69 def copy(self, cr, uid, id, default=None, context=None):
75 'date_confirm': False,
76 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
78 return super(sale_order, self).copy(cr, uid, id, default, context=context)
80 def _amount_line_tax(self, cr, uid, line, context=None):
82 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']:
83 val += c.get('amount', 0.0)
86 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
87 cur_obj = self.pool.get('res.currency')
89 for order in self.browse(cr, uid, ids, context=context):
91 'amount_untaxed': 0.0,
96 cur = order.pricelist_id.currency_id
97 for line in order.order_line:
98 val1 += line.price_subtotal
99 val += self._amount_line_tax(cr, uid, line, context=context)
100 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
101 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
102 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
106 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
108 for sale in self.browse(cursor, user, ids, context=context):
113 for invoice in sale.invoice_ids:
114 if invoice.state not in ('draft', 'cancel'):
115 tot += invoice.amount_untaxed
117 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
122 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
124 for sale in self.browse(cursor, user, ids, context=context):
130 def _invoiced(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
134 invoice_existence = False
135 for invoice in sale.invoice_ids:
136 if invoice.state!='cancel':
137 invoice_existence = True
138 if invoice.state != 'paid':
141 if not invoice_existence or sale.state == 'manual':
145 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
154 clause += 'AND inv.state = \'paid\''
156 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
157 sale_clause = ', sale_order AS sale '
160 cursor.execute('SELECT rel.order_id ' \
161 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
162 'WHERE rel.invoice_id = inv.id ' + clause)
163 res = cursor.fetchall()
165 cursor.execute('SELECT sale.id ' \
166 'FROM sale_order AS sale ' \
167 'WHERE sale.id NOT IN ' \
168 '(SELECT rel.order_id ' \
169 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
170 res.extend(cursor.fetchall())
172 return [('id', '=', 0)]
173 return [('id', 'in', [x[0] for x in res])]
175 def _get_order(self, cr, uid, ids, context=None):
177 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
178 result[line.order_id.id] = True
181 def _get_default_shop(self, cr, uid, context=None):
182 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
183 shop_ids = self.pool.get('sale.shop').search(cr, uid, [('company_id','=',company_id)], context=context)
185 raise osv.except_osv(_('Error!'), _('There is no default shop for the current user\'s company!'))
189 'name': fields.char('Order Reference', size=64, required=True,
190 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
191 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
193 'client_order_ref': fields.char('Customer Reference', size=64),
194 'state': fields.selection([
195 ('draft', 'Draft Quotation'),
196 ('sent', 'Quotation Sent'),
197 ('cancel', 'Cancelled'),
198 ('waiting_date', 'Waiting Schedule'),
199 ('progress', 'Sales Order'),
200 ('manual', 'Sale to Invoice'),
201 ('invoice_except', 'Invoice Exception'),
203 ], 'Status', readonly=True, track_visibility='onchange',
204 help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
205 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
206 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
207 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
208 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
209 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
210 '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."),
211 '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."),
212 'order_policy': fields.selection([
213 ('manual', 'On Demand'),
214 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
215 help="""This field controls how invoice and delivery operations are synchronized."""),
216 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
217 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
218 '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."),
220 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
221 '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)."),
222 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
223 'invoiced': fields.function(_invoiced, string='Paid',
224 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
225 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
226 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
227 'note': fields.text('Terms and conditions'),
229 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
231 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
232 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234 multi='sums', help="The amount without tax.", track_visibility='always'),
235 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
237 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
238 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240 multi='sums', help="The tax amount."),
241 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
243 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
244 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
246 multi='sums', help="The total amount."),
248 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
249 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
250 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
251 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
254 'date_order': fields.date.context_today,
255 'order_policy': 'manual',
257 'user_id': lambda obj, cr, uid, context: uid,
258 'name': lambda obj, cr, uid, context: '/',
259 'invoice_quantity': 'order',
260 'shop_id': _get_default_shop,
261 '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'],
262 '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'],
265 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
270 def unlink(self, cr, uid, ids, context=None):
271 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
273 for s in sale_orders:
274 if s['state'] in ['draft', 'cancel']:
275 unlink_ids.append(s['id'])
277 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before !'))
279 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
281 def copy_quotation(self, cr, uid, ids, context=None):
282 id = self.copy(cr, uid, ids[0], context=None)
283 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
284 view_id = view_ref and view_ref[1] or False,
286 'type': 'ir.actions.act_window',
287 'name': _('Sales Order'),
288 'res_model': 'sale.order',
297 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
298 context = context or {}
302 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
305 return {'value': value}
307 'title': _('Pricelist Warning!'),
308 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
310 return {'warning': warning, 'value': value}
312 def onchange_partner_id(self, cr, uid, ids, part, context=None):
314 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
316 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
317 #if the chosen partner is not a company and has a parent company, use the parent to choose the delivery, the
318 #invoicing addresses and all the fields related to the partner.
319 if part.parent_id and not part.is_company:
320 part = part.parent_id
321 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
322 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
323 payment_term = part.property_payment_term and part.property_payment_term.id or False
324 fiscal_position = part.property_account_position and part.property_account_position.id or False
325 dedicated_salesman = part.user_id and part.user_id.id or uid
327 'partner_invoice_id': addr['invoice'],
328 'partner_shipping_id': addr['delivery'],
329 'payment_term': payment_term,
330 'fiscal_position': fiscal_position,
331 'user_id': dedicated_salesman,
334 val['pricelist_id'] = pricelist
335 return {'value': val}
337 def create(self, cr, uid, vals, context=None):
338 if vals.get('name','/')=='/':
339 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
340 return super(sale_order, self).create(cr, uid, vals, context=context)
342 def button_dummy(self, cr, uid, ids, context=None):
345 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
346 # can be removed after 6.1.
347 def _inv_get(self, cr, uid, order, context=None):
350 def _prepare_invoice(self, cr, uid, order, lines, context=None):
351 """Prepare the dict of values to create the new invoice for a
352 sales order. This method may be overridden to implement custom
353 invoice generation (making sure to call super() to establish
354 a clean extension chain).
356 :param browse_record order: sale.order record to invoice
357 :param list(int) line: list of invoice line IDs that must be
358 attached to the invoice
359 :return: dict of value to create() the invoice
363 journal_ids = self.pool.get('account.journal').search(cr, uid,
364 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
367 raise osv.except_osv(_('Error!'),
368 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
370 'name': order.client_order_ref or '',
371 'origin': order.name,
372 'type': 'out_invoice',
373 'reference': order.client_order_ref or order.name,
374 'account_id': order.partner_id.property_account_receivable.id,
375 'partner_id': order.partner_invoice_id.id,
376 'journal_id': journal_ids[0],
377 'invoice_line': [(6, 0, lines)],
378 'currency_id': order.pricelist_id.currency_id.id,
379 'comment': order.note,
380 'payment_term': order.payment_term and order.payment_term.id or False,
381 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
382 'date_invoice': context.get('date_invoice', False),
383 'company_id': order.company_id.id,
384 'user_id': order.user_id and order.user_id.id or False
387 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
388 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
391 def _make_invoice(self, cr, uid, order, lines, context=None):
392 inv_obj = self.pool.get('account.invoice')
393 obj_invoice_line = self.pool.get('account.invoice.line')
396 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
397 from_line_invoice_ids = []
398 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
399 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
400 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
401 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
402 for preinv in order.invoice_ids:
403 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
404 for preline in preinv.invoice_line:
405 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
406 lines.append(inv_line_id)
407 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
408 inv_id = inv_obj.create(cr, uid, inv, context=context)
409 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
410 if data.get('value', False):
411 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
412 inv_obj.button_compute(cr, uid, [inv_id])
415 def print_quotation(self, cr, uid, ids, context=None):
417 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
419 assert len(ids) == 1, 'This option should only be used for a single id at a time'
420 wf_service = netsvc.LocalService("workflow")
421 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
423 'model': 'sale.order',
425 'form': self.read(cr, uid, ids[0], context=context),
427 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
429 def manual_invoice(self, cr, uid, ids, context=None):
430 """ create invoices for the given sales orders (ids), and open the form
431 view of one of the newly created invoices
433 mod_obj = self.pool.get('ir.model.data')
434 wf_service = netsvc.LocalService("workflow")
436 # create invoices through the sales orders' workflow
437 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
439 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
440 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
441 # determine newly created invoices
442 new_inv_ids = list(inv_ids1 - inv_ids0)
444 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
445 res_id = res and res[1] or False,
448 'name': _('Customer Invoices'),
452 'res_model': 'account.invoice',
453 'context': "{'type':'out_invoice'}",
454 'type': 'ir.actions.act_window',
457 'res_id': new_inv_ids and new_inv_ids[0] or False,
460 def action_view_invoice(self, cr, uid, ids, context=None):
462 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
464 mod_obj = self.pool.get('ir.model.data')
465 act_obj = self.pool.get('ir.actions.act_window')
467 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
468 id = result and result[1] or False
469 result = act_obj.read(cr, uid, [id], context=context)[0]
470 #compute the number of invoices to display
472 for so in self.browse(cr, uid, ids, context=context):
473 inv_ids += [invoice.id for invoice in so.invoice_ids]
474 #choose the view_mode accordingly
476 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
478 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
479 result['views'] = [(res and res[1] or False, 'form')]
480 result['res_id'] = inv_ids and inv_ids[0] or False
483 def test_no_product(self, cr, uid, order, context):
484 for line in order.order_line:
485 if line.product_id and (line.product_id.type<>'service'):
489 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
491 states = ['confirmed', 'done', 'exception']
495 invoice = self.pool.get('account.invoice')
496 obj_sale_order_line = self.pool.get('sale.order.line')
497 partner_currency = {}
500 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
501 # last day of the last month as invoice date
503 context['date_invoice'] = date_invoice
504 for o in self.browse(cr, uid, ids, context=context):
505 currency_id = o.pricelist_id.currency_id.id
506 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
507 raise osv.except_osv(
509 _('You cannot group sales having different currencies for the same partner.'))
511 partner_currency[o.partner_id.id] = currency_id
513 for line in o.order_line:
516 elif (line.state in states):
517 lines.append(line.id)
518 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
520 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
522 for o in self.browse(cr, uid, ids, context=context):
523 for i in o.invoice_ids:
524 if i.state == 'draft':
526 for val in invoices.values():
528 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
531 invoice_ref += o.name + '|'
532 self.write(cr, uid, [o.id], {'state': 'progress'})
533 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
534 #remove last '|' in invoice_ref
535 if len(invoice_ref) >= 1:
536 invoice_ref = invoice_ref[:-1]
537 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
539 for order, il in val:
540 res = self._make_invoice(cr, uid, order, il, context=context)
541 invoice_ids.append(res)
542 self.write(cr, uid, [order.id], {'state': 'progress'})
543 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
546 def action_invoice_cancel(self, cr, uid, ids, context=None):
547 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
550 def action_invoice_end(self, cr, uid, ids, context=None):
551 for this in self.browse(cr, uid, ids, context=context):
552 for line in this.order_line:
553 if line.state == 'exception':
554 line.write({'state': 'confirmed'})
555 if this.state == 'invoice_except':
556 this.write({'state': 'progress'})
559 def action_cancel(self, cr, uid, ids, context=None):
560 wf_service = netsvc.LocalService("workflow")
563 sale_order_line_obj = self.pool.get('sale.order.line')
564 for sale in self.browse(cr, uid, ids, context=context):
565 for inv in sale.invoice_ids:
566 if inv.state not in ('draft', 'cancel'):
567 raise osv.except_osv(
568 _('Cannot cancel this sales order!'),
569 _('First cancel all invoices attached to this sales order.'))
570 for r in self.read(cr, uid, ids, ['invoice_ids']):
571 for inv in r['invoice_ids']:
572 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
573 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
575 self.write(cr, uid, ids, {'state': 'cancel'})
578 def action_button_confirm(self, cr, uid, ids, context=None):
579 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
580 wf_service = netsvc.LocalService('workflow')
581 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
583 # redisplay the record as a sales order
584 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
585 view_id = view_ref and view_ref[1] or False,
587 'type': 'ir.actions.act_window',
588 'name': _('Sales Order'),
589 'res_model': 'sale.order',
598 def action_wait(self, cr, uid, ids, context=None):
599 context = context or {}
600 for o in self.browse(cr, uid, ids):
602 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
603 noprod = self.test_no_product(cr, uid, o, context)
604 if (o.order_policy == 'manual') or noprod:
605 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
607 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
608 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
611 def action_quotation_send(self, cr, uid, ids, context=None):
613 This function opens a window to compose an email, with the edi sale template message loaded by default
615 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
616 ir_model_data = self.pool.get('ir.model.data')
618 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
622 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
624 compose_form_id = False
627 'default_model': 'sale.order',
628 'default_res_id': ids[0],
629 'default_use_template': bool(template_id),
630 'default_template_id': template_id,
631 'default_composition_mode': 'comment',
632 'mark_so_as_sent': True
635 'type': 'ir.actions.act_window',
638 'res_model': 'mail.compose.message',
639 'views': [(compose_form_id, 'form')],
640 'view_id': compose_form_id,
645 def action_done(self, cr, uid, ids, context=None):
646 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
650 # TODO add a field price_unit_uos
651 # - update it on change product and unit price
652 # - use it in report if there is a uos
653 class sale_order_line(osv.osv):
655 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
656 tax_obj = self.pool.get('account.tax')
657 cur_obj = self.pool.get('res.currency')
661 for line in self.browse(cr, uid, ids, context=context):
662 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
663 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
664 cur = line.order_id.pricelist_id.currency_id
665 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
668 def _get_uom_id(self, cr, uid, *args):
670 proxy = self.pool.get('ir.model.data')
671 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
673 except Exception, ex:
676 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
677 res = dict.fromkeys(ids, False)
678 for this in self.browse(cr, uid, ids, context=context):
679 res[this.id] = this.invoice_lines and \
680 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
683 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
684 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
685 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
686 sale_order_line sol ON (sol.order_id = rel.order_id)
687 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
688 return [i[0] for i in cr.fetchall()]
690 _name = 'sale.order.line'
691 _description = 'Sales Order Line'
693 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
694 'name': fields.text('Description', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
695 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
696 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
697 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
698 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
700 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
701 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
702 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
703 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
704 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."),
705 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
706 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
707 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
708 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
709 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
710 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
711 'product_uos': fields.many2one('product.uom', 'Product UoS'),
712 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
713 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
714 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
715 help='* The \'Draft\' status is set when the related sales order in draft status. \
716 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
717 \n* The \'Exception\' status is set when the related sales order is set as exception. \
718 \n* The \'Done\' status is set when the sales order line has been picked. \
719 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
720 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
721 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
722 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
724 _order = 'order_id desc, sequence, id'
726 'product_uom' : _get_uom_id,
728 'product_uom_qty': 1,
729 'product_uos_qty': 1,
732 'type': 'make_to_stock',
736 def _get_line_qty(self, cr, uid, line, context=None):
737 if (line.order_id.invoice_quantity=='order'):
739 return line.product_uos_qty or 0.0
740 return line.product_uom_qty
742 def _get_line_uom(self, cr, uid, line, context=None):
743 if (line.order_id.invoice_quantity=='order'):
745 return line.product_uos.id
746 return line.product_uom.id
748 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
749 """Prepare the dict of values to create the new invoice line for a
750 sales order line. This method may be overridden to implement custom
751 invoice generation (making sure to call super() to establish
752 a clean extension chain).
754 :param browse_record line: sale.order.line record to invoice
755 :param int account_id: optional ID of a G/L account to force
756 (this is used for returning products including service)
757 :return: dict of values to create() the invoice line
760 if not line.invoiced:
763 account_id = line.product_id.property_account_income.id
765 account_id = line.product_id.categ_id.property_account_income_categ.id
767 raise osv.except_osv(_('Error!'),
768 _('Please define income account for this product: "%s" (id:%d).') % \
769 (line.product_id.name, line.product_id.id,))
771 prop = self.pool.get('ir.property').get(cr, uid,
772 'property_account_income_categ', 'product.category',
774 account_id = prop and prop.id or False
775 uosqty = self._get_line_qty(cr, uid, line, context=context)
776 uos_id = self._get_line_uom(cr, uid, line, context=context)
779 pu = round(line.price_unit * line.product_uom_qty / uosqty,
780 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
781 fpos = line.order_id.fiscal_position or False
782 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
784 raise osv.except_osv(_('Error!'),
785 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
788 'sequence': line.sequence,
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 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
813 sales.add(line.order_id.id)
814 create_ids.append(inv_id)
815 # Trigger workflow events
816 wf_service = netsvc.LocalService("workflow")
817 for sale_id in sales:
818 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
821 def button_cancel(self, cr, uid, ids, context=None):
822 for line in self.browse(cr, uid, ids, context=context):
824 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
825 return self.write(cr, uid, ids, {'state': 'cancel'})
827 def button_confirm(self, cr, uid, ids, context=None):
828 return self.write(cr, uid, ids, {'state': 'confirmed'})
830 def button_done(self, cr, uid, ids, context=None):
831 wf_service = netsvc.LocalService("workflow")
832 res = self.write(cr, uid, ids, {'state': 'done'})
833 for line in self.browse(cr, uid, ids, context=context):
834 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
837 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
838 product_obj = self.pool.get('product.product')
840 return {'value': {'product_uom': product_uos,
841 'product_uom_qty': product_uos_qty}, 'domain': {}}
843 product = product_obj.browse(cr, uid, product_id)
845 'product_uom': product.uom_id.id,
847 # FIXME must depend on uos/uom of the product and not only of the coeff.
850 'product_uom_qty': product_uos_qty / product.uos_coeff,
851 'th_weight': product_uos_qty / product.uos_coeff * product.weight
853 except ZeroDivisionError:
855 return {'value': value}
857 def copy_data(self, cr, uid, id, default=None, context=None):
860 default.update({'state': 'draft', 'invoice_lines': []})
861 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
863 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
864 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
865 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
866 context = context or {}
867 lang = lang or context.get('lang',False)
869 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
871 product_uom_obj = self.pool.get('product.uom')
872 partner_obj = self.pool.get('res.partner')
873 product_obj = self.pool.get('product.product')
874 context = {'lang': lang, 'partner_id': partner_id}
876 lang = partner_obj.browse(cr, uid, partner_id).lang
877 context_partner = {'lang': lang, 'partner_id': partner_id}
880 return {'value': {'th_weight': 0,
881 'product_uos_qty': qty}, 'domain': {'product_uom': [],
884 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
888 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
892 uom2 = product_uom_obj.browse(cr, uid, uom)
893 if product_obj.uom_id.category_id.id != uom2.category_id.id:
896 if product_obj.uos_id:
897 uos2 = product_uom_obj.browse(cr, uid, uos)
898 if product_obj.uos_id.category_id.id != uos2.category_id.id:
902 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
903 if update_tax: #The quantity only have changed
904 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
907 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
908 if product_obj.description_sale:
909 result['name'] += '\n'+product_obj.description_sale
911 if (not uom) and (not uos):
912 result['product_uom'] = product_obj.uom_id.id
913 if product_obj.uos_id:
914 result['product_uos'] = product_obj.uos_id.id
915 result['product_uos_qty'] = qty * product_obj.uos_coeff
916 uos_category_id = product_obj.uos_id.category_id.id
918 result['product_uos'] = False
919 result['product_uos_qty'] = qty
920 uos_category_id = False
921 result['th_weight'] = qty * product_obj.weight
922 domain = {'product_uom':
923 [('category_id', '=', product_obj.uom_id.category_id.id)],
925 [('category_id', '=', uos_category_id)]}
926 elif uos and not uom: # only happens if uom is False
927 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
928 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
929 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
930 elif uom: # whether uos is set or not
931 default_uom = product_obj.uom_id and product_obj.uom_id.id
932 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
933 if product_obj.uos_id:
934 result['product_uos'] = product_obj.uos_id.id
935 result['product_uos_qty'] = qty * product_obj.uos_coeff
937 result['product_uos'] = False
938 result['product_uos_qty'] = qty
939 result['th_weight'] = q * product_obj.weight # Round the quantity up
942 uom2 = product_obj.uom_id
946 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
947 'Please set one before choosing a product.')
948 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
950 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
951 product, qty or 1.0, partner_id, {
952 'uom': uom or result.get('product_uom'),
956 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
957 "You have to change either the product, the quantity or the pricelist.")
959 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
961 result.update({'price_unit': price})
964 'title': _('Configuration Error!'),
965 'message' : warning_msgs
967 return {'value': result, 'domain': domain, 'warning': warning}
969 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
970 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
971 lang=False, update_tax=True, date_order=False, context=None):
972 context = context or {}
973 lang = lang or ('lang' in context and context['lang'])
975 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
976 return self.product_id_change(cursor, user, ids, pricelist, product,
977 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
978 partner_id=partner_id, lang=lang, update_tax=update_tax,
979 date_order=date_order, context=context)
981 def unlink(self, cr, uid, ids, context=None):
984 """Allows to delete sales order lines in draft,cancel states"""
985 for rec in self.browse(cr, uid, ids, context=context):
986 if rec.state not in ['draft', 'cancel']:
987 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
988 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
991 class mail_compose_message(osv.Model):
992 _inherit = 'mail.compose.message'
994 def send_mail(self, cr, uid, ids, context=None):
995 context = context or {}
996 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
997 context = dict(context, mail_post_autofollow=True)
998 wf_service = netsvc.LocalService("workflow")
999 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1000 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1002 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: