[IMP] change unnecessary code
[odoo/odoo.git] / addons / sale_stock / sale_stock.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
12 #    This program is distributed in the hope that it will be useful,
13
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.
17 #
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/>.
20 #
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 _
27 import pytz
28 from openerp import SUPERUSER_ID
29
30 class sale_order(osv.osv):
31     _inherit = "sale.order"
32     def copy(self, cr, uid, id, default=None, context=None):
33         if not default:
34             default = {}
35         default.update({
36             'shipped': False,
37             'picking_ids': []
38         })
39         return super(sale_order, self).copy(cr, uid, id, default, context=context)
40
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)
44         if not warehouse_ids:
45             raise osv.except_osv(_('Error!'), _('There is no warehouse defined for selected company.'))
46         return warehouse_ids[0]
47
48     def _get_shipped(self, cr, uid, ids, name, args, context=None):
49         res = {}
50         for sale in self.browse(cr, uid, ids, context=context):
51             group = sale.procurement_group_id
52             if group:
53                 res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids])
54             else:
55                 res[sale.id] = False
56         return res
57
58     def _get_orders(self, cr, uid, ids, context=None):
59         res = set()
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)
63         return list(res)
64
65     def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
66         res = {}
67         for sale in self.browse(cr, uid, ids, context=context):
68             if not sale.procurement_group_id:
69                 res[sale.id] = []
70                 continue
71             picking_ids = {}
72             for procurement in sale.procurement_group_id.procurement_ids:
73                 for move in procurement.move_ids:
74                     if move.picking_id:
75                         picking_ids[move.picking_id.id] = True
76             res[sale.id] = picking_ids.keys()
77         return res
78
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
83         return vals
84
85     _columns = {
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'),
95             ('done', 'Done'),
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'),
113     }
114     _defaults = {
115         'warehouse_id': _get_default_warehouse,
116         'picking_policy': 'direct',
117         'order_policy': 'manual',
118     }
119     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
120         val = {}
121         if warehouse_id:
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}
126
127     # FP Note: to change, take the picking related to the moves related to the
128     # procurements related to SO lines
129
130     def action_view_delivery(self, cr, uid, ids, context=None):
131         '''
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.
135         '''
136         mod_obj = self.pool.get('ir.model.data')
137         act_obj = self.pool.get('ir.actions.act_window')
138
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]
142
143         #compute the number of delivery orders to display
144         pick_ids = []
145         for so in self.browse(cr, uid, ids, context=context):
146             pick_ids += [picking.id for picking in so.picking_ids]
147
148         #choose the view_mode accordingly
149         if len(pick_ids) > 1:
150             result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
151         else:
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
155         return result
156
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'})
166         return res
167
168     def action_cancel(self, cr, uid, ids, context=None):
169         if context is None:
170             context = {}
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)])
184                  #        if proc_ids:
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)
188
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)
195         return res
196
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")
203         finished = True
204         canceled = False
205         write_done_ids = []
206         write_cancel_ids = []
207         for order in self.browse(cr, uid, ids, context={}):
208             for line in order.order_line:
209                 for procurement in line.procurement_ids:
210                     if procurement.state != 'done':
211                         write_done_ids.append(line.id)
212                     else:
213                         finished = False
214                     if (procurement.state == 'cancel'):
215                         canceled = True
216                         if line.state != 'exception':
217                             write_cancel_ids.append(line.id)
218         if write_done_ids:
219             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
220         if write_cancel_ids:
221             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
222
223         if mode == 'finished':
224             return finished
225         elif mode == 'canceled':
226             return canceled
227
228
229     def action_ship_end(self, cr, uid, ids, context=None):
230         for order in self.browse(cr, uid, ids, context=context):
231             val = {'shipped': True}
232             if order.state == 'shipping_except':
233                 val['state'] = 'progress'
234                 if (order.order_policy == 'manual'):
235                     for line in order.order_line:
236                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
237                             val['state'] = 'manual'
238                             break
239             for line in order.order_line:
240                 towrite = []
241                 if line.state == 'exception':
242                     towrite.append(line.id)
243                 if towrite:
244                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
245             res = self.write(cr, uid, [order.id], val)
246         return True
247
248     def has_stockable_products(self, cr, uid, ids, *args):
249         for order in self.browse(cr, uid, ids):
250             for order_line in order.order_line:
251                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
252                     return True
253         return False
254
255     def procurement_lines_get(self, cr, uid, ids, *args):
256         res = []
257         for order in self.browse(cr, uid, ids, context={}):
258             for line in order.order_line:
259                 if line.procurement_id:
260                     res.append(line.procurement_id.id)
261         return res
262
263 class sale_order_line(osv.osv):
264     _inherit = 'sale.order.line'
265
266     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
267         res = {}
268         for line in self.browse(cr, uid, ids, context=context):
269             try:
270                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
271             except:
272                 res[line.id] = 1
273         return res
274
275     _columns = {
276         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
277         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
278     }
279
280     _defaults = {
281         'product_packaging': False,
282     }
283
284
285     def button_cancel(self, cr, uid, ids, context=None):
286         res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
287         for line in self.browse(cr, uid, ids, context=context):
288             for move_line in line.move_ids:
289                 if move_line.state != 'cancel':
290                     raise osv.except_osv(
291                             _('Cannot cancel sales order line!'),
292                             _('You must first cancel stock moves attached to this sales order line.'))
293         return res
294
295     def copy_data(self, cr, uid, id, default=None, context=None):
296         if not default:
297             default = {}
298         default.update({'move_ids': []})
299         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
300
301     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
302                                    partner_id=False, packaging=False, flag=False, context=None):
303         if not product:
304             return {'value': {'product_packaging': False}}
305         product_obj = self.pool.get('product.product')
306         product_uom_obj = self.pool.get('product.uom')
307         pack_obj = self.pool.get('product.packaging')
308         warning = {}
309         result = {}
310         warning_msgs = ''
311         if flag:
312             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
313                     product=product, qty=qty, uom=uom, partner_id=partner_id,
314                     packaging=packaging, flag=False, context=context)
315             warning_msgs = res.get('warning') and res['warning']['message']
316
317         products = product_obj.browse(cr, uid, product, context=context)
318         if not products.packaging:
319             packaging = result['product_packaging'] = False
320         elif not packaging and products.packaging and not flag:
321             packaging = products.packaging[0].id
322             result['product_packaging'] = packaging
323
324         if packaging:
325             default_uom = products.uom_id and products.uom_id.id
326             pack = pack_obj.browse(cr, uid, packaging, context=context)
327             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
328 #            qty = qty - qty % q + q
329             if qty and (q and not (qty % q) == 0):
330                 ean = pack.ean or _('(n/a)')
331                 qty_pack = pack.qty
332                 type_ul = pack.ul
333                 if not warning_msgs:
334                     warn_msg = _("You selected a quantity of %d Units.\n"
335                                 "But it's not compatible with the selected packaging.\n"
336                                 "Here is a proposition of quantities according to the packaging:\n"
337                                 "EAN: %s Quantity: %s Type of ul: %s") % \
338                                     (qty, ean, qty_pack, type_ul.name)
339                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
340                 warning = {
341                        'title': _('Configuration Error!'),
342                        'message': warning_msgs
343                 }
344             result['product_uom_qty'] = qty
345
346         return {'value': result, 'warning': warning}
347
348
349     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
350             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
351             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
352         context = context or {}
353         product_uom_obj = self.pool.get('product.uom')
354         partner_obj = self.pool.get('res.partner')
355         product_obj = self.pool.get('product.product')
356         warning = {}
357         res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
358             uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
359             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
360
361         if not product:
362             res['value'].update({'product_packaging': False})
363             return res
364
365         #update of result obtained in super function
366         product_obj = product_obj.browse(cr, uid, product, context=context)
367         res['value']['delay'] = (product_obj.sale_delay or 0.0)
368         #res['value']['type'] = product_obj.procure_method
369
370         #check if product is available, and if not: raise an error
371         uom2 = False
372         if uom:
373             uom2 = product_uom_obj.browse(cr, uid, uom)
374             if product_obj.uom_id.category_id.id != uom2.category_id.id:
375                 uom = False
376         if not uom2:
377             uom2 = product_obj.uom_id
378
379         # Calling product_packaging_change function after updating UoM
380         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
381         res['value'].update(res_packing.get('value', {}))
382         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
383         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
384         if (product_obj.type=='product') and int(compare_qty) == -1:
385           #and (product_obj.procure_method=='make_to_stock'): --> need to find alternative for procure_method
386             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
387                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
388                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
389                      max(0,product_obj.qty_available), product_obj.uom_id.name)
390             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
391
392         #update of warning messages
393         if warning_msgs:
394             warning = {
395                        'title': _('Configuration Error!'),
396                        'message' : warning_msgs
397                     }
398         res.update({'warning': warning})
399         return res
400