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