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