[MERGE] forward port of branch 8.0 up to 2e092ac
[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         'factor': 1.0,
160     }
161
162     _sql_constraints = [
163         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
164     ]
165
166     def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True, rounding_method='UP'):
167         if not from_uom_id or not qty or not to_uom_id:
168             return qty
169         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
170         if uoms[0].id == from_uom_id:
171             from_unit, to_unit = uoms[0], uoms[-1]
172         else:
173             from_unit, to_unit = uoms[-1], uoms[0]
174         return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round, rounding_method=rounding_method)
175
176     def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, rounding_method='UP', context=None):
177         if context is None:
178             context = {}
179         if from_unit.category_id.id != to_unit.category_id.id:
180             if context.get('raise-exception', True):
181                 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,))
182             else:
183                 return qty
184         amount = qty/from_unit.factor
185         if to_unit:
186             amount = amount * to_unit.factor
187             if round:
188                 amount = float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
189         return amount
190
191     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
192         if not from_uom_id or not price or not to_uom_id:
193             return price
194         from_unit, to_unit = self.browse(cr, uid, [from_uom_id, to_uom_id])
195         if from_unit.category_id.id != to_unit.category_id.id:
196             return price
197         amount = price * from_unit.factor
198         if to_uom_id:
199             amount = amount / to_unit.factor
200         return amount
201
202     def onchange_type(self, cursor, user, ids, value):
203         if value == 'reference':
204             return {'value': {'factor': 1, 'factor_inv': 1}}
205         return {}
206
207     def write(self, cr, uid, ids, vals, context=None):
208         if isinstance(ids, (int, long)):
209             ids = [ids]
210         if 'category_id' in vals:
211             for uom in self.browse(cr, uid, ids, context=context):
212                 if uom.category_id.id != vals['category_id']:
213                     raise osv.except_osv(_('Warning!'),_("Cannot change the category of existing Unit of Measure '%s'.") % (uom.name,))
214         return super(product_uom, self).write(cr, uid, ids, vals, context=context)
215
216
217
218 class product_ul(osv.osv):
219     _name = "product.ul"
220     _description = "Logistic Unit"
221     _columns = {
222         'name' : fields.char('Name', select=True, required=True, translate=True),
223         'type' : fields.selection([('unit','Unit'),('pack','Pack'),('box', 'Box'), ('pallet', 'Pallet')], 'Type', required=True),
224         'height': fields.float('Height', help='The height of the package'),
225         'width': fields.float('Width', help='The width of the package'),
226         'length': fields.float('Length', help='The length of the package'),
227         'weight': fields.float('Empty Package Weight'),
228     }
229
230
231 #----------------------------------------------------------
232 # Categories
233 #----------------------------------------------------------
234 class product_category(osv.osv):
235
236     def name_get(self, cr, uid, ids, context=None):
237         if isinstance(ids, (list, tuple)) and not len(ids):
238             return []
239         if isinstance(ids, (long, int)):
240             ids = [ids]
241         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
242         res = []
243         for record in reads:
244             name = record['name']
245             if record['parent_id']:
246                 name = record['parent_id'][1]+' / '+name
247             res.append((record['id'], name))
248         return res
249
250     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
251         if not args:
252             args = []
253         if not context:
254             context = {}
255         if name:
256             # Be sure name_search is symetric to name_get
257             name = name.split(' / ')[-1]
258             ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
259         else:
260             ids = self.search(cr, uid, args, limit=limit, context=context)
261         return self.name_get(cr, uid, ids, context)
262
263     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
264         res = self.name_get(cr, uid, ids, context=context)
265         return dict(res)
266
267     _name = "product.category"
268     _description = "Product Category"
269     _columns = {
270         'name': fields.char('Name', required=True, translate=True, select=True),
271         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
272         'parent_id': fields.many2one('product.category','Parent Category', select=True, ondelete='cascade'),
273         'child_id': fields.one2many('product.category', 'parent_id', string='Child Categories'),
274         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of product categories."),
275         '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."),
276         'parent_left': fields.integer('Left Parent', select=1),
277         'parent_right': fields.integer('Right Parent', select=1),
278     }
279
280
281     _defaults = {
282         'type' : 'normal',
283     }
284
285     _parent_name = "parent_id"
286     _parent_store = True
287     _parent_order = 'sequence, name'
288     _order = 'parent_left'
289
290     _constraints = [
291         (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
292     ]
293
294
295 class produce_price_history(osv.osv):
296     """
297     Keep track of the ``product.template`` standard prices as they are changed.
298     """
299
300     _name = 'product.price.history'
301     _rec_name = 'datetime'
302     _order = 'datetime desc'
303
304     _columns = {
305         'company_id': fields.many2one('res.company', required=True),
306         'product_template_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
307         'datetime': fields.datetime('Historization Time'),
308         'cost': fields.float('Historized Cost'),
309     }
310
311     def _get_default_company(self, cr, uid, context=None):
312         if 'force_company' in context:
313             return context['force_company']
314         else:
315             company = self.pool['res.users'].browse(cr, uid, uid,
316                 context=context).company_id
317             return company.id if company else False
318
319     _defaults = {
320         'datetime': fields.datetime.now,
321         'company_id': _get_default_company,
322     }
323
324
325 #----------------------------------------------------------
326 # Product Attributes
327 #----------------------------------------------------------
328 class product_attribute(osv.osv):
329     _name = "product.attribute"
330     _description = "Product Attribute"
331     _columns = {
332         'name': fields.char('Name', translate=True, required=True),
333         'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values', copy=True),
334     }
335
336 class product_attribute_value(osv.osv):
337     _name = "product.attribute.value"
338     _order = 'sequence'
339     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
340         result = dict.fromkeys(ids, 0)
341         if not context.get('active_id'):
342             return result
343
344         for obj in self.browse(cr, uid, ids, context=context):
345             for price_id in obj.price_ids:
346                 if price_id.product_tmpl_id.id == context.get('active_id'):
347                     result[obj.id] = price_id.price_extra
348                     break
349         return result
350
351     def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
352         if context is None:
353             context = {}
354         if 'active_id' not in context:
355             return None
356         p_obj = self.pool['product.attribute.price']
357         p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
358         if p_ids:
359             p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
360         else:
361             p_obj.create(cr, uid, {
362                     'product_tmpl_id': context['active_id'],
363                     'value_id': id,
364                     'price_extra': value,
365                 }, context=context)
366
367     def name_get(self, cr, uid, ids, context=None):
368         if context and not context.get('show_attribute', True):
369             return super(product_attribute_value, self).name_get(cr, uid, ids, context=context)
370         res = []
371         for value in self.browse(cr, uid, ids, context=context):
372             res.append([value.id, "%s: %s" % (value.attribute_id.name, value.name)])
373         return res
374
375     _columns = {
376         'sequence': fields.integer('Sequence', help="Determine the display order"),
377         'name': fields.char('Value', translate=True, required=True),
378         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='cascade'),
379         'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
380         'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
381             fnct_inv=_set_price_extra,
382             digits_compute=dp.get_precision('Product Price'),
383             help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
384         'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
385     }
386     _sql_constraints = [
387         ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
388     ]
389     _defaults = {
390         'price_extra': 0.0,
391     }
392     def unlink(self, cr, uid, ids, context=None):
393         ctx = dict(context or {}, active_test=False)
394         product_ids = self.pool['product.product'].search(cr, uid, [('attribute_value_ids', 'in', ids)], context=ctx)
395         if product_ids:
396             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.'))
397         return super(product_attribute_value, self).unlink(cr, uid, ids, context=context)
398
399 class product_attribute_price(osv.osv):
400     _name = "product.attribute.price"
401     _columns = {
402         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
403         'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True, ondelete='cascade'),
404         'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
405     }
406
407 class product_attribute_line(osv.osv):
408     _name = "product.attribute.line"
409     _rec_name = 'attribute_id'
410     _columns = {
411         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
412         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='restrict'),
413         'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
414     }
415
416
417 #----------------------------------------------------------
418 # Products
419 #----------------------------------------------------------
420 class product_template(osv.osv):
421     _name = "product.template"
422     _inherit = ['mail.thread']
423     _description = "Product Template"
424     _order = "name"
425
426     def _get_image(self, cr, uid, ids, name, args, context=None):
427         result = dict.fromkeys(ids, False)
428         for obj in self.browse(cr, uid, ids, context=context):
429             result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
430         return result
431
432     def _set_image(self, cr, uid, id, name, value, args, context=None):
433         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
434
435     def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
436         return self._is_product_variant_impl(cr, uid, ids, name, arg, context=context)
437
438     def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
439         return dict.fromkeys(ids, False)
440
441     def _product_template_price(self, cr, uid, ids, name, arg, context=None):
442         plobj = self.pool.get('product.pricelist')
443         res = {}
444         quantity = context.get('quantity') or 1.0
445         pricelist = context.get('pricelist', False)
446         partner = context.get('partner', False)
447         if pricelist:
448             # Support context pricelists specified as display_name or ID for compatibility
449             if isinstance(pricelist, basestring):
450                 pricelist_ids = plobj.name_search(
451                     cr, uid, pricelist, operator='=', context=context, limit=1)
452                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
453
454             if isinstance(pricelist, (int, long)):
455                 products = self.browse(cr, uid, ids, context=context)
456                 qtys = map(lambda x: (x, quantity, partner), products)
457                 pl = plobj.browse(cr, uid, pricelist, context=context)
458                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
459                 for id in ids:
460                     res[id] = price.get(id, 0.0)
461         for id in ids:
462             res.setdefault(id, 0.0)
463         return res
464
465     def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
466         if context is None:
467             context = {}
468         if date is None:
469             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
470         price_history_obj = self.pool.get('product.price.history')
471         history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
472         if history_ids:
473             return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
474         return 0.0
475
476     def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
477         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
478         if context is None:
479             context = {}
480         price_history_obj = self.pool['product.price.history']
481         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
482         company_id = context.get('force_company', user_company)
483         price_history_obj.create(cr, uid, {
484             'product_template_id': product_tmpl_id,
485             'cost': value,
486             'company_id': company_id,
487         }, context=context)
488
489     def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
490         res = {}
491         for product in self.browse(cr, uid, ids):
492             res[product.id] = len(product.product_variant_ids)
493         return res
494
495     _columns = {
496         'name': fields.char('Name', required=True, translate=True, select=True),
497         'product_manager': fields.many2one('res.users','Product Manager'),
498         'description': fields.text('Description',translate=True,
499             help="A precise description of the Product, used only for internal information purposes."),
500         'description_purchase': fields.text('Purchase Description',translate=True,
501             help="A description of the Product that you want to communicate to your suppliers. "
502                  "This description will be copied to every Purchase Order, Receipt and Supplier Invoice/Refund."),
503         'description_sale': fields.text('Sale Description',translate=True,
504             help="A description of the Product that you want to communicate to your customers. "
505                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
506         '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."),        
507         'rental': fields.boolean('Can be Rent'),
508         'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
509         'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
510         '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."),
511         'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
512         'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), 
513                                           help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", 
514                                           groups="base.group_user", string="Cost Price"),
515         'volume': fields.float('Volume', help="The volume in m3."),
516         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
517         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
518         'warranty': fields.float('Warranty'),
519         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
520         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
521         'state': fields.selection([('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 'standard_price' in vals:
730             for prod_template_id in ids:
731                 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
732         res = super(product_template, self).write(cr, uid, ids, vals, context=context)
733         if 'attribute_line_ids' in vals or vals.get('active'):
734             self.create_variant_ids(cr, uid, ids, context=context)
735         if 'active' in vals and not vals.get('active'):
736             ctx = context and context.copy() or {}
737             ctx.update(active_test=False)
738             product_ids = []
739             for product in self.browse(cr, uid, ids, context=ctx):
740                 product_ids = map(int,product.product_variant_ids)
741             self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
742         return res
743
744     def copy(self, cr, uid, id, default=None, context=None):
745         if default is None:
746             default = {}
747         template = self.browse(cr, uid, id, context=context)
748         default['name'] = _("%s (copy)") % (template['name'])
749         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
750
751     _defaults = {
752         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
753         'list_price': 1,
754         'standard_price': 0.0,
755         'sale_ok': 1,        
756         'uom_id': _get_uom_id,
757         'uom_po_id': _get_uom_id,
758         'uos_coeff': 1.0,
759         'mes_type': 'fixed',
760         'categ_id' : _default_category,
761         'type' : 'consu',
762         'active': True,
763     }
764
765     def _check_uom(self, cursor, user, ids, context=None):
766         for product in self.browse(cursor, user, ids, context=context):
767             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
768                 return False
769         return True
770
771     def _check_uos(self, cursor, user, ids, context=None):
772         for product in self.browse(cursor, user, ids, context=context):
773             if product.uos_id \
774                     and product.uos_id.category_id.id \
775                     == product.uom_id.category_id.id:
776                 return False
777         return True
778
779     _constraints = [
780         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
781     ]
782
783     def name_get(self, cr, user, ids, context=None):
784         if context is None:
785             context = {}
786         if 'partner_id' in context:
787             pass
788         return super(product_template, self).name_get(cr, user, ids, context)
789
790
791
792
793
794 class product_product(osv.osv):
795     _name = "product.product"
796     _description = "Product"
797     _inherits = {'product.template': 'product_tmpl_id'}
798     _inherit = ['mail.thread']
799     _order = 'default_code,name_template'
800
801     def _product_price(self, cr, uid, ids, name, arg, context=None):
802         plobj = self.pool.get('product.pricelist')
803         res = {}
804         if context is None:
805             context = {}
806         quantity = context.get('quantity') or 1.0
807         pricelist = context.get('pricelist', False)
808         partner = context.get('partner', False)
809         if pricelist:
810             # Support context pricelists specified as display_name or ID for compatibility
811             if isinstance(pricelist, basestring):
812                 pricelist_ids = plobj.name_search(
813                     cr, uid, pricelist, operator='=', context=context, limit=1)
814                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
815
816             if isinstance(pricelist, (int, long)):
817                 products = self.browse(cr, uid, ids, context=context)
818                 qtys = map(lambda x: (x, quantity, partner), products)
819                 pl = plobj.browse(cr, uid, pricelist, context=context)
820                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
821                 for id in ids:
822                     res[id] = price.get(id, 0.0)
823         for id in ids:
824             res.setdefault(id, 0.0)
825         return res
826
827     def view_header_get(self, cr, uid, view_id, view_type, context=None):
828         if context is None:
829             context = {}
830         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
831         if (context.get('categ_id', False)):
832             return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
833         return res
834
835     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
836         product_uom_obj = self.pool.get('product.uom')
837         res = dict.fromkeys(ids, 0.0)
838
839         for product in self.browse(cr, uid, ids, context=context):
840             if 'uom' in context:
841                 uom = product.uos_id or product.uom_id
842                 res[product.id] = product_uom_obj._compute_price(cr, uid,
843                         uom.id, product.list_price, context['uom'])
844             else:
845                 res[product.id] = product.list_price
846             res[product.id] =  res[product.id] + product.price_extra
847
848         return res
849
850     def _set_product_lst_price(self, cr, uid, id, name, value, args, context=None):
851         product_uom_obj = self.pool.get('product.uom')
852
853         product = self.browse(cr, uid, id, context=context)
854         if 'uom' in context:
855             uom = product.uos_id or product.uom_id
856             value = product_uom_obj._compute_price(cr, uid,
857                     context['uom'], value, uom.id)
858         value =  value - product.price_extra
859         
860         return product.write({'list_price': value})
861
862     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
863         for supinfo in product.seller_ids:
864             if supinfo.name.id == partner_id:
865                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
866         res = {'code': product.default_code, 'name': product.name}
867         return res
868
869     def _product_code(self, cr, uid, ids, name, arg, context=None):
870         res = {}
871         if context is None:
872             context = {}
873         for p in self.browse(cr, uid, ids, context=context):
874             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
875         return res
876
877     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
878         res = {}
879         if context is None:
880             context = {}
881         for p in self.browse(cr, uid, ids, context=context):
882             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
883             if not data['code']:
884                 data['code'] = p.code
885             if not data['name']:
886                 data['name'] = p.name
887             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
888         return res
889
890     def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
891         return dict.fromkeys(ids, True)
892
893     def _get_name_template_ids(self, cr, uid, ids, context=None):
894         result = set()
895         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
896         for el in template_ids:
897             result.add(el)
898         return list(result)
899
900     def _get_image_variant(self, cr, uid, ids, name, args, context=None):
901         result = dict.fromkeys(ids, False)
902         for obj in self.browse(cr, uid, ids, context=context):
903             result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
904         return result
905
906     def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
907         image = tools.image_resize_image_big(value)
908         res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
909         product = self.browse(cr, uid, id, context=context)
910         if not product.product_tmpl_id.image:
911             product.write({'image_variant': None})
912             product.product_tmpl_id.write({'image': image})
913         return res
914
915     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
916         result = dict.fromkeys(ids, False)
917         for product in self.browse(cr, uid, ids, context=context):
918             price_extra = 0.0
919             for variant_id in product.attribute_value_ids:
920                 for price_id in variant_id.price_ids:
921                     if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
922                         price_extra += price_id.price_extra
923             result[product.id] = price_extra
924         return result
925
926     _columns = {
927         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
928         '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"),
929         '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')),
930         'code': fields.function(_product_code, type='char', string='Internal Reference'),
931         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
932         'default_code' : fields.char('Internal Reference', select=True),
933         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
934         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True, auto_join=True),
935         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
936         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
937             'product.template': (_get_name_template_ids, ['name'], 10),
938             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
939         }, select=True),
940         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
941         'is_product_variant': fields.function( _is_product_variant_impl, type='boolean', string='Is product variant'),
942
943         # image: all image fields are base64 encoded and PIL-supported
944         'image_variant': fields.binary("Variant Image",
945             help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
946
947         'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
948             string="Big-sized image", type="binary",
949             help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
950                  "resized as a 1024x1024px image, with aspect ratio preserved."),
951         'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
952             string="Small-sized image", type="binary",
953             help="Image of the product variant (Small-sized image of product template if false)."),
954         'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
955             string="Medium-sized image", type="binary",
956             help="Image of the product variant (Medium-sized image of product template if false)."),
957     }
958
959     _defaults = {
960         'active': 1,
961         'color': 0,
962     }
963
964     def unlink(self, cr, uid, ids, context=None):
965         unlink_ids = []
966         unlink_product_tmpl_ids = []
967         for product in self.browse(cr, uid, ids, context=context):
968             # Check if product still exists, in case it has been unlinked by unlinking its template
969             if not product.exists():
970                 continue
971             tmpl_id = product.product_tmpl_id.id
972             # Check if the product is last product of this template
973             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
974             if not other_product_ids:
975                 unlink_product_tmpl_ids.append(tmpl_id)
976             unlink_ids.append(product.id)
977         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
978         # delete templates after calling super, as deleting template could lead to deleting
979         # products due to ondelete='cascade'
980         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
981         return res
982
983     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
984         if uom_id and uom_po_id:
985             uom_obj=self.pool.get('product.uom')
986             uom=uom_obj.browse(cursor,user,[uom_id])[0]
987             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
988             if uom.category_id.id != uom_po.category_id.id:
989                 return {'value': {'uom_po_id': uom_id}}
990         return False
991
992     def _check_ean_key(self, cr, uid, ids, context=None):
993         for product in self.read(cr, uid, ids, ['ean13'], context=context):
994             if not check_ean(product['ean13']):
995                 return False
996         return True
997
998     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
999
1000     def on_order(self, cr, uid, ids, orderline, quantity):
1001         pass
1002
1003     def name_get(self, cr, user, ids, context=None):
1004         if context is None:
1005             context = {}
1006         if isinstance(ids, (int, long)):
1007             ids = [ids]
1008         if not len(ids):
1009             return []
1010
1011         def _name_get(d):
1012             name = d.get('name','')
1013             code = context.get('display_default_code', True) and d.get('default_code',False) or False
1014             if code:
1015                 name = '[%s] %s' % (code,name)
1016             return (d['id'], name)
1017
1018         partner_id = context.get('partner_id', False)
1019         if partner_id:
1020             partner_ids = [partner_id, self.pool['res.partner'].browse(cr, user, partner_id, context=context).commercial_partner_id.id]
1021         else:
1022             partner_ids = []
1023
1024         # all user don't have access to seller and partner
1025         # check access and use superuser
1026         self.check_access_rights(cr, user, "read")
1027         self.check_access_rule(cr, user, ids, "read", context=context)
1028
1029         result = []
1030         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
1031             variant = ", ".join([v.name for v in product.attribute_value_ids])
1032             name = variant and "%s (%s)" % (product.name, variant) or product.name
1033             sellers = []
1034             if partner_ids:
1035                 sellers = filter(lambda x: x.name.id in partner_ids, product.seller_ids)
1036             if sellers:
1037                 for s in sellers:
1038                     seller_variant = s.product_name and "%s (%s)" % (s.product_name, variant) or False
1039                     mydict = {
1040                               'id': product.id,
1041                               'name': seller_variant or name,
1042                               'default_code': s.product_code or product.default_code,
1043                               }
1044                     result.append(_name_get(mydict))
1045             else:
1046                 mydict = {
1047                           'id': product.id,
1048                           'name': name,
1049                           'default_code': product.default_code,
1050                           }
1051                 result.append(_name_get(mydict))
1052         return result
1053
1054     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
1055         if context is None:
1056             context = {}
1057         if not args:
1058             args = []
1059         if name:
1060             positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
1061             ids = []
1062             if operator in positive_operators:
1063                 ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
1064                 if not ids:
1065                     ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
1066             if not ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
1067                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1068                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1069                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1070                 # Performing a quick memory merge of ids in Python will give much better performance
1071                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1072                 if not limit or len(ids) < limit:
1073                     # we may underrun the limit because of dupes in the results, that's fine
1074                     limit2 = (limit - len(ids)) if limit else False
1075                     ids.update(self.search(cr, user, args + [('name', operator, name), ('id', 'not in', list(ids))], limit=limit2, context=context))
1076                 ids = list(ids)
1077             elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
1078                 ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
1079             if not ids and operator in positive_operators:
1080                 ptrn = re.compile('(\[(.*?)\])')
1081                 res = ptrn.search(name)
1082                 if res:
1083                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1084             # still no results, partner in context: search on supplier info as last hope to find something
1085             if not ids and context.get('partner_id'):
1086                 supplier_ids = self.pool['product.supplierinfo'].search(
1087                     cr, user, [
1088                         ('name', '=', context.get('partner_id')),
1089                         '|',
1090                         ('product_code', operator, name),
1091                         ('product_name', operator, name)
1092                     ], context=context)
1093                 if supplier_ids:
1094                     ids = self.search(cr, user, [('product_tmpl_id.seller_ids', 'in', supplier_ids)], limit=limit, context=context)
1095         else:
1096             ids = self.search(cr, user, args, limit=limit, context=context)
1097         result = self.name_get(cr, user, ids, context=context)
1098         return result
1099
1100     #
1101     # Could be overrided for variants matrices prices
1102     #
1103     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1104         products = self.browse(cr, uid, ids, context=context)
1105         return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1106
1107     def copy(self, cr, uid, id, default=None, context=None):
1108         if context is None:
1109             context={}
1110
1111         product = self.browse(cr, uid, id, context)
1112         if context.get('variant'):
1113             # if we copy a variant or create one, we keep the same template
1114             default['product_tmpl_id'] = product.product_tmpl_id.id
1115         elif 'name' not in default:
1116             default['name'] = _("%s (copy)") % (product.name,)
1117
1118         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1119
1120     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1121         if context is None:
1122             context = {}
1123         if context.get('search_default_categ_id'):
1124             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1125         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1126
1127     def open_product_template(self, cr, uid, ids, context=None):
1128         """ Utility method used to add an "Open Template" button in product views """
1129         product = self.browse(cr, uid, ids[0], context=context)
1130         return {'type': 'ir.actions.act_window',
1131                 'res_model': 'product.template',
1132                 'view_mode': 'form',
1133                 'res_id': product.product_tmpl_id.id,
1134                 'target': 'new'}
1135
1136     def create(self, cr, uid, vals, context=None):
1137         if context is None:
1138             context = {}
1139         ctx = dict(context or {}, create_product_product=True)
1140         return super(product_product, self).create(cr, uid, vals, context=ctx)
1141
1142
1143
1144     def need_procurement(self, cr, uid, ids, context=None):
1145         return False
1146
1147     def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
1148         '''
1149         Computes product's invoicing quantity in UoS from quantity in UoM.
1150         Takes into account the
1151         :param uom: Source unit
1152         :param qty: Source quantity
1153         :param uos: Target UoS unit.
1154         '''
1155         if not uom or not qty or not uos:
1156             return qty
1157         uom_obj = self.pool['product.uom']
1158         product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
1159         product = self.browse(cr, uid, product_id, context=context)
1160         if isinstance(uos, (int, long)):
1161             uos = uom_obj.browse(cr, uid, uos, context=context)
1162         if isinstance(uom, (int, long)):
1163             uom = uom_obj.browse(cr, uid, uom, context=context)
1164         if product.uos_id:  # Product has UoS defined
1165             # We cannot convert directly between units even if the units are of the same category
1166             # as we need to apply the conversion coefficient which is valid only between quantities
1167             # in product's default UoM/UoS
1168             qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id)  # qty in product's default UoM
1169             qty_default_uos = qty_default_uom * product.uos_coeff
1170             return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
1171         else:
1172             return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
1173
1174
1175
1176 class product_packaging(osv.osv):
1177     _name = "product.packaging"
1178     _description = "Packaging"
1179     _rec_name = 'ean'
1180     _order = 'sequence'
1181     _columns = {
1182         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1183         'name' : fields.text('Description'),
1184         'qty' : fields.float('Quantity by Package',
1185             help="The total number of products you can put by pallet or box."),
1186         'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1187         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1188         'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1189         'rows' : fields.integer('Number of Layers', required=True,
1190             help='The number of layers on a pallet or box'),
1191         'product_tmpl_id' : fields.many2one('product.template', 'Product', select=1, ondelete='cascade', required=True),
1192         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1193         'code' : fields.char('Code', help="The code of the transport unit."),
1194         'weight': fields.float('Total Package Weight',
1195             help='The weight of a full package, pallet or box.'),
1196     }
1197
1198     def _check_ean_key(self, cr, uid, ids, context=None):
1199         for pack in self.browse(cr, uid, ids, context=context):
1200             if not check_ean(pack.ean):
1201                 return False
1202         return True
1203
1204     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1205
1206     def name_get(self, cr, uid, ids, context=None):
1207         if not len(ids):
1208             return []
1209         res = []
1210         for pckg in self.browse(cr, uid, ids, context=context):
1211             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1212             p_name += pckg.ul.name
1213             res.append((pckg.id,p_name))
1214         return res
1215
1216     def _get_1st_ul(self, cr, uid, context=None):
1217         cr.execute('select id from product_ul order by id asc limit 1')
1218         res = cr.fetchone()
1219         return (res and res[0]) or False
1220
1221     _defaults = {
1222         'rows' : 3,
1223         'sequence' : 1,
1224         'ul' : _get_1st_ul,
1225     }
1226
1227     def checksum(ean):
1228         salt = '31' * 6 + '3'
1229         sum = 0
1230         for ean_part, salt_part in zip(ean, salt):
1231             sum += int(ean_part) * int(salt_part)
1232         return (10 - (sum % 10)) % 10
1233     checksum = staticmethod(checksum)
1234
1235
1236
1237 class product_supplierinfo(osv.osv):
1238     _name = "product.supplierinfo"
1239     _description = "Information about a product supplier"
1240     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1241         result = {}
1242         for supplier_info in self.browse(cr, uid, ids, context=context):
1243             for field in fields:
1244                 result[supplier_info.id] = {field:False}
1245             qty = supplier_info.min_qty
1246             result[supplier_info.id]['qty'] = qty
1247         return result
1248
1249     _columns = {
1250         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1251         '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."),
1252         '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."),
1253         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1254         '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."),
1255         '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."),
1256         '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."),
1257         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1258         '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."),
1259         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist', copy=True),
1260         'company_id':fields.many2one('res.company', string='Company',select=1),
1261     }
1262     _defaults = {
1263         'min_qty': 0.0,
1264         'sequence': 1,
1265         'delay': 1,
1266         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1267     }
1268
1269     _order = 'sequence'
1270
1271
1272 class pricelist_partnerinfo(osv.osv):
1273     _name = 'pricelist.partnerinfo'
1274     _columns = {
1275         'name': fields.char('Description'),
1276         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1277         '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."),
1278         '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"),
1279     }
1280     _order = 'min_quantity asc'
1281
1282 class res_currency(osv.osv):
1283     _inherit = 'res.currency'
1284
1285     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1286         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1287         digits = cr.fetchone()
1288         if digits and len(digits):
1289             digits = digits[0]
1290             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1291             for currency_id in ids:
1292                 if currency_id == main_currency.id:
1293                     if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1294                         return False
1295         return True
1296
1297     _constraints = [
1298         (_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']),
1299     ]
1300
1301 class decimal_precision(osv.osv):
1302     _inherit = 'decimal.precision'
1303
1304     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1305         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1306         res = cr.fetchone()
1307         if res and len(res):
1308             account_precision_id, digits = res
1309             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1310             for decimal_precision in ids:
1311                 if decimal_precision == account_precision_id:
1312                     if float_compare(main_currency.rounding, 10 ** -digits, precision_digits=6) == -1:
1313                         return False
1314         return True
1315
1316     _constraints = [
1317         (_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']),
1318     ]
1319
1320 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: