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