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