[MERGE] base, various: avoid storing country flags and strengthen update of countries
Currently, 200+ country flags are stored in ALL db's as ir_attachment's (thus in the filestore), and also duplicated at each backup, ... To reduce the footprint of Odoo databases (and their backups), the country flags are now considered as static images and accessed as such, without storing their references in database. ##### New widget for static image URL's A new `image_url` widget is added (frontend & backend), to allow specification of a static URL in Char fields, enabling the access and display of `res.country` flags through their static urls : `/base/static/img/country_flags/<country_code>.png`. ##### Restriction of countries creation & deletion To ensure no inconsistency can be introduced because the flags are now static, we now restrict the creation and deletion of countries. Countries are nearly static data, they shouldn't ever be deleted... This prevents common database inconsistencies, bugs and feature disabling. Manually created countries can easily be wrongly configured, won't behave as expected and/or will crash with Odoo integration (delivery carriers, taxes, VAT validation/formatting). Therefore, creation/deletion of countries is limited to admins, supposed to know what they are doing... Will probably be backported in V14 LINKS Task ID-2325371 COM PR odoo/odoo#52774 UPG PR odoo/upgrade#1338 Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
@@ -21,20 +21,12 @@ class Image(models.AbstractModel):
|
||||
_description = 'Qweb Field Image'
|
||||
_inherit = 'ir.qweb.field.image'
|
||||
|
||||
@api.model
|
||||
def record_to_html(self, record, field_name, options):
|
||||
assert options['tagName'] != 'img',\
|
||||
"Oddly enough, the root tag of an image field can not be img. " \
|
||||
"That is because the image goes into the tag, or it gets the " \
|
||||
"hose again."
|
||||
|
||||
if options.get('qweb_img_raw_data', False):
|
||||
return super(Image, self).record_to_html(record, field_name, options)
|
||||
|
||||
aclasses = ['img', 'img-fluid'] if options.get('qweb_img_responsive', True) else ['img']
|
||||
aclasses += options.get('class', '').split()
|
||||
classes = ' '.join(map(escape, aclasses))
|
||||
def _get_src_urls(self, record, field_name, options):
|
||||
"""Considering the rendering options, returns the src and data-zoom-image urls.
|
||||
|
||||
:return: src, src_zoom urls
|
||||
:rtype: tuple
|
||||
"""
|
||||
max_size = None
|
||||
if options.get('resize'):
|
||||
max_size = options.get('resize')
|
||||
@@ -56,6 +48,30 @@ class Image(models.AbstractModel):
|
||||
|
||||
src = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options.get('preview_image', field_name), max_size, url_quote(filename), sha)
|
||||
|
||||
src_zoom = None
|
||||
if options.get('zoom') and getattr(record, options['zoom'], None):
|
||||
src_zoom = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options['zoom'], max_size, url_quote(filename), sha)
|
||||
elif options.get('zoom'):
|
||||
src_zoom = options['zoom']
|
||||
|
||||
return src, src_zoom
|
||||
|
||||
@api.model
|
||||
def record_to_html(self, record, field_name, options):
|
||||
assert options['tagName'] != 'img',\
|
||||
"Oddly enough, the root tag of an image field can not be img. " \
|
||||
"That is because the image goes into the tag, or it gets the " \
|
||||
"hose again."
|
||||
|
||||
if options.get('qweb_img_raw_data', False):
|
||||
return super(Image, self).record_to_html(record, field_name, options)
|
||||
|
||||
aclasses = ['img', 'img-fluid'] if options.get('qweb_img_responsive', True) else ['img']
|
||||
aclasses += options.get('class', '').split()
|
||||
classes = ' '.join(map(escape, aclasses))
|
||||
|
||||
src, src_zoom = self._get_src_urls(record, field_name, options)
|
||||
|
||||
if options.get('alt-field') and getattr(record, options['alt-field'], None):
|
||||
alt = escape(record[options['alt-field']])
|
||||
elif options.get('alt'):
|
||||
@@ -63,12 +79,6 @@ class Image(models.AbstractModel):
|
||||
else:
|
||||
alt = escape(record.display_name)
|
||||
|
||||
src_zoom = None
|
||||
if options.get('zoom') and getattr(record, options['zoom'], None):
|
||||
src_zoom = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options['zoom'], max_size, url_quote(filename), sha)
|
||||
elif options.get('zoom'):
|
||||
src_zoom = options['zoom']
|
||||
|
||||
itemprop = None
|
||||
if options.get('itemprop'):
|
||||
itemprop = options['itemprop']
|
||||
@@ -96,3 +106,11 @@ class Image(models.AbstractModel):
|
||||
img.append('/>')
|
||||
|
||||
return u''.join(img)
|
||||
|
||||
class ImageUrlConverter(models.AbstractModel):
|
||||
_description = 'Qweb Field Image'
|
||||
_inherit = 'ir.qweb.field.image_url'
|
||||
|
||||
def _get_src_urls(self, record, field_name, options):
|
||||
image_url = record[options.get('preview_image', field_name)]
|
||||
return image_url, options.get("zoom", None)
|
||||
|
||||
@@ -1955,6 +1955,45 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
|
||||
},
|
||||
});
|
||||
|
||||
var CharImageUrl = AbstractField.extend({
|
||||
className: 'o_field_image',
|
||||
description: _lt("Image"),
|
||||
supportedFieldTypes: ['char'],
|
||||
placeholder: "/web/static/src/img/placeholder.png",
|
||||
|
||||
_renderReadonly: function () {
|
||||
var self = this;
|
||||
const url = this.value;
|
||||
if (url) {
|
||||
var $img = $(qweb.render("FieldBinaryImage-img", {widget: this, url: url}));
|
||||
// override css size attributes (could have been defined in css files)
|
||||
// if specified on the widget
|
||||
const width = this.nodeOptions.size ? this.nodeOptions.size[0] : this.attrs.width;
|
||||
const height = this.nodeOptions.size ? this.nodeOptions.size[1] : this.attrs.height;
|
||||
if (width) {
|
||||
$img.attr('width', width);
|
||||
$img.css('max-width', width + 'px');
|
||||
}
|
||||
if (height) {
|
||||
$img.attr('height', height);
|
||||
$img.css('max-height', height + 'px');
|
||||
}
|
||||
this.$('> img').remove();
|
||||
this.$el.prepend($img);
|
||||
|
||||
$img.one('error', function () {
|
||||
$img.attr('src', self.placeholder);
|
||||
self.displayNotification({
|
||||
type: 'info',
|
||||
message: _t("Could not display the specified image url."),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var KanbanFieldBinaryImage = FieldBinaryImage.extend({
|
||||
// In kanban views, there is a weird logic to determine whether or not a
|
||||
// click on a card should open the record in a form view. This logic checks
|
||||
@@ -1966,6 +2005,17 @@ var KanbanFieldBinaryImage = FieldBinaryImage.extend({
|
||||
events: {},
|
||||
});
|
||||
|
||||
var KanbanCharImageUrl = CharImageUrl.extend({
|
||||
// In kanban views, there is a weird logic to determine whether or not a
|
||||
// click on a card should open the record in a form view. This logic checks
|
||||
// if the clicked element has click handlers bound on it, and if so, does
|
||||
// not open the record (assuming that the click will be handled by someone
|
||||
// else). In the case of this widget, there are clicks handler but they
|
||||
// only apply in edit mode, which is never the case in kanban views, so we
|
||||
// simply remove them.
|
||||
events: {},
|
||||
});
|
||||
|
||||
var FieldBinaryFile = AbstractFieldBinary.extend({
|
||||
description: _lt("File"),
|
||||
template: 'FieldBinaryFile',
|
||||
@@ -3539,6 +3589,8 @@ return {
|
||||
AbstractFieldBinary: AbstractFieldBinary,
|
||||
FieldBinaryImage: FieldBinaryImage,
|
||||
KanbanFieldBinaryImage: KanbanFieldBinaryImage,
|
||||
CharImageUrl: CharImageUrl,
|
||||
KanbanCharImageUrl: KanbanCharImageUrl,
|
||||
FieldBoolean: FieldBoolean,
|
||||
BooleanToggle: BooleanToggle,
|
||||
FieldChar: FieldChar,
|
||||
|
||||
@@ -43,7 +43,9 @@ registry
|
||||
.add('CopyClipboardText', basic_fields.TextCopyClipboard)
|
||||
.add('CopyClipboardChar', basic_fields.CharCopyClipboard)
|
||||
.add('image', basic_fields.FieldBinaryImage)
|
||||
.add('image_url', basic_fields.CharImageUrl)
|
||||
.add('kanban.image', basic_fields.KanbanFieldBinaryImage)
|
||||
.add('kanban.image_url', basic_fields.KanbanCharImageUrl)
|
||||
.add('binary', basic_fields.FieldBinaryFile)
|
||||
.add('pdf_viewer', basic_fields.FieldPdfViewer)
|
||||
.add('monetary', basic_fields.FieldMonetary)
|
||||
|
||||
@@ -24,6 +24,9 @@ var _t = core._t;
|
||||
// Base64 images for testing purpose
|
||||
const MY_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
|
||||
const PRODUCT_IMAGE = 'R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7';
|
||||
const FR_FLAG_URL = '/base/static/img/country_flags/fr.png';
|
||||
const EN_FLAG_URL = '/base/static/img/country_flags/gb.png';
|
||||
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
@@ -2889,6 +2892,131 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
/**
|
||||
* Same tests than for Image fields, but for Char fields with image_url widget.
|
||||
*/
|
||||
QUnit.module('FieldChar-ImageUrlWidget', {
|
||||
beforeEach: function () {
|
||||
// specific sixth partner data for image_url widget tests
|
||||
this.data.partner.records.push({id: 6, bar: false, foo: FR_FLAG_URL, int_field: 5, qux: 0.0, timmy: []});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('image fields are correctly rendered', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/> ' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route, args) {
|
||||
if (route === FR_FLAG_URL) {
|
||||
assert.ok(true, "the correct route should have been called.");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(form.$('div[name="foo"]'), 'o_field_image',
|
||||
"the widget should have the correct class");
|
||||
assert.containsOnce(form, 'div[name="foo"] > img',
|
||||
"the widget should contain an image");
|
||||
assert.hasClass(form.$('div[name="foo"] > img'), 'img-fluid',
|
||||
"the image should have the correct class");
|
||||
assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "90",
|
||||
"the image should correctly set its attributes");
|
||||
assert.strictEqual(form.$('div[name="foo"] > img').css('max-width'), "90px",
|
||||
"the image should correctly set its attributes");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.data.partner_type.fields.image = {name: 'image', type: 'char'};
|
||||
this.data.partner_type.records[0].image = EN_FLAG_URL;
|
||||
this.data.partner.records[5].timmy = [12];
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/>' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="image" widget="image_url"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route) {
|
||||
if (route === FR_FLAG_URL) {
|
||||
assert.step("The view's image should have been fetched");
|
||||
return 'wow';
|
||||
}
|
||||
if (route === EN_FLAG_URL) {
|
||||
assert.step("The dialog's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
assert.verifySteps(["The view's image should have been fetched"]);
|
||||
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
'There should be one record in the many2many');
|
||||
|
||||
// Actual flow: click on an element of the m2m to get its form view
|
||||
await testUtils.dom.click(form.$('tbody td:contains(gold)'));
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
'The modal should have opened');
|
||||
assert.verifySteps(["The dialog's image should have been fetched"]);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('image fields in x2many list are loaded correctly', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner_type.fields.image = {name: 'image', type: 'char'};
|
||||
this.data.partner_type.records[0].image = EN_FLAG_URL;
|
||||
this.data.partner.records[5].timmy = [12];
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="timmy" widget="many2many">' +
|
||||
'<tree>' +
|
||||
'<field name="image" widget="image_url"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route) {
|
||||
if (route === EN_FLAG_URL) {
|
||||
assert.ok(true, "The list's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
'There should be one record in the many2many');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('JournalDashboardGraph', {
|
||||
beforeEach: function () {
|
||||
_.extend(this.data.partner.fields, {
|
||||
|
||||
@@ -250,8 +250,8 @@
|
||||
<field name="public" eval="True"/>
|
||||
<field name="name">business_conference.jpg</field>
|
||||
<field name="res_model">ir.ui.view</field>
|
||||
<field name="type">binary</field>
|
||||
<field name="datas" type="base64" file="website/static/src/img/library/business_conference.jpg"/>
|
||||
<field name="type">url</field>
|
||||
<field name="url">/website/static/src/img/library/business_conference.jpg</field>
|
||||
</record>
|
||||
<record id="website.library_image_01" model="ir.attachment">
|
||||
<field name="public" eval="True"/>
|
||||
|
||||
@@ -38,7 +38,7 @@ class WebsiteVisitor(models.Model):
|
||||
|
||||
# localisation and info
|
||||
country_id = fields.Many2one('res.country', 'Country', readonly=True)
|
||||
country_flag = fields.Binary(related="country_id.image", string="Country Flag")
|
||||
country_flag = fields.Char(related="country_id.image_url", string="Country Flag")
|
||||
lang_id = fields.Many2one('res.lang', string='Language', help="Language from the website when visitor has been created")
|
||||
timezone = fields.Selection(_tz_get, string='Timezone')
|
||||
email = fields.Char(string='Email', compute='_compute_email_phone')
|
||||
|
||||
@@ -33,8 +33,6 @@ class UtilPerf(HttpCase):
|
||||
class TestStandardPerformance(UtilPerf):
|
||||
def test_10_perf_sql_img_controller(self):
|
||||
self.authenticate('demo', 'demo')
|
||||
url = '/web/image/res.country/4/image'
|
||||
self.assertEqual(self._get_url_hot_query(url), 4)
|
||||
url = '/web/image/res.users/2/image_256'
|
||||
self.assertEqual(self._get_url_hot_query(url), 8)
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<kanban class="o_wvisitor_kanban" sample="1">
|
||||
<field name="id"/>
|
||||
<field name="country_id"/>
|
||||
<field name="country_flag"/>
|
||||
<field name="email"/>
|
||||
<field name="is_connected"/>
|
||||
<field name="display_name"/>
|
||||
@@ -109,10 +110,10 @@
|
||||
<span class="fa fa-circle text-success" t-if="record.is_connected.raw_value" aria-label="Online" title="Online"/>
|
||||
<span class="fa fa-circle text-danger" t-else="" aria-label="Offline" title="Offline"/>
|
||||
</div>
|
||||
<div>
|
||||
<img t-if="record.country_id.raw_value"
|
||||
t-att-src="kanban_image('res.country', 'image', record.country_id.raw_value)"
|
||||
class="o_country_flag" alt="Country"/>
|
||||
<!-- Double check is necessary for sample view (or error image are shown) -->
|
||||
<div t-if="record.country_id.raw_value and record.country_flag.raw_value">
|
||||
<img t-att-src="record.country_flag.raw_value"
|
||||
class="o_country_flag" alt="Country"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +148,8 @@
|
||||
</div>
|
||||
<strong>
|
||||
<img t-if="record.country_id.raw_value"
|
||||
t-att-src="kanban_image('res.country', 'image', record.country_id.raw_value)" class="o_country_flag" alt="Country"/>
|
||||
t-att-src="record.country_flag.raw_value"
|
||||
class="o_country_flag" alt="Country"/>
|
||||
<field name="display_name"/>
|
||||
</strong>
|
||||
<div class="mb-2">Active <field name="time_since_last_action"/></div>
|
||||
@@ -196,7 +198,9 @@
|
||||
<field name="visitor_page_count" widget="statinfo" string="Page views"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="float-right" attrs="{'invisible': [('country_id', '=', False)]}"><field name="country_flag" widget="image" options='{"size": [32, 32]}'/></div>
|
||||
<div class="float-right" attrs="{'invisible': [('country_id', '=', False)]}">
|
||||
<field name="country_flag" widget="image_url" options='{"size": [32, 32]}'/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1><field name="display_name"/></h1>
|
||||
</div>
|
||||
@@ -227,7 +231,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Web Visitors" decoration-success="is_connected" decoration-danger="not is_connected" sample="1">
|
||||
<!--TODO DBE : Handle no_label in treeview-->
|
||||
<field name="country_flag" widget="image" options='{"size": [20, 20]}' string=" "/>
|
||||
<field name="country_flag" widget="image_url" options='{"size": [20, 20]}' string=" "/>
|
||||
<field name="display_name"/>
|
||||
<field name="create_date"/>
|
||||
<field name="last_connection_datetime"/>
|
||||
|
||||
@@ -92,11 +92,11 @@ class EventSponsor(models.Model):
|
||||
|
||||
sponsor.is_in_opening_hours = opening_from <= now_tz < opening_to
|
||||
|
||||
@api.depends('partner_id.country_id.image')
|
||||
@api.depends('partner_id.country_id.image_url')
|
||||
def _compute_country_flag_url(self):
|
||||
for sponsor in self:
|
||||
if sponsor.partner_id.country_id:
|
||||
sponsor.country_flag_url = self.env['website'].image_url(sponsor.partner_id.country_id, 'image', size=256)
|
||||
sponsor.country_flag_url = sponsor.partner_id.country_id.image_url
|
||||
else:
|
||||
sponsor.country_flag_url = False
|
||||
|
||||
|
||||
@@ -1641,7 +1641,7 @@
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" t-attf-id="country_#{country.id}" t-att-value="country.id"/>
|
||||
<label class="custom-control-label" t-attf-for="country_#{country.id}">
|
||||
<span t-field="country.image" t-options='{"widget": "image", "class": "country_flag"}' class="mr-2"/>
|
||||
<span t-field="country.image_url" t-options='{"widget": "image_url", "class": "country_flag"}' class="mr-2"/>
|
||||
<span t-esc="country.name"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -385,6 +385,17 @@ class ImageConverter(models.AbstractModel):
|
||||
|
||||
return u'<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value.decode('ascii'))
|
||||
|
||||
class ImageUrlConverter(models.AbstractModel):
|
||||
""" ``image_url`` widget rendering, inserts an image tag in the
|
||||
document.
|
||||
"""
|
||||
_name = 'ir.qweb.field.image_url'
|
||||
_description = 'Qweb Field Image'
|
||||
_inherit = 'ir.qweb.field.image'
|
||||
|
||||
@api.model
|
||||
def value_to_html(self, value, options):
|
||||
return u'<img src="%s">' % (value)
|
||||
|
||||
class MonetaryConverter(models.AbstractModel):
|
||||
""" ``monetary`` converter, has a mandatory option
|
||||
|
||||
@@ -10,6 +10,24 @@ 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'
|
||||
@@ -37,7 +55,10 @@ class Country(models.Model):
|
||||
"(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 = fields.Binary(attachment=True)
|
||||
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')
|
||||
@@ -90,6 +111,15 @@ class Country(models.Model):
|
||||
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
|
||||
|
||||
|
||||
class CountryGroup(models.Model):
|
||||
_description = "Country Group"
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0
|
||||
"access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0
|
||||
"access_res_country_group_group_all","res_country_group group_user_all","model_res_country_group",,1,0,0,0
|
||||
"access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1
|
||||
"access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,0,0,0
|
||||
"access_res_country_group_system","res_country_group_system","model_res_country","group_system",1,1,1,1
|
||||
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
|
||||
"access_res_country_group_group_user","res_country_group group_user","model_res_country_group","group_partner_manager",1,1,1,1
|
||||
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
|
||||
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -7,8 +7,8 @@
|
||||
<t t-if="not object.name and object.parent_name">
|
||||
<span itemprop="name" t-esc="object.parent_name"/>
|
||||
</t>
|
||||
<t t-if="options.get('country_image') and 'country_id' in fields and object.country_id and object.country_id.image">
|
||||
<span t-field="object.country_id.image" t-options='{"widget": "image", "class": "country_flag"}'/>
|
||||
<t t-if="options.get('country_image') and 'country_id' in fields and object.country_id and object.country_id.image_url">
|
||||
<span t-field="object.country_id.image_url" t-options='{"widget": "image_url", "class": "country_flag"}'/>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="name">res.country.tree</field>
|
||||
<field name="model">res.country</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Country">
|
||||
<tree string="Country" create="0" delete="0">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
</tree>
|
||||
@@ -19,11 +19,11 @@
|
||||
<field name="name">res.country.form</field>
|
||||
<field name="model">res.country</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<form create="0" delete="0">
|
||||
<sheet>
|
||||
<div class="oe_button_box">
|
||||
</div>
|
||||
<field name="image" widget="image" class="oe_avatar"/>
|
||||
<field name="image_url" widget="image_url" class="oe_avatar"/>
|
||||
<group name="main_group">
|
||||
<group name="country_details">
|
||||
<field name="name"/>
|
||||
|
||||
Reference in New Issue
Block a user