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