One of the main issue with ormcache is that the invalidation clears everything, meaning that some value, slow to compute but with a long lifetime, can be removed from the cache because an easy to invalidate value is cleared, like after writting or creating a product has an example. Most example in the code will try to invalidate the cache of the models doing something like `env['ir.qweb'].clear_caches()` but it is finally equivalent to `env.registry.clear_cache()`, and cross worker. The idea is to have multiple cache, maybe with specific sizes for a specific purpose. Having one per model is maybe a bad idea because it will be difficult to size the LRU correcly, and it is too dynamic. Checking invalidation may be expensive. The proposed solution is closed allow a limited number of named caches, using onse sequence per cache. This is actually close to the cache_longterm. We want to discourage using a specific cache for one use case in the buisness code. Adding a cache shouldn't be something easy, doable in stable. Note that we could also change the invalisation mecanism using an insert only table. We an check the sequence of this table, but also fetch all invalidation messages. Another possible improvement, especially if we have more than x cache is to have a global sequence, checking signaling would mean to check the main sequence, and only the other ones if the main one changed. Note that this poc is inspired from the long term cache but not all use case where applie yet. Part-of: odoo/odoo#119813
200 lines
7.8 KiB
Python
200 lines
7.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import re
|
|
import logging
|
|
from odoo import api, fields, models, tools
|
|
from odoo.osv import expression
|
|
from odoo.exceptions import UserError
|
|
from psycopg2 import IntegrityError
|
|
from odoo.tools.translate import _
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
FLAG_MAPPING = {
|
|
"GF": "fr",
|
|
"BV": "no",
|
|
"BQ": "nl",
|
|
"GP": "fr",
|
|
"HM": "au",
|
|
"YT": "fr",
|
|
"RE": "fr",
|
|
"MF": "fr",
|
|
"UM": "us",
|
|
}
|
|
|
|
NO_FLAG_COUNTRIES = [
|
|
"AQ", #Antarctica
|
|
"SJ", #Svalbard + Jan Mayen : separate jurisdictions : no dedicated flag
|
|
]
|
|
|
|
|
|
class Country(models.Model):
|
|
_name = 'res.country'
|
|
_description = 'Country'
|
|
_order = 'name'
|
|
|
|
name = fields.Char(
|
|
string='Country Name', required=True, translate=True)
|
|
code = fields.Char(
|
|
string='Country Code', size=2,
|
|
required=True,
|
|
help='The ISO country code in two chars. \nYou can use this field for quick search.')
|
|
address_format = fields.Text(string="Layout in Reports",
|
|
help="Display format to use for addresses belonging to this country.\n\n"
|
|
"You can use python-style string pattern with all the fields of the address "
|
|
"(for example, use '%(street)s' to display the field 'street') plus"
|
|
"\n%(state_name)s: the name of the state"
|
|
"\n%(state_code)s: the code of the state"
|
|
"\n%(country_name)s: the name of the country"
|
|
"\n%(country_code)s: the code of the country",
|
|
default='%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s')
|
|
address_view_id = fields.Many2one(
|
|
comodel_name='ir.ui.view', string="Input View",
|
|
domain=[('model', '=', 'res.partner'), ('type', '=', 'form')],
|
|
help="Use this field if you want to replace the usual way to encode a complete address. "
|
|
"Note that the address_format field is used to modify the way to display addresses "
|
|
"(in reports for example), while this field is used to modify the input form for "
|
|
"addresses.")
|
|
currency_id = fields.Many2one('res.currency', string='Currency')
|
|
image_url = fields.Char(
|
|
compute="_compute_image_url", string="Flag",
|
|
help="Url of static flag image",
|
|
)
|
|
phone_code = fields.Integer(string='Country Calling Code')
|
|
country_group_ids = fields.Many2many('res.country.group', 'res_country_res_country_group_rel',
|
|
'res_country_id', 'res_country_group_id', string='Country Groups')
|
|
state_ids = fields.One2many('res.country.state', 'country_id', string='States')
|
|
name_position = fields.Selection([
|
|
('before', 'Before Address'),
|
|
('after', 'After Address'),
|
|
], string="Customer Name Position", default="before",
|
|
help="Determines where the customer/company name should be placed, i.e. after or before the address.")
|
|
vat_label = fields.Char(string='Vat Label', translate=True, prefetch=True, help="Use this field if you want to change vat label.")
|
|
|
|
state_required = fields.Boolean(default=False)
|
|
zip_required = fields.Boolean(default=True)
|
|
|
|
_sql_constraints = [
|
|
('name_uniq', 'unique (name)',
|
|
'The name of the country must be unique!'),
|
|
('code_uniq', 'unique (code)',
|
|
'The code of the country must be unique!')
|
|
]
|
|
|
|
def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None):
|
|
if domain is None:
|
|
domain = []
|
|
|
|
ids = []
|
|
if len(name) == 2:
|
|
ids = list(self._search([('code', 'ilike', name)] + domain, limit=limit, order=order))
|
|
|
|
search_domain = [('name', operator, name)]
|
|
if ids:
|
|
search_domain.append(('id', 'not in', ids))
|
|
ids += list(self._search(search_domain + domain, limit=limit, order=order))
|
|
|
|
return ids
|
|
|
|
@api.model
|
|
@tools.ormcache('code')
|
|
def _phone_code_for(self, code):
|
|
return self.search([('code', '=', code)]).phone_code
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('code'):
|
|
vals['code'] = vals['code'].upper()
|
|
return super(Country, self).create(vals_list)
|
|
|
|
def write(self, vals):
|
|
if vals.get('code'):
|
|
vals['code'] = vals['code'].upper()
|
|
res = super().write(vals)
|
|
if ('code' in vals or 'phone_code' in vals):
|
|
# Intentionally simplified by not clearing the cache in create and unlink.
|
|
self.env.registry.clear_cache()
|
|
if 'address_view_id' in vals:
|
|
# Changing the address view of the company must invalidate the view cached for res.partner
|
|
# because of _view_get_address
|
|
self.env.registry.clear_cache('templates')
|
|
return res
|
|
|
|
def get_address_fields(self):
|
|
self.ensure_one()
|
|
return re.findall(r'\((.+?)\)', self.address_format)
|
|
|
|
@api.depends('code')
|
|
def _compute_image_url(self):
|
|
for country in self:
|
|
if not country.code or country.code in NO_FLAG_COUNTRIES:
|
|
country.image_url = False
|
|
else:
|
|
code = FLAG_MAPPING.get(country.code, country.code.lower())
|
|
country.image_url = "/base/static/img/country_flags/%s.png" % code
|
|
|
|
@api.constrains('address_format')
|
|
def _check_address_format(self):
|
|
for record in self:
|
|
if record.address_format:
|
|
address_fields = self.env['res.partner']._formatting_address_fields() + ['state_code', 'state_name', 'country_code', 'country_name', 'company_name']
|
|
try:
|
|
record.address_format % {i: 1 for i in address_fields}
|
|
except (ValueError, KeyError):
|
|
raise UserError(_('The layout contains an invalid format key'))
|
|
|
|
class CountryGroup(models.Model):
|
|
_description = "Country Group"
|
|
_name = 'res.country.group'
|
|
|
|
name = fields.Char(required=True, translate=True)
|
|
country_ids = fields.Many2many('res.country', 'res_country_res_country_group_rel',
|
|
'res_country_group_id', 'res_country_id', string='Countries')
|
|
|
|
|
|
class CountryState(models.Model):
|
|
_description = "Country state"
|
|
_name = 'res.country.state'
|
|
_order = 'code'
|
|
|
|
country_id = fields.Many2one('res.country', string='Country', required=True)
|
|
name = fields.Char(string='State Name', required=True,
|
|
help='Administrative divisions of a country. E.g. Fed. State, Departement, Canton')
|
|
code = fields.Char(string='State Code', help='The state code.', required=True)
|
|
|
|
_sql_constraints = [
|
|
('name_code_uniq', 'unique(country_id, code)', 'The code of the state must be unique by country!')
|
|
]
|
|
|
|
@api.model
|
|
def _name_search(self, name, domain=None, operator='ilike', limit=None, order=None):
|
|
domain = domain or []
|
|
if self.env.context.get('country_id'):
|
|
domain = expression.AND([domain, [('country_id', '=', self.env.context.get('country_id'))]])
|
|
|
|
if operator == 'ilike' and not (name or '').strip():
|
|
domain1 = []
|
|
domain2 = []
|
|
else:
|
|
domain1 = [('code', '=ilike', name)]
|
|
domain2 = [('name', operator, name)]
|
|
|
|
first_state_ids = []
|
|
if domain1:
|
|
first_state_ids = list(self._search(
|
|
expression.AND([domain1, domain]), limit=limit, order=order,
|
|
))
|
|
return first_state_ids + [
|
|
state_id
|
|
for state_id in self._search(expression.AND([domain2, domain]),
|
|
limit=limit, order=order)
|
|
if state_id not in first_state_ids
|
|
]
|
|
|
|
@api.depends('country_id')
|
|
def _compute_display_name(self):
|
|
for record in self:
|
|
record.display_name = f"{record.name} ({record.country_id.code})"
|