mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-26 07:50:05 -05:00
Merge branch 'master' of gitlab.com:flectra-hq/flectra
This commit is contained in:
commit
0dd70e4eb9
107 files changed
+78879
-41490
No files matched your search
@@ -6,4 +6,4 @@ furniture_certification_answer_1_p1_q2_3,furniture_certification_answer_1,furnit
|
||||
furniture_certification_answer_1_p1_q3,furniture_certification_answer_1,furniture_certification_page_1_question_3,False,text_box,,,,"I really liked the videos, you are awesome!",,
|
||||
furniture_certification_answer_2_p1_q1,furniture_certification_answer_2,furniture_certification_page_1_question_1,False,suggestion,,,,,furniture_certification_page_1_question_1_choice_3,
|
||||
furniture_certification_answer_2_p1_q2,furniture_certification_answer_2,furniture_certification_page_1_question_2,False,suggestion,,,,,furniture_certification_page_1_question_2_choice_4,
|
||||
furniture_certification_answer_2_p1_q3,furniture_certification_answer_2,furniture_certification_page_1_question_3,true,,,,,,,
|
||||
furniture_certification_answer_2_p1_q3,furniture_certification_answer_2,furniture_certification_page_1_question_3,true,,,,,,,
|
||||
|
@@ -24,7 +24,7 @@
|
||||
<field name="zip">94134</field>
|
||||
<field name='country_id' ref='base.us'/>
|
||||
<field name='state_id' ref='state_us_5'/>
|
||||
<field name="phone">+1 (650) 691-3277 </field>
|
||||
<field name="phone">+1 (650) 555-0111 </field>
|
||||
<field name="email">info@yourcompany.example.com</field>
|
||||
<field name="website">www.example.com</field>
|
||||
</record>
|
||||
|
||||
+207
-207
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+276
-254
File diff suppressed because it is too large.
Load diff
+217
-217
File diff suppressed because it is too large.
Load diff
+262
-233
File diff suppressed because it is too large.
Load diff
+363
-340
File diff suppressed because it is too large.
Load diff
+222
-222
File diff suppressed because it is too large.
Load diff
+387
-309
File diff suppressed because it is too large.
Load diff
+263
-234
File diff suppressed because it is too large.
Load diff
+2510
-797
File diff suppressed because it is too large.
Load diff
+504
-467
File diff suppressed because it is too large.
Load diff
+523
-386
File diff suppressed because it is too large.
Load diff
+278
-248
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+229
-229
File diff suppressed because it is too large.
Load diff
+1343
-461
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+219
-219
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * base
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 11.0\n"
|
||||
"Project-Id-Version: Flectra Server 11.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-10-08 07:02+0000\n"
|
||||
"PO-Revision-Date: 2017-10-24 09:14+0000\n"
|
||||
|
||||
+220
-220
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+3026
-1237
File diff suppressed because it is too large.
Load diff
+12318
-5882
File diff suppressed because it is too large.
Load diff
+320
-280
File diff suppressed because it is too large.
Load diff
+334
-268
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+806
-512
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+219
-219
File diff suppressed because it is too large.
Load diff
+308
-283
File diff suppressed because it is too large.
Load diff
+476
-416
File diff suppressed because it is too large.
Load diff
+4618
-2205
File diff suppressed because it is too large.
Load diff
+301
-260
File diff suppressed because it is too large.
Load diff
+286
-286
File diff suppressed because it is too large.
Load diff
+795
-647
File diff suppressed because it is too large.
Load diff
+894
-407
File diff suppressed because it is too large.
Load diff
+11530
-5166
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+273
-250
File diff suppressed because it is too large.
Load diff
+2341
-801
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+238
-238
File diff suppressed because it is too large.
Load diff
+344
-322
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+306
-284
File diff suppressed because it is too large.
Load diff
+273
-243
File diff suppressed because it is too large.
Load diff
+1215
-496
File diff suppressed because it is too large.
Load diff
+220
-220
File diff suppressed because it is too large.
Load diff
+464
-411
File diff suppressed because it is too large.
Load diff
+325
-285
File diff suppressed because it is too large.
Load diff
+3682
-1011
File diff suppressed because it is too large.
Load diff
+627
-321
File diff suppressed because it is too large.
Load diff
+429
-391
File diff suppressed because it is too large.
Load diff
+262
-233
File diff suppressed because it is too large.
Load diff
+554
-368
File diff suppressed because it is too large.
Load diff
+207
-207
File diff suppressed because it is too large.
Load diff
+217
-217
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+10321
-5182
File diff suppressed because it is too large.
Load diff
+336
-305
File diff suppressed because it is too large.
Load diff
+3091
-679
File diff suppressed because it is too large.
Load diff
+787
-519
File diff suppressed because it is too large.
Load diff
+2630
-671
File diff suppressed because it is too large.
Load diff
+1017
-637
File diff suppressed because it is too large.
Load diff
+405
-373
File diff suppressed because it is too large.
Load diff
@@ -19,7 +19,8 @@ import idna
|
||||
|
||||
from flectra import api, fields, models, tools, _
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tools import ustr, pycompat, formataddr
|
||||
from flectra.tools import ustr, pycompat, formataddr, encapsulate_email, email_domain_extract
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_test_logger = logging.getLogger('flectra.tests')
|
||||
@@ -284,7 +285,7 @@ class IrMailServer(models.Model):
|
||||
"""
|
||||
email_from = email_from or self._get_default_from_address()
|
||||
assert email_from, "You must either provide a sender address explicitly or configure "\
|
||||
"using the combintion of `mail.catchall.domain` and `mail.default.from` "\
|
||||
"using the combination of `mail.catchall.domain` and `mail.default.from` "\
|
||||
"ICPs, in the server configuration file or with the "\
|
||||
"--email-from startup parameter."
|
||||
|
||||
@@ -305,7 +306,12 @@ class IrMailServer(models.Model):
|
||||
if references:
|
||||
msg['references'] = references
|
||||
msg['Subject'] = subject
|
||||
|
||||
email_from, return_path = self._get_email_from(email_from)
|
||||
msg['From'] = email_from
|
||||
if return_path:
|
||||
headers.setdefault('Return-Path', return_path)
|
||||
|
||||
del msg['Reply-To']
|
||||
msg['Reply-To'] = reply_to or email_from
|
||||
msg['To'] = email_to
|
||||
@@ -333,6 +339,35 @@ class IrMailServer(models.Model):
|
||||
msg.add_attachment(fcontent, maintype, subtype, filename=fname)
|
||||
return msg
|
||||
|
||||
def _get_email_from(self, email_from):
|
||||
"""Logic which determines which email to use when sending the email.
|
||||
|
||||
- If the system parameter `mail.force.smtp.from` is set we encapsulate all
|
||||
outgoing email from
|
||||
- If the previous system parameter is not set and if both `mail.dynamic.smtp.from`
|
||||
and `mail.catchall.domain` are set, we encapsulate the FROM only if the domain
|
||||
of the email is not the same as the domain of the catchall parameter
|
||||
- Otherwise we do not encapsulate the email and given email_from is used as is
|
||||
|
||||
:param email_from: The initial FROM headers
|
||||
:return: The FROM to used in the headers and optionally the Return-Path
|
||||
"""
|
||||
force_smtp_from = self.env['ir.config_parameter'].sudo().get_param('mail.force.smtp.from')
|
||||
dynamic_smtp_from = self.env['ir.config_parameter'].sudo().get_param('mail.dynamic.smtp.from')
|
||||
catchall_domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain')
|
||||
|
||||
if force_smtp_from:
|
||||
rfc2822_force_smtp_from = extract_rfc2822_addresses(force_smtp_from)
|
||||
rfc2822_force_smtp_from = rfc2822_force_smtp_from[0] if rfc2822_force_smtp_from else None
|
||||
return encapsulate_email(email_from, force_smtp_from), rfc2822_force_smtp_from
|
||||
|
||||
elif dynamic_smtp_from and catchall_domain and email_domain_extract(email_from) != catchall_domain:
|
||||
rfc2822_dynamic_smtp_from = extract_rfc2822_addresses(dynamic_smtp_from)
|
||||
rfc2822_dynamic_smtp_from = rfc2822_dynamic_smtp_from[0] if rfc2822_dynamic_smtp_from else None
|
||||
return encapsulate_email(email_from, dynamic_smtp_from), rfc2822_dynamic_smtp_from
|
||||
|
||||
return email_from, None
|
||||
|
||||
@api.model
|
||||
def _get_default_bounce_address(self):
|
||||
'''Compute the default bounce address.
|
||||
|
||||
@@ -430,8 +430,18 @@ class IrModel(models.Model):
|
||||
if tools.table_kind(cr, Model._table) not in ('r', None):
|
||||
# not a regular table, so disable schema upgrades
|
||||
Model._auto = False
|
||||
cr.execute('SELECT * FROM %s LIMIT 0' % Model._table)
|
||||
columns = {desc[0] for desc in cr.description}
|
||||
cr.execute(
|
||||
'''
|
||||
SELECT a.attname
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class t
|
||||
ON a.attrelid = t.oid
|
||||
AND t.relname = %s
|
||||
WHERE a.attnum > 0 -- skip system columns
|
||||
''',
|
||||
[Model._table]
|
||||
)
|
||||
columns = {colinfo[0] for colinfo in cr.fetchall()}
|
||||
Model._log_access = set(models.LOG_ACCESS_COLUMNS) <= columns
|
||||
|
||||
|
||||
@@ -1023,6 +1033,8 @@ class IrModelFields(models.Model):
|
||||
model_id = self.env['ir.model']._get_id(model_name)
|
||||
for field in self.env[model_name]._fields.values():
|
||||
rows.append(self._reflect_field_params(field, model_id))
|
||||
if not rows:
|
||||
return
|
||||
cols = list(unique(['model', 'name'] + list(rows[0])))
|
||||
expected = [tuple(row[col] for col in cols) for row in rows]
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import threading
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import werkzeug.urls
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.core import publish_string
|
||||
@@ -784,7 +785,7 @@ class Module(models.Model):
|
||||
_logger.warning(msg)
|
||||
raise UserError(msg)
|
||||
|
||||
apps_server = urls.url_parse(self.get_apps_server())
|
||||
apps_server = werkzeug.urls.url_parse(self.get_apps_server())
|
||||
|
||||
OPENERP = flectra.release.product_name.lower()
|
||||
tmp = tempfile.mkdtemp()
|
||||
@@ -795,7 +796,7 @@ class Module(models.Model):
|
||||
if not url:
|
||||
continue # nothing to download, local version is already the last one
|
||||
|
||||
up = urls.url_parse(url)
|
||||
up = werkzeug.urls.url_parse(url)
|
||||
if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc:
|
||||
raise AccessDenied()
|
||||
|
||||
|
||||
@@ -224,12 +224,13 @@ class IrSequence(models.Model):
|
||||
|
||||
return res
|
||||
|
||||
self.ensure_one()
|
||||
d = _interpolation_dict()
|
||||
try:
|
||||
interpolated_prefix = _interpolate(self.prefix, d)
|
||||
interpolated_suffix = _interpolate(self.suffix, d)
|
||||
except ValueError:
|
||||
raise UserError(_('Invalid prefix or suffix for sequence \'%s\'') % (self.get('name')))
|
||||
raise UserError(_('Invalid prefix or suffix for sequence \'%s\'') % self.name)
|
||||
return interpolated_prefix, interpolated_suffix
|
||||
|
||||
def get_next_char(self, number_next):
|
||||
|
||||
@@ -517,7 +517,7 @@ class Partner(models.Model):
|
||||
self.invalidate_cache(['user_ids'], self._ids)
|
||||
for partner in self:
|
||||
if partner.active and partner.user_ids:
|
||||
raise ValidationError(_('You cannot archive a contact linked to an internal user.'))
|
||||
raise ValidationError(_('You cannot archive a contact linked to a portal or internal user.'))
|
||||
# res.partner must only allow to set the company_id of a partner if it
|
||||
# is the same as the company of all users that inherit from this partner
|
||||
# (this is to allow the code from res_users to write to the partner!) or
|
||||
|
||||
@@ -948,7 +948,7 @@ class Users(models.Model):
|
||||
"and *might* be a proxy. If your Flectra is behind a proxy, "
|
||||
"it may be mis-configured. Check that you are running "
|
||||
"Flectra in Proxy Mode and that the proxy is properly configured, see "
|
||||
"https://www.flectrahq.com/documentation/14.0/setup/deploy.html#https for details.",
|
||||
"https://doc.flectrahq.com/2.0/administration/deployment/deploy.html#https for details.",
|
||||
source
|
||||
)
|
||||
raise AccessDenied(_("Too many login failures, please wait a bit before trying again."))
|
||||
|
||||
@@ -19,7 +19,7 @@ stream
|
||||
BT
|
||||
/F1 100 Tf
|
||||
10 400 Td
|
||||
(!Flectra Test!) Tj
|
||||
(!Odoo Test!) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
|
||||
@@ -114,6 +114,17 @@ class TestExpression(SavepointCaseWithUserDemo):
|
||||
test('not ilike', 'B', ['0', 'a'])
|
||||
test('not like', 'AB', ['0', 'a', 'b', 'a b'])
|
||||
|
||||
def test_09_hierarchy_filtered_domain(self):
|
||||
Partner = self.env['res.partner']
|
||||
p = Partner.create({'name': 'dummy'})
|
||||
|
||||
# hierarchy without parent
|
||||
self.assertFalse(p.parent_id)
|
||||
p2 = self._search(Partner, [('parent_id', 'child_of', p.id)], [('id', '=', p.id)])
|
||||
self.assertEqual(p2, p)
|
||||
p3 = self._search(Partner, [('parent_id', 'parent_of', p.id)], [('id', '=', p.id)])
|
||||
self.assertEqual(p3, p)
|
||||
|
||||
def test_10_hierarchy_in_m2m(self):
|
||||
Partner = self.env['res.partner']
|
||||
Category = self.env['res.partner.category']
|
||||
|
||||
@@ -510,8 +510,8 @@ class TestCustomFields(common.TransactionCase):
|
||||
})
|
||||
query_count = self.cr.sql_log_count - query_count
|
||||
|
||||
# create the related field, and assert it only takes 3 extra queries
|
||||
with self.assertQueryCount(query_count + 3):
|
||||
# create the related field, and assert it only takes 1 extra queries
|
||||
with self.assertQueryCount(query_count + 1):
|
||||
self.env['ir.model.fields'].create({
|
||||
'model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
'name': 'x_oh_boy',
|
||||
|
||||
@@ -447,6 +447,76 @@ class EmailConfigCase(SavepointCase):
|
||||
)
|
||||
self.assertEqual(message["From"], "settings@example.com")
|
||||
|
||||
def test_email_from_rewrite(self):
|
||||
get_email_from = self.env['ir.mail_server']._get_email_from
|
||||
set_param = self.env['ir.config_parameter'].set_param
|
||||
|
||||
# Standard case, no setting
|
||||
set_param('mail.force.smtp.from', False)
|
||||
set_param('mail.dynamic.smtp.from', False)
|
||||
set_param('mail.catchall.domain', 'flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('admin@test.example.com')
|
||||
self.assertEqual(email_from, 'admin@test.example.com')
|
||||
self.assertFalse(return_path)
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@test.example.com>')
|
||||
self.assertEqual(email_from, '"Admin" <admin@test.example.com>')
|
||||
self.assertFalse(return_path)
|
||||
|
||||
# We always force the email FROM
|
||||
set_param('mail.force.smtp.from', 'email_force@domain.com')
|
||||
set_param('mail.dynamic.smtp.from', False)
|
||||
set_param('mail.catchall.domain', 'flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('admin@test.example.com')
|
||||
self.assertEqual(email_from, '"admin@test.example.com" <email_force@domain.com>')
|
||||
self.assertEqual(return_path, 'email_force@domain.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@test.example.com>')
|
||||
self.assertEqual(email_from, '"Admin (admin@test.example.com)" <email_force@domain.com>')
|
||||
self.assertEqual(return_path, 'email_force@domain.com')
|
||||
|
||||
# We always force the email FROM (notification email contains a name part)
|
||||
set_param('mail.force.smtp.from', '"Your notification bot" <email_force@domain.com>')
|
||||
set_param('mail.dynamic.smtp.from', False)
|
||||
set_param('mail.catchall.domain', 'flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@test.example.com>')
|
||||
self.assertEqual(email_from, '"Admin (admin@test.example.com)" <email_force@domain.com>',
|
||||
msg='Should drop the name part of the forced email')
|
||||
self.assertEqual(return_path, 'email_force@domain.com')
|
||||
|
||||
# We dynamically force the email FROM
|
||||
set_param('mail.force.smtp.from', False)
|
||||
set_param('mail.dynamic.smtp.from', 'notification@flectra.example.com')
|
||||
set_param('mail.catchall.domain', 'flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@test.example.com>')
|
||||
self.assertEqual(email_from, '"Admin (admin@test.example.com)" <notification@flectra.example.com>',
|
||||
msg='Domain is not the same as the catchall domain, we should force the email FROM')
|
||||
self.assertEqual(return_path, 'notification@flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@flectra.example.com>')
|
||||
self.assertEqual(email_from, '"Admin" <admin@flectra.example.com>',
|
||||
msg='Domain is the same as the catchall domain, we should not force the email FROM')
|
||||
self.assertFalse(return_path)
|
||||
|
||||
# We dynamically force the email FROM (notification email contains a name part)
|
||||
set_param('mail.force.smtp.from', False)
|
||||
set_param('mail.dynamic.smtp.from', '"Your notification bot" <notification@flectra.example.com>')
|
||||
set_param('mail.catchall.domain', 'flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@test.example.com>')
|
||||
self.assertEqual(email_from, '"Admin (admin@test.example.com)" <notification@flectra.example.com>',
|
||||
msg='Domain is not the same as the catchall domain, we should force the email FROM')
|
||||
self.assertEqual(return_path, 'notification@flectra.example.com')
|
||||
|
||||
email_from, return_path = get_email_from('"Admin" <admin@flectra.example.com>')
|
||||
self.assertEqual(email_from, '"Admin" <admin@flectra.example.com>',
|
||||
msg='Domain is the same as the catchall domain, we should not force the email FROM')
|
||||
self.assertFalse(return_path)
|
||||
|
||||
|
||||
class TestEmailMessage(TransactionCase):
|
||||
def test_as_string(self):
|
||||
|
||||
@@ -557,6 +557,30 @@ class TestQWebNS(TransactionCase):
|
||||
rendered = view2.with_context(lang=current_lang)._render().strip()
|
||||
self.assertEqual(rendered, b'9/000/000*00')
|
||||
|
||||
def test_render_barcode(self):
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'bacode_test',
|
||||
'barcode': 'test'
|
||||
})
|
||||
|
||||
view = self.env['ir.ui.view'].create({
|
||||
'name': "a_barcode_view",
|
||||
'type': 'qweb',
|
||||
})
|
||||
|
||||
view.arch = u"""<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'width': 100, 'height': 30}"/>"""
|
||||
rendered = view._render(values={'partner': partner}).strip().decode()
|
||||
self.assertRegex(rendered, r'<div><img alt="Barcode test" src="data:image/png;base64,\S+"></div>')
|
||||
|
||||
partner.barcode = '4012345678901'
|
||||
view.arch = u"""<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'EAN13', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
|
||||
ean_rendered = view._render(values={'partner': partner}).strip().decode()
|
||||
self.assertRegex(ean_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
|
||||
|
||||
view.arch = u"""<div t-field="partner.barcode" t-options="{'widget': 'barcode', 'symbology': 'auto', 'width': 100, 'height': 30, 'img_style': 'width:100%;', 'img_alt': 'Barcode'}"/>"""
|
||||
auto_rendered = view._render(values={'partner': partner}).strip().decode()
|
||||
self.assertRegex(auto_rendered, r'<div><img style="width:100%;" alt="Barcode" src="data:image/png;base64,\S+"></div>')
|
||||
|
||||
|
||||
from copy import deepcopy
|
||||
class FileSystemLoader(object):
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
||||
<a type="edit" class="dropdown-item">Module Info</a>
|
||||
<a t-if="record.website.raw_value" role="menuitem" class="dropdown-item" t-att-href="record.website.raw_value" target="_blank">Learn More</a>
|
||||
<a t-if="record.website.raw_value" role="menuitem" class="dropdown-item o-hidden-ios" t-att-href="record.website.raw_value" target="_blank">Learn More</a>
|
||||
<a t-if="installed" name="button_immediate_upgrade" type="object" role="menuitem" class="dropdown-item">Upgrade</a>
|
||||
<a t-if="installed" name="button_uninstall_wizard" type="object" role="menuitem" class="dropdown-item">Uninstall</a>
|
||||
</div>
|
||||
@@ -194,7 +194,7 @@
|
||||
<code groups="base.group_no_one"><field name="name"/></code>
|
||||
</p>
|
||||
<div class="oe_module_action">
|
||||
<a t-att-href="record.website.raw_value" target="_blank" attrs="{'invisible':[('website', '=', '')]}" class="btn btn-sm btn-secondary float-right" role="button">Learn More</a>
|
||||
<a t-att-href="record.website.raw_value" target="_blank" attrs="{'invisible':[('website', '=', '')]}" class="btn btn-sm btn-secondary float-right o-hidden-ios" role="button">Learn More</a>
|
||||
<a type="edit" class="btn btn-secondary btn-sm float-right" role="button" attrs="{'invisible': [('website', '<>', '')]}">Module Info</a>
|
||||
<button type="object" class="btn btn-primary btn-sm" name="button_immediate_install" states="uninstalled" t-if="! record.to_buy.raw_value">Install</button>
|
||||
<a href="https://flectrahq.com/pricing?utm_source=db&utm_medium=module#hosting=on_premise" class="btn btn-info btn-sm" states="uninstalled,uninstallable" t-if="record.to_buy.raw_value" role="button">Upgrade</a>
|
||||
|
||||
@@ -468,7 +468,7 @@
|
||||
</div>
|
||||
</group>
|
||||
<group string="Developer API Keys" help="test">
|
||||
<a href="https://www.flectrahq.com/documentation/master/webservices/flectra.html#api-keys" target="_blank">
|
||||
<a href="https://doc.flectrahq.com/2.0/developer/webservices/flectra.html#api-keys" target="_blank">
|
||||
<i class="fa fa-fw o_button_icon fa-arrow-right"></i>
|
||||
<span>Documentation</span>
|
||||
</a>
|
||||
|
||||
@@ -12,7 +12,7 @@ class FlectraBaseChecker(checkers.BaseChecker):
|
||||
'E8502': (
|
||||
'Bad usage of _, _lt function.',
|
||||
'gettext-variable',
|
||||
'See https://flectrahq.com/documentation/14.0/reference/translations.html#variables'
|
||||
'See https://doc.flectrahq.com/2.0/reference/translations.html#variables'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# pylint: disable=unbalanced-tuple-unpacking
|
||||
|
||||
import logging
|
||||
import re
|
||||
@@ -13,13 +14,16 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
TSTRING_RE = re.compile(r'_l?t\(\s*`.*?\s*`\s*\)', re.DOTALL)
|
||||
EXPRESSION_RE = re.compile(r'\$\{.+?\}')
|
||||
UNDERSCORE_RE = re.compile(r'\b_\(\s*[\'"]')
|
||||
|
||||
class TestJsTranslations(lint_case.LintCase):
|
||||
|
||||
def check_text(self, text):
|
||||
""" Search for translated template strings that contains an expression
|
||||
:param text: The js text to search
|
||||
:return: A list of tuple with line number and invalid template string or an empty list
|
||||
""" Search for translation errors in the text
|
||||
|
||||
:param text: The js text to search
|
||||
:return: A list of tuple with line number and invalid template string,
|
||||
or None for underscore errors
|
||||
"""
|
||||
error_list = list()
|
||||
for m in TSTRING_RE.finditer(text):
|
||||
@@ -27,6 +31,11 @@ class TestJsTranslations(lint_case.LintCase):
|
||||
if EXPRESSION_RE.search(template_string):
|
||||
line_nb = text[:m.start()].count('\n') + 1
|
||||
error_list.append((line_nb, template_string))
|
||||
|
||||
for m in UNDERSCORE_RE.finditer(text):
|
||||
lineno = text[:m.start()].count('\n') + 1
|
||||
error_list.append((lineno, None))
|
||||
|
||||
return error_list
|
||||
|
||||
def test_regular_expression(self):
|
||||
@@ -39,11 +48,10 @@ class TestJsTranslations(lint_case.LintCase):
|
||||
"""
|
||||
error_list = self.check_text(bad_js)
|
||||
self.assertEqual(len(error_list), 1)
|
||||
for line, template_string in error_list:
|
||||
template_string = error_list[0][1]
|
||||
self.assertIn('invalid template-string', template_string)
|
||||
self.assertNotIn('but valid template-string', template_string)
|
||||
self.assertEqual(line, 4)
|
||||
[(line, template_string)] = error_list
|
||||
self.assertEqual(line, 4)
|
||||
self.assertIn('invalid template-string', template_string)
|
||||
self.assertNotIn('but valid template-string', template_string)
|
||||
|
||||
def test_regular_expression_long(self):
|
||||
bad_js = """
|
||||
@@ -54,25 +62,49 @@ class TestJsTranslations(lint_case.LintCase):
|
||||
|
||||
error_list = self.check_text(bad_js)
|
||||
self.assertEqual(len(error_list), 1)
|
||||
for line, template_string in error_list:
|
||||
template_string = error_list[0][1]
|
||||
self.assertIn('foo ${this + is(a, very) - long == expression}', template_string)
|
||||
self.assertEqual(line, 2)
|
||||
[(line, template_string)] = error_list
|
||||
self.assertEqual(line, 2)
|
||||
self.assertIn('foo ${this + is(a, very) - long == expression}', template_string)
|
||||
|
||||
def test_matches_underscore(self):
|
||||
bad_js = """
|
||||
const thing1 = _('literal0');
|
||||
const thing0 = _([]);
|
||||
const thing2 = _("literal1");
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.check_text(bad_js),
|
||||
[(2, None), (4, None)]
|
||||
)
|
||||
|
||||
def test_js_translations(self):
|
||||
""" Test that there are no translation of JS template strings """
|
||||
""" Test that there are no translation of JS template strings or underscore
|
||||
calls misused as translation markers
|
||||
"""
|
||||
|
||||
counter = 0
|
||||
failures = 0
|
||||
for js_file in self.iter_module_files('*.js'):
|
||||
# lodash has string methods and occurrences of `_('text')` in its comments
|
||||
if js_file.endswith('/lodash.js'):
|
||||
continue
|
||||
|
||||
counter += 1
|
||||
with tools.file_open(js_file, 'r') as f:
|
||||
js_txt = f.read()
|
||||
error_list = self.check_text(js_txt)
|
||||
for line_number, template_string in error_list:
|
||||
failures += 1
|
||||
mod, relative_path, _ = get_resource_from_path(js_file)
|
||||
_logger.error("Translation of a template string found in `%s/%s` at line %s: %s", mod, relative_path, line_number, template_string)
|
||||
|
||||
error_list = self.check_text(js_txt)
|
||||
for line_number, template_string in error_list:
|
||||
failures += 1
|
||||
mod, relative_path, _ = get_resource_from_path(js_file)
|
||||
if template_string:
|
||||
prefix = "Translation of a template string"
|
||||
suffix = template_string
|
||||
else:
|
||||
prefix = "underscore.js used as translation function"
|
||||
suffix = "_t and _lt are the JS translation functions"
|
||||
|
||||
_logger.error("%s found in `%s/%s` at line %s: %s", prefix, mod, relative_path, line_number, suffix)
|
||||
|
||||
_logger.info('%s files tested', counter)
|
||||
if failures > 0:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * test_new_api
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0alpha1\n"
|
||||
"Project-Id-Version: Flectra Server 13.0alpha1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-08 06:43+0000\n"
|
||||
"PO-Revision-Date: 2019-07-08 06:43+0000\n"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Flectra Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * test_new_api
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0alpha1\n"
|
||||
"Project-Id-Version: Flectra Server 13.0alpha1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-08 06:43+0000\n"
|
||||
"PO-Revision-Date: 2019-07-08 06:43+0000\n"
|
||||
|
||||
@@ -692,6 +692,40 @@ class ComputeUnassigned(models.Model):
|
||||
record.bares = record.foo
|
||||
|
||||
|
||||
class ComputeOne2many(models.Model):
|
||||
_name = 'test_new_api.one2many'
|
||||
_description = "A computed editable one2many field with a domain"
|
||||
|
||||
name = fields.Char()
|
||||
line_ids = fields.One2many(
|
||||
'test_new_api.one2many.line', 'container_id',
|
||||
compute='_compute_line_ids', store=True, readonly=False,
|
||||
domain=[('count', '>', 0)],
|
||||
)
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_line_ids(self):
|
||||
# increment counter of line with the same name, or create a new line
|
||||
for record in self:
|
||||
if not record.name:
|
||||
continue
|
||||
for line in record.line_ids:
|
||||
if line.name == record.name:
|
||||
line.count += 1
|
||||
break
|
||||
else:
|
||||
record.line_ids = [(0, 0, {'name': record.name})]
|
||||
|
||||
|
||||
class ComputeOne2manyLine(models.Model):
|
||||
_name = 'test_new_api.one2many.line'
|
||||
_description = "Line of a computed one2many"
|
||||
|
||||
name = fields.Char()
|
||||
count = fields.Integer(default=1)
|
||||
container_id = fields.Many2one('test_new_api.one2many', required=True)
|
||||
|
||||
|
||||
class ModelBinary(models.Model):
|
||||
_name = 'test_new_api.model_binary'
|
||||
_description = 'Test Image field'
|
||||
|
||||
@@ -29,6 +29,8 @@ access_test_new_api_compute_onchange,access_test_new_api_compute_onchange,model_
|
||||
access_test_new_api_compute_onchange_line,access_test_new_api_compute_onchange_line,model_test_new_api_compute_onchange_line,,1,1,1,1
|
||||
access_test_new_api_compute_dynamic_depends,access_test_new_api_compute_dynamic_depends,model_test_new_api_compute_dynamic_depends,,1,1,1,1
|
||||
access_test_new_api_compute_unassigned,access_test_new_api_compute_unassigned,model_test_new_api_compute_unassigned,,1,1,1,1
|
||||
access_test_new_api_one2many,access_test_new_api_one2many,model_test_new_api_one2many,,1,1,1,1
|
||||
access_test_new_api_one2many_line,access_test_new_api_one2many_line,model_test_new_api_one2many_line,,1,1,1,1
|
||||
access_test_new_api_binary_svg,access_test_new_api_binary_svg,model_test_new_api_binary_svg,,1,1,1,1
|
||||
access_test_new_api_monetary_base,access_test_new_api_monetary_base,model_test_new_api_monetary_base,,1,1,1,1
|
||||
access_test_new_api_monetary_related,access_test_new_api_monetary_related,model_test_new_api_monetary_related,,1,1,1,1
|
||||
|
||||
|
@@ -823,3 +823,47 @@ class TestComputeOnchange(common.TransactionCase):
|
||||
]
|
||||
result = record.onchange({'line_ids': line_ids}, 'line_ids', spec)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_computed_editable_one2many_domain(self):
|
||||
""" Test a computed, editable one2many field with a domain. """
|
||||
record = self.env['test_new_api.one2many'].create({'name': 'foo'})
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
])
|
||||
|
||||
# trigger recomputation by changing name
|
||||
record.name = 'bar'
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
{'name': 'bar', 'count': 1},
|
||||
])
|
||||
|
||||
# manually adding a line should not trigger recomputation
|
||||
record.line_ids.create({'name': 'baz', 'container_id': record.id})
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
{'name': 'bar', 'count': 1},
|
||||
{'name': 'baz', 'count': 1},
|
||||
])
|
||||
|
||||
# changing the field in the domain should not trigger recomputation...
|
||||
record.line_ids[-1].count = 2
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
{'name': 'bar', 'count': 1},
|
||||
{'name': 'baz', 'count': 2},
|
||||
])
|
||||
|
||||
# ...and may show cache inconsistencies
|
||||
record.line_ids[-1].count = 0
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
{'name': 'bar', 'count': 1},
|
||||
{'name': 'baz', 'count': 0},
|
||||
])
|
||||
record.flush()
|
||||
record.invalidate_cache()
|
||||
self.assertRecordValues(record.line_ids, [
|
||||
{'name': 'foo', 'count': 1},
|
||||
{'name': 'bar', 'count': 1},
|
||||
])
|
||||
@@ -1,8 +1,8 @@
|
||||
# This is a test PO file, not a complete one. It is manually maintained
|
||||
# to test the import translation behavior of Odoo.
|
||||
# to test the import translation behavior of Flectra.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~11.4\n"
|
||||
"Project-Id-Version: Flectra Server saas~11.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-09-18 13:20+0000\n"
|
||||
"PO-Revision-Date: 2018-09-18 13:20+0000\n"
|
||||
|
||||
+1
-1
@@ -3021,7 +3021,7 @@ class _RelationalMulti(_Relational):
|
||||
|
||||
def _setup_regular_full(self, model):
|
||||
super(_RelationalMulti, self)._setup_regular_full(model)
|
||||
if isinstance(self.domain, list):
|
||||
if not self.compute and isinstance(self.domain, list):
|
||||
self.depends = tuple(unique(itertools.chain(self.depends, (
|
||||
self.name + '.' + arg[0]
|
||||
for arg in self.domain
|
||||
|
||||
+1
-1
@@ -781,7 +781,7 @@ class HttpRequest(WebRequest):
|
||||
|
||||
Flectra URLs are CSRF-protected by default (when accessed with unsafe
|
||||
HTTP methods). See
|
||||
https://doc.flectrahq.com/2.0/reference/http.html#csrf for
|
||||
https://doc.flectrahq.com/2.0/developer/reference/http.html#csrf for
|
||||
more details.
|
||||
|
||||
* if this endpoint is accessed through Flectra via py-QWeb form, embed a CSRF
|
||||
|
||||
+4
-4
@@ -911,7 +911,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
if lines2:
|
||||
# merge first line with record's main line
|
||||
for j, val in enumerate(lines2[0]):
|
||||
if val or isinstance(val, bool):
|
||||
if val or isinstance(val, (int, float)):
|
||||
current[j] = val
|
||||
# append the other lines at the end
|
||||
lines += lines2[1:]
|
||||
@@ -5296,6 +5296,9 @@ Fields:
|
||||
result.append(self.browse())
|
||||
else:
|
||||
(key, comparator, value) = d
|
||||
if comparator in ('child_of', 'parent_of'):
|
||||
result.append(self.search([('id', 'in', self.ids), d]))
|
||||
continue
|
||||
if key.endswith('.id'):
|
||||
key = key[:-3]
|
||||
if key == 'id':
|
||||
@@ -5312,9 +5315,6 @@ Fields:
|
||||
records_ids = OrderedSet()
|
||||
for rec in self:
|
||||
data = rec.mapped(key)
|
||||
if comparator in ('child_of', 'parent_of'):
|
||||
value = data.search([(data._parent_name, comparator, value)]).ids
|
||||
comparator = 'in'
|
||||
if isinstance(data, BaseModel):
|
||||
v = value
|
||||
if (isinstance(value, list) or isinstance(value, tuple)) and len(value):
|
||||
|
||||
@@ -88,6 +88,32 @@ class FlectraHook(object):
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
class UpgradeHook(object):
|
||||
"""Makes the legacy `migrations` package being `flectra.upgrade`"""
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if re.match(r"^flectra.addons.base.maintenance.migrations\b", name):
|
||||
# We can't trigger a DeprecationWarning in this case.
|
||||
# In order to be cross-versions, the multi-versions upgrade scripts (0.0.0 scripts),
|
||||
# the tests, and the common files (utility functions) still needs to import from the
|
||||
# legacy name.
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
assert name not in sys.modules
|
||||
|
||||
canonical_upgrade = name.replace("flectra.addons.base.maintenance.migrations", "flectra.upgrade")
|
||||
|
||||
if canonical_upgrade in sys.modules:
|
||||
mod = sys.modules[canonical_upgrade]
|
||||
else:
|
||||
mod = importlib.import_module(canonical_upgrade)
|
||||
|
||||
sys.modules[name] = mod
|
||||
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
def initialize_sys_path():
|
||||
"""
|
||||
Setup the addons path ``flectra.addons.__path__`` with various defaults
|
||||
@@ -126,6 +152,7 @@ def initialize_sys_path():
|
||||
|
||||
# hook deprecated module alias from openerp to flectra and "crm"-like to flectra.addons
|
||||
if not getattr(initialize_sys_path, 'called', False): # only initialize once
|
||||
sys.meta_path.insert(0, UpgradeHook())
|
||||
sys.meta_path.insert(0, FlectraHook())
|
||||
sys.meta_path.insert(0, AddonsHook())
|
||||
initialize_sys_path.called = True
|
||||
|
||||
@@ -137,6 +137,7 @@ def init_logger():
|
||||
'reportlab.lib.rl_safe_eval',# reportlab importing ABC from collections
|
||||
'ofxparse',# ofxparse importing ABC from collections
|
||||
'astroid', # deprecated imp module (fixed in 2.5.1)
|
||||
'requests_toolbelt', # importing ABC from collections (fixed in 0.9)
|
||||
]:
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning, module=module)
|
||||
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user