[FIX] product: remove wrong field on product.template. TODO: move packaging field...
[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 import math
23 import re
24 import time
25 from _common import ceiling
26
27 from openerp import SUPERUSER_ID
28 from openerp import tools
29 from openerp.osv import osv, fields
30 from openerp.tools.translate import _
31 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
32 import psycopg2
33
34 import openerp.addons.decimal_precision as dp
35
36 def ean_checksum(eancode):
37     """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length"""
38     if len(eancode) != 13:
39         return -1
40     oddsum=0
41     evensum=0
42     total=0
43     eanvalue=eancode
44     reversevalue = eanvalue[::-1]
45     finalean=reversevalue[1:]
46
47     for i in range(len(finalean)):
48         if i % 2 == 0:
49             oddsum += int(finalean[i])
50         else:
51             evensum += int(finalean[i])
52     total=(oddsum * 3) + evensum
53
54     check = int(10 - math.ceil(total % 10.0)) %10
55     return check
56
57 def check_ean(eancode):
58     """returns True if eancode is a valid ean13 string, or null"""
59     if not eancode:
60         return True
61     if len(eancode) != 13:
62         return False
63     try:
64         int(eancode)
65     except:
66         return False
67     return ean_checksum(eancode) == int(eancode[-1])
68
69 def sanitize_ean13(ean13):
70     """Creates and returns a valid ean13 from an invalid one"""
71     if not ean13:
72         return "0000000000000"
73     ean13 = re.sub("[A-Za-z]","0",ean13);
74     ean13 = re.sub("[^0-9]","",ean13);
75     ean13 = ean13[:13]
76     if len(ean13) < 13:
77         ean13 = ean13 + '0' * (13-len(ean13))
78     return ean13[:-1] + str(ean_checksum(ean13))
79
80 #----------------------------------------------------------
81 # UOM
82 #----------------------------------------------------------
83
84 class product_uom_categ(osv.osv):
85     _name = 'product.uom.categ'
86     _description = 'Product uom categ'
87     _columns = {
88         'name': fields.char('Name', required=True, translate=True),
89     }
90
91 class product_uom(osv.osv):
92     _name = 'product.uom'
93     _description = 'Product Unit of Measure'
94
95     def _compute_factor_inv(self, factor):
96         return factor and (1.0 / factor) or 0.0
97
98     def _factor_inv(self, cursor, user, ids, name, arg, context=None):
99         res = {}
100         for uom in self.browse(cursor, user, ids, context=context):
101             res[uom.id] = self._compute_factor_inv(uom.factor)
102         return res
103
104     def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
105         return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
106
107     def name_create(self, cr, uid, name, context=None):
108         """ The UoM category and factor are required, so we'll have to add temporary values
109             for imported UoMs """
110         uom_categ = self.pool.get('product.uom.categ')
111         # look for the category based on the english name, i.e. no context on purpose!
112         # TODO: should find a way to have it translated but not created until actually used
113         categ_misc = 'Unsorted/Imported Units'
114         categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
115         if categ_id:
116             categ_id = categ_id[0]
117         else:
118             categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
119         uom_id = self.create(cr, uid, {self._rec_name: name,
120                                        'category_id': categ_id,
121                                        'factor': 1})
122         return self.name_get(cr, uid, [uom_id], context=context)[0]
123
124     def create(self, cr, uid, data, context=None):
125         if 'factor_inv' in data:
126             if data['factor_inv'] != 1:
127                 data['factor'] = self._compute_factor_inv(data['factor_inv'])
128             del(data['factor_inv'])
129         return super(product_uom, self).create(cr, uid, data, context)
130
131     _order = "name"
132     _columns = {
133         'name': fields.char('Unit of Measure', required=True, translate=True),
134         'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade',
135             help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
136         'factor': fields.float('Ratio', required=True,digits=(12, 12),
137             help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
138                     '1 * (reference unit) = ratio * (this unit)'),
139         'factor_inv': fields.function(_factor_inv, digits=(12,12),
140             fnct_inv=_factor_inv_write,
141             string='Bigger Ratio',
142             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
143                     '1 * (this unit) = ratio * (reference unit)', required=True),
144         'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
145             help="The computed quantity will be a multiple of this value. "\
146                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
147         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
148         'uom_type': fields.selection([('bigger','Bigger than the reference Unit of Measure'),
149                                       ('reference','Reference Unit of Measure for this category'),
150                                       ('smaller','Smaller than the reference Unit of Measure')],'Type', required=1),
151     }
152
153     _defaults = {
154         'active': 1,
155         'rounding': 0.01,
156         'uom_type': 'reference',
157     }
158
159     _sql_constraints = [
160         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
161     ]
162
163     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True):
164         if not from_uom_id or not qty or not to_uom_id:
165             return qty
166         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
167         if uoms[0].id == from_uom_id:
168             from_unit, to_unit = uoms[0], uoms[-1]
169         else:
170             from_unit, to_unit = uoms[-1], uoms[0]
171         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round)
172
173     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, context=None):
174         if context is None:
175             context = {}
176         if from_unit.category_id.id != to_unit.category_id.id:
177             if context.get('raise-exception', True):
178                 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,))
179             else:
180                 return qty
181         amount = qty / from_unit.factor
182         if to_unit:
183             amount = amount * to_unit.factor
184             if round:
185                 amount = ceiling(amount, to_unit.rounding)
186         return amount
187
188     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
189         if not from_uom_id or not price or not to_uom_id:
190             return price
191         from_unit, to_unit = self.browse(cr, uid, [from_uom_id, to_uom_id])
192         if from_unit.category_id.id != to_unit.category_id.id:
193             return price
194         amount = price * from_unit.factor
195         if to_uom_id:
196             amount = amount / to_unit.factor
197         return amount
198
199     def onchange_type(self, cursor, user, ids, value):
200         if value == 'reference':
201             return {'value': {'factor': 1, 'factor_inv': 1}}
202         return {}
203
204     def write(self, cr, uid, ids, vals, context=None):
205         if 'category_id' in vals:
206             for uom in self.browse(cr, uid, ids, context=context):
207                 if uom.category_id.id != vals['category_id']:
208                     raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
209         return super(product_uom, self).write(cr, uid, ids, vals, context=context)
210
211
212
213 class product_ul(osv.osv):
214     _name = "product.ul"
215     _description = "Logistic Unit"
216     _columns = {
217         'name' : fields.char('Name', select=True, required=True, translate=True),
218         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
219         'height': fields.float('Height', help='The height of the package'),
220         'width': fields.float('Width', help='The width of the package'),
221         'length': fields.float('Length', help='The length of the package'),
222         'weight': fields.float('Empty Package Weight'),
223     }
224
225
226 #----------------------------------------------------------
227 # Categories
228 #----------------------------------------------------------
229 class product_category(osv.osv):
230
231     def name_get(self, cr, uid, ids, context=None):
232         if isinstance(ids, (list, tuple)) and not len(ids):
233             return []
234         if isinstance(ids, (long, int)):
235             ids = [ids]
236         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
237         res = []
238         for record in reads:
239             name = record['name']
240             if record['parent_id']:
241                 name = record['parent_id'][1]+' / '+name
242             res.append((record['id'], name))
243         return res
244
245     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
246         if not args:
247             args = []
248         if not context:
249             context = {}
250         if name:
251             # Be sure name_search is symetric to name_get
252             name = name.split(' / ')[-1]
253             ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
254         else:
255             ids = self.search(cr, uid, args, limit=limit, context=context)
256         return self.name_get(cr, uid, ids, context)
257
258     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
259         res = self.name_get(cr, uid, ids, context=context)
260         return dict(res)
261
262     _name = "product.category"
263     _description = "Product Category"
264     _columns = {
265         'name': fields.char('Name', required=True, translate=True, select=True),
266         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
267         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
268         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
269         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
270         'type': fields.selection([('view','View'), ('normal','Normal')], 'Category Type', help="A category of the view type is a virtual category that can be used as the parent of another category to create a hierarchical structure."),
271         'parent_left': fields.integer('Left Parent', select=1),
272         'parent_right': fields.integer('Right Parent', select=1),
273     }
274
275
276     _defaults = {
277         'type' : 'normal',
278     }
279
280     _parent_name = "parent_id"
281     _parent_store = True
282     _parent_order = 'sequence, name'
283     _order = 'parent_left'
284
285     _constraints = [
286         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
287     ]
288
289
290 class produce_price_history(osv.osv):
291     """
292     Keep track of the ``product.template`` standard prices as they are changed.
293     """
294
295     _name = 'product.price.history'
296     _rec_name = 'datetime'
297     _order = 'datetime desc'
298
299     _columns = {
300         'company_id': fields.many2one('res.company', required=True),
301         'product_template_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
302         'datetime': fields.datetime('Historization Time'),
303         'cost': fields.float('Historized Cost'),
304     }
305
306     def _get_default_company(self, cr, uid, context=None):
307         if 'force_company' in context:
308             return context['force_company']
309         else:
310             company = self.pool['res.users'].browse(cr, uid, uid,
311                 context=context).company_id
312             return company.id if company else False
313
314     _defaults = {
315         'datetime': fields.datetime.now,
316         'company_id': _get_default_company,
317     }
318
319
320 #----------------------------------------------------------
321 # Product Attributes
322 #----------------------------------------------------------
323 class product_attribute(osv.osv):
324     _name = "product.attribute"
325     _description = "Product Attribute"
326     _columns = {
327         'name': fields.char('Name', translate=True, required=True),
328         'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values'),
329     }
330
331 class product_attribute_value(osv.osv):
332     _name = "product.attribute.value"
333     _order = 'sequence'
334     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
335         result = dict.fromkeys(ids, 0)
336         if not context.get('active_id'):
337             return result
338
339         for obj in self.browse(cr, uid, ids, context=context):
340             for price_id in obj.price_ids:
341                 if price_id.product_tmpl_id.id == context.get('active_id'):
342                     result[obj.id] = price_id.price_extra
343                     break
344         return result
345
346     def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
347         if 'active_id' not in context:
348             return None
349         p_obj = self.pool['product.attribute.price']
350         p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
351         if p_ids:
352             p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
353         else:
354             p_obj.create(cr, uid, p_ids, {
355                     'product_tmpl_id': context['active_id'],
356                     'value_id': id,
357                     'price_extra': value,
358                 }, context=context)
359
360     _columns = {
361         'sequence': fields.integer('Sequence', help="Determine the display order"),
362         'name': fields.char('Value', translate=True, required=True),
363         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
364         'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
365         'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
366             fnct_inv=_set_price_extra,
367             digits_compute=dp.get_precision('Product Price'),
368             help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
369         'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
370     }
371     _sql_constraints = [
372         ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
373     ]
374     _defaults = {
375         'price_extra': 0.0,
376     }
377
378 class product_attribute_price(osv.osv):
379     _name = "product.attribute.price"
380     _columns = {
381         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
382         'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True),
383         'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
384     }
385
386 class product_attribute_line(osv.osv):
387     _name = "product.attribute.line"
388     _columns = {
389         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
390         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
391         'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
392     }
393
394
395 #----------------------------------------------------------
396 # Products
397 #----------------------------------------------------------
398 class product_template(osv.osv):
399     _name = "product.template"
400     _inherit = ['mail.thread']
401     _description = "Product Template"
402
403     def _get_image(self, cr, uid, ids, name, args, context=None):
404         result = dict.fromkeys(ids, False)
405         for obj in self.browse(cr, uid, ids, context=context):
406             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
407         return result
408
409     def _set_image(self, cr, uid, id, name, value, args, context=None):
410         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
411
412     def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
413         return self.is_product_variant(cr, uid, ids, name, arg, context=context)
414
415     def is_product_variant(self, cr, uid, ids, name, arg, context=None):
416         prod = self.pool.get('product.product')
417         res = dict.fromkeys(ids, False)
418         ctx = dict(context, active_test=True)
419         for product in self.browse(cr, uid, ids, context=context):
420             res[product.id] = prod.search(cr, uid, [('product_tmpl_id','=',product.id)], context=ctx, count=True) == 1
421         return res
422
423
424     def _product_template_price(self, cr, uid, ids, name, arg, context=None):
425         plobj = self.pool.get('product.pricelist')
426         res = {}
427         quantity = context.get('quantity') or 1.0
428         pricelist = context.get('pricelist', False)
429         partner = context.get('partner', False)
430         if pricelist:
431             # Support context pricelists specified as display_name or ID for compatibility
432             if isinstance(pricelist, basestring):
433                 pricelist_ids = plobj.name_search(
434                     cr, uid, pricelist, operator='=', context=context, limit=1)
435                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
436
437             if isinstance(pricelist, (int, long)):
438                 products = self.browse(cr, uid, ids, context=context)
439                 qtys = map(lambda x: (x, quantity, partner), products)
440                 pl = plobj.browse(cr, uid, pricelist, context=context)
441                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
442                 for id in ids:
443                     res[id] = price.get(id, 0.0)
444         for id in ids:
445             res.setdefault(id, 0.0)
446         return res
447
448     def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
449         if context is None:
450             context = {}
451         if date is None:
452             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
453         price_history_obj = self.pool.get('product.price.history')
454         history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
455         if history_ids:
456             return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
457         return 0.0
458
459     def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
460         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
461         if context is None:
462             context = {}
463         price_history_obj = self.pool['product.price.history']
464         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
465         company_id = context.get('force_company', user_company)
466         price_history_obj.create(cr, uid, {
467             'product_template_id': product_tmpl_id,
468             'cost': value,
469             'company_id': company_id,
470         }, context=context)
471
472     def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
473         res = {}
474         for product in self.browse(cr, uid, ids):
475             res[product.id] = len(product.product_variant_ids)
476         return res
477
478     _columns = {
479         'name': fields.char('Name', required=True, translate=True, select=True),
480         'product_manager': fields.many2one('res.users','Product Manager'),
481         'description': fields.text('Description',translate=True,
482             help="A precise description of the Product, used only for internal information purposes."),
483         'description_purchase': fields.text('Purchase Description',translate=True,
484             help="A description of the Product that you want to communicate to your suppliers. "
485                  "This description will be copied to every Purchase Order, Reception and Supplier Invoice/Refund."),
486         'description_sale': fields.text('Sale Description',translate=True,
487             help="A description of the Product that you want to communicate to your customers. "
488                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
489         'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Consumable are product where you don't manage stock, a service is a non-material product provided by a company or an individual."),        
490         'rental': fields.boolean('Can be Rent'),
491         'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
492         'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
493         'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."),
494         'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
495         'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), 
496                                           help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", 
497                                           groups="base.group_user", string="Cost Price"),
498         'volume': fields.float('Volume', help="The volume in m3."),
499         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
500         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
501         'warranty': fields.float('Warranty'),
502         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
503         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
504         'state': fields.selection([('',''),
505             ('draft', 'In Development'),
506             ('sellable','Normal'),
507             ('end','End of Lifecycle'),
508             ('obsolete','Obsolete')], 'Status'),
509         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
510         '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."),
511         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
512             help='Specify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
513         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
514             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
515             ' uos = uom * coeff'),
516         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
517         'company_id': fields.many2one('res.company', 'Company', select=1),
518         # image: all image fields are base64 encoded and PIL-supported
519         'image': fields.binary("Image",
520             help="This field holds the image used as image for the product, limited to 1024x1024px."),
521         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
522             string="Medium-sized image", type="binary", multi="_get_image", 
523             store={
524                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
525             },
526             help="Medium-sized image of the product. It is automatically "\
527                  "resized as a 128x128px image, with aspect ratio preserved, "\
528                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
529         'image_small': fields.function(_get_image, fnct_inv=_set_image,
530             string="Small-sized image", type="binary", multi="_get_image",
531             store={
532                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
533             },
534             help="Small-sized image of the product. It is automatically "\
535                  "resized as a 64x64px image, with aspect ratio preserved. "\
536                  "Use this field anywhere a small image is required."),
537
538         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
539         'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
540             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."),
541         'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
542             help="This is minimum quantity to purchase from Main Supplier."),
543         'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
544             help="Main Supplier who has highest priority in Supplier List."),
545
546         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
547         'color': fields.integer('Color Index'),
548         'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
549
550         'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
551         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
552         'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
553
554         # related to display product product information if is_product_variant
555         'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
556         'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
557     }
558
559     def _price_get_list_price(self, product):
560         return 0.0
561
562     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
563         if context is None:
564             context = {}
565
566         if 'currency_id' in context:
567             pricetype_obj = self.pool.get('product.price.type')
568             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
569             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
570
571         res = {}
572         product_uom_obj = self.pool.get('product.uom')
573         for product in products:
574             res[product.id] = product[ptype] or 0.0
575             if ptype == 'list_price':
576                 res[product.id] += product._name == "product.product" and product.price_extra or 0.0
577             if 'uom' in context:
578                 uom = product.uom_id or product.uos_id
579                 res[product.id] = product_uom_obj._compute_price(cr, uid,
580                         uom.id, res[product.id], context['uom'])
581             # Convert from price_type currency to asked one
582             if 'currency_id' in context:
583                 # Take the price_type currency from the product field
584                 # This is right cause a field cannot be in more than one currency
585                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
586                     context['currency_id'], res[product.id],context=context)
587
588         return res
589
590     def _get_uom_id(self, cr, uid, *args):
591         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
592
593     def _default_category(self, cr, uid, context=None):
594         if context is None:
595             context = {}
596         if 'categ_id' in context and context['categ_id']:
597             return context['categ_id']
598         md = self.pool.get('ir.model.data')
599         res = False
600         try:
601             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
602         except ValueError:
603             res = False
604         return res
605
606     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
607         if uom_id:
608             return {'value': {'uom_po_id': uom_id}}
609         return {}
610
611     def create_variant_ids(self, cr, uid, ids, context=None):
612         product_obj = self.pool.get("product.product")
613         ctx = context and context.copy() or {}
614
615         if ctx.get("create_product_variant"):
616             return None
617
618         ctx.update(active_test=False, create_product_variant=True)
619
620         tmpl_ids = self.browse(cr, uid, ids, context=ctx)
621         for tmpl_id in tmpl_ids:
622
623             # list of values combination
624             all_variants = [[]]
625             for variant_id in tmpl_id.attribute_line_ids:
626                 if len(variant_id.value_ids) > 1:
627                     temp_variants = []
628                     for value_id in variant_id.value_ids:
629                         for variant in all_variants:
630                             temp_variants.append(variant + [int(value_id)])
631                     all_variants = temp_variants
632
633             # check product
634             variant_ids_to_active = []
635             variants_active_ids = []
636             variants_inactive = []
637             for product_id in tmpl_id.product_variant_ids:
638                 variants = map(int,product_id.attribute_value_ids)
639                 if variants in all_variants:
640                     variants_active_ids.append(product_id.id)
641                     all_variants.pop(all_variants.index(variants))
642                     if not product_id.active:
643                         variant_ids_to_active.append(product_id.id)
644                 else:
645                     variants_inactive.append(product_id)
646             if variant_ids_to_active:
647                 product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
648
649             # create new product
650             for variant_ids in all_variants:
651                 values = {
652                     'product_tmpl_id': tmpl_id.id,
653                     'attribute_value_ids': [(6, 0, variant_ids)]
654                 }
655                 id = product_obj.create(cr, uid, values, context=ctx)
656                 variants_active_ids.append(id)
657
658             # unlink or inactive product
659             for variant_id in map(int,variants_inactive):
660                 try:
661                     with cr.savepoint():
662                         product_obj.unlink(cr, uid, [variant_id], context=ctx)
663                 except (psycopg2.Error, osv.except_osv):
664                     product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
665                     pass
666         return True
667
668     def create(self, cr, uid, vals, context=None):
669         ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
670         product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
671         if not context or "create_product_product" not in context:
672             self.create_variant_ids(cr, uid, [product_template_id], context=context)
673         self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
674         return product_template_id
675
676     def write(self, cr, uid, ids, vals, context=None):
677         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
678         if isinstance(id, (int, long)):
679             ids = [ids]
680         if 'uom_po_id' in vals:
681             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
682             for product in self.browse(cr, uid, ids, context=context):
683                 old_uom = product.uom_po_id
684                 if old_uom.category_id.id != new_uom.category_id.id:
685                     raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may deactivate this product from the 'Procurements' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
686         if 'standard_price' in vals:
687             for prod_template_id in ids:
688                 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
689         res = super(product_template, self).write(cr, uid, ids, vals, context=context)
690         if 'attribute_line_ids' in vals or vals.get('active'):
691             self.create_variant_ids(cr, uid, ids, context=context)
692         if 'active' in vals and not vals.get('active'):
693             ctx = context and context.copy() or {}
694             ctx.update(active_test=False)
695             product_ids = []
696             for product in self.browse(cr, uid, ids, context=ctx):
697                 product_ids = map(int,product.product_variant_ids)
698             self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
699         return res
700
701     def copy(self, cr, uid, id, default=None, context=None):
702         if default is None:
703             default = {}
704         template = self.browse(cr, uid, id, context=context)
705         default['name'] = _("%s (copy)") % (template['name'])
706         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
707
708     _defaults = {
709         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
710         'list_price': 1,
711         'standard_price': 0.0,
712         'sale_ok': 1,        
713         'uom_id': _get_uom_id,
714         'uom_po_id': _get_uom_id,
715         'uos_coeff': 1.0,
716         'mes_type': 'fixed',
717         'categ_id' : _default_category,
718         'type' : 'consu',
719         'active': True,
720     }
721
722     def _check_uom(self, cursor, user, ids, context=None):
723         for product in self.browse(cursor, user, ids, context=context):
724             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
725                 return False
726         return True
727
728     def _check_uos(self, cursor, user, ids, context=None):
729         for product in self.browse(cursor, user, ids, context=context):
730             if product.uos_id \
731                     and product.uos_id.category_id.id \
732                     == product.uom_id.category_id.id:
733                 return False
734         return True
735
736     _constraints = [
737         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
738     ]
739
740     def name_get(self, cr, user, ids, context=None):
741         if context is None:
742             context = {}
743         if 'partner_id' in context:
744             pass
745         return super(product_template, self).name_get(cr, user, ids, context)
746
747
748 class product_product(osv.osv):
749     _name = "product.product"
750     _description = "Product"
751     _inherits = {'product.template': 'product_tmpl_id'}
752     _inherit = ['mail.thread']
753     _order = 'default_code,name_template'
754
755     def _product_price(self, cr, uid, ids, name, arg, context=None):
756         plobj = self.pool.get('product.pricelist')
757         res = {}
758         if context is None:
759             context = {}
760         quantity = context.get('quantity') or 1.0
761         pricelist = context.get('pricelist', False)
762         partner = context.get('partner', False)
763         if pricelist:
764             # Support context pricelists specified as display_name or ID for compatibility
765             if isinstance(pricelist, basestring):
766                 pricelist_ids = plobj.name_search(
767                     cr, uid, pricelist, operator='=', context=context, limit=1)
768                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
769
770             if isinstance(pricelist, (int, long)):
771                 products = self.browse(cr, uid, ids, context=context)
772                 qtys = map(lambda x: (x, quantity, partner), products)
773                 pl = plobj.browse(cr, uid, pricelist, context=context)
774                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
775                 for id in ids:
776                     res[id] = price.get(id, 0.0)
777         for id in ids:
778             res.setdefault(id, 0.0)
779         return res
780
781     def view_header_get(self, cr, uid, view_id, view_type, context=None):
782         if context is None:
783             context = {}
784         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
785         if (context.get('categ_id', False)):
786             return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
787         return res
788
789     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
790         res = {}
791         product_uom_obj = self.pool.get('product.uom')
792         res = dict.fromkeys(ids, 0.0)
793
794         for product in self.browse(cr, uid, ids, context=context):
795             if 'uom' in context:
796                 uom = product.uos_id or product.uom_id
797                 res[product.id] = product_uom_obj._compute_price(cr, uid,
798                         uom.id, product.list_price, context['uom'])
799             else:
800                 res[product.id] = product.list_price
801             price_extra = 0.0
802             for variant_id in product.attribute_value_ids:
803                 price_extra += variant_id.price_extra
804             res[product.id] =  (res[product.id] or 0.0) + price_extra
805         return res
806
807     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
808         for supinfo in product.seller_ids:
809             if supinfo.name.id == partner_id:
810                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
811         res = {'code': product.default_code, 'name': product.name}
812         return res
813
814     def _product_code(self, cr, uid, ids, name, arg, context=None):
815         res = {}
816         if context is None:
817             context = {}
818         for p in self.browse(cr, uid, ids, context=context):
819             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
820         return res
821
822     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
823         res = {}
824         if context is None:
825             context = {}
826         for p in self.browse(cr, uid, ids, context=context):
827             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
828             if not data['code']:
829                 data['code'] = p.code
830             if not data['name']:
831                 data['name'] = p.name
832             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
833         return res
834
835     def is_product_variant(self, cr, uid, ids, name, arg, context=None):
836         return dict.fromkeys(ids, True)
837
838     def _get_name_template_ids(self, cr, uid, ids, context=None):
839         result = set()
840         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
841         for el in template_ids:
842             result.add(el)
843         return list(result)
844
845     def _get_image_variant(self, cr, uid, ids, name, args, context=None):
846         result = dict.fromkeys(ids, False)
847         for obj in self.browse(cr, uid, ids, context=context):
848             result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
849         return result
850
851     def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
852         image = tools.image_resize_image_big(value)
853         res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
854         product = self.browse(cr, uid, id, context=context)
855         if not product.product_tmpl_id.image:
856             product.write({'image_variant': None}, context=context)
857             product.product_tmpl_id.write({'image': image}, context=context)
858         return res
859
860     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
861         result = dict.fromkeys(ids, False)
862         for product in self.browse(cr, uid, ids, context=context):
863             price_extra = 0.0
864             for variant_id in product.attribute_value_ids:
865                 for price_id in variant_id.price_ids:
866                     if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
867                         price_extra += price_id.price_extra
868             result[product.id] = price_extra
869         return result
870
871     _columns = {
872         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
873         'price_extra': fields.function(_get_price_extra, type='float', string='Variant Extra Price', help="This is le sum of the extra price of all attributes"),
874         'lst_price': fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
875         'code': fields.function(_product_code, type='char', string='Internal Reference'),
876         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
877         'default_code' : fields.char('Internal Reference', select=True),
878         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
879         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
880         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
881         'packaging': fields.one2many('product.packaging', 'product_id', 'Packaging', 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."),
882         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
883             'product.template': (_get_name_template_ids, ['name'], 10),
884             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
885         }, select=True),
886         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True),
887
888         # image: all image fields are base64 encoded and PIL-supported
889         'image_variant': fields.binary("Variant Image",
890             help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
891
892         'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
893             string="Big-sized image", type="binary",
894             help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
895                  "resized as a 1024x1024px image, with aspect ratio preserved."),
896         'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
897             string="Small-sized image", type="binary",
898             help="Image of the product variant (Small-sized image of product template if false)."),
899         'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
900             string="Medium-sized image", type="binary",
901             help="Image of the product variant (Medium-sized image of product template if false)."),
902     }
903
904     _defaults = {
905         'active': 1,
906         'color': 0,
907     }
908
909     def unlink(self, cr, uid, ids, context=None):
910         unlink_ids = []
911         unlink_product_tmpl_ids = []
912         for product in self.browse(cr, uid, ids, context=context):
913             tmpl_id = product.product_tmpl_id.id
914             # Check if the product is last product of this template
915             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
916             if not other_product_ids:
917                 unlink_product_tmpl_ids.append(tmpl_id)
918             unlink_ids.append(product.id)
919         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
920         # delete templates after calling super, as deleting template could lead to deleting
921         # products due to ondelete='cascade'
922         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
923         return res
924
925     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
926         if uom_id and uom_po_id:
927             uom_obj=self.pool.get('product.uom')
928             uom=uom_obj.browse(cursor,user,[uom_id])[0]
929             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
930             if uom.category_id.id != uom_po.category_id.id:
931                 return {'value': {'uom_po_id': uom_id}}
932         return False
933
934     def _check_ean_key(self, cr, uid, ids, context=None):
935         for product in self.read(cr, uid, ids, ['ean13'], context=context):
936             if not check_ean(product['ean13']):
937                 return False
938         return True
939
940     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
941
942     def on_order(self, cr, uid, ids, orderline, quantity):
943         pass
944
945     def name_get(self, cr, user, ids, context=None):
946         if context is None:
947             context = {}
948         if isinstance(ids, (int, long)):
949             ids = [ids]
950         if not len(ids):
951             return []
952
953         def _name_get(d):
954             name = d.get('name','')
955             code = d.get('default_code',False)
956             if code:
957                 name = '[%s] %s' % (code,name)
958             return (d['id'], name)
959
960         partner_id = context.get('partner_id', False)
961
962         # all user don't have access to seller and partner
963         # check access and use superuser
964         self.check_access_rights(cr, user, "read")
965         self.check_access_rule(cr, user, ids, "read", context=context)
966
967         result = []
968         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
969             variant = ", ".join([v.name for v in product.attribute_value_ids])
970             name = variant and "%s (%s)" % (product.name, variant) or product.name
971             sellers = []
972             if partner_id:
973                 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
974             if sellers:
975                 for s in sellers:
976                     mydict = {
977                               'id': product.id,
978                               'name': s.product_name or name,
979                               'default_code': s.product_code or product.default_code,
980                               }
981                     result.append(_name_get(mydict))
982             else:
983                 mydict = {
984                           'id': product.id,
985                           'name': name,
986                           'default_code': product.default_code,
987                           }
988                 result.append(_name_get(mydict))
989         return result
990
991     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
992         if not args:
993             args = []
994         if name:
995             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
996             if not ids:
997                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
998             if not ids:
999                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1000                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1001                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1002                 # Performing a quick memory merge of ids in Python will give much better performance
1003                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1004                 if not limit or len(ids) < limit:
1005                     # we may underrun the limit because of dupes in the results, that's fine
1006                     limit2 = (limit - len(ids)) if limit else False
1007                     ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
1008                 ids = list(ids)
1009             if not ids:
1010                 ptrn = re.compile('(\[(.*?)\])')
1011                 res = ptrn.search(name)
1012                 if res:
1013                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1014         else:
1015             ids = self.search(cr, user, args, limit=limit, context=context)
1016         result = self.name_get(cr, user, ids, context=context)
1017         return result
1018
1019     #
1020     # Could be overrided for variants matrices prices
1021     #
1022     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1023         products = self.browse(cr, uid, ids, context=context)
1024         return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1025
1026     def copy(self, cr, uid, id, default=None, context=None):
1027         if context is None:
1028             context={}
1029
1030         product = self.browse(cr, uid, id, context)
1031         if context.get('variant'):
1032             # if we copy a variant or create one, we keep the same template
1033             default['product_tmpl_id'] = product.product_tmpl_id.id
1034         elif 'name' not in default:
1035             default['name'] = _("%s (copy)") % (product.name,)
1036
1037         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1038
1039     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1040         if context is None:
1041             context = {}
1042         if context.get('search_default_categ_id'):
1043             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1044         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1045
1046     def open_product_template(self, cr, uid, ids, context=None):
1047         """ Utility method used to add an "Open Template" button in product views """
1048         product = self.browse(cr, uid, ids[0], context=context)
1049         return {'type': 'ir.actions.act_window',
1050                 'res_model': 'product.template',
1051                 'view_mode': 'form',
1052                 'res_id': product.product_tmpl_id.id,
1053                 'target': 'new'}
1054
1055     def create(self, cr, uid, vals, context=None):
1056         if context is None:
1057             context = {}
1058         ctx = dict(context or {}, create_product_product=True)
1059         return super(product_product, self).create(cr, uid, vals, context=ctx)
1060
1061
1062 class product_packaging(osv.osv):
1063     _name = "product.packaging"
1064     _description = "Packaging"
1065     _rec_name = 'ean'
1066     _order = 'sequence'
1067     _columns = {
1068         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1069         'name' : fields.text('Description'),
1070         'qty' : fields.float('Quantity by Package',
1071             help="The total number of products you can put by pallet or box."),
1072         'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1073         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1074         'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1075         'rows' : fields.integer('Number of Layers', required=True,
1076             help='The number of layers on a pallet or box'),
1077         'product_id' : fields.many2one('product.product', 'Product', select=1, ondelete='cascade', required=True),
1078         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1079         'code' : fields.char('Code', help="The code of the transport unit."),
1080         'weight': fields.float('Total Package Weight',
1081             help='The weight of a full package, pallet or box.'),
1082     }
1083
1084     def _check_ean_key(self, cr, uid, ids, context=None):
1085         for pack in self.browse(cr, uid, ids, context=context):
1086             if not check_ean(pack.ean):
1087                 return False
1088         return True
1089
1090     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1091
1092     def name_get(self, cr, uid, ids, context=None):
1093         if not len(ids):
1094             return []
1095         res = []
1096         for pckg in self.browse(cr, uid, ids, context=context):
1097             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1098             p_name += pckg.ul.name
1099             res.append((pckg.id,p_name))
1100         return res
1101
1102     def _get_1st_ul(self, cr, uid, context=None):
1103         cr.execute('select id from product_ul order by id asc limit 1')
1104         res = cr.fetchone()
1105         return (res and res[0]) or False
1106
1107     _defaults = {
1108         'rows' : 3,
1109         'sequence' : 1,
1110         'ul' : _get_1st_ul,
1111     }
1112
1113     def checksum(ean):
1114         salt = '31' * 6 + '3'
1115         sum = 0
1116         for ean_part, salt_part in zip(ean, salt):
1117             sum += int(ean_part) * int(salt_part)
1118         return (10 - (sum % 10)) % 10
1119     checksum = staticmethod(checksum)
1120
1121
1122
1123 class product_supplierinfo(osv.osv):
1124     _name = "product.supplierinfo"
1125     _description = "Information about a product supplier"
1126     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1127         result = {}
1128         for supplier_info in self.browse(cr, uid, ids, context=context):
1129             for field in fields:
1130                 result[supplier_info.id] = {field:False}
1131             qty = supplier_info.min_qty
1132             result[supplier_info.id]['qty'] = qty
1133         return result
1134
1135     _columns = {
1136         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1137         'product_name': fields.char('Supplier Product Name', help="This supplier's product name will be used when printing a request for quotation. Keep empty to use the internal one."),
1138         'product_code': fields.char('Supplier Product Code', help="This supplier's product code will be used when printing a request for quotation. Keep empty to use the internal one."),
1139         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1140         'product_uom': fields.related('product_tmpl_id', 'uom_po_id', type='many2one', relation='product.uom', string="Supplier Unit of Measure", readonly="1", help="This comes from the product form."),
1141         'min_qty': fields.float('Minimal Quantity', required=True, help="The minimal quantity to purchase to this supplier, expressed in the supplier Product Unit of Measure if not empty, in the default unit of measure of the product otherwise."),
1142         'qty': fields.function(_calc_qty, store=True, type='float', string='Quantity', multi="qty", help="This is a quantity which is converted into Default Unit of Measure."),
1143         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1144         '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."),
1145         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist'),
1146         'company_id':fields.many2one('res.company','Company',select=1),
1147     }
1148     _defaults = {
1149         'qty': 0.0,
1150         'sequence': 1,
1151         'delay': 1,
1152         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1153     }
1154     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
1155         """
1156         Calculate price from supplier pricelist.
1157         @param supplier_ids: Ids of res.partner object.
1158         @param product_id: Id of product.
1159         @param product_qty: specify quantity to purchase.
1160         """
1161         if type(supplier_ids) in (int,long,):
1162             supplier_ids = [supplier_ids]
1163         res = {}
1164         product_pool = self.pool.get('product.product')
1165         partner_pool = self.pool.get('res.partner')
1166         pricelist_pool = self.pool.get('product.pricelist')
1167         currency_pool = self.pool.get('res.currency')
1168         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
1169         # Compute price from standard price of product
1170         product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
1171         product = product_pool.browse(cr, uid, product_id, context=context)
1172         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
1173             price = product_price
1174             # Compute price from Purchase pricelist of supplier
1175             pricelist_id = supplier.property_product_pricelist_purchase.id
1176             if pricelist_id:
1177                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
1178                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
1179
1180             # Compute price from supplier pricelist which are in Supplier Information
1181             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
1182             if supplier_info_ids:
1183                 cr.execute('SELECT * ' \
1184                     'FROM pricelist_partnerinfo ' \
1185                     'WHERE suppinfo_id IN %s' \
1186                     'AND min_quantity <= %s ' \
1187                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
1188                 res2 = cr.dictfetchone()
1189                 if res2:
1190                     price = res2['price']
1191             res[supplier.id] = price
1192         return res
1193     _order = 'sequence'
1194
1195
1196 class pricelist_partnerinfo(osv.osv):
1197     _name = 'pricelist.partnerinfo'
1198     _columns = {
1199         'name': fields.char('Description'),
1200         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1201         'min_quantity': fields.float('Quantity', required=True, help="The minimal quantity to trigger this rule, expressed in the supplier Unit of Measure if any or in the default Unit of Measure of the product otherrwise."),
1202         'price': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), help="This price will be considered as a price for the supplier Unit of Measure if any or the default Unit of Measure of the product otherwise"),
1203     }
1204     _order = 'min_quantity asc'
1205
1206 class res_currency(osv.osv):
1207     _inherit = 'res.currency'
1208
1209     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1210         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1211         digits = cr.fetchone()
1212         if digits and len(digits):
1213             digits = digits[0]
1214             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1215             for currency_id in ids:
1216                 if currency_id == main_currency.id:
1217                     if main_currency.rounding < 10 ** -digits:
1218                         return False
1219         return True
1220
1221     _constraints = [
1222         (_check_main_currency_rounding, 'Error! You cannot define a rounding factor for the company\'s main currency that is smaller than the decimal precision of \'Account\'.', ['rounding']),
1223     ]
1224
1225 class decimal_precision(osv.osv):
1226     _inherit = 'decimal.precision'
1227
1228     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1229         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1230         res = cr.fetchone()
1231         if res and len(res):
1232             account_precision_id, digits = res
1233             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1234             for decimal_precision in ids:
1235                 if decimal_precision == account_precision_id:
1236                     if main_currency.rounding < 10 ** -digits:
1237                         return False
1238         return True
1239
1240     _constraints = [
1241         (_check_main_currency_rounding, 'Error! You cannot define the decimal precision of \'Account\' as greater than the rounding factor of the company\'s main currency', ['digits']),
1242     ]
1243
1244 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: