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
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 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41 'company_id': fields.many2one('res.company', 'Company', required=False),
44 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
49 class sale_order(osv.osv):
51 _description = "Sales Order"
53 def copy(self, cr, uid, id, default=None, context=None):
61 'date_confirm': False,
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context=context)
66 def _amount_line_tax(self, cr, uid, line, context=None):
68 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.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)['taxes']:
69 val += c.get('amount', 0.0)
72 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids, context=context):
77 'amount_untaxed': 0.0,
82 cur = order.pricelist_id.currency_id
83 for line in order.order_line:
84 val1 += line.price_subtotal
85 val += self._amount_line_tax(cr, uid, line, context=context)
86 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
92 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 tmp[id] = {'picked': 0.0, 'total': 0.0}
100 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
104 stock_picking p on (p.id=m.picking_id)
106 procurement_order mp on (mp.move_id=m.id)
108 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
110 for item in cr.dictfetchall():
111 if item['move_state'] == 'cancel':
114 if item['picking_type'] == 'in':#this is a returned picking
115 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
116 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
117 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
119 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
120 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
121 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
123 for order in self.browse(cr, uid, ids, context=context):
125 res[order.id] = 100.0
127 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
137 for invoice in sale.invoice_ids:
138 if invoice.state not in ('draft', 'cancel'):
139 tot += invoice.amount_untaxed
141 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
146 def _invoiced(self, cursor, user, ids, name, arg, context=None):
148 for sale in self.browse(cursor, user, ids, context=context):
151 for invoice in sale.invoice_ids:
152 if invoice.state!='cancel':
153 invoices.append(invoice)
154 for invoice in invoices:
155 if invoice.state != 'paid':
162 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
171 clause += 'AND inv.state = \'paid\''
173 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
174 sale_clause = ', sale_order AS sale '
177 cursor.execute('SELECT rel.order_id ' \
178 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
179 'WHERE rel.invoice_id = inv.id ' + clause)
180 res = cursor.fetchall()
182 cursor.execute('SELECT sale.id ' \
183 'FROM sale_order AS sale ' \
184 'WHERE sale.id NOT IN ' \
185 '(SELECT rel.order_id ' \
186 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
187 res.extend(cursor.fetchall())
189 return [('id', '=', 0)]
190 return [('id', 'in', [x[0] for x in res])]
192 def _get_order(self, cr, uid, ids, context=None):
194 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
195 result[line.order_id.id] = True
199 'name': fields.char('Order Reference', size=64, required=True,
200 readonly=True, states={'draft': [('readonly', False)]}, select=True),
201 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
202 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
203 'client_order_ref': fields.char('Customer Reference', size=64),
204 'state': fields.selection([
205 ('draft', 'Quotation'),
206 ('waiting_date', 'Waiting Schedule'),
207 ('manual', 'To Invoice'),
208 ('progress', 'In Progress'),
209 ('shipping_except', 'Shipping Exception'),
210 ('invoice_except', 'Invoice Exception'),
212 ('cancel', 'Cancelled')
213 ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
214 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
215 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
216 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
217 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
218 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
219 'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
220 'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact who requested the order or quotation."),
221 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
223 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
224 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225 'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
226 'order_policy': fields.selection([
227 ('prepaid', 'Pay before delivery'),
228 ('manual', 'Deliver & invoice on demand'),
229 ('picking', 'Invoice based on deliveries'),
230 ('postpaid', 'Invoice on order after delivery'),
231 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
232 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
233 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
234 - The 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
235 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
236 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
238 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
240 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
241 '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)."),
242 'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is a list of picking that has been generated for this sales order."),
243 'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
244 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246 'invoiced': fields.function(_invoiced, string='Paid',
247 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248 'note': fields.text('Notes'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
255 multi='sums', help="The amount without tax."),
256 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
261 multi='sums', help="The tax amount."),
262 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
264 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
267 multi='sums', help="The total amount."),
269 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
270 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
275 'picking_policy': 'direct',
276 'date_order': fields.date.context_today,
277 'order_policy': 'manual',
279 'user_id': lambda obj, cr, uid, context: uid,
280 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281 'invoice_quantity': 'order',
282 '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'],
283 'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
284 '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'],
287 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
292 def unlink(self, cr, uid, ids, context=None):
293 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
295 for s in sale_orders:
296 if s['state'] in ['draft', 'cancel']:
297 unlink_ids.append(s['id'])
299 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
301 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
303 def onchange_shop_id(self, cr, uid, ids, shop_id):
306 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
307 v['project_id'] = shop.project_id.id
308 # Que faire si le client a une pricelist a lui ?
309 if shop.pricelist_id.id:
310 v['pricelist_id'] = shop.pricelist_id.id
313 def action_cancel_draft(self, cr, uid, ids, *args):
316 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
317 line_ids = map(lambda x: x[0], cr.fetchall())
318 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
319 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
320 wf_service = netsvc.LocalService("workflow")
322 # Deleting the existing instance of workflow for SO
323 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
324 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
325 for (id,name) in self.name_get(cr, uid, ids):
326 message = _("The sales order '%s' has been set in draft state.") %(name,)
327 self.log(cr, uid, id, message)
330 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
331 if (not pricelist_id) or (not order_lines):
334 'title': _('Pricelist Warning!'),
335 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
337 return {'warning': warning}
339 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
344 val['partner_invoice_id'] = order_id
346 val['partner_shipping_id'] = order_id
347 return {'value': val}
349 def onchange_partner_id(self, cr, uid, ids, part):
351 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
353 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
354 part = self.pool.get('res.partner').browse(cr, uid, part)
355 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
356 payment_term = part.property_payment_term and part.property_payment_term.id or False
357 fiscal_position = part.property_account_position and part.property_account_position.id or False
358 dedicated_salesman = part.user_id and part.user_id.id or uid
360 'partner_invoice_id': addr['invoice'],
361 'partner_order_id': addr['contact'],
362 'partner_shipping_id': addr['delivery'],
363 'payment_term': payment_term,
364 'fiscal_position': fiscal_position,
365 'user_id': dedicated_salesman,
368 val['pricelist_id'] = pricelist
369 return {'value': val}
371 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
375 if policy == 'prepaid':
377 elif policy == 'picking':
378 inv_qty = 'procurement'
379 return {'value': {'invoice_quantity': inv_qty}}
381 def write(self, cr, uid, ids, vals, context=None):
382 if vals.get('order_policy', False):
383 if vals['order_policy'] == 'prepaid':
384 vals.update({'invoice_quantity': 'order'})
385 elif vals['order_policy'] == 'picking':
386 vals.update({'invoice_quantity': 'procurement'})
387 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
389 def create(self, cr, uid, vals, context=None):
390 if vals.get('order_policy', False):
391 if vals['order_policy'] == 'prepaid':
392 vals.update({'invoice_quantity': 'order'})
393 if vals['order_policy'] == 'picking':
394 vals.update({'invoice_quantity': 'procurement'})
395 return super(sale_order, self).create(cr, uid, vals, context=context)
397 def button_dummy(self, cr, uid, ids, context=None):
400 #FIXME: the method should return the list of invoices created (invoice_ids)
401 # and not the id of the last invoice created (res). The problem is that we
402 # cannot change it directly since the method is called by the sales order
403 # workflow and I suppose it expects a single id...
404 def _inv_get(self, cr, uid, order, context=None):
407 def _make_invoice(self, cr, uid, order, lines, context=None):
408 journal_obj = self.pool.get('account.journal')
409 inv_obj = self.pool.get('account.invoice')
410 obj_invoice_line = self.pool.get('account.invoice.line')
414 journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
416 raise osv.except_osv(_('Error !'),
417 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
418 a = order.partner_id.property_account_receivable.id
419 pay_term = order.payment_term and order.payment_term.id or False
420 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
421 from_line_invoice_ids = []
422 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
423 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
424 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
425 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
426 for preinv in order.invoice_ids:
427 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
428 for preline in preinv.invoice_line:
429 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
430 lines.append(inv_line_id)
432 'name': order.client_order_ref or '',
433 'origin': order.name,
434 'type': 'out_invoice',
435 'reference': order.client_order_ref or order.name,
437 'partner_id': order.partner_id.id,
438 'journal_id': journal_ids[0],
439 'address_invoice_id': order.partner_invoice_id.id,
440 'address_contact_id': order.partner_order_id.id,
441 'invoice_line': [(6, 0, lines)],
442 'currency_id': order.pricelist_id.currency_id.id,
443 'comment': order.note,
444 'payment_term': pay_term,
445 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
446 'date_invoice': context.get('date_invoice',False),
447 'company_id': order.company_id.id,
448 'user_id': order.user_id and order.user_id.id or False
450 inv.update(self._inv_get(cr, uid, order))
451 inv_id = inv_obj.create(cr, uid, inv, context=context)
452 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime(DEFAULT_SERVER_DATE_FORMAT))
453 if data.get('value', False):
454 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
455 inv_obj.button_compute(cr, uid, [inv_id])
458 def manual_invoice(self, cr, uid, ids, context=None):
459 mod_obj = self.pool.get('ir.model.data')
460 wf_service = netsvc.LocalService("workflow")
464 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
465 inv_ids.add(record.id)
466 # inv_ids would have old invoices if any
468 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
469 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
470 inv_ids1.add(record.id)
471 inv_ids = list(inv_ids1.difference(inv_ids))
473 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
474 res_id = res and res[1] or False,
477 'name': _('Customer Invoices'),
481 'res_model': 'account.invoice',
482 'context': "{'type':'out_invoice'}",
483 'type': 'ir.actions.act_window',
486 'res_id': inv_ids and inv_ids[0] or False,
489 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
493 picking_obj = self.pool.get('stock.picking')
494 invoice = self.pool.get('account.invoice')
495 obj_sale_order_line = self.pool.get('sale.order.line')
496 partner_currency = {}
499 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
500 # last day of the last month as invoice date
502 context['date_inv'] = date_inv
503 for o in self.browse(cr, uid, ids, context=context):
504 currency_id = o.pricelist_id.currency_id.id
505 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
506 raise osv.except_osv(
508 _('You cannot group sales having different currencies for the same partner.'))
510 partner_currency[o.partner_id.id] = currency_id
512 for line in o.order_line:
515 elif (line.state in states):
516 lines.append(line.id)
517 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
519 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
521 for o in self.browse(cr, uid, ids, context=context):
522 for i in o.invoice_ids:
523 if i.state == 'draft':
525 for val in invoices.values():
527 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
530 invoice_ref += o.name + '|'
531 self.write(cr, uid, [o.id], {'state': 'progress'})
532 if o.order_policy == 'picking':
533 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
534 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
535 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
537 for order, il in val:
538 res = self._make_invoice(cr, uid, order, il, context=context)
539 invoice_ids.append(res)
540 self.write(cr, uid, [order.id], {'state': 'progress'})
541 if order.order_policy == 'picking':
542 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
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):
549 for sale in self.browse(cr, uid, ids, context=context):
550 for line in sale.order_line:
552 # Check if the line is invoiced (has asociated invoice
553 # lines from non-cancelled invoices).
556 for iline in line.invoice_lines:
557 if iline.invoice_id and iline.invoice_id.state != 'cancel':
560 # Update the line (only when needed)
561 if line.invoiced != invoiced:
562 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
563 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
566 def action_invoice_end(self, cr, uid, ids, context=None):
567 for order in self.browse(cr, uid, ids, context=context):
569 # Update the sale order lines state (and invoiced flag).
571 for line in order.order_line:
574 # Check if the line is invoiced (has asociated invoice
575 # lines from non-cancelled invoices).
578 for iline in line.invoice_lines:
579 if iline.invoice_id and iline.invoice_id.state != 'cancel':
582 if line.invoiced != invoiced:
583 vals['invoiced'] = invoiced
584 # If the line was in exception state, now it gets confirmed.
585 if line.state == 'exception':
586 vals['state'] = 'confirmed'
587 # Update the line (only when needed).
589 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
591 # Update the sales order state.
593 if order.state == 'invoice_except':
594 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
597 def action_cancel(self, cr, uid, ids, context=None):
598 wf_service = netsvc.LocalService("workflow")
601 sale_order_line_obj = self.pool.get('sale.order.line')
602 proc_obj = self.pool.get('procurement.order')
603 for sale in self.browse(cr, uid, ids, context=context):
604 for pick in sale.picking_ids:
605 if pick.state not in ('draft', 'cancel'):
606 raise osv.except_osv(
607 _('Could not cancel sales order !'),
608 _('You must first cancel all picking attached to this sales order.'))
609 if pick.state == 'cancel':
610 for mov in pick.move_lines:
611 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
613 for proc in proc_ids:
614 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
615 for r in self.read(cr, uid, ids, ['picking_ids']):
616 for pick in r['picking_ids']:
617 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
618 for inv in sale.invoice_ids:
619 if inv.state not in ('draft', 'cancel'):
620 raise osv.except_osv(
621 _('Could not cancel this sales order !'),
622 _('You must first cancel all invoices attached to this sales order.'))
623 for r in self.read(cr, uid, ids, ['invoice_ids']):
624 for inv in r['invoice_ids']:
625 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
626 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
628 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
629 self.log(cr, uid, sale.id, message)
630 self.write(cr, uid, ids, {'state': 'cancel'})
633 def action_wait(self, cr, uid, ids, context=None):
634 for o in self.browse(cr, uid, ids):
636 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
637 if (o.order_policy == 'manual'):
638 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
640 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
641 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
642 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
643 self.log(cr, uid, o.id, message)
646 def procurement_lines_get(self, cr, uid, ids, *args):
648 for order in self.browse(cr, uid, ids, context={}):
649 for line in order.order_line:
650 if line.procurement_id:
651 res.append(line.procurement_id.id)
654 # if mode == 'finished':
655 # returns True if all lines are done, False otherwise
656 # if mode == 'canceled':
657 # returns True if there is at least one canceled line, False otherwise
658 def test_state(self, cr, uid, ids, mode, *args):
659 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
664 write_cancel_ids = []
665 for order in self.browse(cr, uid, ids, context={}):
666 for line in order.order_line:
667 if (not line.procurement_id) or (line.procurement_id.state=='done'):
668 if line.state != 'done':
669 write_done_ids.append(line.id)
672 if line.procurement_id:
673 if (line.procurement_id.state == 'cancel'):
675 if line.state != 'exception':
676 write_cancel_ids.append(line.id)
680 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
682 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
684 if mode == 'finished':
686 elif mode == 'canceled':
692 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
695 'origin': order.name,
696 'date_planned': date_planned,
697 'product_id': line.product_id.id,
698 'product_qty': line.product_uom_qty,
699 'product_uom': line.product_uom.id,
700 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
701 or line.product_uom_qty,
702 'product_uos': (line.product_uos and line.product_uos.id)\
703 or line.product_uom.id,
704 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
705 'procure_method': line.type,
707 'company_id': order.company_id.id,
711 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
712 location_id = order.shop_id.warehouse_id.lot_stock_id.id
713 output_id = order.shop_id.warehouse_id.lot_output_id.id
715 'name': line.name[:250],
716 'picking_id': picking_id,
717 'product_id': line.product_id.id,
718 'date': date_planned,
719 'date_expected': date_planned,
720 'product_qty': line.product_uom_qty,
721 'product_uom': line.product_uom.id,
722 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
723 'product_uos': (line.product_uos and line.product_uos.id)\
724 or line.product_uom.id,
725 'product_packaging': line.product_packaging.id,
726 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
727 'location_id': location_id,
728 'location_dest_id': output_id,
729 'sale_line_id': line.id,
730 'tracking_id': False,
734 'company_id': order.company_id.id,
735 'price_unit': line.product_id.standard_price or 0.0
738 def _prepare_order_picking(self, cr, uid, order, context=None):
739 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
742 'origin': order.name,
743 'date': order.date_order,
746 'move_type': order.picking_policy,
748 'address_id': order.partner_shipping_id.id,
750 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
751 'company_id': order.company_id.id,
754 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
755 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
757 Define ship_recreate for process after shipping exception
758 param order: sale order to which the order lines belong
759 param line: sale order line records to procure
760 param move_id: the ID of stock move
761 param proc_id: the ID of procurement
763 move_obj = self.pool.get('stock.move')
764 if order.state == 'shipping_except':
765 for pick in order.picking_ids:
766 for move in pick.move_lines:
767 if move.state == 'cancel':
768 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
770 for mov in move_obj.browse(cr, uid, mov_ids):
771 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
772 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
773 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
776 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
777 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
778 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
781 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
782 """Create the required procurements to supply sale order lines, also connecting
783 the procurements to appropriate stock moves in order to bring the goods to the
784 sale order's requested location.
786 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
787 a standard outgoing picking will be created to wrap the stock moves, as returned
788 by :meth:`~._prepare_order_picking`.
790 Modules that wish to customize the procurements or partition the stock moves over
791 multiple stock pickings may override this method and call ``super()`` with
792 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
794 :param browse_record order: sale order to which the order lines belong
795 :param list(browse_record) order_lines: sale order line records to procure
796 :param int picking_id: optional ID of a stock picking to which the created stock moves
797 will be added. A new picking will be created if ommitted.
800 move_obj = self.pool.get('stock.move')
801 picking_obj = self.pool.get('stock.picking')
802 procurement_obj = self.pool.get('procurement.order')
805 for line in order_lines:
806 if line.state == 'done':
809 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
812 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
814 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
815 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
817 # a service has no stock move
820 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
821 proc_ids.append(proc_id)
822 line.write({'procurement_id': proc_id})
823 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
825 wf_service = netsvc.LocalService("workflow")
827 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
829 for proc_id in proc_ids:
830 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
833 if order.state == 'shipping_except':
834 val['state'] = 'progress'
835 val['shipped'] = False
837 if (order.order_policy == 'manual'):
838 for line in order.order_line:
839 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
840 val['state'] = 'manual'
845 def action_ship_create(self, cr, uid, ids, context=None):
846 for order in self.browse(cr, uid, ids, context=context):
847 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
850 def action_ship_end(self, cr, uid, ids, context=None):
851 for order in self.browse(cr, uid, ids, context=context):
852 val = {'shipped': True}
853 if order.state == 'shipping_except':
854 val['state'] = 'progress'
855 if (order.order_policy == 'manual'):
856 for line in order.order_line:
857 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
858 val['state'] = 'manual'
860 for line in order.order_line:
862 if line.state == 'exception':
863 towrite.append(line.id)
865 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
866 self.write(cr, uid, [order.id], val)
869 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
870 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
872 part = inv['partner_id'] and inv['partner_id'][0]
873 pr = inv['amount_untaxed'] or 0.0
874 partnertype = 'customer'
877 'name': 'Order: '+name,
879 'description': 'Order '+str(inv['id']),
882 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
884 'partner_type': partnertype,
886 'planned_revenue': pr,
890 self.pool.get('res.partner.event').create(cr, uid, event)
892 def has_stockable_products(self, cr, uid, ids, *args):
893 for order in self.browse(cr, uid, ids):
894 for order_line in order.order_line:
895 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
900 # TODO add a field price_unit_uos
901 # - update it on change product and unit price
902 # - use it in report if there is a uos
903 class sale_order_line(osv.osv):
905 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
906 tax_obj = self.pool.get('account.tax')
907 cur_obj = self.pool.get('res.currency')
911 for line in self.browse(cr, uid, ids, context=context):
912 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
913 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
914 cur = line.order_id.pricelist_id.currency_id
915 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
918 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
920 for line in self.browse(cr, uid, ids, context=context):
922 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
927 def _get_uom_id(self, cr, uid, *args):
929 proxy = self.pool.get('ir.model.data')
930 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
932 except Exception, ex:
935 _name = 'sale.order.line'
936 _description = 'Sales Order Line'
938 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
939 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
940 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
941 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
942 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
943 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
944 'invoiced': fields.boolean('Invoiced', readonly=True),
945 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
946 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
947 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
948 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
949 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
950 help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
951 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
952 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
953 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
954 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
955 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
956 'product_uos': fields.many2one('product.uom', 'Product UoS'),
957 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
958 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
959 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
960 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
961 'notes': fields.text('Notes'),
962 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
963 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
964 help='* The \'Draft\' state is set when the related sales order in draft state. \
965 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
966 \n* The \'Exception\' state is set when the related sales order is set as exception. \
967 \n* The \'Done\' state is set when the sales order line has been picked. \
968 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
969 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
970 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
971 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
973 _order = 'sequence, id'
975 'product_uom' : _get_uom_id,
978 'product_uom_qty': 1,
979 'product_uos_qty': 1,
983 'type': 'make_to_stock',
984 'product_packaging': False,
988 def _prepare_order_line_invoice_line(self, cr, uid, ids, line, account_id=False, context=None):
989 """ Builds the invoice line dict from a sale order line
990 @param line: sale order line object
991 @param account_id: the id of the account to force eventually (the method is used for picking return including service)
992 @return: dict that will be used to create the invoice line
995 def _get_line_qty(line):
996 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
998 return line.product_uos_qty or 0.0
999 return line.product_uom_qty
1001 return self.pool.get('procurement.order').quantity_get(cr, uid,
1002 line.procurement_id.id, context=context)
1004 def _get_line_uom(line):
1005 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1006 if line.product_uos:
1007 return line.product_uos.id
1008 return line.product_uom.id
1010 return self.pool.get('procurement.order').uom_get(cr, uid,
1011 line.procurement_id.id, context=context)
1013 if not line.invoiced:
1016 account_id = line.product_id.product_tmpl_id.property_account_income.id
1018 account_id = line.product_id.categ_id.property_account_income_categ.id
1020 raise osv.except_osv(_('Error !'),
1021 _('There is no income account defined for this product: "%s" (id:%d)') % \
1022 (line.product_id.name, line.product_id.id,))
1024 prop = self.pool.get('ir.property').get(cr, uid,
1025 'property_account_income_categ', 'product.category',
1027 account_id = prop and prop.id or False
1028 uosqty = _get_line_qty(line)
1029 uos_id = _get_line_uom(line)
1032 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1033 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1034 fpos = line.order_id.fiscal_position or False
1035 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1037 raise osv.except_osv(_('Error !'),
1038 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1041 'origin': line.order_id.name,
1042 'account_id': account_id,
1045 'discount': line.discount,
1047 'product_id': line.product_id.id or False,
1048 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1050 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1055 def invoice_line_create(self, cr, uid, ids, context=None):
1061 for line in self.browse(cr, uid, ids, context=context):
1062 vals = self._prepare_order_line_invoice_line(cr, uid, ids, line, False, context)
1064 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1065 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1066 self.write(cr, uid, [line.id], {'invoiced': True})
1067 sales.add(line.order_id.id)
1068 create_ids.append(inv_id)
1069 # Trigger workflow events
1070 wf_service = netsvc.LocalService("workflow")
1071 for sale_id in sales:
1072 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1075 def button_cancel(self, cr, uid, ids, context=None):
1076 for line in self.browse(cr, uid, ids, context=context):
1078 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1079 for move_line in line.move_ids:
1080 if move_line.state != 'cancel':
1081 raise osv.except_osv(
1082 _('Could not cancel sales order line!'),
1083 _('You must first cancel stock moves attached to this sales order line.'))
1084 return self.write(cr, uid, ids, {'state': 'cancel'})
1086 def button_confirm(self, cr, uid, ids, context=None):
1087 return self.write(cr, uid, ids, {'state': 'confirmed'})
1089 def button_done(self, cr, uid, ids, context=None):
1090 wf_service = netsvc.LocalService("workflow")
1091 res = self.write(cr, uid, ids, {'state': 'done'})
1092 for line in self.browse(cr, uid, ids, context=context):
1093 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1096 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1097 product_obj = self.pool.get('product.product')
1099 return {'value': {'product_uom': product_uos,
1100 'product_uom_qty': product_uos_qty}, 'domain': {}}
1102 product = product_obj.browse(cr, uid, product_id)
1104 'product_uom': product.uom_id.id,
1106 # FIXME must depend on uos/uom of the product and not only of the coeff.
1109 'product_uom_qty': product_uos_qty / product.uos_coeff,
1110 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1112 except ZeroDivisionError:
1114 return {'value': value}
1116 def copy_data(self, cr, uid, id, default=None, context=None):
1119 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1120 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1122 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1123 partner_id=False, packaging=False, flag=False, context=None):
1125 return {'value': {'product_packaging': False}}
1126 product_obj = self.pool.get('product.product')
1127 product_uom_obj = self.pool.get('product.uom')
1128 pack_obj = self.pool.get('product.packaging')
1133 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1134 product=product, qty=qty, uom=uom, partner_id=partner_id,
1135 packaging=packaging, flag=False, context=context)
1136 warning_msgs = res.get('warning') and res['warning']['message']
1138 products = product_obj.browse(cr, uid, product, context=context)
1139 if not products.packaging:
1140 packaging = result['product_packaging'] = False
1141 elif not packaging and products.packaging and not flag:
1142 packaging = products.packaging[0].id
1143 result['product_packaging'] = packaging
1146 default_uom = products.uom_id and products.uom_id.id
1147 pack = pack_obj.browse(cr, uid, packaging, context=context)
1148 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1149 # qty = qty - qty % q + q
1150 if qty and (q and not (qty % q) == 0):
1151 ean = pack.ean or _('(n/a)')
1154 if not warning_msgs:
1155 warn_msg = _("You selected a quantity of %d Units.\n"
1156 "But it's not compatible with the selected packaging.\n"
1157 "Here is a proposition of quantities according to the packaging:\n"
1158 "EAN: %s Quantity: %s Type of ul: %s") % \
1159 (qty, ean, qty_pack, type_ul.name)
1160 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1162 'title': _('Configuration Error !'),
1163 'message': warning_msgs
1165 result['product_uom_qty'] = qty
1167 return {'value': result, 'warning': warning}
1169 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1170 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1171 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1172 context = context or {}
1173 lang = lang or context.get('lang',False)
1175 raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1177 product_uom_obj = self.pool.get('product.uom')
1178 partner_obj = self.pool.get('res.partner')
1179 product_obj = self.pool.get('product.product')
1180 context = {'lang': lang, 'partner_id': partner_id}
1182 lang = partner_obj.browse(cr, uid, partner_id).lang
1183 context_partner = {'lang': lang, 'partner_id': partner_id}
1186 return {'value': {'th_weight': 0, 'product_packaging': False,
1187 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1190 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1192 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1193 result = res.get('value', {})
1194 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1195 product_obj = product_obj.browse(cr, uid, product, context=context)
1199 uom2 = product_uom_obj.browse(cr, uid, uom)
1200 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1203 if product_obj.uos_id:
1204 uos2 = product_uom_obj.browse(cr, uid, uos)
1205 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1209 if product_obj.description_sale:
1210 result['notes'] = product_obj.description_sale
1211 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1212 if update_tax: #The quantity only have changed
1213 result['delay'] = (product_obj.sale_delay or 0.0)
1214 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1215 result.update({'type': product_obj.procure_method})
1218 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1220 if (not uom) and (not uos):
1221 result['product_uom'] = product_obj.uom_id.id
1222 if product_obj.uos_id:
1223 result['product_uos'] = product_obj.uos_id.id
1224 result['product_uos_qty'] = qty * product_obj.uos_coeff
1225 uos_category_id = product_obj.uos_id.category_id.id
1227 result['product_uos'] = False
1228 result['product_uos_qty'] = qty
1229 uos_category_id = False
1230 result['th_weight'] = qty * product_obj.weight
1231 domain = {'product_uom':
1232 [('category_id', '=', product_obj.uom_id.category_id.id)],
1234 [('category_id', '=', uos_category_id)]}
1236 elif uos and not uom: # only happens if uom is False
1237 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1238 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1239 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1240 elif uom: # whether uos is set or not
1241 default_uom = product_obj.uom_id and product_obj.uom_id.id
1242 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1243 if product_obj.uos_id:
1244 result['product_uos'] = product_obj.uos_id.id
1245 result['product_uos_qty'] = qty * product_obj.uos_coeff
1247 result['product_uos'] = False
1248 result['product_uos_qty'] = qty
1249 result['th_weight'] = q * product_obj.weight # Round the quantity up
1252 uom2 = product_obj.uom_id
1253 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1254 and (product_obj.procure_method=='make_to_stock'):
1255 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1256 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1257 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1258 max(0,product_obj.qty_available), product_obj.uom_id.name)
1259 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1263 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1264 'Please set one before choosing a product.')
1265 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1267 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1268 product, qty or 1.0, partner_id, {
1269 'uom': uom or result.get('product_uom'),
1273 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1274 "You have to change either the product, the quantity or the pricelist.")
1276 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1278 result.update({'price_unit': price})
1281 'title': _('Configuration Error !'),
1282 'message' : warning_msgs
1284 return {'value': result, 'domain': domain, 'warning': warning}
1286 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1287 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1288 lang=False, update_tax=True, date_order=False, context=None):
1289 context = context or {}
1290 lang = lang or ('lang' in context and context['lang'])
1291 res = self.product_id_change(cursor, user, ids, pricelist, product,
1292 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1293 partner_id=partner_id, lang=lang, update_tax=update_tax,
1294 date_order=date_order, context=context)
1295 if 'product_uom' in res['value']:
1296 del res['value']['product_uom']
1298 res['value']['price_unit'] = 0.0
1301 def unlink(self, cr, uid, ids, context=None):
1304 """Allows to delete sales order lines in draft,cancel states"""
1305 for rec in self.browse(cr, uid, ids, context=context):
1306 if rec.state not in ['draft', 'cancel']:
1307 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1308 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1312 class sale_config_picking_policy(osv.osv_memory):
1313 _name = 'sale.config.picking_policy'
1314 _inherit = 'res.config'
1317 'name': fields.char('Name', size=64),
1318 'sale_orders': fields.boolean('Based on Sales Orders',),
1319 'deli_orders': fields.boolean('Based on Delivery Orders'),
1320 'task_work': fields.boolean('Based on Tasks\' Work'),
1321 'timesheet': fields.boolean('Based on Timesheet'),
1322 'order_policy': fields.selection([
1323 ('manual', 'Invoice Based on Sales Orders'),
1324 ('picking', 'Invoice Based on Deliveries'),
1325 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1326 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1327 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1330 'order_policy': 'manual',
1331 'time_unit': lambda self, cr, uid, c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c) and self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c)[0] or False,
1334 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1337 res.update({'order_policy': 'manual'})
1339 res.update({'order_policy': 'picking'})
1340 return {'value':res}
1342 def execute(self, cr, uid, ids, context=None):
1343 ir_values_obj = self.pool.get('ir.values')
1344 data_obj = self.pool.get('ir.model.data')
1345 menu_obj = self.pool.get('ir.ui.menu')
1346 module_obj = self.pool.get('ir.module.module')
1347 module_upgrade_obj = self.pool.get('base.module.upgrade')
1350 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1352 wizard = self.browse(cr, uid, ids)[0]
1354 if wizard.sale_orders:
1355 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1356 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1358 if wizard.deli_orders:
1359 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1360 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1362 if wizard.task_work:
1363 module_name.append('project_timesheet')
1364 module_name.append('project_mrp')
1365 module_name.append('account_analytic_analysis')
1367 if wizard.timesheet:
1368 module_name.append('account_analytic_analysis')
1370 if wizard.charge_delivery:
1371 module_name.append('delivery')
1373 if len(module_name):
1375 need_install = False
1377 for module in module_name:
1378 data_id = module_obj.name_search(cr, uid , module, [], '=')
1379 module_ids.append(data_id[0][0])
1381 for module in module_obj.browse(cr, uid, module_ids):
1382 if module.state == 'uninstalled':
1383 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1387 pooler.restart_pool(cr.dbname, update_module=True)[1]
1389 if wizard.time_unit:
1390 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1391 product_obj = self.pool.get('product.product')
1392 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1394 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1395 if wizard.task_work and wizard.time_unit:
1396 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1397 self.pool.get('res.company').write(cr, uid, [company_id], {
1398 'project_time_mode_id': wizard.time_unit.id
1401 sale_config_picking_policy()
1403 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: