diff --git a/base_usability/__init__.py b/base_usability/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/base_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_usability/__manifest__.py b/base_usability/__manifest__.py new file mode 100644 index 0000000..06efa3e --- /dev/null +++ b/base_usability/__manifest__.py @@ -0,0 +1,39 @@ +# © 2014-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Base Usability', + 'version': '12.0.0.1.0', + 'category': 'Partner', + 'license': 'AGPL-3', + 'summary': 'Better usability in base module', + 'description': """ +Base Usability +============== + +This module adds *track_visibility='onchange'* on all the important fields of the Partner object. + +By default, Odoo doesn't display the title field on all the partner form views. This module fixes it (it replaces the module base_title_on_partner). + +It also adds a log message at INFO level when sending an email via SMTP. + +It displays the local modules with installable filter. +A group by 'State' is added to module search view. + +It provides a _display_report_header method on the res.company object and +_display_full_address on res.partner which are useful for reporting. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['base'], + 'data': [ + 'security/group.xml', + 'views/partner_view.xml', + 'views/partner_bank_view.xml', + 'views/users_view.xml', + 'views/country_view.xml', + 'views/module_view.xml', + ], + 'installable': True, +} diff --git a/base_usability/fix-odoo-base-multi-company-partners-not-shared.diff b/base_usability/fix-odoo-base-multi-company-partners-not-shared.diff new file mode 100644 index 0000000..f19a3f8 --- /dev/null +++ b/base_usability/fix-odoo-base-multi-company-partners-not-shared.diff @@ -0,0 +1,19 @@ +diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py +index 083607f9..99ae8857 100644 +--- a/odoo/addons/base/models/res_users.py ++++ b/odoo/addons/base/models/res_users.py +@@ -426,7 +426,13 @@ class Users(models.Model): + for user in users: + user.partner_id.active = user.active + if user.partner_id.company_id: +- user.partner_id.write({'company_id': user.company_id.id}) ++ # AKRETION HACK: if you have a multi-company setup where ++ # partners are NOT shared between companies, having ++ # company_id=False on partners related to users ++ # avoids a lot of trouble (you should also disable 'read' ++ # on the ir.rule 'user rule' (XMLID base.res_users_rule) ++ # user.partner_id.write({'company_id': user.company_id.id}) ++ user.partner_id.write({'company_id': False}) + return users + + @api.multi diff --git a/base_usability/models/__init__.py b/base_usability/models/__init__.py new file mode 100644 index 0000000..c4d8b8b --- /dev/null +++ b/base_usability/models/__init__.py @@ -0,0 +1,5 @@ +from . import users +from . import partner +from . import company +from . import mail +from . import misc diff --git a/base_usability/models/company.py b/base_usability/models/company.py new file mode 100644 index 0000000..650520d --- /dev/null +++ b/base_usability/models/company.py @@ -0,0 +1,81 @@ +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, api, _ + + +class ResCompany(models.Model): + _inherit = 'res.company' + + @api.model + def generate_line(self, fields, options, icon=True, separator=' - '): + assert fields + assert options + content = [] + for field in fields: + value = False + if isinstance(field, tuple) and len(field) == 2: + value = field[0] + label = field[1] + uicon = False + elif isinstance(field, str) and field in options: + value = options[field]['value'] + label = options[field].get('label') + uicon = options[field].get('icon') + if value: + prefix = icon and uicon or label + if prefix: + content.append('%s %s' % (prefix, value)) + else: + content.append(value) + line = separator.join(content) + return line + + @api.multi + def _prepare_header_options(self): + self.ensure_one() + options = { + 'phone': { + 'value': self.phone, + # http://www.fileformat.info/info/unicode/char/1f4de/index.htm + 'icon': '\U0001F4DE', + 'label': _('Tel:')}, + 'email': { + 'value': self.email, + # http://www.fileformat.info/info/unicode/char/2709/index.htm + 'icon': '\u2709', + 'label': _('E-mail:')}, + 'website': { + 'value': self.website, + 'icon': '\U0001f310', + 'label': _('Website:')}, + 'vat': { + 'value': self.vat, + 'label': _('TVA :')}, # TODO translate + } + return options + + def _report_company_legal_name(self): + '''Method inherited in the module base_company_extension''' + self.ensure_one() + return self.name + + # for reports + @api.multi + def _display_report_header( + self, line_details=[['phone', 'website'], ['vat']], + icon=True, line_separator=' - '): + self.ensure_one() + res = '' + address = self.partner_id._display_address(without_company=True) + address = address.replace('\n', ' - ') + + line1 = '%s - %s' % (self._report_company_legal_name(), address) + lines = [line1] + options = self._prepare_header_options() + for details in line_details: + line = self.generate_line( + details, options, icon=icon, separator=line_separator) + lines.append(line) + res = '\n'.join(lines) + return res diff --git a/base_usability/models/mail.py b/base_usability/models/mail.py new file mode 100644 index 0000000..35c7093 --- /dev/null +++ b/base_usability/models/mail.py @@ -0,0 +1,35 @@ +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, api +from odoo.addons.base.models.ir_mail_server import extract_rfc2822_addresses +import logging + +logger = logging.getLogger(__name__) + + +class IrMailServer(models.Model): + _inherit = "ir.mail_server" + + @api.model + def send_email( + self, message, mail_server_id=None, smtp_server=None, + smtp_port=None, smtp_user=None, smtp_password=None, + smtp_encryption=None, smtp_debug=False, smtp_session=None): + # Start copy from native method + smtp_from = message['Return-Path'] or\ + self._get_default_bounce_address() or message['From'] + from_rfc2822 = extract_rfc2822_addresses(smtp_from) + smtp_from = from_rfc2822[-1] + # End copy from native method + logger.info( + "Sending email from '%s' to '%s' Cc '%s' Bcc '%s' " + "with subject '%s'", + smtp_from, message.get('To'), message.get('Cc'), + message.get('Bcc'), message.get('Subject')) + return super(IrMailServer, self).send_email( + message, mail_server_id=mail_server_id, + smtp_server=smtp_server, smtp_port=smtp_port, + smtp_user=smtp_user, smtp_password=smtp_password, + smtp_encryption=smtp_encryption, smtp_debug=smtp_debug, + smtp_session=smtp_session) diff --git a/base_usability/models/misc.py b/base_usability/models/misc.py new file mode 100644 index 0000000..53c1968 --- /dev/null +++ b/base_usability/models/misc.py @@ -0,0 +1,48 @@ +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api +from odoo.tools import misc +from odoo.tools import float_compare + + +class BaseLanguageExport(models.TransientModel): + _inherit = 'base.language.export' + + # Default format for language files = format used by OpenERP modules + format = fields.Selection(default='po') + + +class BaseLanguageInstall(models.TransientModel): + _inherit = 'base.language.install' + + overwrite = fields.Boolean(default=True) + + +class BaseUsabilityInstalled(models.AbstractModel): + _name = "base.usability.installed" + _description = "technical flag to see if base_usability module is installed" + + +formatLang_original = misc.formatLang + +def formatLang(self, value, digits=None, grouping=True, monetary=False, + dp=False, currency_obj=False, int_no_digits=True): + with api.Environment.manage(): + env = api.Environment(self.cr, self.uid, {}) + if ( + 'base.usability.installed' in env and + int_no_digits and + not monetary and + isinstance(value, float) and + dp): + prec = env['decimal.precision'].precision_get(dp) + if not float_compare(value, int(value), precision_digits=prec): + digits = 0 + dp = False + res = formatLang_original( + self, value, digits=digits, grouping=grouping, monetary=monetary, + dp=dp, currency_obj=currency_obj) + return res + +misc.formatLang = formatLang diff --git a/base_usability/models/partner.py b/base_usability/models/partner.py new file mode 100644 index 0000000..b4349bb --- /dev/null +++ b/base_usability/models/partner.py @@ -0,0 +1,139 @@ +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + # track_visibility is handled in the 'mail' module, and base_usability + # doesn't depend on 'mail', but that doesn't hurt, it will just be + # ignored if mail is not installed + name = fields.Char(track_visibility='onchange') + parent_id = fields.Many2one(track_visibility='onchange') + ref = fields.Char(track_visibility='onchange', copy=False) + lang = fields.Selection(track_visibility='onchange') + user_id = fields.Many2one(track_visibility='onchange') + vat = fields.Char(track_visibility='onchange') + customer = fields.Boolean(track_visibility='onchange') + supplier = fields.Boolean(track_visibility='onchange') + type = fields.Selection(track_visibility='onchange') + street = fields.Char(track_visibility='onchange') + street2 = fields.Char(track_visibility='onchange') + zip = fields.Char(track_visibility='onchange') + city = fields.Char(track_visibility='onchange') + state_id = fields.Many2one(track_visibility='onchange') + country_id = fields.Many2one(track_visibility='onchange') + email = fields.Char(track_visibility='onchange') + is_company = fields.Boolean(track_visibility='onchange') + active = fields.Boolean(track_visibility='onchange') + company_id = fields.Many2one(track_visibility='onchange') + # For reports + name_title = fields.Char( + compute='_compute_name_title', string='Name with Title') + + @api.multi + @api.depends('name', 'title') + def _compute_name_title(self): + for partner in self: + name_title = partner.name + if partner.title and not partner.is_company: + partner_lg = partner + # If prefer to read the lang of the partner than the lang + # of the context. That way, an English man will be displayed + # with his title in English whatever the environment + if partner.lang: + partner_lg = partner.with_context(lang=partner.lang) + title = partner_lg.title.shortcut or partner_lg.title.name + name_title = ' '.join([title, name_title]) + partner.name_title = name_title + + @api.multi + def _display_address(self, without_company=False): + '''Remove empty lines''' + res = super(ResPartner, self)._display_address( + without_company=without_company) + while "\n\n" in res: + res = res.replace('\n\n', '\n') + return res + + # for reports + @api.multi + def _display_full_address( + self, details=[ + 'company', 'name', 'address', 'phone', + 'mobile', 'email'], + icon=True): + self.ensure_one() + # To make the icons work with py3o with PDF export, on the py3o server: + # 1) sudo apt-get install fonts-symbola + # 2) start libreoffice in xvfb (don't use --headless) (To confirm) + if self.is_company: + company = self.name + name = False + else: + name = self.name_title + company = self.parent_id and self.parent_id.is_company and\ + self.parent_id.name or False + options = { + 'name': { + 'value': name, + }, + 'company': { + 'value': company, + }, + 'phone': { + 'value': self.phone, + # http://www.fileformat.info/info/unicode/char/1f4de/index.htm + 'icon': '\U0001F4DE', + 'label': _('Tel:'), + }, + 'mobile': { + 'value': self.mobile, + # http://www.fileformat.info/info/unicode/char/1f4f1/index.htm + 'icon': '\U0001F4F1', + 'label': _('Mobile:'), + }, + 'email': { + 'value': self.email, + # http://www.fileformat.info/info/unicode/char/2709/index.htm + 'icon': '\u2709', + 'label': _('E-mail:'), + }, + 'website': { + 'value': self.website, + # http://www.fileformat.info/info/unicode/char/1f310/index.htm + 'icon': '\U0001f310', + 'label': _('Website:'), + }, + 'address': { + 'value': self._display_address(without_company=True), + } + } + res = [] + for detail in details: + if options.get(detail) and options[detail]['value']: + entry = options[detail] + prefix = icon and entry.get('icon') or entry.get('label') + if prefix: + res.append('%s %s' % (prefix, entry['value'])) + else: + res.append('%s' % entry['value']) + res = '\n'.join(res) + return res + + +class ResPartnerCategory(models.Model): + _inherit = 'res.partner.category' + + name = fields.Char(translate=False) + + +class ResPartnerBank(models.Model): + _inherit = 'res.partner.bank' + + # In the 'base' module, they didn't put any string, so the bank name is + # displayed as 'Name', which the string of the related field it + # points to + bank_name = fields.Char(string='Bank Name') diff --git a/base_usability/models/users.py b/base_usability/models/users.py new file mode 100644 index 0000000..ffebe32 --- /dev/null +++ b/base_usability/models/users.py @@ -0,0 +1,40 @@ +# Copyright 2018 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, api, SUPERUSER_ID, _ +from odoo.exceptions import UserError +import logging +logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = 'res.users' + + @api.model + def default_get(self, fields_list): + res = super(ResUsers, self).default_get(fields_list) + # For a new partner auto-created when you create a new user, we prefer + # customer=False and supplier=True by default + res.update({ + 'customer': False, + 'supplier': True, + }) + return res + + @api.model + def _script_partners_linked_to_users_no_company(self): + if self.env.user.id != SUPERUSER_ID: + raise UserError(_('You must run this script as admin user')) + logger.info( + 'START to set company_id=False on partners related to users') + users = self.search( + ['|', ('active', '=', True), ('active', '=', False)]) + for user in users: + if user.partner_id.company_id: + user.partner_id.company_id = False + logger.info( + 'Wrote company_id=False on user %s ID %d', + user.login, user.id) + logger.info( + 'END setting company_id=False on partners related to users') + return True diff --git a/base_usability/security/group.xml b/base_usability/security/group.xml new file mode 100644 index 0000000..bb84ffa --- /dev/null +++ b/base_usability/security/group.xml @@ -0,0 +1,22 @@ + + + + + + + + + Nobody (used to hide native menus) + + + diff --git a/base_usability/static/description/icon.png b/base_usability/static/description/icon.png new file mode 100644 index 0000000..c91da79 Binary files /dev/null and b/base_usability/static/description/icon.png differ diff --git a/base_usability/views/country_view.xml b/base_usability/views/country_view.xml new file mode 100644 index 0000000..9a5e486 --- /dev/null +++ b/base_usability/views/country_view.xml @@ -0,0 +1,51 @@ + + + + + + + base_usability.res.country.state.search + res.country.state + + + + + + + + + + + + + + base_usability.res.country.search + res.country + + + + + + + + + + + + + + base_usability.res.country.form + res.country + + + + + + + + + diff --git a/base_usability/views/module_view.xml b/base_usability/views/module_view.xml new file mode 100644 index 0000000..3688e44 --- /dev/null +++ b/base_usability/views/module_view.xml @@ -0,0 +1,27 @@ + + + + + + + ir.module.module + + + + + + + + + + + + + {'search_default_installable': 1} + + + diff --git a/base_usability/views/partner_bank_view.xml b/base_usability/views/partner_bank_view.xml new file mode 100644 index 0000000..93e7366 --- /dev/null +++ b/base_usability/views/partner_bank_view.xml @@ -0,0 +1,22 @@ + + + + + + + base_usability.res.partner.bank.tree + res.partner.bank + + + + 0 + handle + + + + + diff --git a/base_usability/views/partner_view.xml b/base_usability/views/partner_view.xml new file mode 100644 index 0000000..6b1ab69 --- /dev/null +++ b/base_usability/views/partner_view.xml @@ -0,0 +1,67 @@ + + + + + + + base_usability.title.on.partner.form + res.partner + + + + + width: 650px; + + + email + + + + + + + + + + base_usability.title.on.partner.simplified.form + res.partner + + + + + + + + + + base_usability.res.partner.tree + res.partner + + + + 0 + + + + + + + + + base_usability.partner.search.form + res.partner + + + + Name or Email or Reference + + ['|','|',('display_name','ilike',self),('ref','=ilike',self + '%'),('email','ilike',self)] + + + + + diff --git a/base_usability/views/users_view.xml b/base_usability/views/users_view.xml new file mode 100644 index 0000000..e692049 --- /dev/null +++ b/base_usability/views/users_view.xml @@ -0,0 +1,21 @@ + + + + + + + base_usability.res.users.tree + res.users + + + + + + + + +