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