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