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