[MERGE] forward port of branch saas-5 up to 39bee35
[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         return dict.fromkeys(ids, False)
428
429     def _product_template_price(self, cr, uid, ids, name, arg, context=None):
430         plobj = self.pool.get('product.pricelist')
431         res = {}
432         quantity = context.get('quantity') or 1.0
433         pricelist = context.get('pricelist', False)
434         partner = context.get('partner', False)
435         if pricelist:
436             # Support context pricelists specified as display_name or ID for compatibility
437             if isinstance(pricelist, basestring):
438                 pricelist_ids = plobj.name_search(
439                     cr, uid, pricelist, operator='=', context=context, limit=1)
440                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
441
442             if isinstance(pricelist, (int, long)):
443                 products = self.browse(cr, uid, ids, context=context)
444                 qtys = map(lambda x: (x, quantity, partner), products)
445                 pl = plobj.browse(cr, uid, pricelist, context=context)
446                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
447                 for id in ids:
448                     res[id] = price.get(id, 0.0)
449         for id in ids:
450             res.setdefault(id, 0.0)
451         return res
452
453     def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
454         if context is None:
455             context = {}
456         if date is None:
457             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
458         price_history_obj = self.pool.get('product.price.history')
459         history_ids = price_history_obj.search(cr, uid, [('company_id', '=', company_id), ('product_template_id', '=', product_tmpl), ('datetime', '<=', date)], limit=1)
460         if history_ids:
461             return price_history_obj.read(cr, uid, history_ids[0], ['cost'], context=context)['cost']
462         return 0.0
463
464     def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
465         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
466         if context is None:
467             context = {}
468         price_history_obj = self.pool['product.price.history']
469         user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
470         company_id = context.get('force_company', user_company)
471         price_history_obj.create(cr, uid, {
472             'product_template_id': product_tmpl_id,
473             'cost': value,
474             'company_id': company_id,
475         }, context=context)
476
477     def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
478         res = {}
479         for product in self.browse(cr, uid, ids):
480             res[product.id] = len(product.product_variant_ids)
481         return res
482
483     _columns = {
484         'name': fields.char('Name', required=True, translate=True, select=True),
485         'product_manager': fields.many2one('res.users','Product Manager'),
486         'description': fields.text('Description',translate=True,
487             help="A precise description of the Product, used only for internal information purposes."),
488         'description_purchase': fields.text('Purchase Description',translate=True,
489             help="A description of the Product that you want to communicate to your suppliers. "
490                  "This description will be copied to every Purchase Order, Receipt and Supplier Invoice/Refund."),
491         'description_sale': fields.text('Sale Description',translate=True,
492             help="A description of the Product that you want to communicate to your customers. "
493                  "This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
494         '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."),        
495         'rental': fields.boolean('Can be Rent'),
496         'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
497         'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
498         '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."),
499         'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
500         'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), 
501                                           help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", 
502                                           groups="base.group_user", string="Cost Price"),
503         'volume': fields.float('Volume', help="The volume in m3."),
504         'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."),
505         'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
506         'warranty': fields.float('Warranty'),
507         'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
508         'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
509         'state': fields.selection([('',''),
510             ('draft', 'In Development'),
511             ('sellable','Normal'),
512             ('end','End of Lifecycle'),
513             ('obsolete','Obsolete')], 'Status'),
514         'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True, help="Default Unit of Measure used for all stock operation."),
515         'uom_po_id': fields.many2one('product.uom', 'Purchase Unit of Measure', required=True, help="Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."),
516         'uos_id' : fields.many2one('product.uom', 'Unit of Sale',
517             help='Specify a unit of measure here if invoicing is made in another unit of measure than inventory. Keep empty to use the default unit of measure.'),
518         'uos_coeff': fields.float('Unit of Measure -> UOS Coeff', digits_compute= dp.get_precision('Product UoS'),
519             help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
520             ' uos = uom * coeff'),
521         'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
522         'company_id': fields.many2one('res.company', 'Company', select=1),
523         # image: all image fields are base64 encoded and PIL-supported
524         'image': fields.binary("Image",
525             help="This field holds the image used as image for the product, limited to 1024x1024px."),
526         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
527             string="Medium-sized image", type="binary", multi="_get_image", 
528             store={
529                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
530             },
531             help="Medium-sized image of the product. It is automatically "\
532                  "resized as a 128x128px image, with aspect ratio preserved, "\
533                  "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
534         'image_small': fields.function(_get_image, fnct_inv=_set_image,
535             string="Small-sized image", type="binary", multi="_get_image",
536             store={
537                 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
538             },
539             help="Small-sized image of the product. It is automatically "\
540                  "resized as a 64x64px image, with aspect ratio preserved. "\
541                  "Use this field anywhere a small image is required."),
542         'packaging_ids': fields.one2many(
543             'product.packaging', 'product_tmpl_id', 'Logistical Units',
544             help="Gives the different ways to package the same product. This has no impact on "
545                  "the picking order and is mainly used if you use the EDI module."),
546         'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
547         'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
548             help="This is the average delay in days between the purchase order confirmation and the receipts for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
549         'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
550             help="This is minimum quantity to purchase from Main Supplier."),
551         'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
552             help="Main Supplier who has highest priority in Supplier List."),
553
554         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
555         'color': fields.integer('Color Index'),
556         'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
557
558         'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
559         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
560         'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
561
562         # related to display product product information if is_product_variant
563         'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
564         'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
565     }
566
567     def _price_get_list_price(self, product):
568         return 0.0
569
570     def _price_get(self, cr, uid, products, ptype='list_price', context=None):
571         if context is None:
572             context = {}
573
574         if 'currency_id' in context:
575             pricetype_obj = self.pool.get('product.price.type')
576             price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
577             price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
578
579         res = {}
580         product_uom_obj = self.pool.get('product.uom')
581         for product in products:
582             res[product.id] = product[ptype] or 0.0
583             if ptype == 'list_price':
584                 res[product.id] += product._name == "product.product" and product.price_extra or 0.0
585             if 'uom' in context:
586                 uom = product.uom_id or product.uos_id
587                 res[product.id] = product_uom_obj._compute_price(cr, uid,
588                         uom.id, res[product.id], context['uom'])
589             # Convert from price_type currency to asked one
590             if 'currency_id' in context:
591                 # Take the price_type currency from the product field
592                 # This is right cause a field cannot be in more than one currency
593                 res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
594                     context['currency_id'], res[product.id],context=context)
595
596         return res
597
598     def _get_uom_id(self, cr, uid, *args):
599         return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
600
601     def _default_category(self, cr, uid, context=None):
602         if context is None:
603             context = {}
604         if 'categ_id' in context and context['categ_id']:
605             return context['categ_id']
606         md = self.pool.get('ir.model.data')
607         res = False
608         try:
609             res = md.get_object_reference(cr, uid, 'product', 'product_category_all')[1]
610         except ValueError:
611             res = False
612         return res
613
614     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
615         if uom_id:
616             return {'value': {'uom_po_id': uom_id}}
617         return {}
618
619     def create_variant_ids(self, cr, uid, ids, context=None):
620         product_obj = self.pool.get("product.product")
621         ctx = context and context.copy() or {}
622
623         if ctx.get("create_product_variant"):
624             return None
625
626         ctx.update(active_test=False, create_product_variant=True)
627
628         tmpl_ids = self.browse(cr, uid, ids, context=ctx)
629         for tmpl_id in tmpl_ids:
630
631             # list of values combination
632             all_variants = [[]]
633             for variant_id in tmpl_id.attribute_line_ids:
634                 if len(variant_id.value_ids) > 1:
635                     temp_variants = []
636                     for value_id in variant_id.value_ids:
637                         for variant in all_variants:
638                             temp_variants.append(variant + [int(value_id)])
639                     all_variants = temp_variants
640
641             # check product
642             variant_ids_to_active = []
643             variants_active_ids = []
644             variants_inactive = []
645             for product_id in tmpl_id.product_variant_ids:
646                 variants = map(int,product_id.attribute_value_ids)
647                 if variants in all_variants:
648                     variants_active_ids.append(product_id.id)
649                     all_variants.pop(all_variants.index(variants))
650                     if not product_id.active:
651                         variant_ids_to_active.append(product_id.id)
652                 else:
653                     variants_inactive.append(product_id)
654             if variant_ids_to_active:
655                 product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
656
657             # create new product
658             for variant_ids in all_variants:
659                 values = {
660                     'product_tmpl_id': tmpl_id.id,
661                     'attribute_value_ids': [(6, 0, variant_ids)]
662                 }
663                 id = product_obj.create(cr, uid, values, context=ctx)
664                 variants_active_ids.append(id)
665
666             # unlink or inactive product
667             for variant_id in map(int,variants_inactive):
668                 try:
669                     with cr.savepoint():
670                         product_obj.unlink(cr, uid, [variant_id], context=ctx)
671                 except (psycopg2.Error, osv.except_osv):
672                     product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
673                     pass
674         return True
675
676     def create(self, cr, uid, vals, context=None):
677         ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
678         product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
679         if not context or "create_product_product" not in context:
680             self.create_variant_ids(cr, uid, [product_template_id], context=context)
681         self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
682
683         # TODO: this is needed to set given values to first variant after creation
684         # these fields should be moved to product as lead to confusion
685         related_vals = {}
686         if vals.get('ean13'):
687             related_vals['ean13'] = vals['ean13']
688         if vals.get('default_code'):
689             related_vals['default_code'] = vals['default_code']
690         if related_vals:
691             self.write(cr, uid, product_template_id, related_vals, context=context)
692
693         return product_template_id
694
695     def write(self, cr, uid, ids, vals, context=None):
696         ''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
697         if isinstance(ids, (int, long)):
698             ids = [ids]
699         if 'uom_po_id' in vals:
700             new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
701             for product in self.browse(cr, uid, ids, context=context):
702                 old_uom = product.uom_po_id
703                 if old_uom.category_id.id != new_uom.category_id.id:
704                     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,))
705         if 'standard_price' in vals:
706             for prod_template_id in ids:
707                 self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
708         res = super(product_template, self).write(cr, uid, ids, vals, context=context)
709         if 'attribute_line_ids' in vals or vals.get('active'):
710             self.create_variant_ids(cr, uid, ids, context=context)
711         if 'active' in vals and not vals.get('active'):
712             ctx = context and context.copy() or {}
713             ctx.update(active_test=False)
714             product_ids = []
715             for product in self.browse(cr, uid, ids, context=ctx):
716                 product_ids = map(int,product.product_variant_ids)
717             self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
718         return res
719
720     def copy(self, cr, uid, id, default=None, context=None):
721         if default is None:
722             default = {}
723         template = self.browse(cr, uid, id, context=context)
724         default['name'] = _("%s (copy)") % (template['name'])
725         return super(product_template, self).copy(cr, uid, id, default=default, context=context)
726
727     _defaults = {
728         'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c),
729         'list_price': 1,
730         'standard_price': 0.0,
731         'sale_ok': 1,        
732         'uom_id': _get_uom_id,
733         'uom_po_id': _get_uom_id,
734         'uos_coeff': 1.0,
735         'mes_type': 'fixed',
736         'categ_id' : _default_category,
737         'type' : 'consu',
738         'active': True,
739     }
740
741     def _check_uom(self, cursor, user, ids, context=None):
742         for product in self.browse(cursor, user, ids, context=context):
743             if product.uom_id.category_id.id != product.uom_po_id.category_id.id:
744                 return False
745         return True
746
747     def _check_uos(self, cursor, user, ids, context=None):
748         for product in self.browse(cursor, user, ids, context=context):
749             if product.uos_id \
750                     and product.uos_id.category_id.id \
751                     == product.uom_id.category_id.id:
752                 return False
753         return True
754
755     _constraints = [
756         (_check_uom, 'Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.', ['uom_id']),
757     ]
758
759     def name_get(self, cr, user, ids, context=None):
760         if context is None:
761             context = {}
762         if 'partner_id' in context:
763             pass
764         return super(product_template, self).name_get(cr, user, ids, context)
765
766
767 class product_product(osv.osv):
768     _name = "product.product"
769     _description = "Product"
770     _inherits = {'product.template': 'product_tmpl_id'}
771     _inherit = ['mail.thread']
772     _order = 'default_code,name_template'
773
774     def _product_price(self, cr, uid, ids, name, arg, context=None):
775         plobj = self.pool.get('product.pricelist')
776         res = {}
777         if context is None:
778             context = {}
779         quantity = context.get('quantity') or 1.0
780         pricelist = context.get('pricelist', False)
781         partner = context.get('partner', False)
782         if pricelist:
783             # Support context pricelists specified as display_name or ID for compatibility
784             if isinstance(pricelist, basestring):
785                 pricelist_ids = plobj.name_search(
786                     cr, uid, pricelist, operator='=', context=context, limit=1)
787                 pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
788
789             if isinstance(pricelist, (int, long)):
790                 products = self.browse(cr, uid, ids, context=context)
791                 qtys = map(lambda x: (x, quantity, partner), products)
792                 pl = plobj.browse(cr, uid, pricelist, context=context)
793                 price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
794                 for id in ids:
795                     res[id] = price.get(id, 0.0)
796         for id in ids:
797             res.setdefault(id, 0.0)
798         return res
799
800     def view_header_get(self, cr, uid, view_id, view_type, context=None):
801         if context is None:
802             context = {}
803         res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
804         if (context.get('categ_id', False)):
805             return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
806         return res
807
808     def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
809         product_uom_obj = self.pool.get('product.uom')
810         res = dict.fromkeys(ids, 0.0)
811
812         for product in self.browse(cr, uid, ids, context=context):
813             if 'uom' in context:
814                 uom = product.uos_id or product.uom_id
815                 res[product.id] = product_uom_obj._compute_price(cr, uid,
816                         uom.id, product.list_price, context['uom'])
817             else:
818                 res[product.id] = product.list_price
819             res[product.id] =  res[product.id] + product.price_extra
820
821         return res
822
823     def _set_product_lst_price(self, cr, uid, id, name, value, args, context=None):
824         product_uom_obj = self.pool.get('product.uom')
825
826         product = self.browse(cr, uid, id, context=context)
827         if 'uom' in context:
828             uom = product.uos_id or product.uom_id
829             value = product_uom_obj._compute_price(cr, uid,
830                     context['uom'], value, uom.id)
831         value =  value - product.price_extra
832         
833         return product.write({'list_price': value}, context=context)
834
835     def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
836         for supinfo in product.seller_ids:
837             if supinfo.name.id == partner_id:
838                 return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
839         res = {'code': product.default_code, 'name': product.name}
840         return res
841
842     def _product_code(self, cr, uid, ids, name, arg, context=None):
843         res = {}
844         if context is None:
845             context = {}
846         for p in self.browse(cr, uid, ids, context=context):
847             res[p.id] = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)['code']
848         return res
849
850     def _product_partner_ref(self, cr, uid, ids, name, arg, context=None):
851         res = {}
852         if context is None:
853             context = {}
854         for p in self.browse(cr, uid, ids, context=context):
855             data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
856             if not data['code']:
857                 data['code'] = p.code
858             if not data['name']:
859                 data['name'] = p.name
860             res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
861         return res
862
863     def _is_product_variant_impl(self, cr, uid, ids, name, arg, context=None):
864         return dict.fromkeys(ids, True)
865
866     def _get_name_template_ids(self, cr, uid, ids, context=None):
867         result = set()
868         template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
869         for el in template_ids:
870             result.add(el)
871         return list(result)
872
873     def _get_image_variant(self, cr, uid, ids, name, args, context=None):
874         result = dict.fromkeys(ids, False)
875         for obj in self.browse(cr, uid, ids, context=context):
876             result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
877         return result
878
879     def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
880         image = tools.image_resize_image_big(value)
881         res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
882         product = self.browse(cr, uid, id, context=context)
883         if not product.product_tmpl_id.image:
884             product.write({'image_variant': None}, context=context)
885             product.product_tmpl_id.write({'image': image}, context=context)
886         return res
887
888     def _get_price_extra(self, cr, uid, ids, name, args, context=None):
889         result = dict.fromkeys(ids, False)
890         for product in self.browse(cr, uid, ids, context=context):
891             price_extra = 0.0
892             for variant_id in product.attribute_value_ids:
893                 for price_id in variant_id.price_ids:
894                     if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
895                         price_extra += price_id.price_extra
896             result[product.id] = price_extra
897         return result
898
899     _columns = {
900         'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
901         '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"),
902         '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')),
903         'code': fields.function(_product_code, type='char', string='Internal Reference'),
904         'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
905         'default_code' : fields.char('Internal Reference', select=True),
906         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
907         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True, auto_join=True),
908         'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
909         'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
910             'product.template': (_get_name_template_ids, ['name'], 10),
911             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
912         }, select=True),
913         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
914
915         # image: all image fields are base64 encoded and PIL-supported
916         'image_variant': fields.binary("Variant Image",
917             help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
918
919         'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
920             string="Big-sized image", type="binary",
921             help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
922                  "resized as a 1024x1024px image, with aspect ratio preserved."),
923         'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
924             string="Small-sized image", type="binary",
925             help="Image of the product variant (Small-sized image of product template if false)."),
926         'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
927             string="Medium-sized image", type="binary",
928             help="Image of the product variant (Medium-sized image of product template if false)."),
929     }
930
931     _defaults = {
932         'active': 1,
933         'color': 0,
934     }
935
936     def unlink(self, cr, uid, ids, context=None):
937         unlink_ids = []
938         unlink_product_tmpl_ids = []
939         for product in self.browse(cr, uid, ids, context=context):
940             # Check if product still exists, in case it has been unlinked by unlinking its template
941             if not product.exists():
942                 continue
943             tmpl_id = product.product_tmpl_id.id
944             # Check if the product is last product of this template
945             other_product_ids = self.search(cr, uid, [('product_tmpl_id', '=', tmpl_id), ('id', '!=', product.id)], context=context)
946             if not other_product_ids:
947                 unlink_product_tmpl_ids.append(tmpl_id)
948             unlink_ids.append(product.id)
949         res = super(product_product, self).unlink(cr, uid, unlink_ids, context=context)
950         # delete templates after calling super, as deleting template could lead to deleting
951         # products due to ondelete='cascade'
952         self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context)
953         return res
954
955     def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id):
956         if uom_id and uom_po_id:
957             uom_obj=self.pool.get('product.uom')
958             uom=uom_obj.browse(cursor,user,[uom_id])[0]
959             uom_po=uom_obj.browse(cursor,user,[uom_po_id])[0]
960             if uom.category_id.id != uom_po.category_id.id:
961                 return {'value': {'uom_po_id': uom_id}}
962         return False
963
964     def _check_ean_key(self, cr, uid, ids, context=None):
965         for product in self.read(cr, uid, ids, ['ean13'], context=context):
966             if not check_ean(product['ean13']):
967                 return False
968         return True
969
970     _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
971
972     def on_order(self, cr, uid, ids, orderline, quantity):
973         pass
974
975     def name_get(self, cr, user, ids, context=None):
976         if context is None:
977             context = {}
978         if isinstance(ids, (int, long)):
979             ids = [ids]
980         if not len(ids):
981             return []
982
983         def _name_get(d):
984             name = d.get('name','')
985             code = context.get('display_default_code', True) and d.get('default_code',False) or False
986             if code:
987                 name = '[%s] %s' % (code,name)
988             return (d['id'], name)
989
990         partner_id = context.get('partner_id', False)
991
992         # all user don't have access to seller and partner
993         # check access and use superuser
994         self.check_access_rights(cr, user, "read")
995         self.check_access_rule(cr, user, ids, "read", context=context)
996
997         result = []
998         for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
999             variant = ", ".join([v.name for v in product.attribute_value_ids])
1000             name = variant and "%s (%s)" % (product.name, variant) or product.name
1001             sellers = []
1002             if partner_id:
1003                 sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
1004             if sellers:
1005                 for s in sellers:
1006                     mydict = {
1007                               'id': product.id,
1008                               'name': s.product_name or name,
1009                               'default_code': s.product_code or product.default_code,
1010                               }
1011                     result.append(_name_get(mydict))
1012             else:
1013                 mydict = {
1014                           'id': product.id,
1015                           'name': name,
1016                           'default_code': product.default_code,
1017                           }
1018                 result.append(_name_get(mydict))
1019         return result
1020
1021     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
1022         if not args:
1023             args = []
1024         if name:
1025             ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context)
1026             if not ids:
1027                 ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context)
1028             if not ids:
1029                 # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
1030                 # on a database with thousands of matching products, due to the huge merge+unique needed for the
1031                 # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
1032                 # Performing a quick memory merge of ids in Python will give much better performance
1033                 ids = set(self.search(cr, user, args + [('default_code', operator, name)], limit=limit, context=context))
1034                 if not limit or len(ids) < limit:
1035                     # we may underrun the limit because of dupes in the results, that's fine
1036                     limit2 = (limit - len(ids)) if limit else False
1037                     ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
1038                 ids = list(ids)
1039             if not ids:
1040                 ptrn = re.compile('(\[(.*?)\])')
1041                 res = ptrn.search(name)
1042                 if res:
1043                     ids = self.search(cr, user, [('default_code','=', res.group(2))] + args, limit=limit, context=context)
1044         else:
1045             ids = self.search(cr, user, args, limit=limit, context=context)
1046         result = self.name_get(cr, user, ids, context=context)
1047         return result
1048
1049     #
1050     # Could be overrided for variants matrices prices
1051     #
1052     def price_get(self, cr, uid, ids, ptype='list_price', context=None):
1053         products = self.browse(cr, uid, ids, context=context)
1054         return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
1055
1056     def copy(self, cr, uid, id, default=None, context=None):
1057         if context is None:
1058             context={}
1059
1060         product = self.browse(cr, uid, id, context)
1061         if context.get('variant'):
1062             # if we copy a variant or create one, we keep the same template
1063             default['product_tmpl_id'] = product.product_tmpl_id.id
1064         elif 'name' not in default:
1065             default['name'] = _("%s (copy)") % (product.name,)
1066
1067         return super(product_product, self).copy(cr, uid, id, default=default, context=context)
1068
1069     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
1070         if context is None:
1071             context = {}
1072         if context.get('search_default_categ_id'):
1073             args.append((('categ_id', 'child_of', context['search_default_categ_id'])))
1074         return super(product_product, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1075
1076     def open_product_template(self, cr, uid, ids, context=None):
1077         """ Utility method used to add an "Open Template" button in product views """
1078         product = self.browse(cr, uid, ids[0], context=context)
1079         return {'type': 'ir.actions.act_window',
1080                 'res_model': 'product.template',
1081                 'view_mode': 'form',
1082                 'res_id': product.product_tmpl_id.id,
1083                 'target': 'new'}
1084
1085     def create(self, cr, uid, vals, context=None):
1086         if context is None:
1087             context = {}
1088         ctx = dict(context or {}, create_product_product=True)
1089         return super(product_product, self).create(cr, uid, vals, context=ctx)
1090
1091
1092 class product_packaging(osv.osv):
1093     _name = "product.packaging"
1094     _description = "Packaging"
1095     _rec_name = 'ean'
1096     _order = 'sequence'
1097     _columns = {
1098         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."),
1099         'name' : fields.text('Description'),
1100         'qty' : fields.float('Quantity by Package',
1101             help="The total number of products you can put by pallet or box."),
1102         'ul' : fields.many2one('product.ul', 'Package Logistic Unit', required=True),
1103         'ul_qty' : fields.integer('Package by layer', help='The number of packages by layer'),
1104         'ul_container': fields.many2one('product.ul', 'Pallet Logistic Unit'),
1105         'rows' : fields.integer('Number of Layers', required=True,
1106             help='The number of layers on a pallet or box'),
1107         'product_tmpl_id' : fields.many2one('product.template', 'Product', select=1, ondelete='cascade', required=True),
1108         'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."),
1109         'code' : fields.char('Code', help="The code of the transport unit."),
1110         'weight': fields.float('Total Package Weight',
1111             help='The weight of a full package, pallet or box.'),
1112     }
1113
1114     def _check_ean_key(self, cr, uid, ids, context=None):
1115         for pack in self.browse(cr, uid, ids, context=context):
1116             if not check_ean(pack.ean):
1117                 return False
1118         return True
1119
1120     _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
1121
1122     def name_get(self, cr, uid, ids, context=None):
1123         if not len(ids):
1124             return []
1125         res = []
1126         for pckg in self.browse(cr, uid, ids, context=context):
1127             p_name = pckg.ean and '[' + pckg.ean + '] ' or ''
1128             p_name += pckg.ul.name
1129             res.append((pckg.id,p_name))
1130         return res
1131
1132     def _get_1st_ul(self, cr, uid, context=None):
1133         cr.execute('select id from product_ul order by id asc limit 1')
1134         res = cr.fetchone()
1135         return (res and res[0]) or False
1136
1137     _defaults = {
1138         'rows' : 3,
1139         'sequence' : 1,
1140         'ul' : _get_1st_ul,
1141     }
1142
1143     def checksum(ean):
1144         salt = '31' * 6 + '3'
1145         sum = 0
1146         for ean_part, salt_part in zip(ean, salt):
1147             sum += int(ean_part) * int(salt_part)
1148         return (10 - (sum % 10)) % 10
1149     checksum = staticmethod(checksum)
1150
1151
1152
1153 class product_supplierinfo(osv.osv):
1154     _name = "product.supplierinfo"
1155     _description = "Information about a product supplier"
1156     def _calc_qty(self, cr, uid, ids, fields, arg, context=None):
1157         result = {}
1158         for supplier_info in self.browse(cr, uid, ids, context=context):
1159             for field in fields:
1160                 result[supplier_info.id] = {field:False}
1161             qty = supplier_info.min_qty
1162             result[supplier_info.id]['qty'] = qty
1163         return result
1164
1165     _columns = {
1166         'name' : fields.many2one('res.partner', 'Supplier', required=True,domain = [('supplier','=',True)], ondelete='cascade', help="Supplier of this product"),
1167         '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."),
1168         '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."),
1169         'sequence' : fields.integer('Sequence', help="Assigns the priority to the list of product supplier."),
1170         '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."),
1171         '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."),
1172         '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."),
1173         'product_tmpl_id' : fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade', select=True, oldname='product_id'),
1174         '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."),
1175         'pricelist_ids': fields.one2many('pricelist.partnerinfo', 'suppinfo_id', 'Supplier Pricelist', copy=True),
1176         'company_id':fields.many2one('res.company','Company',select=1),
1177     }
1178     _defaults = {
1179         'min_qty': 0.0,
1180         'sequence': 1,
1181         'delay': 1,
1182         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
1183     }
1184     def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
1185         """
1186         Calculate price from supplier pricelist.
1187         @param supplier_ids: Ids of res.partner object.
1188         @param product_id: Id of product.
1189         @param product_qty: specify quantity to purchase.
1190         """
1191         if type(supplier_ids) in (int,long,):
1192             supplier_ids = [supplier_ids]
1193         res = {}
1194         product_pool = self.pool.get('product.product')
1195         partner_pool = self.pool.get('res.partner')
1196         pricelist_pool = self.pool.get('product.pricelist')
1197         currency_pool = self.pool.get('res.currency')
1198         currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
1199         # Compute price from standard price of product
1200         product_price = product_pool.price_get(cr, uid, [product_id], 'standard_price', context=context)[product_id]
1201         product = product_pool.browse(cr, uid, product_id, context=context)
1202         for supplier in partner_pool.browse(cr, uid, supplier_ids, context=context):
1203             price = product_price
1204             # Compute price from Purchase pricelist of supplier
1205             pricelist_id = supplier.property_product_pricelist_purchase.id
1206             if pricelist_id:
1207                 price = pricelist_pool.price_get(cr, uid, [pricelist_id], product_id, product_qty, context=context).setdefault(pricelist_id, 0)
1208                 price = currency_pool.compute(cr, uid, pricelist_pool.browse(cr, uid, pricelist_id).currency_id.id, currency_id, price)
1209
1210             # Compute price from supplier pricelist which are in Supplier Information
1211             supplier_info_ids = self.search(cr, uid, [('name','=',supplier.id),('product_tmpl_id','=',product.product_tmpl_id.id)])
1212             if supplier_info_ids:
1213                 cr.execute('SELECT * ' \
1214                     'FROM pricelist_partnerinfo ' \
1215                     'WHERE suppinfo_id IN %s' \
1216                     'AND min_quantity <= %s ' \
1217                     'ORDER BY min_quantity DESC LIMIT 1', (tuple(supplier_info_ids),product_qty,))
1218                 res2 = cr.dictfetchone()
1219                 if res2:
1220                     price = res2['price']
1221             res[supplier.id] = price
1222         return res
1223     _order = 'sequence'
1224
1225
1226 class pricelist_partnerinfo(osv.osv):
1227     _name = 'pricelist.partnerinfo'
1228     _columns = {
1229         'name': fields.char('Description'),
1230         'suppinfo_id': fields.many2one('product.supplierinfo', 'Partner Information', required=True, ondelete='cascade'),
1231         '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."),
1232         '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"),
1233     }
1234     _order = 'min_quantity asc'
1235
1236 class res_currency(osv.osv):
1237     _inherit = 'res.currency'
1238
1239     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1240         cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',))
1241         digits = cr.fetchone()
1242         if digits and len(digits):
1243             digits = digits[0]
1244             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1245             for currency_id in ids:
1246                 if currency_id == main_currency.id:
1247                     if main_currency.rounding < 10 ** -digits:
1248                         return False
1249         return True
1250
1251     _constraints = [
1252         (_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']),
1253     ]
1254
1255 class decimal_precision(osv.osv):
1256     _inherit = 'decimal.precision'
1257
1258     def _check_main_currency_rounding(self, cr, uid, ids, context=None):
1259         cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',))
1260         res = cr.fetchone()
1261         if res and len(res):
1262             account_precision_id, digits = res
1263             main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
1264             for decimal_precision in ids:
1265                 if decimal_precision == account_precision_id:
1266                     if main_currency.rounding < 10 ** -digits:
1267                         return False
1268         return True
1269
1270     _constraints = [
1271         (_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']),
1272     ]
1273
1274 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: