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