Task misc -Step 2
[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.safe_eval import safe_eval as eval
27 from openerp.tools.translate import _
28 import pytz
29 from openerp import SUPERUSER_ID
30
31 class sale_order(osv.osv):
32     _inherit = "sale.order"
33
34     def copy(self, cr, uid, id, default=None, context=None):
35         if not default:
36             default = {}
37         default.update({
38             'shipped': False,
39             'picking_ids': []
40         })
41         return super(sale_order, self).copy(cr, uid, id, default, context=context)
42
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)
46         if not warehouse_ids:
47             raise osv.except_osv(_('Error!'), _('There is no warehouse defined for selected company.'))
48         return warehouse_ids[0]
49
50     def _get_shipped(self, cr, uid, ids, name, args, context=None):
51         res = {}
52         for sale in self.browse(cr, uid, ids, context=context):
53             group = sale.procurement_group_id
54             if group:
55                 res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids])
56             else:
57                 res[sale.id] = False
58         return res
59
60     def _get_orders(self, cr, uid, ids, context=None):
61         res = set()
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)
65         return list(res)
66     
67     def _get_orders_procurements(self, cr, uid, ids, context=None):
68         res = set()
69         for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context):
70             if proc.sale_line_id:
71                 res.add(proc.sale_line_id.order_id.id)
72         return list(res)
73     
74     def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
75         res = {}
76         for sale in self.browse(cr, uid, ids, context=context):
77             if not sale.procurement_group_id:
78                 res[sale.id] = []
79                 continue
80             picking_ids = set()
81             for procurement in sale.procurement_group_id.procurement_ids:
82                 for move in procurement.move_ids:
83                     if move.picking_id:
84                         picking_ids.add(move.picking_id.id)
85             res[sale.id] = list(picking_ids)
86         return res
87
88     def _prepare_order_line_procurement(self, cr, uid, order, line, group_id = False, context=None):
89         vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
90         location_id = order.partner_shipping_id.property_stock_customer.id
91         vals['location_id'] = location_id
92         return vals
93
94     _columns = {
95         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
96         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
97             'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
98             help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
99         'order_policy': fields.selection([
100                 ('manual', 'On Demand'),
101                 ('picking', 'On Delivery Order'),
102                 ('prepaid', 'Before Delivery'),
103             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
104             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."""),
105         'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={
106                 'stock.move': (_get_orders, ['state'], 10),
107                 'procurement.order': (_get_orders_procurements, ['state'], 10)
108             }),
109         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
110         'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'),
111     }
112     _defaults = {
113         'warehouse_id': _get_default_warehouse,
114         'picking_policy': 'direct',
115         'order_policy': 'manual',
116     }
117     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
118         val = {}
119         if warehouse_id:
120             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
121             if warehouse.company_id:
122                 val['company_id'] = warehouse.company_id.id
123         return {'value': val}
124
125     # FP Note: to change, take the picking related to the moves related to the
126     # procurements related to SO lines
127
128     def action_view_delivery(self, cr, uid, ids, context=None):
129         '''
130         This function returns an action that display existing delivery orders
131         of given sales order ids. It can either be a in a list or in a form
132         view, if there is only one delivery order to show.
133         '''
134         
135         mod_obj = self.pool.get('ir.model.data')
136         act_obj = self.pool.get('ir.actions.act_window')
137
138         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree_all')
139         id = result and result[1] or False
140         result = act_obj.read(cr, uid, [id], context=context)[0]
141
142         #compute the number of delivery orders to display
143         pick_ids = []
144         for so in self.browse(cr, uid, ids, context=context):
145             pick_ids += [picking.id for picking in so.picking_ids]
146             
147         #choose the view_mode accordingly
148         if len(pick_ids) > 1:
149             result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
150         else:
151             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
152             result['views'] = [(res and res[1] or False, 'form')]
153             result['res_id'] = pick_ids and pick_ids[0] or False
154         return result
155
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         move_obj = self.pool.get("stock.move")
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                 for picking in order.picking_ids:
166                     move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context)
167         return res
168
169     def action_cancel(self, cr, uid, ids, context=None):
170         if context is None:
171             context = {}
172         sale_order_line_obj = self.pool.get('sale.order.line')
173         proc_obj = self.pool.get('procurement.order')
174         stock_obj = self.pool.get('stock.picking')
175         for sale in self.browse(cr, uid, ids, context=context):
176             for pick in sale.picking_ids:
177                 if pick.state not in ('draft', 'cancel'):
178                     raise osv.except_osv(
179                         _('Cannot cancel sales order!'),
180                         _('You must first cancel all delivery order(s) attached to this sales order.'))
181                  # FP Note: not sure we need this
182                  #if pick.state == 'cancel':
183                  #    for mov in pick.move_lines:
184                  #        proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
185                  #        if proc_ids:
186                  #            proc_obj.signal_button_check(cr, uid, proc_ids)
187             stock_obj.signal_button_cancel(cr, uid, [p.id for p in sale.picking_ids])
188         return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
189
190     def action_wait(self, cr, uid, ids, context=None):
191         res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
192         for o in self.browse(cr, uid, ids):
193             noprod = self.test_no_product(cr, uid, o, context)
194             if noprod and o.order_policy=='picking':
195                 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
196         return res
197
198     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
199         date_planned = super(sale_order, self)._get_date_planned(cr, uid, order, line, start_date, context=context)
200         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
201         return date_planned
202
203     def _prepare_procurement_group(self, cr, uid, order, context=None):
204         res = super(sale_order, self)._prepare_procurement_group(cr, uid, order, context=None)
205         res.update({'move_type': order.picking_policy})
206         return res
207
208     def action_ship_end(self, cr, uid, ids, context=None):
209         super(sale_order, self).action_ship_end(cr, uid, ids, context=context)
210         for order in self.browse(cr, uid, ids, context=context):
211             val = {'shipped': True}
212             if order.state == 'shipping_except':
213                 val['state'] = 'progress'
214                 if (order.order_policy == 'manual'):
215                     for line in order.order_line:
216                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
217                             val['state'] = 'manual'
218                             break
219             res = self.write(cr, uid, [order.id], val)
220         return True
221
222
223
224
225     def has_stockable_products(self, cr, uid, ids, *args):
226         for order in self.browse(cr, uid, ids):
227             for order_line in order.order_line:
228                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
229                     return True
230         return False
231
232
233 class sale_order_line(osv.osv):
234     _inherit = 'sale.order.line'
235
236     def need_procurement(self, cr, uid, ids, context=None):
237         #when sale is installed alone, there is no need to create procurements, but with sale_stock
238         #we must create a procurement for each product that is not a service.
239         for line in self.browse(cr, uid, ids, context=context):
240             if line.product_id and line.product_id.type != 'service':
241                 return True
242         return super(sale_order_line, self).need_procurement(cr, uid, ids, context=context)
243
244     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
245         res = {}
246         for line in self.browse(cr, uid, ids, context=context):
247             try:
248                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
249             except:
250                 res[line.id] = 1
251         return res
252
253     _columns = {
254         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
255         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
256     }
257
258     _defaults = {
259         'product_packaging': False,
260     }
261
262     def button_cancel(self, cr, uid, ids, context=None):
263         res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
264         for line in self.browse(cr, uid, ids, context=context):
265             for move_line in line.move_ids:
266                 if move_line.state != 'cancel':
267                     raise osv.except_osv(
268                             _('Cannot cancel sales order line!'),
269                             _('You must first cancel stock moves attached to this sales order line.'))
270         return res
271
272     def copy_data(self, cr, uid, id, default=None, context=None):
273         if not default:
274             default = {}
275         default.update({'move_ids': []})
276         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
277
278     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
279                                    partner_id=False, packaging=False, flag=False, context=None):
280         if not product:
281             return {'value': {'product_packaging': False}}
282         product_obj = self.pool.get('product.product')
283         product_uom_obj = self.pool.get('product.uom')
284         pack_obj = self.pool.get('product.packaging')
285         warning = {}
286         result = {}
287         warning_msgs = ''
288         if flag:
289             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
290                     product=product, qty=qty, uom=uom, partner_id=partner_id,
291                     packaging=packaging, flag=False, context=context)
292             warning_msgs = res.get('warning') and res['warning']['message']
293
294         products = product_obj.browse(cr, uid, product, context=context)
295         if not products.packaging:
296             packaging = result['product_packaging'] = False
297         elif not packaging and products.packaging and not flag:
298             packaging = products.packaging[0].id
299             result['product_packaging'] = packaging
300
301         if packaging:
302             default_uom = products.uom_id and products.uom_id.id
303             pack = pack_obj.browse(cr, uid, packaging, context=context)
304             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
305 #            qty = qty - qty % q + q
306             if qty and (q and not (qty % q) == 0):
307                 ean = pack.ean or _('(n/a)')
308                 qty_pack = pack.qty
309                 type_ul = pack.ul
310                 if not warning_msgs:
311                     warn_msg = _("You selected a quantity of %d Units.\n"
312                                 "But it's not compatible with the selected packaging.\n"
313                                 "Here is a proposition of quantities according to the packaging:\n"
314                                 "EAN: %s Quantity: %s Type of ul: %s") % \
315                                     (qty, ean, qty_pack, type_ul.name)
316                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
317                 warning = {
318                        'title': _('Configuration Error!'),
319                        'message': warning_msgs
320                 }
321             result['product_uom_qty'] = qty
322
323         return {'value': result, 'warning': warning}
324
325
326     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
327             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
328             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, warehouse_id=False, context=None):
329         context = context or {}
330         product_uom_obj = self.pool.get('product.uom')
331         product_obj = self.pool.get('product.product')
332         warning = {}
333         res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
334             uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
335             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
336
337         if not product:
338             res['value'].update({'product_packaging': False})
339             return res
340
341         #update of result obtained in super function
342         product_obj = product_obj.browse(cr, uid, product, context=context)
343         res['value']['delay'] = (product_obj.sale_delay or 0.0)
344
345         # Calling product_packaging_change function after updating UoM
346         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
347         res['value'].update(res_packing.get('value', {}))
348         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
349
350         #determine if the product is MTO or not (for a further check)
351         isMto = False
352         if warehouse_id:
353             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
354             for product_route in product_obj.route_ids:
355                 if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id:
356                     isMto = True
357                     break
358         else:
359             try:
360                 mto_route_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'route_warehouse0_mto').id
361             except:
362                 # if route MTO not found in ir_model_data, we treat the product as in MTS
363                 mto_route_id = False
364             if mto_route_id:
365                 for product_route in product_obj.route_ids:
366                     if product_route.id == mto_route_id:
367                         isMto = True
368                         break
369
370         #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO
371         if not isMto:
372             uom2 = False
373             if uom:
374                 uom2 = product_uom_obj.browse(cr, uid, uom)
375                 if product_obj.uom_id.category_id.id != uom2.category_id.id:
376                     uom = False
377             if not uom2:
378                 uom2 = product_obj.uom_id
379             compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
380             if (product_obj.type=='product') and int(compare_qty) == -1:
381               #and (product_obj.procure_method=='make_to_stock'): --> need to find alternative for procure_method
382                 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
383                         (qty, uom2 and uom2.name or product_obj.uom_id.name,
384                          max(0,product_obj.virtual_available), product_obj.uom_id.name,
385                          max(0,product_obj.qty_available), product_obj.uom_id.name)
386                 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
387
388         #update of warning messages
389         if warning_msgs:
390             warning = {
391                        'title': _('Configuration Error!'),
392                        'message' : warning_msgs
393                     }
394         res.update({'warning': warning})
395         return res
396
397 class stock_move(osv.osv):
398     _inherit = 'stock.move'
399
400     def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
401         invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
402         if move.procurement_id and move.procurement_id.sale_line_id:
403             sale_line = move.procurement_id.sale_line_id
404             self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], {
405                 'invoice_lines': [(4, invoice_line_id)]
406             }, context=context)
407             self.pool.get('sale.order').write(cr, uid, [sale_line.order_id.id], {
408                 'invoice_ids': [(4, invoice_line_vals['invoice_id'])],
409             })
410         return invoice_line_id
411
412     def _get_master_data(self, cr, uid, move, company, context=None):
413         if move.procurement_id and move.procurement_id.sale_line_id:
414             sale_order = move.procurement_id.sale_line_id.order_id
415             return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id
416         return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context)
417
418     def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None):
419         res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context)
420         if move.procurement_id and move.procurement_id.sale_line_id:
421             sale_line = move.procurement_id.sale_line_id
422             res['invoice_line_tax_id'] = [(6, 0, [x.id for x in sale_line.tax_id])]
423             res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False
424             res['price_unit'] = sale_line.price_unit
425             res['discount'] = sale_line.discount
426         return res