diff --git a/addons/base_address_extended/__init__.py b/addons/base_address_extended/__init__.py new file mode 100644 index 00000000000..81fc81d29c9 --- /dev/null +++ b/addons/base_address_extended/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models diff --git a/addons/base_address_extended/__manifest__.py b/addons/base_address_extended/__manifest__.py new file mode 100644 index 00000000000..c9c78846b26 --- /dev/null +++ b/addons/base_address_extended/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Extended Addresses', + 'summary': 'Add extra fields on addresses', + 'sequence': '19', + 'category': 'Base', + 'complexity': 'easy', + 'description': """ +Extended Addresses Management +============================= + +This module holds all extra fields one may need to manage accurately addresses. + +For example, in legal reports, some countries need to split the street into several fields, +with the street name, the house number, and room number. + """, + 'data': [ + 'views/base_address_extended.xml' + ], + 'depends': ['base'], +} diff --git a/addons/base_address_extended/models/__init__.py b/addons/base_address_extended/models/__init__.py new file mode 100644 index 00000000000..0e2d5d905fa --- /dev/null +++ b/addons/base_address_extended/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base_address_extended diff --git a/addons/base_address_extended/models/base_address_extended.py b/addons/base_address_extended/models/base_address_extended.py new file mode 100644 index 00000000000..8e599c08402 --- /dev/null +++ b/addons/base_address_extended/models/base_address_extended.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +STREET_FIELDS = ('street_name', 'street_number', 'street_number2') + + +class ResCountry(models.Model): + _inherit = 'res.country' + + street_format = fields.Text( + help="Format to use for streets belonging to this country.\n\n" + "You can use the python-style string pattern with all the fields of the street " + "(for example, use '%(street_name)s, %(street_number)s' if you want to display " + "the street name, followed by a comma and the house number)" + "\n%(street_name)s: the name of the street" + "\n%(street_number)s: the house number" + "\n%(street_number2)s: the door number", + default='%(street_number)s/%(street_number2)s %(street_name)s', required=True) + +class Partner(models.Model): + _inherit = ['res.partner'] + _name = 'res.partner' + + street_name = fields.Char('Street Name', compute='_split_street', + inverse='_set_street', store=True) + street_number = fields.Char('House Number', compute='_split_street', + inverse='_set_street', store=True) + street_number2 = fields.Char('Door Number', compute='_split_street', + inverse='_set_street', store=True) + + def get_street_fields(self): + """Returns the fields that can be used in a street format. + Overwrite this function if you want to add your own fields.""" + return STREET_FIELDS + + @api.multi + def _set_street(self): + """Updates the street field. + Writes the `street` field on the partners when one of the sub-fields in STREET_FIELDS + has been touched""" + street_fields = self.get_street_fields() + for partner in self: + street_format = (partner.country_id.street_format or + '%(street_number)s/%(street_number2)s %(street_name)s') + previous_field = None + previous_pos = 0 + street_value = "" + separator = "" + # iter on fields in street_format, detected as '%()s' + for re_match in re.finditer(r'%\(\w+\)s', street_format): + # [2:-2] is used to remove the extra chars '%(' and ')s' + field_name = re_match.group()[2:-2] + field_pos = re_match.start() + if field_name not in street_fields: + raise UserError(_("Unrecognized field %s in street format.") % field_name) + if not previous_field: + # first iteration: add heading chars in street_format + if partner[field_name]: + street_value += street_format[0:field_pos] + partner[field_name] + else: + # get the substring between 2 fields, to be used as separator + separator = street_format[previous_pos:field_pos] + if street_value and partner[field_name]: + street_value += separator + if partner[field_name]: + street_value += partner[field_name] + previous_field = field_name + previous_pos = re_match.end() + + # add trailing chars in street_format + street_value += street_format[previous_pos:] + + # /!\ Note that we must use a sql query to bypass the orm as it would call _split_street() + # that would try to set the fields we just modified. + self._cr.execute('UPDATE res_partner SET street = %s WHERE ID = %s', (street_value, partner.id)) + #invalidate the cache for the field we manually set + self.invalidate_cache(['street'], [partner.id]) + + @api.multi + @api.depends('street') + def _split_street(self): + """Splits street value into sub-fields. + Recomputes the fields of STREET_FIELDS when `street` of a partner is updated""" + street_fields = self.get_street_fields() + for partner in self: + if not partner.street: + partner.street_name = '' + partner.street_number = '' + partner.street_number2 = '' + continue + + street_format = (partner.country_id.street_format or + '%(street_number)s/%(street_number2)s %(street_name)s') + vals = {} + previous_pos = 0 + street_raw = partner.street + field_name = None + # iter on fields in street_format, detected as '%()s' + for re_match in re.finditer(r'%\(\w+\)s', street_format): + field_pos = re_match.start() + if not field_name: + #first iteration: remove the heading chars + street_raw = street_raw[field_pos:] + + # get the substring between 2 fields, to be used as separator + separator = street_format[previous_pos:field_pos] + field_value = None + if separator and field_name: + #maxsplit set to 1 to unpack only the first element and let the rest untouched + tmp = street_raw.split(separator, 1) + if len(tmp) == 2: + field_value, street_raw = tmp + vals[field_name] = field_value + if field_value or not field_name: + # select next field to find (first pass OR field found) + # [2:-2] is used to remove the extra chars '%(' and ')s' + field_name = re_match.group()[2:-2] + else: + # value not found: keep looking for the same field + pass + if field_name not in street_fields: + raise UserError(_("Unrecognized field %s in street format.") % field_name) + previous_pos = re_match.end() + + # last field value is what remains in street_raw minus trailing chars in street_format + trailing_chars = street_format[previous_pos:] + if trailing_chars and street_raw.endswith(trailing_chars): + vals[field_name] = street_raw[:-len(trailing_chars)] + else: + vals[field_name] = street_raw + # assign the values to the fields + # /!\ Note that a write(vals) would cause a recursion since it would bypass the cache + for k, v in vals.items(): + partner[k] = v diff --git a/addons/base_address_extended/tests/__init__.py b/addons/base_address_extended/tests/__init__.py new file mode 100644 index 00000000000..72bcdae08e5 --- /dev/null +++ b/addons/base_address_extended/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import test_street_fields diff --git a/addons/base_address_extended/tests/test_street_fields.py b/addons/base_address_extended/tests/test_street_fields.py new file mode 100644 index 00000000000..d5817070ffd --- /dev/null +++ b/addons/base_address_extended/tests/test_street_fields.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestStreetFields(TransactionCase): + + def setUp(self): + super(TestStreetFields, self).setUp() + self.Partner = self.env['res.partner'] + self.env.ref('base.be').write({'street_format': '%(street_name)s, %(street_number)s/%(street_number2)s'}) + self.env.ref('base.us').write({'street_format': '%(street_number)s/%(street_number2)s %(street_name)s'}) + self.env.ref('base.ch').write({'street_format': 'header %(street_name)s, %(street_number)s - %(street_number2)s trailer'}) + + def create_and_assert(self, partner_name, country_id, street, street_name, street_number, street_number2): + partner = self.Partner.create({'name': partner_name + '-1', 'street': street, 'country_id': country_id}) + self.assertEqual(partner.street_name or '', street_name, 'wrong street name for %s: %s' % (partner_name, partner.street_name)) + self.assertEqual(partner.street_number or '', street_number, 'wrong house number for %s: %s' % (partner_name, partner.street_number)) + self.assertEqual(partner.street_number2 or '', street_number2, 'wrong door number for %s: %s' % (partner_name, partner.street_number2)) + partner = self.Partner.create({ + 'name': partner_name + '-2', + 'street_name': street_name, + 'street_number': street_number, + 'street_number2': street_number2, + 'country_id': country_id, + }) + self.assertEqual(partner.street or '', street, 'wrong street for %s: %s' % (partner_name, partner.street)) + return partner + + def write_and_assert(self, partner, vals, street, street_name, street_number, street_number2): + partner.write(vals) + self.assertEqual(partner.street_name or '', street_name, 'wrong street name: %s' % partner.street_name) + self.assertEqual(partner.street_number or '', street_number, 'wrong house number: %s' % partner.street_number) + self.assertEqual(partner.street_number2 or '', street_number2, 'wrong door number: %s' % partner.street_number2) + self.assertEqual(partner.street or '', street, 'wrong street: %s' % partner.street) + + def test_00_res_partner_name_create(self): + self.create_and_assert('Test00', self.env.ref('base.us').id, '40/2b Chaussee de Namur', 'Chaussee de Namur', '40', '2b') + self.create_and_assert('Test01', self.env.ref('base.us').id, '40 Chaussee de Namur', 'Chaussee de Namur', '40', '') + self.create_and_assert('Test02', self.env.ref('base.us').id, 'Chaussee de Namur', 'de Namur', 'Chaussee', '') + + def test_01_header_trailer(self): + self.create_and_assert('Test10', self.env.ref('base.ch').id, 'header Chaussee de Namur, 40 - 2b trailer', 'Chaussee de Namur', '40', '2b') + self.create_and_assert('Test11', self.env.ref('base.ch').id, 'header Chaussee de Namur, 40 trailer', 'Chaussee de Namur', '40', '') + self.create_and_assert('Test12', self.env.ref('base.ch').id, 'header Chaussee de Namur trailer', 'Chaussee de Namur', '', '') + + def test_02_res_partner_write(self): + p1 = self.create_and_assert('Test20', self.env.ref('base.be').id, 'Chaussee de Namur, 40/2b', 'Chaussee de Namur', '40', '2b') + self.write_and_assert(p1, {'street': 'Chaussee de Namur, 43'}, 'Chaussee de Namur, 43', 'Chaussee de Namur', '43', '') + self.write_and_assert(p1, {'street': 'Chaussee de Namur'}, 'Chaussee de Namur', 'Chaussee de Namur', '', '') + self.write_and_assert(p1, {'street_name': 'Chee de Namur', 'street_number': '40'}, 'Chee de Namur, 40', 'Chee de Namur', '40', '') + self.write_and_assert(p1, {'street_number2': '4'}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') + #we don't recompute the street fields when we change the country + self.write_and_assert(p1, {'country_id': self.env.ref('base.us').id}, 'Chee de Namur, 40/4', 'Chee de Namur', '40', '4') diff --git a/addons/base_address_extended/views/base_address_extended.xml b/addons/base_address_extended/views/base_address_extended.xml new file mode 100644 index 00000000000..9f6ab4be442 --- /dev/null +++ b/addons/base_address_extended/views/base_address_extended.xml @@ -0,0 +1,54 @@ + + + + + view_res_country_extended_form + res.country + + + + + + + + + + view_partner_structured_form + res.partner + + + + + +
+ +
+
+
+
+ + + view_partner_address_structured_form + res.partner + + + + + +
+
+
+
+
+ +