diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 1609fa2a16f..906f4b2b6c6 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -21,7 +21,6 @@ import math import openerp -import os from osv import osv, fields import re import tools @@ -58,20 +57,20 @@ class res_partner_category(osv.osv): return super(res_partner_category, self).name_get(cr, uid, ids, context=context) if isinstance(ids, (int, long)): ids = [ids] - reads = self.read(cr, uid, ids, ['name','parent_id'], context=context) + reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context) res = [] for record in reads: name = record['name'] if record['parent_id']: - name = record['parent_id'][1]+' / '+name + name = record['parent_id'][1] + ' / ' + name res.append((record['id'], name)) return res def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): if not args: - args=[] + args = [] if not context: - context={} + context = {} if name: # Be sure name_search is symetric to name_get name = name.split(' / ')[-1] @@ -85,23 +84,23 @@ class res_partner_category(osv.osv): res = self.name_get(cr, uid, ids, context=context) return dict(res) - _description='Partner Categories' + _description = 'Partner Categories' _name = 'res.partner.category' _columns = { 'name': fields.char('Category Name', required=True, size=64, translate=True), 'parent_id': fields.many2one('res.partner.category', 'Parent Category', select=True, ondelete='cascade'), 'complete_name': fields.function(_name_get_fnc, type="char", string='Full Name'), 'child_ids': fields.one2many('res.partner.category', 'parent_id', 'Child Categories'), - 'active' : fields.boolean('Active', help="The active field allows you to hide the category without removing it."), - 'parent_left' : fields.integer('Left parent', select=True), - 'parent_right' : fields.integer('Right parent', select=True), + 'active': fields.boolean('Active', help="The active field allows you to hide the category without removing it."), + 'parent_left': fields.integer('Left parent', select=True), + 'parent_right': fields.integer('Right parent', select=True), 'partner_ids': fields.many2many('res.partner', id1='category_id', id2='partner_id', string='Partners'), } _constraints = [ (osv.osv._check_recursion, 'Error ! You can not create recursive categories.', ['parent_id']) ] _defaults = { - 'active' : lambda *a: 1, + 'active': lambda *a: 1, } _parent_store = True _parent_order = 'name' @@ -113,7 +112,7 @@ class res_partner_title(osv.osv): _columns = { 'name': fields.char('Title', required=True, size=46, translate=True), 'shortcut': fields.char('Abbreviation', size=16, translate=True), - 'domain': fields.selection([('partner','Partner'),('contact','Contact')], 'Domain', required=True, size=24) + 'domain': fields.selection([('partner', 'Partner'), ('contact', 'Contact')], 'Domain', required=True, size=24) } _defaults = { 'domain': 'contact', @@ -129,13 +128,13 @@ POSTAL_ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'countr ADDRESS_FIELDS = POSTAL_ADDRESS_FIELDS + ('email', 'phone', 'fax', 'mobile', 'website', 'ref', 'lang') class res_partner(osv.osv): - _description='Partner' + _description = 'Partner' _name = "res.partner" def _address_display(self, cr, uid, ids, name, args, context=None): - res={} + res = {} for partner in self.browse(cr, uid, ids, context=context): - res[partner.id] =self._display_address(cr, uid, partner, context=context) + res[partner.id] = self._display_address(cr, uid, partner, context=context) return res def _get_image(self, cr, uid, ids, name, args, context=None): @@ -143,7 +142,7 @@ class res_partner(osv.osv): for obj in self.browse(cr, uid, ids, context=context): result[obj.id] = tools.image_get_resized_images(obj.image) return result - + def _set_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) @@ -151,7 +150,7 @@ class res_partner(osv.osv): _columns = { 'name': fields.char('Name', size=128, required=True, select=True), 'date': fields.date('Date', select=1), - 'title': fields.many2one('res.partner.title','Title'), + 'title': fields.many2one('res.partner.title', 'Title'), 'parent_id': fields.many2one('res.partner', 'Owned by'), 'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts'), 'ref': fields.char('Reference', size=64, select=1), @@ -162,9 +161,9 @@ class res_partner(osv.osv): "It is important to set a value for this field. You should use the same timezone " "that is otherwise used to pick and render date and time values: your computer's timezone."), 'user_id': fields.many2one('res.users', 'Salesperson', help='The internal user that is in charge of communicating with this partner if any.'), - 'vat': fields.char('TIN',size=32 ,help="Tax Identification Number. Check the box if the partner is subjected to taxes. Used by the some of the legal statements."), + 'vat': fields.char('TIN', size=32, help="Tax Identification Number. Check the box if the partner is subjected to taxes. Used by the some of the legal statements."), 'bank_ids': fields.one2many('res.partner.bank', 'partner_id', 'Banks'), - 'website': fields.char('Website',size=64, help="Website of Partner or Company"), + 'website': fields.char('Website', size=64, help="Website of Partner or Company"), 'comment': fields.text('Notes'), 'address': fields.one2many('res.partner.address', 'partner_id', 'Contacts'), # should be removed in version 7, but kept until then for backward compatibility 'category_id': fields.many2many('res.partner.category', id1='partner_id', id2='category_id', string='Tags'), @@ -175,8 +174,8 @@ class res_partner(osv.osv): 'supplier': fields.boolean('Supplier', help="Check this box if the partner is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."), 'employee': fields.boolean('Employee', help="Check this box if the partner is an Employee."), 'function': fields.char('Job Position', size=128), - 'type': fields.selection( [('default','Default'), ('invoice','Invoice'), - ('delivery','Delivery'), ('contact','Contact'), + 'type': fields.selection([('default', 'Default'), ('invoice', 'Invoice'), + ('delivery', 'Delivery'), ('contact', 'Contact'), ('other', 'Other')], 'Address Type', help="Used to select automatically the right address according to the context in sales and purchases documents."), 'street': fields.char('Street', size=128), @@ -193,25 +192,24 @@ class res_partner(osv.osv): 'birthdate': fields.char('Birthdate', size=64), 'is_company': fields.boolean('Company', help="Check if the contact is a company, otherwise it is a person"), 'use_parent_address': fields.boolean('Use Company Address', help="Select this if you want to set company's address information for this contact"), + # image: all image fields are base64 encoded and PIL-supported 'image': fields.binary("Image", - help="This field holds the image used as avatar for the "\ - "partner. The image is base64 encoded, and PIL-supported. "\ - "It is limited to a 1024x1024 px image."), + help="This field holds the image used as avatar for the partner, limited to 1024x1024px"), 'image_medium': fields.function(_get_image, fnct_inv=_set_image, string="Medium-sized image", type="binary", multi="_get_image", - store = { + store={ 'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), }, help="Medium-sized image of the partner. It is automatically "\ - "resized as a 180x180 px image, with aspect ratio preserved. "\ + "resized as a 128x128px image, with aspect ratio preserved. "\ "Use this field in form views or some kanban views."), 'image_small': fields.function(_get_image, fnct_inv=_set_image, string="Small-sized image", type="binary", multi="_get_image", - store = { + store={ 'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), }, help="Small-sized image of the partner. It is automatically "\ - "resized as a 50x50 px image, with aspect ratio preserved. "\ + "resized as a 64x64px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required."), 'company_id': fields.many2one('res.company', 'Company', select=1), 'color': fields.integer('Color Index'), @@ -230,40 +228,28 @@ class res_partner(osv.osv): if is_company: image = open(openerp.modules.get_module_resource('base', 'static/src/img', 'company_image.png')).read() else: - from PIL import Image - from StringIO import StringIO - color = (255,255,255) - if colorize: - from random import random - color = (int(random() * 192 + 32), int(random() * 192 + 32), int(random() * 192 + 32)) - face = Image.open(openerp.modules.get_module_resource('base', 'static/src/img', 'avatar.png')) - avatar = Image.new('RGB', face.size) - avatar.paste(color) - avatar.paste(face, mask=face) - buffer = StringIO() - avatar.save(buffer, 'PNG') - image = buffer.getvalue() - return image.encode('base64') + image = tools.image_colorize(open(openerp.modules.get_module_resource('base', 'static/src/img', 'avatar.png')).read()) + return tools.image_resize_image_big(image.encode('base64')) _defaults = { 'active': True, - 'lang': lambda self, cr, uid, context: context.get('lang', 'en_US'), - 'tz': lambda self, cr, uid, context: context.get('tz', False), + 'lang': lambda self, cr, uid, ctx: ctx.get('lang', 'en_US'), + 'tz': lambda self, cr, uid, ctx: ctx.get('tz', False), 'customer': True, 'category_id': _default_category, - 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c), + 'company_id': lambda self, cr, uid, ctx: self.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=ctx), 'color': 0, 'is_company': False, 'type': 'default', 'use_parent_address': True, - 'image': lambda self, cr, uid, context: self._get_default_image(cr, uid, context.get('default_is_company', False), context), + 'image': lambda self, cr, uid, ctx: self._get_default_image(cr, uid, ctx.get('default_is_company', False), ctx), } def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} name = self.read(cr, uid, [id], ['name'], context)[0]['name'] - default.update({'name': _('%s (copy)')%(name)}) + default.update({'name': _('%s (copy)') % (name)}) return super(res_partner, self).copy(cr, uid, id, default, context) def onchange_type(self, cr, uid, ids, is_company, context=None): diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index d0f07daa004..5e1ea88f5ac 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -244,14 +244,14 @@ class res_users(osv.osv): return result _defaults = { - 'password' : '', - 'active' : True, + 'password': '', + 'active': True, 'customer': False, 'menu_id': _get_menu, 'company_id': _get_company, 'company_ids': _get_companies, 'groups_id': _get_group, - 'image': lambda self, cr, uid, context: self.pool.get('res.partner')._get_default_image(cr, uid, False, context, colorize=True), + 'image': lambda self, cr, uid, ctx={}: self.pool.get('res.partner')._get_default_image(cr, uid, False, ctx, colorize=True), } def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): diff --git a/openerp/tools/image.py b/openerp/tools/image.py index 1caafb23a18..f673906f328 100644 --- a/openerp/tools/image.py +++ b/openerp/tools/image.py @@ -20,9 +20,12 @@ ############################################################################## import io -from PIL import Image import StringIO +from PIL import Image +from PIL import ImageFilter +from random import random + # ---------------------------------------- # Image resizing # ---------------------------------------- @@ -47,15 +50,17 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file image. - past the thumbnail on the transparent background and center it. - + :param base64_source: base64-encoded version of the source - image + image; if False, returns False :param size: tuple(height, width) :param encoding: the output encoding :param filetype: the output filetype :param avoid_if_small: do not resize if image height and width are smaller than the expected size. """ + if not base64_source: + return False image_stream = io.BytesIO(base64_source.decode(encoding)) image = Image.open(image_stream) # check image size: do not create a thumbnail if avoiding smaller images @@ -63,6 +68,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file return base64_source # create a thumbnail: will resize and keep ratios image.thumbnail(size, Image.ANTIALIAS) + image = image.filter(ImageFilter.SHARPEN) # create a transparent image for background background = Image.new('RGBA', size, (255, 255, 255, 0)) # past the resized image on the background @@ -72,42 +78,57 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file background.save(background_stream, filetype) return background_stream.getvalue().encode(encoding) -def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG'): +def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG', avoid_if_small=True): """ Wrapper on image_resize_image, to resize images larger than the standard 'big' image size: 1024x1024px. - :param base64_source: base64 encoded source image. If False, - the function returns False. + :param size, encoding, filetype, avoid_if_small: refer to image_resize_image """ - if not base64_source: - return False - return image_resize_image(base64_source, size, encoding, filetype, True) + return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small) -def image_resize_image_medium(base64_source, size=(180, 180), encoding='base64', filetype='PNG'): +def image_resize_image_medium(base64_source, size=(128, 128), encoding='base64', filetype='PNG', avoid_if_small=False): """ Wrapper on image_resize_image, to resize to the standard 'medium' image size: 180x180. - :param base64_source: base64 encoded source image. If False, - the function returns False. + :param size, encoding, filetype, avoid_if_small: refer to image_resize_image """ - if not base64_source: - return False - return image_resize_image(base64_source, size, encoding, filetype) - -def image_resize_image_small(base64_source, size=(50, 50), encoding='base64', filetype='PNG'): + return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small) + +def image_resize_image_small(base64_source, size=(64, 64), encoding='base64', filetype='PNG', avoid_if_small=False): """ Wrapper on image_resize_image, to resize to the standard 'small' image size: 50x50. - :param base64_source: base64 encoded source image. If False, - the function returns False. + :param size, encoding, filetype, avoid_if_small: refer to image_resize_image """ - if not base64_source: - return False - return image_resize_image(base64_source, size, encoding, filetype) + return image_resize_image(base64_source, size, encoding, filetype, avoid_if_small) + +# ---------------------------------------- +# Colors +# --------------------------------------- + +def image_colorize(original, randomize=True, color=(255, 255, 255)): + """ Add a color to the transparent background of an image. + :param original: file object on the original image file + :param randomize: randomize the background color + :param color: background-color, if not randomize + """ + # create a new image, based on the original one + original = Image.open(io.BytesIO(original)) + image = Image.new('RGB', original.size) + # generate the background color, past it as background + if randomize: + color = (int(random() * 192 + 32), int(random() * 192 + 32), int(random() * 192 + 32)) + image.paste(color) + image.paste(original, mask=original) + # return the new image + buffer = StringIO.StringIO() + image.save(buffer, 'PNG') + return buffer.getvalue() # ---------------------------------------- # Misc image tools # --------------------------------------- def image_get_resized_images(base64_source, return_big=False, return_medium=True, return_small=True, - big_name='image', medium_name='image_medium', small_name='image_small'): + big_name='image', medium_name='image_medium', small_name='image_small', + avoid_resize_big=True, avoid_resize_medium=False, avoid_resize_small=False): """ Standard tool function that returns a dictionary containing the big, medium and small versions of the source image. This function is meant to be used for the methods of functional fields for @@ -116,24 +137,22 @@ def image_get_resized_images(base64_source, return_big=False, return_medium=True Default parameters are given to be used for the getter of functional image fields, for example with res.users or res.partner. It returns only image_medium and image_small values, to update those fields. - - :param base64_source: if set to False, other values are set to False - also. The purpose is to be linked to the fields that hold images in - OpenERP and that are binary fields. - :param return_big: if set, return_dict contains the 'big_name' entry - :param return_medium: if set, return_dict contains the 'medium_name' entry - :param return_small: if set, return_dict contains the 'small_name' entry - :param big_name: name related to the big version of the image; - 'image' by default. - :param medium_name: name related to the medium version of the - image; 'image_medium' by default. - :param small_name: name related to the small version of the - image; 'image_small' by default. + + :param base64_source: base64-encoded version of the source + image; if False, all returnes values will be False + :param return_{..}: if set, computes and return the related resizing + of the image + :param {..}_name: key of the resized image in the return dictionary; + 'image', 'image_medium' and 'image_small' by default. + :param avoid_resize_[..]: see avoid_if_small parameter :return return_dict: dictionary with resized images, depending on previous parameters. """ return_dict = dict() - if return_big: return_dict[big_name] = image_resize_image_big(base64_source) - if return_medium: return_dict[medium_name] = image_resize_image_medium(base64_source) - if return_small: return_dict[small_name] = image_resize_image_small(base64_source) - return return_dict \ No newline at end of file + if return_big: + return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big) + if return_medium: + return_dict[medium_name] = image_resize_image_medium(base64_source, avoid_if_small=avoid_resize_medium) + if return_small: + return_dict[small_name] = image_resize_image_small(base64_source, avoid_if_small=avoid_resize_small) + return return_dict