3163365ffc0de7faaff610408bd90b78229012f9
[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             #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])
213                 if cancel:
214                     canceled = True
215                     if line.state != 'exception':
216                             write_cancel_ids.append(line.id)
217                 if not doneorcancel:
218                     finished = False 
219                 if doneorcancel and not cancel:
220                     write_done_ids.append(line.id)
221
222         if write_done_ids:
223             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
224         if write_cancel_ids:
225             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
226
227         if mode == 'finished':
228             return finished
229         elif mode == 'canceled':
230             return canceled
231
232
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'
242                             break
243             for line in order.order_line:
244                 towrite = []
245                 if line.state == 'exception':
246                     towrite.append(line.id)
247                 if towrite:
248                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
249             res = self.write(cr, uid, [order.id], val)
250         return True
251
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'):
256                     return True
257         return False
258
259     def procurement_lines_get(self, cr, uid, ids, *args):
260         res = []
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]
264         return res
265
266 class sale_order_line(osv.osv):
267     _inherit = 'sale.order.line'
268
269     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
270         res = {}
271         for line in self.browse(cr, uid, ids, context=context):
272             try:
273                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
274             except:
275                 res[line.id] = 1
276         return res
277
278     _columns = {
279         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
280         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
281     }
282
283     _defaults = {
284         'product_packaging': False,
285     }
286
287
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.'))
296         return res
297
298     def copy_data(self, cr, uid, id, default=None, context=None):
299         if not default:
300             default = {}
301         default.update({'move_ids': []})
302         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
303
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):
306         if not product:
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')
311         warning = {}
312         result = {}
313         warning_msgs = ''
314         if flag:
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']
319
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
326
327         if 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)')
334                 qty_pack = pack.qty
335                 type_ul = pack.ul
336                 if not warning_msgs:
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"
343                 warning = {
344                        'title': _('Configuration Error!'),
345                        'message': warning_msgs
346                 }
347             result['product_uom_qty'] = qty
348
349         return {'value': result, 'warning': warning}
350
351
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')
359         warning = {}
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)
363
364         if not product:
365             res['value'].update({'product_packaging': False})
366             return res
367
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
372
373         #check if product is available, and if not: raise an error
374         uom2 = False
375         if uom:
376             uom2 = product_uom_obj.browse(cr, uid, uom)
377             if product_obj.uom_id.category_id.id != uom2.category_id.id:
378                 uom = False
379         if not uom2:
380             uom2 = product_obj.uom_id
381
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"
394
395         #update of warning messages
396         if warning_msgs:
397             warning = {
398                        'title': _('Configuration Error!'),
399                        'message' : warning_msgs
400                     }
401         res.update({'warning': warning})
402         return res
403