parent
c39e302253
commit
33b5d4aba8
16
__init__.py
16
__init__.py
|
@ -2,19 +2,21 @@
|
|||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.pool import Pool
|
||||
|
||||
from .party import *
|
||||
from .routing import *
|
||||
from .static_file import *
|
||||
from .currency import *
|
||||
from .template import *
|
||||
from .party import Address, Party, ContactMechanism, NereidUser, Permission, \
|
||||
UserPermission
|
||||
from .routing import URLMap, WebSite, URLRule, URLRuleDefaults, \
|
||||
WebsiteCountry, WebsiteCurrency
|
||||
from .static_file import NereidStaticFolder, NereidStaticFile
|
||||
from .currency import Currency, Language
|
||||
from .template import ContextProcessors
|
||||
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
Address,
|
||||
Party,
|
||||
NereidUser,
|
||||
ContactMechanism,
|
||||
NereidUser,
|
||||
Permission,
|
||||
UserPermission,
|
||||
URLMap,
|
||||
|
@ -25,8 +27,8 @@ def register():
|
|||
WebsiteCurrency,
|
||||
NereidStaticFolder,
|
||||
NereidStaticFile,
|
||||
Language,
|
||||
Currency,
|
||||
ContextProcessors,
|
||||
Language,
|
||||
module='nereid', type_='model'
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ from nereid import request
|
|||
|
||||
__all__ = ['Currency', 'Language']
|
||||
|
||||
|
||||
class Currency(ModelSQL, ModelView):
|
||||
'''Currency Manipulation for core.'''
|
||||
__name__ = 'currency.currency'
|
||||
|
|
1
i18n.py
1
i18n.py
|
@ -70,6 +70,7 @@ def ngettext(singular, plural, n, **variables):
|
|||
return (plural if n > 1 else singular) % variables
|
||||
return t.ungettext(singular, plural, n) % variables
|
||||
|
||||
|
||||
def make_lazy_gettext(lookup_func):
|
||||
"""Creates a lazy gettext function dispatches to a gettext
|
||||
function as returned by `lookup_func`.
|
||||
|
|
110
party.py
110
party.py
|
@ -43,7 +43,7 @@ class RegistrationForm(Form):
|
|||
"""
|
||||
return get_translations()
|
||||
|
||||
name = TextField(_('Name'), [validators.Required(),])
|
||||
name = TextField(_('Name'), [validators.Required(), ])
|
||||
email = TextField(_('e-mail'), [validators.Required(), validators.Email()])
|
||||
password = PasswordField(_('New Password'), [
|
||||
validators.Required(),
|
||||
|
@ -68,12 +68,12 @@ class AddressForm(Form):
|
|||
"""
|
||||
return get_translations()
|
||||
|
||||
name = TextField(_('Name'), [validators.Required(),])
|
||||
street = TextField(_('Street'), [validators.Required(),])
|
||||
name = TextField(_('Name'), [validators.Required(), ])
|
||||
street = TextField(_('Street'), [validators.Required(), ])
|
||||
streetbis = TextField(_('Street (Bis)'))
|
||||
zip = TextField(_('Post Code'), [validators.Required(),])
|
||||
city = TextField(_('City'), [validators.Required(),])
|
||||
country = SelectField(_('Country'), [validators.Required(),], coerce=int)
|
||||
zip = TextField(_('Post Code'), [validators.Required(), ])
|
||||
city = TextField(_('City'), [validators.Required(), ])
|
||||
country = SelectField(_('Country'), [validators.Required(), ], coerce=int)
|
||||
subdivision = IntegerField(_('State/County'), [validators.Required()])
|
||||
email = TextField(_('Email'))
|
||||
phone = TextField(_('Phone'))
|
||||
|
@ -192,7 +192,6 @@ class Address(ModelSQL, ModelView):
|
|||
return render_template('address.jinja')
|
||||
|
||||
|
||||
|
||||
class Party(ModelSQL, ModelView):
|
||||
"Party"
|
||||
__name__ = 'party.party'
|
||||
|
@ -202,14 +201,17 @@ class Party(ModelSQL, ModelView):
|
|||
|
||||
class ProfileForm(Form):
|
||||
"""User Profile Form"""
|
||||
display_name = TextField('Display Name', [validators.Required(),],
|
||||
display_name = TextField(
|
||||
'Display Name', [validators.Required(), ],
|
||||
description="Your display name"
|
||||
)
|
||||
timezone = SelectField('Timezone',
|
||||
choices = [(tz, tz) for tz in pytz.common_timezones],
|
||||
timezone = SelectField(
|
||||
'Timezone',
|
||||
choices=[(tz, tz) for tz in pytz.common_timezones],
|
||||
coerce=unicode, description="Your timezone"
|
||||
)
|
||||
email = TextField('Email', [validators.Required(), validators.Email()],
|
||||
email = TextField(
|
||||
'Email', [validators.Required(), validators.Email()],
|
||||
description="Your Login Email. This Cannot be edited."
|
||||
)
|
||||
|
||||
|
@ -221,8 +223,10 @@ class NereidUser(ModelSQL, ModelView):
|
|||
__name__ = "nereid.user"
|
||||
_rec_name = 'display_name'
|
||||
|
||||
party = fields.Many2One('party.party', 'Party', required=True,
|
||||
ondelete='CASCADE', select=1)
|
||||
party = fields.Many2One(
|
||||
'party.party', 'Party', required=True,
|
||||
ondelete='CASCADE', select=1
|
||||
)
|
||||
|
||||
display_name = fields.Char('Display Name', required=True)
|
||||
|
||||
|
@ -254,8 +258,10 @@ class NereidUser(ModelSQL, ModelView):
|
|||
[(x, x) for x in pytz.common_timezones], 'Timezone', translate=False
|
||||
)
|
||||
|
||||
permissions = fields.Many2Many('nereid.permission-nereid.user',
|
||||
'nereid_user', 'permission', 'Permissions')
|
||||
permissions = fields.Many2Many(
|
||||
'nereid.permission-nereid.user',
|
||||
'nereid_user', 'permission', 'Permissions'
|
||||
)
|
||||
|
||||
def get_permissions(self):
|
||||
"""
|
||||
|
@ -265,19 +271,29 @@ class NereidUser(ModelSQL, ModelView):
|
|||
# everytime.
|
||||
return frozenset([p.value for p in self.permissions])
|
||||
|
||||
def has_permissions(self, permissions):
|
||||
"""Check if the user has required permissions for access
|
||||
def has_permissions(self, perm_all=None, perm_any=None):
|
||||
"""Check if the user has all required permissions in perm_all and
|
||||
has any permission from perm_any for access
|
||||
|
||||
:param permissions: A set/frozenset of permission values/keywords
|
||||
:param perm_all: A set/frozenset of all permission values/keywords.
|
||||
:param perm_any: A set/frozenset of any permission values/keywords.
|
||||
|
||||
:return: True/False
|
||||
"""
|
||||
if not isinstance(permissions, (set, frozenset)):
|
||||
permissions = frozenset(permissions)
|
||||
current_user_permissions = self.get_permissions()
|
||||
if permissions.issubset(current_user_permissions):
|
||||
if not perm_all and not perm_any:
|
||||
# Access allowed if no permission is required
|
||||
return True
|
||||
if not isinstance(perm_all, (set, frozenset)):
|
||||
perm_all = frozenset(perm_all if perm_all else [])
|
||||
if not isinstance(perm_any, (set, frozenset)):
|
||||
perm_any = frozenset(perm_any if perm_any else [])
|
||||
current_user_permissions = self.get_permissions()
|
||||
|
||||
if perm_all and not perm_all.issubset(current_user_permissions):
|
||||
return False
|
||||
if perm_any and not perm_any.intersection(current_user_permissions):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def default_timezone():
|
||||
|
@ -345,9 +361,11 @@ class NereidUser(ModelSQL, ModelView):
|
|||
existing = cls.search([
|
||||
('email', '=', request.form['email']),
|
||||
('company', '=', request.nereid_website.company.id),
|
||||
])
|
||||
]
|
||||
)
|
||||
if existing:
|
||||
flash(_('A registration already exists with this email. '
|
||||
flash(_(
|
||||
'A registration already exists with this email. '
|
||||
'Please contact customer care')
|
||||
)
|
||||
else:
|
||||
|
@ -359,7 +377,8 @@ class NereidUser(ModelSQL, ModelView):
|
|||
'email': registration_form.email.data,
|
||||
'password': registration_form.password.data,
|
||||
'company': request.nereid_website.company.id,
|
||||
})
|
||||
}
|
||||
)
|
||||
nereid_user.save()
|
||||
nereid_user.create_act_code()
|
||||
registration.send(nereid_user)
|
||||
|
@ -381,9 +400,9 @@ class NereidUser(ModelSQL, ModelView):
|
|||
"""
|
||||
email_message = render_email(
|
||||
CONFIG['smtp_from'], self.email, _('Account Activation'),
|
||||
text_template = 'emails/activation-text.jinja',
|
||||
html_template = 'emails/activation-html.jinja',
|
||||
nereid_user = self
|
||||
text_template='emails/activation-text.jinja',
|
||||
html_template='emails/activation-html.jinja',
|
||||
nereid_user=self
|
||||
)
|
||||
server = get_smtp_server()
|
||||
server.sendmail(
|
||||
|
@ -451,9 +470,9 @@ class NereidUser(ModelSQL, ModelView):
|
|||
{'password': form.password.data}
|
||||
)
|
||||
session.pop('allow_new_password')
|
||||
flash(_('Your password has been successfully changed! '
|
||||
'Please login again')
|
||||
)
|
||||
flash(_(
|
||||
'Your password has been successfully changed! '
|
||||
'Please login again'))
|
||||
session.pop('user')
|
||||
return redirect(url_for('nereid.website.login'))
|
||||
|
||||
|
@ -513,10 +532,12 @@ class NereidUser(ModelSQL, ModelView):
|
|||
the link, he can change his password.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
user_ids = cls.search([
|
||||
user_ids = cls.search(
|
||||
[
|
||||
('email', '=', request.form['email']),
|
||||
('company', '=', request.nereid_website.company.id),
|
||||
])
|
||||
]
|
||||
)
|
||||
|
||||
if not user_ids or not request.form['email']:
|
||||
flash(_('Invalid email address'))
|
||||
|
@ -540,9 +561,9 @@ class NereidUser(ModelSQL, ModelView):
|
|||
"""
|
||||
email_message = render_email(
|
||||
CONFIG['smtp_from'], self.email, _('Account Password Reset'),
|
||||
text_template = 'emails/reset-text.jinja',
|
||||
html_template = 'emails/reset-html.jinja',
|
||||
nereid_user = self
|
||||
text_template='emails/reset-text.jinja',
|
||||
html_template='emails/reset-html.jinja',
|
||||
nereid_user=self
|
||||
)
|
||||
server = get_smtp_server()
|
||||
server.sendmail(
|
||||
|
@ -804,14 +825,14 @@ class ContactMechanism(ModelSQL, ModelView):
|
|||
return redirect(request.referrer)
|
||||
|
||||
|
||||
|
||||
class Permission(ModelSQL, ModelView):
|
||||
"Nereid Permissions"
|
||||
__name__ = 'nereid.permission'
|
||||
|
||||
name = fields.Char('Name', required=True, select=True)
|
||||
value = fields.Char('Value', required=True, select=True)
|
||||
nereid_users = fields.Many2Many('nereid.permission-nereid.user',
|
||||
nereid_users = fields.Many2Many(
|
||||
'nereid.permission-nereid.user',
|
||||
'permission', 'nereid_user', 'Nereid Users'
|
||||
)
|
||||
|
||||
|
@ -824,12 +845,15 @@ class Permission(ModelSQL, ModelView):
|
|||
]
|
||||
|
||||
|
||||
|
||||
class UserPermission(ModelSQL):
|
||||
"Nereid User Permissions"
|
||||
__name__ = 'nereid.permission-nereid.user'
|
||||
|
||||
permission = fields.Many2One('nereid.permission', 'Permission',
|
||||
ondelete='CASCADE', select=True, required=True)
|
||||
nereid_user = fields.Many2One('nereid.user', 'User',
|
||||
ondelete='CASCADE', select=True, required=True)
|
||||
permission = fields.Many2One(
|
||||
'nereid.permission', 'Permission',
|
||||
ondelete='CASCADE', select=True, required=True
|
||||
)
|
||||
nereid_user = fields.Many2One(
|
||||
'nereid.user', 'User',
|
||||
ondelete='CASCADE', select=True, required=True
|
||||
)
|
||||
|
|
32
routing.py
32
routing.py
|
@ -1,6 +1,5 @@
|
|||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from ast import literal_eval
|
||||
|
||||
import pytz
|
||||
from werkzeug import abort, redirect
|
||||
|
@ -11,7 +10,6 @@ from nereid.globals import session, request
|
|||
from nereid.helpers import login_required, key_from_list, get_flashed_messages
|
||||
from nereid.signals import login, failed_login, logout
|
||||
from trytond.model import ModelView, ModelSQL, fields
|
||||
from trytond.backend import TableHandler
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
@ -78,7 +76,7 @@ class URLMap(ModelSQL, ModelView):
|
|||
for URL Rule construction. A wrapper around the
|
||||
URL RULE get_rule_arguments
|
||||
"""
|
||||
rule_args = [ ]
|
||||
rule_args = []
|
||||
for rule in self.rules:
|
||||
rule_args.append(rule.get_rule_arguments())
|
||||
return rule_args
|
||||
|
@ -135,8 +133,10 @@ class WebSite(ModelSQL, ModelView):
|
|||
'website', 'currency', 'Currencies Available')
|
||||
|
||||
#: Default language
|
||||
default_language = fields.Many2One('ir.lang', 'Default Language',
|
||||
required=True)
|
||||
default_language = fields.Many2One(
|
||||
'ir.lang', 'Default Language',
|
||||
required=True
|
||||
)
|
||||
|
||||
#: The res.user with which the nereid application will be loaded
|
||||
#: .. versionadded: 0.3
|
||||
|
@ -172,8 +172,8 @@ class WebSite(ModelSQL, ModelView):
|
|||
"""
|
||||
Return the list of countries in JSON
|
||||
"""
|
||||
return jsonify(result = [
|
||||
{'key': c.id, 'value': c.name} \
|
||||
return jsonify(result=[
|
||||
{'key': c.id, 'value': c.name}
|
||||
for c in request.nereid_website.countries
|
||||
])
|
||||
|
||||
|
@ -189,7 +189,7 @@ class WebSite(ModelSQL, ModelView):
|
|||
Subdivision = Pool().get('country.subdivision')
|
||||
subdivisions = Subdivision.search([('country', '=', country)])
|
||||
return jsonify(
|
||||
result = [{
|
||||
result=[{
|
||||
'id': s.id,
|
||||
'name': s.name,
|
||||
'code': s.code,
|
||||
|
@ -285,8 +285,8 @@ class WebSite(ModelSQL, ModelView):
|
|||
data into the context
|
||||
"""
|
||||
return dict(
|
||||
user = request.nereid_user,
|
||||
party = request.nereid_user.party,
|
||||
user=request.nereid_user,
|
||||
party=request.nereid_user.party,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -314,8 +314,9 @@ class WebSite(ModelSQL, ModelView):
|
|||
'id': c.id,
|
||||
'name': c.name,
|
||||
'symbol': c.symbol,
|
||||
} for c in self.currencies]
|
||||
cache.set(cache_key, rv, 60*60)
|
||||
} for c in self.currencies
|
||||
]
|
||||
cache.set(cache_key, rv, 60 * 60)
|
||||
return rv
|
||||
|
||||
@staticmethod
|
||||
|
@ -346,7 +347,6 @@ class WebSite(ModelSQL, ModelView):
|
|||
return jsonify(status=cls._user_status())
|
||||
|
||||
|
||||
|
||||
class URLRule(ModelSQL, ModelView):
|
||||
"""
|
||||
URL Rule
|
||||
|
@ -472,8 +472,10 @@ class URLRuleDefaults(ModelSQL, ModelView):
|
|||
|
||||
key = fields.Char('Key', required=True, select=True)
|
||||
value = fields.Char('Value', required=True, select=True)
|
||||
rule = fields.Many2One('nereid.url_rule', 'Rule', required=True,
|
||||
select=True)
|
||||
rule = fields.Many2One(
|
||||
'nereid.url_rule', 'Rule', required=True,
|
||||
select=True
|
||||
)
|
||||
|
||||
|
||||
class WebsiteCountry(ModelSQL):
|
||||
|
|
|
@ -91,8 +91,9 @@ class NereidStaticFile(ModelSQL, ModelView):
|
|||
], 'File Type')
|
||||
|
||||
#: URL of the remote file if the :attr:`type` is remote
|
||||
remote_path = fields.Char('Remote File', select=True, translate=True,
|
||||
states = {
|
||||
remote_path = fields.Char(
|
||||
'Remote File', select=True, translate=True,
|
||||
states={
|
||||
'required': Equal(Eval('type'), 'remote'),
|
||||
'invisible': Not(Equal(Eval('type'), 'remote'))
|
||||
}
|
||||
|
|
14
template.py
14
template.py
|
@ -11,11 +11,15 @@ class ContextProcessors(ModelSQL, ModelView):
|
|||
__name__ = 'nereid.template.context_processor'
|
||||
_rec_name = 'method'
|
||||
|
||||
method = fields.Char('Method', required=True,
|
||||
help="Context processor method in <model>.<method>")
|
||||
model = fields.Char('Model',
|
||||
method = fields.Char(
|
||||
'Method', required=True,
|
||||
help="Context processor method in <model>.<method>"
|
||||
)
|
||||
model = fields.Char(
|
||||
'Model',
|
||||
help="This will restrict the loading when URLs with"
|
||||
" the model are called")
|
||||
" the model are called"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_processors(cls):
|
||||
|
@ -23,7 +27,7 @@ class ContextProcessors(ModelSQL, ModelView):
|
|||
Return the list of processors. Separate function
|
||||
since its important to have caching on this
|
||||
"""
|
||||
result = { }
|
||||
result = {}
|
||||
ctx_processors = cls.search([])
|
||||
for ctx_proc in ctx_processors:
|
||||
model, method = ctx_proc.method.rsplit('.', 1)
|
||||
|
|
|
@ -36,7 +36,6 @@ class TestNereid(unittest.TestCase):
|
|||
test_depends()
|
||||
|
||||
|
||||
|
||||
def suite():
|
||||
test_suite = trytond.tests.test_tryton.suite()
|
||||
test_suite.addTests(
|
||||
|
|
|
@ -166,8 +166,7 @@ class TestAddress(NereidTestCase):
|
|||
'phone': '1234567890',
|
||||
'country': self.available_countries[0].id,
|
||||
'subdivision': self.country_obj(
|
||||
self.available_countries[0]
|
||||
).subdivisions[0].id,
|
||||
self.available_countries[0]).subdivisions[0].id,
|
||||
}
|
||||
|
||||
with app.test_client() as c:
|
||||
|
|
|
@ -306,7 +306,8 @@ class TestAuth(NereidTestCase):
|
|||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(
|
||||
"The current password you entered is invalid" in response.data
|
||||
"The current password you entered is invalid"
|
||||
in response.data
|
||||
)
|
||||
|
||||
response = c.post('/en_US/change-password', data={
|
||||
|
@ -318,7 +319,8 @@ class TestAuth(NereidTestCase):
|
|||
response = c.get('/en_US')
|
||||
|
||||
# Login now using new password
|
||||
response = c.post('/en_US/login',
|
||||
response = c.post(
|
||||
'/en_US/login',
|
||||
data={
|
||||
'email': data['email'],
|
||||
'password': 'new-password'
|
||||
|
@ -397,7 +399,8 @@ class TestAuth(NereidTestCase):
|
|||
regd_user = self.nereid_user_obj(regd_user.id)
|
||||
self.assertFalse(regd_user.activation_code)
|
||||
|
||||
response = c.post('/en_US/login',
|
||||
response = c.post(
|
||||
'/en_US/login',
|
||||
data={
|
||||
'email': data['email'],
|
||||
'password': 'wrong-password'
|
||||
|
@ -405,7 +408,8 @@ class TestAuth(NereidTestCase):
|
|||
)
|
||||
self.assertEqual(response.status_code, 200) # Login rejected
|
||||
|
||||
response = c.post('/en_US/login',
|
||||
response = c.post(
|
||||
'/en_US/login',
|
||||
data={
|
||||
'email': data['email'],
|
||||
'password': 'reset-password'
|
||||
|
@ -509,6 +513,7 @@ class TestAuth(NereidTestCase):
|
|||
self.nereid_user_obj.write(
|
||||
[self.guest_user], {'permissions': [('set', [perm_admin])]}
|
||||
)
|
||||
|
||||
@permissions_required(['admin'])
|
||||
def test_permission_2():
|
||||
return True
|
||||
|
@ -594,6 +599,67 @@ class TestAuth(NereidTestCase):
|
|||
response = c.get('/en_US/me')
|
||||
self.assertEqual(response.data, 'Regd User')
|
||||
|
||||
def test_0100_has_permission(self):
|
||||
'''
|
||||
Test the functionality of has_permissions
|
||||
'''
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT):
|
||||
self.setup_defaults()
|
||||
p1, p2, p3, p4 = self.nereid_permission_obj.create([
|
||||
{'name': 'p1', 'value': 'nereid.perm1'},
|
||||
{'name': 'p2', 'value': 'nereid.perm2'},
|
||||
{'name': 'p3', 'value': 'nereid.perm3'},
|
||||
{'name': 'p4', 'value': 'nereid.perm4'},
|
||||
])
|
||||
self.nereid_user_obj.write(
|
||||
[self.guest_user],
|
||||
{
|
||||
'permissions': [
|
||||
('add', [p1, p2])
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# all = [], any = [] = True
|
||||
self.assertTrue(self.guest_user.has_permissions())
|
||||
|
||||
# all = [p1, p2], any = [] == True
|
||||
self.assertTrue(self.guest_user.has_permissions(
|
||||
perm_all=[p1.value, p2.value]
|
||||
))
|
||||
|
||||
# all = [p1, p2], any = [p3, p4] == False
|
||||
self.assertFalse(self.guest_user.has_permissions(
|
||||
perm_all=[p1.value, p2.value],
|
||||
perm_any=[p3.value, p4.value]
|
||||
))
|
||||
|
||||
# all = [p1, p3], any = [] == False
|
||||
self.assertFalse(self.guest_user.has_permissions(
|
||||
perm_all=[p1.value, p3.value],
|
||||
))
|
||||
|
||||
# all = [p1, p3], any = [p1, p3, p4] == False
|
||||
self.assertFalse(self.guest_user.has_permissions(
|
||||
perm_all=[p1.value, p3.value],
|
||||
perm_any=[p1.value, p3.value, p4.value]
|
||||
))
|
||||
|
||||
# all = [p1, p2], any = [p1, p3, p4] == True
|
||||
self.assertTrue(self.guest_user.has_permissions(
|
||||
perm_all=[p1.value, p2.value],
|
||||
perm_any=[p1.value, p3.value, p4.value]
|
||||
))
|
||||
|
||||
# all = [], any = [p1, p2, p3] == True
|
||||
self.assertTrue(self.guest_user.has_permissions(
|
||||
perm_any=[p1.value, p2.value, p3.value]
|
||||
))
|
||||
|
||||
# all = [], any = [p3, p4] == False
|
||||
self.assertFalse(self.guest_user.has_permissions(
|
||||
perm_any=[p3.value, p4.value]
|
||||
))
|
||||
|
||||
|
||||
def suite():
|
||||
|
|
|
@ -121,7 +121,6 @@ class TestCurrency(NereidTestCase):
|
|||
self.currency_obj.convert(Decimal('100')), Decimal('100')
|
||||
)
|
||||
|
||||
|
||||
def test_0020_currency_from_language(self):
|
||||
"""
|
||||
Set the currency for the language and check if the currency
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[tryton]
|
||||
version=2.8.0.2
|
||||
version=2.8.0.5
|
||||
depends:
|
||||
ir
|
||||
res
|
||||
|
|
Loading…
Reference in New Issue