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,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
22 from datetime import datetime, timedelta
23 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
24 from dateutil.relativedelta import relativedelta
25 from openerp.osv import fields, osv
26 from openerp.tools.safe_eval import safe_eval as eval
27 from openerp.tools.translate import _
29 from openerp import SUPERUSER_ID
31 class sale_order(osv.osv):
32 _inherit = "sale.order"
34 def copy(self, cr, uid, id, default=None, context=None):
41 return super(sale_order, self).copy(cr, uid, id, default, context=context)
43 def _get_default_warehouse(self, cr, uid, context=None):
44 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
45 warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
48 return warehouse_ids[0]
50 def _get_shipped(self, cr, uid, ids, name, args, context=None):
52 for sale in self.browse(cr, uid, ids, context=context):
53 group = sale.procurement_group_id
55 res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids])
60 def _get_orders(self, cr, uid, ids, context=None):
62 for move in self.browse(cr, uid, ids, context=context):
63 if move.procurement_id and move.procurement_id.sale_line_id:
64 res.add(move.procurement_id.sale_line_id.order_id.id)
67 def _get_orders_procurements(self, cr, uid, ids, context=None):
69 for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context):
70 if proc.state =='done' and proc.sale_line_id:
71 res.add(proc.sale_line_id.order_id.id)
74 def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
76 for sale in self.browse(cr, uid, ids, context=context):
77 if not sale.procurement_group_id:
80 res[sale.id] = self.pool.get('stock.picking').search(cr, uid, [('group_id', '=', sale.procurement_group_id.id)], context=context)
83 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
84 vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
85 location_id = order.partner_shipping_id.property_stock_customer.id
86 vals['location_id'] = location_id
87 routes = line.route_id and [(4, line.route_id.id)] or []
88 vals['route_ids'] = routes
89 vals['warehouse_id'] = order.warehouse_id and order.warehouse_id.id or False
90 vals['partner_dest_id'] = order.partner_shipping_id.id
94 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
95 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
96 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
97 help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
98 'order_policy': fields.selection([
99 ('manual', 'On Demand'),
100 ('picking', 'On Delivery Order'),
101 ('prepaid', 'Before Delivery'),
102 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
103 help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""),
104 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={
105 'procurement.order': (_get_orders_procurements, ['state'], 10)
107 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
108 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'),
111 'warehouse_id': _get_default_warehouse,
112 'picking_policy': 'direct',
113 'order_policy': 'manual',
115 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
118 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
119 if warehouse.company_id:
120 val['company_id'] = warehouse.company_id.id
121 return {'value': val}
123 # FP Note: to change, take the picking related to the moves related to the
124 # procurements related to SO lines
126 def action_view_delivery(self, cr, uid, ids, context=None):
128 This function returns an action that display existing delivery orders
129 of given sales order ids. It can either be a in a list or in a form
130 view, if there is only one delivery order to show.
133 mod_obj = self.pool.get('ir.model.data')
134 act_obj = self.pool.get('ir.actions.act_window')
136 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree_all')
137 id = result and result[1] or False
138 result = act_obj.read(cr, uid, [id], context=context)[0]
140 #compute the number of delivery orders to display
142 for so in self.browse(cr, uid, ids, context=context):
143 pick_ids += [picking.id for picking in so.picking_ids]
145 #choose the view_mode accordingly
146 if len(pick_ids) > 1:
147 result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
149 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
150 result['views'] = [(res and res[1] or False, 'form')]
151 result['res_id'] = pick_ids and pick_ids[0] or False
155 # TODO: FP Note: I guess it's better to do:
156 # if order_policy<>picking: super()
157 # else: call invoice_on_picking_method()
158 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
159 move_obj = self.pool.get("stock.move")
160 res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
161 for order in self.browse(cr, uid, ids, context=context):
162 if order.order_policy == 'picking':
163 for picking in order.picking_ids:
164 move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context)
167 def action_cancel(self, cr, uid, ids, context=None):
170 sale_order_line_obj = self.pool.get('sale.order.line')
171 proc_obj = self.pool.get('procurement.order')
172 stock_obj = self.pool.get('stock.picking')
173 for sale in self.browse(cr, uid, ids, context=context):
174 for pick in sale.picking_ids:
175 if pick.state not in ('draft', 'cancel'):
176 raise osv.except_osv(
177 _('Cannot cancel sales order!'),
178 _('You must first cancel all delivery order(s) attached to this sales order.'))
179 # FP Note: not sure we need this
180 #if pick.state == 'cancel':
181 # for mov in pick.move_lines:
182 # proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
184 # proc_obj.signal_button_check(cr, uid, proc_ids)
185 stock_obj.signal_button_cancel(cr, uid, [p.id for p in sale.picking_ids])
186 return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
188 def action_wait(self, cr, uid, ids, context=None):
189 res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
190 for o in self.browse(cr, uid, ids):
191 noprod = self.test_no_product(cr, uid, o, context)
192 if noprod and o.order_policy=='picking':
193 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
196 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
197 date_planned = super(sale_order, self)._get_date_planned(cr, uid, order, line, start_date, context=context)
198 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
201 def _prepare_procurement_group(self, cr, uid, order, context=None):
202 res = super(sale_order, self)._prepare_procurement_group(cr, uid, order, context=None)
203 res.update({'move_type': order.picking_policy})
206 def action_ship_end(self, cr, uid, ids, context=None):
207 super(sale_order, self).action_ship_end(cr, uid, ids, context=context)
208 for order in self.browse(cr, uid, ids, context=context):
209 val = {'shipped': True}
210 if order.state == 'shipping_except':
211 val['state'] = 'progress'
212 if (order.order_policy == 'manual'):
213 for line in order.order_line:
214 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
215 val['state'] = 'manual'
217 res = self.write(cr, uid, [order.id], val)
223 def has_stockable_products(self, cr, uid, ids, *args):
224 for order in self.browse(cr, uid, ids):
225 for order_line in order.order_line:
226 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
231 class sale_order_line(osv.osv):
232 _inherit = 'sale.order.line'
234 def need_procurement(self, cr, uid, ids, context=None):
235 #when sale is installed alone, there is no need to create procurements, but with sale_stock
236 #we must create a procurement for each product that is not a service.
237 for line in self.browse(cr, uid, ids, context=context):
238 if line.product_id and line.product_id.type != 'service':
240 return super(sale_order_line, self).need_procurement(cr, uid, ids, context=context)
242 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
244 for line in self.browse(cr, uid, ids, context=context):
246 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
252 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
253 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
254 'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]),
258 'product_packaging': False,
261 def button_cancel(self, cr, uid, ids, context=None):
262 res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
263 for line in self.browse(cr, uid, ids, context=context):
264 for move_line in line.move_ids:
265 if move_line.state != 'cancel':
266 raise osv.except_osv(
267 _('Cannot cancel sales order line!'),
268 _('You must first cancel stock moves attached to this sales order line.'))
271 def copy_data(self, cr, uid, id, default=None, context=None):
274 default.update({'move_ids': []})
275 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
277 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
278 partner_id=False, packaging=False, flag=False, context=None):
280 return {'value': {'product_packaging': False}}
281 product_obj = self.pool.get('product.product')
282 product_uom_obj = self.pool.get('product.uom')
283 pack_obj = self.pool.get('product.packaging')
288 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
289 product=product, qty=qty, uom=uom, partner_id=partner_id,
290 packaging=packaging, flag=False, context=context)
291 warning_msgs = res.get('warning') and res['warning'].get('message', '') or ''
293 products = product_obj.browse(cr, uid, product, context=context)
294 if not products.packaging:
295 packaging = result['product_packaging'] = False
298 default_uom = products.uom_id and products.uom_id.id
299 pack = pack_obj.browse(cr, uid, packaging, context=context)
300 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
301 # qty = qty - qty % q + q
302 if qty and (q and not (qty % q) == 0):
303 ean = pack.ean or _('(n/a)')
307 warn_msg = _("You selected a quantity of %d Units.\n"
308 "But it's not compatible with the selected packaging.\n"
309 "Here is a proposition of quantities according to the packaging:\n"
310 "EAN: %s Quantity: %s Type of ul: %s") % \
311 (qty, ean, qty_pack, type_ul.name)
312 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
314 'title': _('Configuration Error!'),
315 'message': warning_msgs
317 result['product_uom_qty'] = qty
319 return {'value': result, 'warning': warning}
322 def product_id_change_with_wh(self, cr, uid, ids, pricelist, product, qty=0,
323 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
324 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, warehouse_id=False, context=None):
325 context = context or {}
326 product_uom_obj = self.pool.get('product.uom')
327 product_obj = self.pool.get('product.product')
329 res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
330 uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
331 lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
334 res['value'].update({'product_packaging': False})
337 #update of result obtained in super function
338 product_obj = product_obj.browse(cr, uid, product, context=context)
339 res['value']['delay'] = (product_obj.sale_delay or 0.0)
341 # Calling product_packaging_change function after updating UoM
342 res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
343 res['value'].update(res_packing.get('value', {}))
344 warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
346 #determine if the product is MTO or not (for a further check)
349 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
350 for product_route in product_obj.route_ids:
351 if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id:
356 mto_route_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'route_warehouse0_mto').id
358 # if route MTO not found in ir_model_data, we treat the product as in MTS
361 for product_route in product_obj.route_ids:
362 if product_route.id == mto_route_id:
366 #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO
370 uom2 = product_uom_obj.browse(cr, uid, uom, context=context)
371 if product_obj.uom_id.category_id.id != uom2.category_id.id:
374 uom2 = product_obj.uom_id
375 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
376 if (product_obj.type=='product') and int(compare_qty) == -1:
377 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
378 (qty, uom2 and uom2.name or product_obj.uom_id.name,
379 max(0,product_obj.virtual_available), product_obj.uom_id.name,
380 max(0,product_obj.qty_available), product_obj.uom_id.name)
381 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
383 #update of warning messages
386 'title': _('Configuration Error!'),
387 'message' : warning_msgs
389 res.update({'warning': warning})
392 class stock_move(osv.osv):
393 _inherit = 'stock.move'
395 def action_cancel(self, cr, uid, ids, context=None):
397 for move in self.browse(cr, uid, ids, context=context):
398 if move.procurement_id and move.procurement_id.sale_line_id:
399 sale_ids.append(move.procurement_id.sale_line_id.order_id.id)
401 self.pool.get('sale.order').signal_ship_except(cr, uid, sale_ids)
402 return super(stock_move, self).action_cancel(cr, uid, ids, context=context)
404 def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
405 invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
406 if move.procurement_id and move.procurement_id.sale_line_id:
407 sale_line = move.procurement_id.sale_line_id
408 self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], {
409 'invoice_lines': [(4, invoice_line_id)]
411 self.pool.get('sale.order').write(cr, uid, [sale_line.order_id.id], {
412 'invoice_ids': [(4, invoice_line_vals['invoice_id'])],
414 return invoice_line_id
416 def _get_master_data(self, cr, uid, move, company, context=None):
417 if move.procurement_id and move.procurement_id.sale_line_id:
418 sale_order = move.procurement_id.sale_line_id.order_id
419 return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id
420 return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context)
422 def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None):
423 res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context)
424 if move.procurement_id and move.procurement_id.sale_line_id:
425 sale_line = move.procurement_id.sale_line_id
426 res['invoice_line_tax_id'] = [(6, 0, [x.id for x in sale_line.tax_id])]
427 res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False
428 res['price_unit'] = sale_line.price_unit
429 res['discount'] = sale_line.discount
433 class stock_location_route(osv.osv):
434 _inherit = "stock.location.route"
436 'sale_selectable': fields.boolean("Selectable on Sales Order Line")
440 class stock_picking(osv.osv):
441 _inherit = "stock.picking"
443 def _get_sale_id(self, cr, uid, ids, name, args, context=None):
444 sale_obj = self.pool.get("sale.order")
446 for picking in self.browse(cr, uid, ids, context=context):
447 res[picking.id] = False
449 sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
451 res[picking.id] = sale_ids[0]
455 'sale_id': fields.function(_get_sale_id, type="many2one", relation="sale.order", string="Sale Order"),
458 def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
459 sale_obj = self.pool.get('sale.order')
460 sale_line_obj = self.pool.get('sale.order.line')
461 invoice_line_obj = self.pool.get('account.invoice.line')
462 invoice_id = super(stock_picking, self)._create_invoice_from_picking(cr, uid, picking, vals, context=context)
464 sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
466 sale_line_ids = sale_line_obj.search(cr, uid, [('order_id', 'in', sale_ids), ('product_id.type', '=', 'service'), ('invoiced', '=', False)], context=context)
468 created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context)
469 invoice_line_obj.write(cr, uid, created_lines, {'invoice_id': invoice_id}, context=context)