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.translate import _
28 from openerp import SUPERUSER_ID
30 class sale_order(osv.osv):
31 _inherit = "sale.order"
32 def copy(self, cr, uid, id, default=None, context=None):
39 return super(sale_order, self).copy(cr, uid, id, default, context=context)
41 def _get_default_warehouse(self, cr, uid, context=None):
42 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
43 warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
45 raise osv.except_osv(_('Error!'), _('There is no warehouse defined for selected company.'))
46 return warehouse_ids[0]
48 def _get_shipped(self, cr, uid, ids, name, args, context=None):
50 for sale in self.browse(cr, uid, ids, context=context):
51 group = sale.procurement_group_id
53 res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids])
58 def _get_orders(self, cr, uid, ids, context=None):
60 for move in self.browse(cr, uid, ids, context=context):
61 if move.procurement_id and move.procurement_id.sale_line_id:
62 res.add(move.procurement_id.sale_line_id.order_id.id)
65 def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
67 for sale in self.browse(cr, uid, ids, context=context):
68 if not sale.procurement_group_id:
72 for procurement in sale.procurement_group_id.procurement_ids:
73 for move in procurement.move_ids:
75 picking_ids[move.picking_id.id] = True
76 res[sale.id] = picking_ids.keys()
79 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id = False, context=None):
80 vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
81 location_id = order.partner_shipping_id.property_stock_customer.id
82 vals['location_id'] = location_id
86 'state': fields.selection([
87 ('draft', 'Draft Quotation'),
88 ('sent', 'Quotation Sent'),
89 ('cancel', 'Cancelled'),
90 ('waiting_date', 'Waiting Schedule'),
91 ('progress', 'Sales Order'),
92 ('manual', 'Sale to Invoice'),
93 ('shipping_except', 'Shipping Exception'),
94 ('invoice_except', 'Invoice Exception'),
96 ], 'Status', readonly=True, help="Gives the status of the quotation or sales order.\
97 \nThe exception status is automatically set when a cancel operation occurs \
98 in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
99 but waiting for the scheduler to run on the order date.", select=True),
100 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
101 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
102 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
103 help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
104 'order_policy': fields.selection([
105 ('manual', 'On Demand'),
106 ('picking', 'On Delivery Order'),
107 ('prepaid', 'Before Delivery'),
108 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
109 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."""),
110 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={'stock.move': (_get_orders, ['state'], 10)}),
111 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
112 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'),
115 'warehouse_id': _get_default_warehouse,
116 'picking_policy': 'direct',
117 'order_policy': 'manual',
119 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
122 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
123 if warehouse.company_id:
124 val['company_id'] = warehouse.company_id.id
125 return {'value': val}
127 # FP Note: to change, take the picking related to the moves related to the
128 # procurements related to SO lines
130 def action_view_delivery(self, cr, uid, ids, context=None):
132 This function returns an action that display existing delivery orders
133 of given sales order ids. It can either be a in a list or in a form
134 view, if there is only one delivery order to show.
136 mod_obj = self.pool.get('ir.model.data')
137 act_obj = self.pool.get('ir.actions.act_window')
139 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
140 id = result and result[1] or False
141 result = act_obj.read(cr, uid, [id], context=context)[0]
143 #compute the number of delivery orders to display
145 for so in self.browse(cr, uid, ids, context=context):
146 pick_ids += [picking.id for picking in so.picking_ids]
148 #choose the view_mode accordingly
149 if len(pick_ids) > 1:
150 result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
152 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
153 result['views'] = [(res and res[1] or False, 'form')]
154 result['res_id'] = pick_ids and pick_ids[0] or False
157 # TODO: FP Note: I guess it's better to do:
158 # if order_policy<>picking: super()
159 # else: call invoice_on_picking_method()
160 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
161 picking_obj = self.pool.get('stock.picking')
162 res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
163 for order in self.browse(cr, uid, ids, context=context):
164 if order.order_policy == 'picking':
165 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
168 def action_cancel(self, cr, uid, ids, context=None):
171 sale_order_line_obj = self.pool.get('sale.order.line')
172 proc_obj = self.pool.get('procurement.order')
173 stock_obj = self.pool.get('stock.picking')
174 for sale in self.browse(cr, uid, ids, context=context):
175 for pick in sale.picking_ids:
176 if pick.state not in ('draft', 'cancel'):
177 raise osv.except_osv(
178 _('Cannot cancel sales order!'),
179 _('You must first cancel all delivery order(s) attached to this sales order.'))
180 # FP Note: not sure we need this
181 #if pick.state == 'cancel':
182 # for mov in pick.move_lines:
183 # proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
185 # proc_obj.signal_button_check(cr, uid, proc_ids)
186 stock_obj.signal_button_cancel(cr, uid, [p.id for p in sale.picking_ids])
187 return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
189 def action_wait(self, cr, uid, ids, context=None):
190 res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
191 for o in self.browse(cr, uid, ids):
192 noprod = self.test_no_product(cr, uid, o, context)
193 if noprod and o.order_policy=='picking':
194 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
197 # if mode == 'finished':
198 # returns True if all lines are done, False otherwise
199 # if mode == 'canceled':
200 # returns True if there is at least one canceled line, False otherwise
201 def test_state(self, cr, uid, ids, mode, *args):
202 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
206 write_cancel_ids = []
207 for order in self.browse(cr, uid, ids, context={}):
208 #TODO: Need to rethink what happens when cancelling
209 for line in order.order_line:
210 states = [x.state for x in line.procurement_ids]
211 cancel = all([x == 'cancel' for x in states])
212 doneorcancel = all([x in ('done', 'cancel') for x in states])
215 if line.state != 'exception':
216 write_cancel_ids.append(line.id)
219 if doneorcancel and not cancel:
220 write_done_ids.append(line.id)
223 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
225 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
227 if mode == 'finished':
229 elif mode == 'canceled':
233 def action_ship_end(self, cr, uid, ids, context=None):
234 for order in self.browse(cr, uid, ids, context=context):
235 val = {'shipped': True}
236 if order.state == 'shipping_except':
237 val['state'] = 'progress'
238 if (order.order_policy == 'manual'):
239 for line in order.order_line:
240 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
241 val['state'] = 'manual'
243 for line in order.order_line:
245 if line.state == 'exception':
246 towrite.append(line.id)
248 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
249 res = self.write(cr, uid, [order.id], val)
252 def has_stockable_products(self, cr, uid, ids, *args):
253 for order in self.browse(cr, uid, ids):
254 for order_line in order.order_line:
255 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
259 def procurement_lines_get(self, cr, uid, ids, *args):
261 for order in self.browse(cr, uid, ids, context={}):
262 for line in order.order_line:
263 res += [x.id for x in line.procurement_ids]
266 class sale_order_line(osv.osv):
267 _inherit = 'sale.order.line'
269 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
271 for line in self.browse(cr, uid, ids, context=context):
273 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
279 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
280 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
284 'product_packaging': False,
288 def button_cancel(self, cr, uid, ids, context=None):
289 res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
290 for line in self.browse(cr, uid, ids, context=context):
291 for move_line in line.move_ids:
292 if move_line.state != 'cancel':
293 raise osv.except_osv(
294 _('Cannot cancel sales order line!'),
295 _('You must first cancel stock moves attached to this sales order line.'))
298 def copy_data(self, cr, uid, id, default=None, context=None):
301 default.update({'move_ids': []})
302 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
304 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
305 partner_id=False, packaging=False, flag=False, context=None):
307 return {'value': {'product_packaging': False}}
308 product_obj = self.pool.get('product.product')
309 product_uom_obj = self.pool.get('product.uom')
310 pack_obj = self.pool.get('product.packaging')
315 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
316 product=product, qty=qty, uom=uom, partner_id=partner_id,
317 packaging=packaging, flag=False, context=context)
318 warning_msgs = res.get('warning') and res['warning']['message']
320 products = product_obj.browse(cr, uid, product, context=context)
321 if not products.packaging:
322 packaging = result['product_packaging'] = False
323 elif not packaging and products.packaging and not flag:
324 packaging = products.packaging[0].id
325 result['product_packaging'] = packaging
328 default_uom = products.uom_id and products.uom_id.id
329 pack = pack_obj.browse(cr, uid, packaging, context=context)
330 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
331 # qty = qty - qty % q + q
332 if qty and (q and not (qty % q) == 0):
333 ean = pack.ean or _('(n/a)')
337 warn_msg = _("You selected a quantity of %d Units.\n"
338 "But it's not compatible with the selected packaging.\n"
339 "Here is a proposition of quantities according to the packaging:\n"
340 "EAN: %s Quantity: %s Type of ul: %s") % \
341 (qty, ean, qty_pack, type_ul.name)
342 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
344 'title': _('Configuration Error!'),
345 'message': warning_msgs
347 result['product_uom_qty'] = qty
349 return {'value': result, 'warning': warning}
352 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
353 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
354 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
355 context = context or {}
356 product_uom_obj = self.pool.get('product.uom')
357 partner_obj = self.pool.get('res.partner')
358 product_obj = self.pool.get('product.product')
360 res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
361 uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
362 lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
365 res['value'].update({'product_packaging': False})
368 #update of result obtained in super function
369 product_obj = product_obj.browse(cr, uid, product, context=context)
370 res['value']['delay'] = (product_obj.sale_delay or 0.0)
371 #res['value']['type'] = product_obj.procure_method
373 #check if product is available, and if not: raise an error
376 uom2 = product_uom_obj.browse(cr, uid, uom)
377 if product_obj.uom_id.category_id.id != uom2.category_id.id:
380 uom2 = product_obj.uom_id
382 # Calling product_packaging_change function after updating UoM
383 res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
384 res['value'].update(res_packing.get('value', {}))
385 warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
386 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
387 if (product_obj.type=='product') and int(compare_qty) == -1:
388 #and (product_obj.procure_method=='make_to_stock'): --> need to find alternative for procure_method
389 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
390 (qty, uom2 and uom2.name or product_obj.uom_id.name,
391 max(0,product_obj.virtual_available), product_obj.uom_id.name,
392 max(0,product_obj.qty_available), product_obj.uom_id.name)
393 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
395 #update of warning messages
398 'title': _('Configuration Error!'),
399 'message' : warning_msgs
401 res.update({'warning': warning})