1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4import re
5
6from odoo import api, fields, models, _
7from odoo.exceptions import UserError
8
9
10class Partner(models.Model):
11    _inherit = ['res.partner']
12
13    street_name = fields.Char(
14        'Street Name', compute='_compute_street_data', inverse='_inverse_street_data', store=True)
15    street_number = fields.Char(
16        'House', compute='_compute_street_data', inverse='_inverse_street_data', store=True)
17    street_number2 = fields.Char(
18        'Door', compute='_compute_street_data', inverse='_inverse_street_data', store=True)
19
20    def _inverse_street_data(self):
21        """Updates the street field.
22        Writes the `street` field on the partners when one of the sub-fields in STREET_FIELDS
23        has been touched"""
24        street_fields = self._get_street_fields()
25        for partner in self:
26            street_format = (partner.country_id.street_format or
27                '%(street_number)s/%(street_number2)s %(street_name)s')
28            previous_field = None
29            previous_pos = 0
30            street_value = ""
31            separator = ""
32            # iter on fields in street_format, detected as '%(<field_name>)s'
33            for re_match in re.finditer(r'%\(\w+\)s', street_format):
34                # [2:-2] is used to remove the extra chars '%(' and ')s'
35                field_name = re_match.group()[2:-2]
36                field_pos = re_match.start()
37                if field_name not in street_fields:
38                    raise UserError(_("Unrecognized field %s in street format.", field_name))
39                if not previous_field:
40                    # first iteration: add heading chars in street_format
41                    if partner[field_name]:
42                        street_value += street_format[0:field_pos] + partner[field_name]
43                else:
44                    # get the substring between 2 fields, to be used as separator
45                    separator = street_format[previous_pos:field_pos]
46                    if street_value and partner[field_name]:
47                        street_value += separator
48                    if partner[field_name]:
49                        street_value += partner[field_name]
50                previous_field = field_name
51                previous_pos = re_match.end()
52
53            # add trailing chars in street_format
54            street_value += street_format[previous_pos:]
55            partner.street = street_value
56
57    @api.depends('street')
58    def _compute_street_data(self):
59        """Splits street value into sub-fields.
60        Recomputes the fields of STREET_FIELDS when `street` of a partner is updated"""
61        street_fields = self._get_street_fields()
62        for partner in self:
63            if not partner.street:
64                for field in street_fields:
65                    partner[field] = None
66                continue
67
68            street_format = (partner.country_id.street_format or
69                '%(street_number)s/%(street_number2)s %(street_name)s')
70            street_raw = partner.street
71            vals = self._split_street_with_params(street_raw, street_format)
72            # assign the values to the fields
73            for k, v in vals.items():
74                partner[k] = v
75            for k in set(street_fields) - set(vals):
76                partner[k] = None
77
78    def _split_street_with_params(self, street_raw, street_format):
79        street_fields = self._get_street_fields()
80        vals = {}
81        previous_pos = 0
82        field_name = None
83        # iter on fields in street_format, detected as '%(<field_name>)s'
84        for re_match in re.finditer(r'%\(\w+\)s', street_format):
85            field_pos = re_match.start()
86            if not field_name:
87                #first iteration: remove the heading chars
88                street_raw = street_raw[field_pos:]
89
90            # get the substring between 2 fields, to be used as separator
91            separator = street_format[previous_pos:field_pos]
92            field_value = None
93            if separator and field_name:
94                #maxsplit set to 1 to unpack only the first element and let the rest untouched
95                tmp = street_raw.split(separator, 1)
96                if previous_greedy in vals:
97                    # attach part before space to preceding greedy field
98                    append_previous, sep, tmp[0] = tmp[0].rpartition(' ')
99                    street_raw = separator.join(tmp)
100                    vals[previous_greedy] += sep + append_previous
101                if len(tmp) == 2:
102                    field_value, street_raw = tmp
103                    vals[field_name] = field_value
104            if field_value or not field_name:
105                previous_greedy = None
106                if field_name == 'street_name' and separator == ' ':
107                    previous_greedy = field_name
108                # select next field to find (first pass OR field found)
109                # [2:-2] is used to remove the extra chars '%(' and ')s'
110                field_name = re_match.group()[2:-2]
111            else:
112                # value not found: keep looking for the same field
113                pass
114            if field_name not in street_fields:
115                raise UserError(_("Unrecognized field %s in street format.", field_name))
116            previous_pos = re_match.end()
117
118        # last field value is what remains in street_raw minus trailing chars in street_format
119        trailing_chars = street_format[previous_pos:]
120        if trailing_chars and street_raw.endswith(trailing_chars):
121            vals[field_name] = street_raw[:-len(trailing_chars)]
122        else:
123            vals[field_name] = street_raw
124        return vals
125
126    def write(self, vals):
127        res = super(Partner, self).write(vals)
128        if 'country_id' in vals and 'street' not in vals:
129            self._inverse_street_data()
130        return res
131
132    def _formatting_address_fields(self):
133        """Returns the list of address fields usable to format addresses."""
134        return super(Partner, self)._formatting_address_fields() + self._get_street_fields()
135
136    def _get_street_fields(self):
137        """Returns the fields that can be used in a street format.
138        Overwrite this function if you want to add your own fields."""
139        return ['street_name', 'street_number', 'street_number2']
140