[MERGE] base_vat: support for Norwegian VAT codes, courtesy of Rolv RĂ¥en (adEgo)
[odoo/odoo.git] / addons / product / product.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 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import osv, fields
23 import decimal_precision as dp
24
25 import math
26 from _common import rounding
27 import re
28 from tools.translate import _
29
30 def is_pair(x):
31     return not x%2
32
33 def check_ean(eancode):
34     if not eancode:
35         return True
36     if len(eancode) <> 13:
37         return False
38     try:
39         int(eancode)
40     except:
41         return False
42     oddsum=0
43     evensum=0
44     total=0
45     eanvalue=eancode
46     reversevalue = eanvalue[::-1]
47     finalean=reversevalue[1:]
48
49     for i in range(len(finalean)):
50         if is_pair(i):
51             oddsum += int(finalean[i])
52         else:
53             evensum += int(finalean[i])
54     total=(oddsum * 3) + evensum
55
56     check = int(10 - math.ceil(total % 10.0)) %10
57
58     if check != int(eancode[-1]):
59         return False
60     return True
61 #----------------------------------------------------------
62 # UOM
63 #----------------------------------------------------------
64
65 class product_uom_categ(osv.osv):
66     _name = 'product.uom.categ'
67     _description = 'Product uom categ'
68     _columns = {
69         'name': fields.char('Name', size=64, required=True, translate=True),
70     }
71 product_uom_categ()
72
73 class product_uom(osv.osv):
74     _name = 'product.uom'
75     _description = 'Product Unit of Measure'
76
77     def _compute_factor_inv(self, factor):
78         return factor and round(1.0 / factor, 6) or 0.0
79
80     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
81         res = {}
82         for uom in self.browse(cursor, user, ids, context=context):
83             res[uom.id] = self._compute_factor_inv(uom.factor)
84         return res
85
86     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
87         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
88
89     def create(self, cr, uid, data, context=None):
90         if 'factor_inv' in data:
91             if data['factor_inv'] <> 1:
92                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
93             del(data['factor_inv'])
94         return super(product_uom, self).create(cr, uid, data, context)
95
96     _columns = {
97         'name': fields.char('Name', size=64, required=True, translate=True),
98         'category_id': fields.many2one('product.uom.categ', 'UoM Category', required=True, ondelete='cascade',
99             help="Quantity conversions may happen automatically between Units of Measure in the same category, according to their respective ratios."),
100         'factor': fields.float('Ratio', required=True,digits=(12, 12),
101             help='How many times this UoM is smaller than the reference UoM in this category:\n'\
102                     '1 * (reference unit) = ratio * (this unit)'),
103         'factor_inv': fields.function(_factor_inv, digits_compute=dp.get_precision('Product UoM'),
104             fnct_inv=_factor_inv_write,
105             string='Ratio',
106             help='How many times this UoM is bigger than the reference UoM in this category:\n'\
107                     '1 * (this unit) = ratio * (reference unit)', required=True),
108         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product UoM'), required=True,
109             help="The computed quantity will be a multiple of this value. "\
110                  "Use 1.0 for a UoM that cannot be further split, such as a piece."),
111         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
112         'uom_type': fields.selection([('bigger','Bigger than the reference UoM'),
113                                       ('reference','Reference UoM for this category'),
114                                       ('smaller','Smaller than the reference UoM')],'UoM Type', required=1),
115     }
116
117     _defaults = {
118         'active': 1,
119         'rounding': 0.01,
120         'uom_type': 'reference',
121     }
122
123     _sql_constraints = [
124         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
125         ('factor_category_id_uniq', 'unique (category_id, factor)', 'You can not have more than one UOM with same factor for same UOM category'),
126     ]
127
128     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False):
129         if not from_uom_id or not qty or not to_uom_id:
130             return qty
131         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
132         if uoms[0].id == from_uom_id:
133             from_unit, to_unit = uoms[0], uoms[-1]
134         else:
135             from_unit, to_unit = uoms[-1], uoms[0]
136         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit)
137
138     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, context=None):
139         if context is None:
140             context = {}
141         if from_unit.category_id.id <> to_unit.category_id.id:
142             if context.get('raise-exception', True):
143                 raise osv.except_osv(_('Error !'), _('Conversion from Product UoM %s to Default UoM %s is not possible as they both belong to different Category!.') % (from_unit.name,to_unit.name,))
144             else:
145                 return qty
146         amount = qty / from_unit.factor
147         if to_unit:
148             amount = rounding(amount * to_unit.factor, to_unit.rounding)
149         return amount
150
151     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
152         if not from_uom_id or not price or not to_uom_id:
153             return price
154         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
155         if uoms[0].id == from_uom_id:
156             from_unit, to_unit = uoms[0], uoms[-1]
157         else:
158             from_unit, to_unit = uoms[-1], uoms[0]
159         if from_unit.category_id.id <> to_unit.category_id.id:
160             return price
161         amount = price * from_unit.factor
162         if to_uom_id:
163             amount = amount / to_unit.factor
164         return amount
165
166     def onchange_type(self, cursor, user, ids, value):
167         if value == 'reference':
168             return {'value': {'factor': 1, 'factor_inv': 1}}
169         return {}
170
171 product_uom()
172
173
174 class product_ul(osv.osv):
175     _name = "product.ul"
176     _description = "Shipping Unit"
177     _columns = {
178         'name' : fields.char('Name', size=64,select=True, required=True, translate=True),
179         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
180     }
181 product_ul()
182
183
184 #----------------------------------------------------------
185 # Categories
186 #----------------------------------------------------------
187 class product_category(osv.osv):
188
189     def name_get(self, cr, uid, ids, context=None):
190         if not len(ids):
191             return []
192         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
193         res = []
194         for record in reads:
195             name = record['name']
196             if record['parent_id']:
197                 name = record['parent_id'][1]+' / '+name
198             res.append((record['id'], name))
199         return res
200
201     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
202         res = self.name_get(cr, uid, ids, context=context)
203         return dict(res)
204
205     _name = "product.category"
206     _description = "Product Category"
207     _columns = {
208         'name': fields.char('Name', size=64, required=True, translate=True),
209         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
210         'parent_id': fields.many2one('product.category','Parent Category', select=True),
211         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
212         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
213         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type'),
214     }
215
216
217     _defaults = {
218         'type' : lambda *a : 'normal',
219     }
220
221     _order = "sequence, name"
222     def _check_recursion(self, cr, uid, ids, context=None):
223         level = 100
224         while len(ids):
225             cr.execute('select distinct parent_id from product_category where id IN %s',(tuple(ids),))
226             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
227             if not level:
228                 return False
229             level -= 1
230         return True
231
232     _constraints = [
233         (_check_recursion, 'Error ! You can not create recursive categories.', ['parent_id'])
234     ]
235     def child_get(self, cr, uid, ids):
236         return [ids]
237
238 product_category()
239
240
241 #----------------------------------------------------------
242 # Products
243 #----------------------------------------------------------
244 class product_template(osv.osv):
245     _name = "product.template"
246     _description = "Product Template"
247     def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
248         result = {}
249         for product in self.browse(cr, uid, ids, context=context):
250             for field in fields:
251                 result[product.id] = {field:False}
252             result[product.id]['seller_delay'] = 1
253             if product.seller_ids:
254                 partner_list = sorted([(partner_id.sequence, partner_id)
255                                        for partner_id in  product.seller_ids
256                                        if partner_id and isinstance(partner_id.sequence, (int, long))])
257                 main_supplier = partner_list and partner_list[0] and partner_list[0][1] or False
258                 result[product.id]['seller_delay'] =  main_supplier and main_supplier.delay or 1
259                 result[product.id]['seller_qty'] =  main_supplier and main_supplier.qty or 0.0
260                 result[product.id]['seller_id'] = main_supplier and main_supplier.name.id or False
261         return result
262
263     _columns = {
264         'name': fields.char('Name', size=128, required=True, translate=True, select=True),
265         'product_manager': fields.many2one('res.users','Product Manager',help="This is use as task responsible"),
266         'description': fields.text('Description',translate=True),
267         'description_purchase': fields.text('Purchase Description',translate=True),
268         'description_sale': fields.text('Sale Description',translate=True),
269         'type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumable are product where you don't manage stock."),
270         'supply_method': fields.selection([('produce','Produce'),('buy','Buy')], 'Supply method', required=True, help="Produce will generate production order or tasks, according to the product type. Buy will trigger purchase orders when requested."),
271         'sale_delay': fields.float('Customer Lead Time', help="This is the average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
272         'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. This is only for the production order and, if it is a multi-level bill of material, it's only for the level of this product. Different lead times will be summed for all levels and purchase orders."),
273         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."),
274         'rental': fields.boolean('Can be Rent'),
275         'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
276         'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Sale Price'), help="Base price for computing the customer price. Sometimes called the catalog price."),
277         'standard_price': fields.float('Cost Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="Product's cost for accounting stock valuation. It is the base price for the supplier price."),
278         'volume': fields.float('Volume', help="The volume in m3."),
279         'weight': fields.float('Gross weight', help="The gross weight in Kg."),
280         'weight_net': fields.float('Net weight', help="The net weight in Kg."),
281         'cost_method': fields.selection([('standard','Standard Price'), ('average','Average Price')], 'Costing Method', required=True,
282             help="Standard Price: the cost price is fixed and recomputed periodically (usually at the end of the year), Average Price: the cost price is recomputed at each reception of products."),
283         'warranty': fields.float('Warranty (months)'),
284         'sale_ok': fields.boolean('Can be Sold', help="Determines if the product can be visible in the list of product within a selection from a sale order line."),
285         'purchase_ok': fields.boolean('Can be Purchased', help="Determine if the product is visible in the list of products within a selection from a purchase order line."),
286         'state': fields.selection([('',''),
287             ('draft', 'In Development'),
288             ('sellable','Normal'),
289             ('end','End of Lifecycle'),
290             ('obsolete','Obsolete')], 'Status', help="Tells the user if he can use the product or not."),
291         'uom_id': fields.many2one('product.uom', 'Default Unit Of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
292         'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
293         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
294             help='Used by companies that manage two units of measure: invoicing and inventory management. For example, in food industries, you will manage a stock of ham but invoice in Kg. Keep empty to use the default UOM.'),
295         'uos_coeff': fields.float('UOM -> UOS Coeff', digits=(16,4),
296             help='Coefficient to convert UOM to UOS\n'
297             ' uos = uom * coeff'),
298         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type', required=True),
299         'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_delay", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
300         'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_qty", help="This is minimum quantity to purchase from Main Supplier."),
301         'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_id"),
302         'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Partners'),
303         'loc_rack': fields.char('Rack', size=16),
304         'loc_row': fields.char('Row', size=16),
305         'loc_case': fields.char('Case', size=16),
306         'company_id': fields.many2one('res.company', 'Company',select=1),
307     }
308
309     def _get_uom_id(self, cr, uid, *args):
310         cr.execute('select id from product_uom order by id limit 1')
311         res = cr.fetchone()
312         return res and res[0] or False
313
314     def _default_category(self, cr, uid, context=None):
315         if context is None:
316             context = {}
317         if 'categ_id' in context and context['categ_id']:
318             return context['categ_id']
319         md = self.pool.get('ir.model.data')
320         res = False
321         try:
322             res = md.get_object_reference(cr, uid, 'product', 'cat0')[1]
323         except ValueError:
324             res = False
325         return res
326
327     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
328         if uom_id:
329             return {'value': {'uom_po_id': uom_id}}
330         return False
331
332     _defaults = {
333         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
334         'list_price': lambda *a: 1,
335         'cost_method': lambda *a: 'standard',
336         'supply_method': lambda *a: 'buy',
337         'standard_price': lambda *a: 1,
338         'sale_ok': lambda *a: 1,
339         'sale_delay': lambda *a: 7,
340         'produce_delay': lambda *a: 1,
341         'purchase_ok': lambda *a: 1,
342         'procure_method': lambda *a: 'make_to_stock',
343         'uom_id': _get_uom_id,
344         'uom_po_id': _get_uom_id,
345         'uos_coeff' : lambda *a: 1.0,
346         'mes_type' : lambda *a: 'fixed',
347         'categ_id' : _default_category,
348         'type' : lambda *a: 'consu',
349     }
350
351     def _check_uom(self, cursor, user, ids, context=None):
352         for product in self.browse(cursor, user, ids, context=context):
353             if product.uom_id.category_id.id <> product.uom_po_id.category_id.id:
354                 return False
355         return True
356
357     def _check_uos(self, cursor, user, ids, context=None):
358         for product in self.browse(cursor, user, ids, context=context):
359             if product.uos_id \
360                     and product.uos_id.category_id.id \
361                     == product.uom_id.category_id.id:
362                 return False
363         return True
364
365     _constraints = [
366         (_check_uom, 'Error: The default UOM and the purchase UOM must be in the same category.', ['uom_id']),
367     ]
368
369     def name_get(self, cr, user, ids, context=None):
370         if context is None:
371             context = {}
372         if 'partner_id' in context:
373             pass
374         return super(product_template, self).name_get(cr, user, ids, context)
375
376 product_template()
377
378 class product_product(osv.osv):
379     def view_header_get(self, cr, uid, view_id, view_type, context=None):
380         if context is None:
381             context = {}
382         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
383         if (context.get('categ_id', False)):
384             return _('Products: ')+self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
385         return res
386
387     def _product_price(self, cr, uid, ids, name, arg, context=None):
388         res = {}
389         if context is None:
390             context = {}
391         quantity = context.get('quantity') or 1.0
392         pricelist = context.get('pricelist', False)
393         if pricelist:
394             for id in ids:
395                 try:
396                     price = self.pool.get('product.pricelist').price_get(cr,uid,[pricelist], id, quantity, context=context)[pricelist]
397                 except:
398                     price = 0.0
399                 res[id] = price
400         for id in ids:
401             res.setdefault(id, 0.0)
402         return res
403
404     def _get_product_available_func(states, what):
405         def _product_available(self, cr, uid, ids, name, arg, context=None):
406             return {}.fromkeys(ids, 0.0)
407         return _product_available
408
409     _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
410     _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
411     _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
412     _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
413
414     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
415         res = {}
416         product_uom_obj = self.pool.get('product.uom')
417         for id in ids:
418             res.setdefault(id, 0.0)
419         for product in self.browse(cr, uid, ids, context=context):
420             if 'uom' in context:
421                 uom = product.uos_id or product.uom_id
422                 res[product.id] = product_uom_obj._compute_price(cr, uid,
423                         uom.id, product.list_price, context['uom'])
424             else:
425                 res[product.id] = product.list_price
426             res[product.id] =  (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
427         return res
428
429     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
430         for supinfo in product.seller_ids:
431             if supinfo.name.id == partner_id:
432                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
433         res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
434         return res
435
436     def _product_code(self, cr, uid, ids, name, arg, context=None):
437         res = {}
438         if context is None:
439             context = {}
440         for p in self.browse(cr, uid, ids, context=context):
441             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
442         return res
443
444     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
445         res = {}
446         if context is None:
447             context = {}
448         for p in self.browse(cr, uid, ids, context=context):
449             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
450             if not data['variants']:
451                 data['variants'] = p.variants
452             if not data['code']:
453                 data['code'] = p.code
454             if not data['name']:
455                 data['name'] = p.name
456             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
457                     (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
458         return res
459
460     _defaults = {
461         'active': lambda *a: 1,
462         'price_extra': lambda *a: 0.0,
463         'price_margin': lambda *a: 1.0,
464     }
465
466     _name = "product.product"
467     _description = "Product"
468     _table = "product_product"
469     _inherits = {'product.template': 'product_tmpl_id'}
470     _order = 'default_code,name_template'
471     _columns = {
472         'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
473         'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
474         'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
475         'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
476         'price': fields.function(_product_price, type='float', string='Pricelist', digits_compute=dp.get_precision('Sale Price')),
477         'lst_price' : fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Sale Price')),
478         'code': fields.function(_product_code, type='char', string='Reference'),
479         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
480         'default_code' : fields.char('Reference', size=64),
481         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the product without removing it."),
482         'variants': fields.char('Variants', size=64),
483         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade"),
484         'ean13': fields.char('EAN13', size=13),
485         'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
486         'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Sale Price')),
487         'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Sale Price')),
488         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
489         'name_template': fields.related('product_tmpl_id', 'name', string="Name", type='char', size=128, store=True),
490     }
491
492     def unlink(self, cr, uid, ids, context=None):
493         unlink_ids = []
494         unlink_product_tmpl_ids = []
495         for product in self.browse(cr, uid, ids, context=context):
496             tmpl_id = product.product_tmpl_id.id
497             # Check if the product is last product of this template
498             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
499             if not other_product_ids:
500                  unlink_product_tmpl_ids.append(tmpl_id)
501             unlink_ids.append(product.id)
502         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
503         return super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
504
505     def onchange_uom(self, cursor, user, ids, uom_id,uom_po_id):
506         if uom_id and uom_po_id:
507             uom_obj=self.pool.get('product.uom')
508             uom=uom_obj.browse(cursor,user,[uom_id])[0]
509             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
510             if uom.category_id.id != uom_po.category_id.id:
511                 return {'value': {'uom_po_id': uom_id}}
512         return False
513
514     def _check_ean_key(self, cr, uid, ids, context=None):
515         for product in self.browse(cr, uid, ids, context=context):
516             res = check_ean(product.ean13)
517         return res
518
519     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])]
520
521     def on_order(self, cr, uid, ids, orderline, quantity):
522         pass
523
524     def name_get(self, cr, user, ids, context=None):
525         if context is None:
526             context = {}
527         if not len(ids):
528             return []
529         def _name_get(d):
530             name = d.get('name','')
531             code = d.get('default_code',False)
532             if code:
533                 name = '[%s] %s' % (code,name)
534             if d.get('variants'):
535                 name = name + ' - %s' % (d['variants'],)
536             return (d['id'], name)
537
538         partner_id = context.get('partner_id', False)
539
540         result = []
541         for product in self.browse(cr, user, ids, context=context):
542             sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
543             if sellers:
544                 for s in sellers:
545                     mydict = {
546                               'id': product.id,
547                               'name': s.product_name or product.name,
548                               'default_code': s.product_code or product.default_code,
549                               'variants': product.variants
550                               }
551                     result.append(_name_get(mydict))
552             else:
553                 mydict = {
554                           'id': product.id,
555                           'name': product.name,
556                           'default_code': product.default_code,
557                           'variants': product.variants
558                           }
559                 result.append(_name_get(mydict))
560         return result
561
562     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
563         if not args:
564             args=[]
565         if name:
566             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
567             if not len(ids):
568                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
569             if not len(ids):
570                 ids = self.search(cr, user, [('default_code',operator,name)]+ args, limit=limit, context=context)
571                 ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
572             if not len(ids):
573                ptrn=re.compile('(\[(.*?)\])')
574                res = ptrn.search(name)
575                if res:
576                    ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
577         else:
578             ids = self.search(cr, user, args, limit=limit, context=context)
579         result = self.name_get(cr, user, ids, context=context)
580         return result
581
582     #
583     # Could be overrided for variants matrices prices
584     #
585     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
586         if context is None:
587             context = {}
588
589         if 'currency_id' in context:
590             pricetype_obj = self.pool.get('product.price.type')
591             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
592             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
593
594         res = {}
595         product_uom_obj = self.pool.get('product.uom')
596         for product in self.browse(cr, uid, ids, context=context):
597             res[product.id] = product[ptype] or 0.0
598             if ptype == 'list_price':
599                 res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
600                         product.price_extra
601             if 'uom' in context:
602                 uom = product.uom_id or product.uos_id
603                 res[product.id] = product_uom_obj._compute_price(cr, uid,
604                         uom.id, res[product.id], context['uom'])
605             # Convert from price_type currency to asked one
606             if 'currency_id' in context:
607                 # Take the price_type currency from the product field
608                 # This is right cause a field cannot be in more than one currency
609                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
610                     context['currency_id'], res[product.id],context=context)
611
612         return res
613
614     def copy(self, cr, uid, id, default=None, context=None):
615         if context is None:
616             context={}
617
618         product = self.read(cr, uid, id, ['name'], context=context)
619         if not default:
620             default = {}
621         default = default.copy()
622         default['name'] = product['name'] + _(' (copy)')
623
624         if context.get('variant',False):
625             fields = ['product_tmpl_id', 'active', 'variants', 'default_code',
626                     'price_margin', 'price_extra']
627             data = self.read(cr, uid, id, fields=fields, context=context)
628             for f in fields:
629                 if f in default:
630                     data[f] = default[f]
631             data['product_tmpl_id'] = data.get('product_tmpl_id', False) \
632                     and data['product_tmpl_id'][0]
633             del data['id']
634             return self.create(cr, uid, data)
635         else:
636             return super(product_product, self).copy(cr, uid, id, default=default,
637                     context=context)
638 product_product()
639
640 class product_packaging(osv.osv):
641     _name = "product.packaging"
642     _description = "Packaging"
643     _rec_name = 'ean'
644     _order = 'sequence'
645     _columns = {
646         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
647         'name' : fields.text('Description', size=64),
648         'qty' : fields.float('Quantity by Package',
649             help="The total number of products you can put by pallet or box."),
650         'ul' : fields.many2one('product.ul', 'Type of Package', required=True),
651         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
652         'rows' : fields.integer('Number of Layers', required=True,
653             help='The number of layers on a pallet or box'),
654         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
655         'ean' : fields.char('EAN', size=14,
656             help="The EAN code of the package unit."),
657         'code' : fields.char('Code', size=14,
658             help="The code of the transport unit."),
659         'weight': fields.float('Total Package Weight',
660             help='The weight of a full package, pallet or box.'),
661         'weight_ul': fields.float('Empty Package Weight',
662             help='The weight of the empty UL'),
663         'height': fields.float('Height', help='The height of the package'),
664         'width': fields.float('Width', help='The width of the package'),
665         'length': fields.float('Length', help='The length of the package'),
666     }
667
668
669     def _check_ean_key(self, cr, uid, ids, context=None):
670         for pack in self.browse(cr, uid, ids, context=context):
671             res = check_ean(pack.ean)
672         return res
673
674     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
675
676     def name_get(self, cr, uid, ids, context=None):
677         if not len(ids):
678             return []
679         res = []
680         for pckg in self.browse(cr, uid, ids, context=context):
681             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
682             p_name += pckg.ul.name
683             res.append((pckg.id,p_name))
684         return res
685
686     def _get_1st_ul(self, cr, uid, context=None):
687         cr.execute('select id from product_ul order by id asc limit 1')
688         res = cr.fetchone()
689         return (res and res[0]) or False
690
691     _defaults = {
692         'rows' : lambda *a : 3,
693         'sequence' : lambda *a : 1,
694         'ul' : _get_1st_ul,
695     }
696
697     def checksum(ean):
698         salt = '31' * 6 + '3'
699         sum = 0
700         for ean_part, salt_part in zip(ean, salt):
701             sum += int(ean_part) * int(salt_part)
702         return (10 - (sum % 10)) % 10
703     checksum = staticmethod(checksum)
704
705 product_packaging()
706
707
708 class product_supplierinfo(osv.osv):
709     _name = "product.supplierinfo"
710     _description = "Information about a product supplier"
711     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
712         result = {}
713         product_uom_pool = self.pool.get('product.uom')
714         for supplier_info in self.browse(cr, uid, ids, context=context):
715             for field in fields:
716                 result[supplier_info.id] = {field:False}
717             if supplier_info.product_uom.id:
718                 qty = product_uom_pool._compute_qty(cr, uid, supplier_info.product_uom.id, supplier_info.min_qty, to_uom_id=supplier_info.product_id.uom_id.id)
719             else:
720                 qty = supplier_info.min_qty
721             result[supplier_info.id]['qty'] = qty
722         return result
723
724     def _get_uom_id(self, cr, uid, context=None):
725         if context is None:
726             context = {}
727         return context.get('uom_id', False)
728
729     _columns = {
730         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
731         'product_name': fields.char('Supplier Product Name', size=128, help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
732         'product_code': fields.char('Supplier Product Code', size=64, help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
733         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
734         'product_uom': fields.many2one('product.uom', string="Supplier UoM", help="Choose here the Unit of Measure in which the prices and quantities are expressed below."),
735         'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product UoM if not empty, in the default unit of measure of the product otherwise."),
736         'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Uom."),
737         'product_id' : fields.many2one('product.template', 'Product', required=True, ondelete='cascade', select=True),
738         'delay' : fields.integer('Delivery Lead Time', required=True, help="Lead time in days between the confirmation of the purchase order and the reception of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning."),
739         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
740         'company_id':fields.many2one('res.company','Company',select=1),
741     }
742     _defaults = {
743         'qty': lambda *a: 0.0,
744         'sequence': lambda *a: 1,
745         'delay': lambda *a: 1,
746         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
747         'product_uom': _get_uom_id,
748     }
749     def _check_uom(self, cr, uid, ids, context=None):
750         for supplier_info in self.browse(cr, uid, ids, context=context):
751             if supplier_info.product_uom and supplier_info.product_uom.category_id.id <> supplier_info.product_id.uom_id.category_id.id:
752                 return False
753         return True
754
755     _constraints = [
756         (_check_uom, 'Error: The default UOM and the Supplier Product UOM must be in the same category.', ['product_uom']),
757     ]
758     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
759         """
760         Calculate price from supplier pricelist.
761         @param supplier_ids: Ids of res.partner object.
762         @param product_id: Id of product.
763         @param product_qty: specify quantity to purchase.
764         """
765         if type(supplier_ids) in (int,long,):
766             supplier_ids = [supplier_ids]
767         res = {}
768         product_pool = self.pool.get('product.product')
769         partner_pool = self.pool.get('res.partner')
770         pricelist_pool = self.pool.get('product.pricelist')
771         currency_pool = self.pool.get('res.currency')
772         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
773         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
774             # Compute price from standard price of product
775             price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
776
777             # Compute price from Purchase pricelist of supplier
778             pricelist_id = supplier.property_product_pricelist_purchase.id
779             if pricelist_id:
780                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
781                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
782
783             # Compute price from supplier pricelist which are in Supplier Information
784             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_id','=',product_id)])
785             if supplier_info_ids:
786                 cr.execute('SELECT * ' \
787                     'FROM pricelist_partnerinfo ' \
788                     'WHERE suppinfo_id IN %s' \
789                     'AND min_quantity <= %s ' \
790                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
791                 res2 = cr.dictfetchone()
792                 if res2:
793                     price = res2['price']
794             res[supplier.id] = price
795         return res
796     _order = 'sequence'
797 product_supplierinfo()
798
799
800 class pricelist_partnerinfo(osv.osv):
801     _name = 'pricelist.partnerinfo'
802     _columns = {
803         'name': fields.char('Description', size=64),
804         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
805         'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier UoM if any or in the default UoM of the product otherrwise."),
806         'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Purchase Price'), help="This price will be considered as a price for the supplier UoM if any or the default Unit of Measure of the product otherwise"),
807     }
808     _order = 'min_quantity asc'
809 pricelist_partnerinfo()
810 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: