Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

108
ir/__init__.py Executable file
View File

@@ -0,0 +1,108 @@
# 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 trytond.pool import Pool
from . import (
action, attachment, avatar, cache, calendar_, configuration, cron, date,
email_, error, export, lang, message, model, module, note, queue_, routes,
rule, sequence, session, translation, trigger, ui)
__all__ = ['register', 'routes']
def register():
Pool.register(
model.ModelField, # register first for model char migration
configuration.Configuration,
translation.Translation,
translation.TranslationSetStart,
translation.TranslationSetSucceed,
translation.TranslationCleanStart,
translation.TranslationCleanSucceed,
translation.TranslationUpdateStart,
translation.TranslationExportStart,
translation.TranslationExportResult,
sequence.SequenceType,
sequence.Sequence,
sequence.SequenceStrict,
ui.menu.UIMenu,
ui.menu.UIMenuFavorite,
ui.view.View,
ui.view.ShowViewStart,
ui.view.ViewTreeWidth,
ui.view.ViewTreeOptional,
ui.view.ViewTreeState,
ui.view.ViewSearch,
ui.icon.Icon,
action.Action,
action.ActionKeyword,
action.ActionReport,
action.ActionActWindow,
action.ActionActWindowView,
action.ActionActWindowDomain,
action.ActionWizard,
action.ActionURL,
model.Model,
model.ModelAccess,
model.ModelFieldAccess,
model.ModelButton,
model.ModelButtonRule,
model.ModelButtonClick,
model.ModelButtonReset,
model.ModelData,
model.Log,
model.PrintModelGraphStart,
attachment.Attachment,
note.Note,
note.NoteRead,
avatar.Avatar,
avatar.AvatarCache,
cron.Cron,
lang.Lang,
lang.LangConfigStart,
export.Export,
export.ExportLine,
rule.RuleGroup,
rule.Rule,
module.Module,
module.ModuleDependency,
module.ModuleConfigWizardItem,
module.ModuleConfigWizardFirst,
module.ModuleConfigWizardOther,
module.ModuleConfigWizardDone,
module.ModuleActivateUpgradeStart,
module.ModuleActivateUpgradeDone,
module.ModuleConfigStart,
cache.Cache,
date.Date,
trigger.Trigger,
trigger.TriggerLog,
session.Session,
session.SessionWizard,
queue_.Queue,
calendar_.Month,
calendar_.Day,
message.Message,
email_.Email,
email_.EmailAddress,
email_.EmailTemplate,
email_.EmailTemplate_Report,
error.Error,
module='ir', type_='model')
Pool.register(
translation.TranslationSet,
translation.TranslationClean,
translation.TranslationUpdate,
translation.TranslationExport,
translation.TranslationReport,
ui.view.ShowView,
model.PrintModelGraph,
module.ModuleConfigWizard,
module.ModuleActivateUpgrade,
module.ModuleConfig,
lang.LangConfig,
module='ir', type_='wizard')
Pool.register(
model.ModelGraph,
model.ModelWorkflowGraph,
module='ir', type_='report')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1129
ir/action.py Executable file

File diff suppressed because it is too large Load Diff

237
ir/action.xml Executable file
View File

@@ -0,0 +1,237 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<menuitem
name="Actions"
parent="menu_ui"
sequence="50"
id="menu_action"/>
<record model="ir.ui.view" id="action_view_form">
<field name="model">ir.action</field>
<field name="type">form</field>
<field name="name">action_form</field>
</record>
<record model="ir.ui.view" id="action_view_tree">
<field name="model">ir.action</field>
<field name="type">tree</field>
<field name="name">action_list</field>
</record>
<record model="ir.action.act_window" id="act_action_form">
<field name="name">Actions</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.action</field>
</record>
<record model="ir.action.act_window.view"
id="act_action_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="action_view_tree"/>
<field name="act_window" ref="act_action_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_action_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="action_view_form"/>
<field name="act_window" ref="act_action_form"/>
</record>
<menuitem
parent="menu_action"
action="act_action_form"
sequence="10"
id="menu_act_action"/>
<record model="ir.ui.view" id="action_keyword_view_list">
<field name="model">ir.action.keyword</field>
<field name="type">tree</field>
<field name="name">action_keyword_list</field>
</record>
<record model="ir.ui.view" id="action_keyword_view_form">
<field name="model">ir.action.keyword</field>
<field name="type">form</field>
<field name="name">action_keyword_form</field>
</record>
<record model="ir.ui.view" id="action_report_view_form">
<field name="model">ir.action.report</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_form"/>
<field name="name">action_report_form</field>
</record>
<record model="ir.ui.view" id="action_report_view_tree">
<field name="model">ir.action.report</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_tree"/>
<field name="name">action_report_list</field>
</record>
<record model="ir.action.act_window" id="act_action_report_form">
<field name="name">Reports</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.action.report</field>
</record>
<record model="ir.action.act_window.view"
id="act_action_report_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="action_report_view_tree"/>
<field name="act_window" ref="act_action_report_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_action_report_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="action_report_view_form"/>
<field name="act_window" ref="act_action_report_form"/>
</record>
<menuitem
parent="menu_action"
action="act_action_report_form"
sequence="20"
id="menu_action_report_form"
icon="tryton-list"/>
<record model="ir.ui.view" id="action_act_window_view_form">
<field name="model">ir.action.act_window</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_form"/>
<field name="name">action_act_window_form</field>
</record>
<record model="ir.ui.view" id="action_act_window_view_tree">
<field name="model">ir.action.act_window</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_tree"/>
<field name="name">action_act_window_list</field>
</record>
<record model="ir.action.act_window" id="act_action_act_window_form">
<field name="name">Window Actions</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.action.act_window</field>
</record>
<record model="ir.action.act_window.view" id="act_action_act_window_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="action_act_window_view_tree"/>
<field name="act_window" ref="act_action_act_window_form"/>
</record>
<record model="ir.action.act_window.view" id="act_action_act_window_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="action_act_window_view_form"/>
<field name="act_window" ref="act_action_act_window_form"/>
</record>
<menuitem
parent="menu_action"
action="act_action_act_window_form"
sequence="20"
id="menu_action_act_window"
icon="tryton-list"/>
<record model="ir.ui.view" id="act_window_view_view_form">
<field name="model">ir.action.act_window.view</field>
<field name="type">form</field>
<field name="name">action_act_window_view_form</field>
</record>
<record model="ir.ui.view" id="act_window_view_view_list">
<field name="model">ir.action.act_window.view</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">action_act_window_view_list</field>
</record>
<record model="ir.ui.view" id="act_window_view_view_list2">
<field name="model">ir.action.act_window.view</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">action_act_window_view_list2</field>
</record>
<record model="ir.ui.view" id="act_window_domain_view_form">
<field name="model">ir.action.act_window.domain</field>
<field name="type">form</field>
<field name="name">action_act_window_domain_form</field>
</record>
<record model="ir.ui.view" id="act_window_domain_view_list">
<field name="model">ir.action.act_window.domain</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">action_act_window_domain_list</field>
</record>
<record model="ir.ui.view" id="act_window_domain_view_list2">
<field name="model">ir.action.act_window.domain</field>
<field name="type">tree</field>
<field name="priority" eval="20"/>
<field name="name">action_act_window_domain_list2</field>
</record>
<record model="ir.ui.view" id="action_wizard_view_form">
<field name="model">ir.action.wizard</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_form"/>
<field name="name">action_wizard_form</field>
</record>
<record model="ir.ui.view" id="action_wizard_view_tree">
<field name="model">ir.action.wizard</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_tree"/>
<field name="name">action_wizard_list</field>
</record>
<record model="ir.action.act_window" id="act_action_wizard_form">
<field name="name">Wizards</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.action.wizard</field>
</record>
<record model="ir.action.act_window.view"
id="act_action_wizard_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="action_wizard_view_tree"/>
<field name="act_window" ref="act_action_wizard_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_action_wizard_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="action_wizard_view_form"/>
<field name="act_window" ref="act_action_wizard_form"/>
</record>
<menuitem
parent="menu_action"
action="act_action_wizard_form"
id="menu_action_wizard"
icon="tryton-list"/>
<record model="ir.ui.view" id="action_url_view_form">
<field name="model">ir.action.url</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_form"/>
<field name="name">action_url_form</field>
</record>
<record model="ir.ui.view" id="action_url_view_tree">
<field name="model">ir.action.url</field>
<field name="type" eval="None"/>
<field name="inherit" ref="action_view_tree"/>
<field name="name">action_url_list</field>
</record>
<record model="ir.action.act_window" id="act_action_url_form">
<field name="name">URLs</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.action.url</field>
</record>
<record model="ir.action.act_window.view"
id="act_action_url_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="action_url_view_tree"/>
<field name="act_window" ref="act_action_url_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_action_url_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="action_url_view_form"/>
<field name="act_window" ref="act_action_url_form"/>
</record>
<menuitem
parent="menu_action"
action="act_action_url_form"
sequence="20"
id="menu_action_url"
icon="tryton-list"/>
</data>
</tryton>

86
ir/attachment.py Executable file
View File

@@ -0,0 +1,86 @@
# 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 trytond.config import config
from trytond.i18n import lazy_gettext
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.tools import firstline
from trytond.transaction import Transaction
from .resource import ResourceMixin, resource_copy
__all__ = ['AttachmentCopyMixin']
if config.getboolean('attachment', 'filestore', default=True):
file_id = 'file_id'
store_prefix = config.get('attachment', 'store_prefix', default=None)
else:
file_id = None
store_prefix = None
class Attachment(ResourceMixin, ModelSQL, ModelView):
"Attachment"
__name__ = 'ir.attachment'
name = fields.Char('Name', required=True)
type = fields.Selection([
('data', 'Data'),
('link', 'Link'),
], 'Type', required=True)
description = fields.Text('Description')
summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary')
link = fields.Char('Link', states={
'invisible': Eval('type') != 'link',
}, depends=['type'])
data = fields.Binary('Data', filename='name',
file_id=file_id, store_prefix=store_prefix,
states={
'invisible': Eval('type') != 'data',
}, depends=['type'])
file_id = fields.Char('File ID', readonly=True)
data_size = fields.Function(fields.Integer('Data size', states={
'invisible': Eval('type') != 'data',
}, depends=['type']), 'get_size')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [
('create_date', 'DESC'),
('id', 'DESC'),
]
@staticmethod
def default_type():
return 'data'
def get_size(self, name):
with Transaction().set_context({
'%s.%s' % (self.__name__, name[:-len('_size')]): 'size',
}):
record = self.__class__(self.id)
return record.data
@fields.depends('description')
def on_change_with_summary(self, name=None):
return firstline(self.description or '')
@classmethod
def fields_view_get(cls, view_id=None, view_type='form', level=None):
pool = Pool()
ModelData = pool.get('ir.model.data')
if not view_id:
if Transaction().context.get('preview'):
view_id = ModelData.get_id(
'ir', 'attachment_view_form_preview')
return super().fields_view_get(
view_id=view_id, view_type=view_type, level=level)
class AttachmentCopyMixin(
resource_copy(
'ir.attachment', 'attachments',
lazy_gettext('ir.msg_attachments'))):
pass

45
ir/attachment.xml Executable file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="attachment_view_form">
<field name="model">ir.attachment</field>
<field name="type">form</field>
<field name="name">attachment_form</field>
</record>
<record model="ir.ui.view" id="attachment_view_form_preview">
<field name="model">ir.attachment</field>
<field name="type">form</field>
<field name="priority" eval="50"/>
<field name="name">attachment_form_preview</field>
</record>
<record model="ir.ui.view" id="attachment_view_tree">
<field name="model">ir.attachment</field>
<field name="type">tree</field>
<field name="name">attachment_list</field>
</record>
<record model="ir.action.act_window" id="act_attachment_form">
<field name="name">Attachments</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.attachment</field>
</record>
<record model="ir.action.act_window.view"
id="act_attachment_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="attachment_view_tree"/>
<field name="act_window" ref="act_attachment_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_attachment_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="attachment_view_form"/>
<field name="act_window" ref="act_attachment_form"/>
</record>
<menuitem
parent="ir.menu_models"
action="act_attachment_form"
sequence="50"
id="menu_attachment_form"/>
</data>
</tryton>

214
ir/avatar.py Executable file
View File

@@ -0,0 +1,214 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import io
import math
import os
import uuid
from random import Random
from urllib.parse import quote, urljoin
try:
import PIL
from PIL import Image, ImageDraw, ImageFont
except ImportError:
PIL = None
from trytond.config import config
from trytond.model import ModelSQL, Unique, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.wsgi import Base64Converter
from .resource import ResourceMixin
if config.getboolean('database', 'avatar_filestore', default=False):
file_id = 'image_id'
store_prefix = config.get('database', 'avatar_prefix', default=None)
else:
file_id = None
store_prefix = None
URL_BASE = config.get('web', 'avatar_base', default='')
FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'karla.ttf')
class ImageMixin:
__slots__ = ()
image = fields.Binary(
"Image", file_id=file_id, store_prefix=store_prefix)
image_id = fields.Char("Image ID", readonly=True)
class Avatar(ImageMixin, ResourceMixin, ModelSQL):
"Avatar"
__name__ = 'ir.avatar'
uuid = fields.Char("UUID", required=True)
cache = fields.One2Many('ir.avatar.cache', 'avatar', "Cache")
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints = [
('resource_unique', Unique(t, t.resource),
'ir.msg_avatar_resource_unique'),
]
@classmethod
def default_uuid(cls):
return uuid.uuid4().hex
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
values.setdefault('uuid', cls.default_uuid())
return super().create(vlist)
@classmethod
def write(cls, *args):
avatars = sum(args[0:None:2], [])
super().write(*args)
cls.clear_cache(avatars)
@classmethod
def copy(cls, avatars, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('uuid', None)
default.setdefault('cache', None)
return super().copy(avatars, default=default)
@property
def url(self):
if self.image_id or self.image:
return urljoin(
URL_BASE, quote('/avatar/%(database)s/%(uuid)s' % {
'database': Base64Converter(None).to_url(
Transaction().database.name),
'uuid': self.uuid,
}))
def get(self, size=64):
size = min((
2 ** math.ceil(math.log2(size)),
10 * math.ceil(size / 10) if size <= 100
else 50 * math.ceil(size / 50)))
if not (0 < size <= 2048):
raise ValueError("Invalid size")
for avatar in self.cache:
if avatar.size == size:
return avatar.image
if not self.image:
return None
if PIL:
with Transaction().new_transaction():
cache = self._store_cache(size, self._resize(size))
# Save cache only if record is already committed
if self.__class__.search([('id', '=', self.id)]):
cache.save()
return cache.image
else:
return self.image
@classmethod
def convert(cls, image, **_params):
if not PIL or not image:
return image
data = io.BytesIO()
img = Image.open(io.BytesIO(image))
width, height = img.size
size = min(width, height)
img = img.crop((
(width - size) // 2,
(height - size) // 2,
(width + size) // 2,
(height + size) // 2))
if size > 2048:
img = img.resize((2048, 2048))
if img.mode in {'RGBA', 'P'}:
img.convert('RGBA')
background = Image.new('RGBA', img.size, (255, 255, 255))
background.alpha_composite(img)
img = background.convert('RGB')
img.save(data, format='jpeg', optimize=True, **_params)
return data.getvalue()
def _resize(self, size=64, **_params):
if not PIL:
return self.image
data = io.BytesIO()
img = Image.open(io.BytesIO(self.image))
img = img.resize((size, size))
img.save(data, format='jpeg', optimize=True, **_params)
return data.getvalue()
def _store_cache(self, size, image):
pool = Pool()
Cache = pool.get('ir.avatar.cache')
return Cache(
avatar=self,
image=image,
size=size)
@classmethod
def clear_cache(cls, avatars):
pool = Pool()
Cache = pool.get('ir.avatar.cache')
caches = [c for a in avatars for c in a.cache]
Cache.delete(caches)
class AvatarCache(ImageMixin, ModelSQL):
"Avatar Cache"
__name__ = 'ir.avatar.cache'
avatar = fields.Many2One(
'ir.avatar', "Avatar", required=True, ondelete='CASCADE')
size = fields.Integer(
"Size", required=True,
domain=[
('size', '>', 0),
('size', '<=', 2048),
])
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints = [
('size_unique', Unique(t, t.avatar, t.size),
'ir.msg_avatar_size_unique'),
]
cls._order.append(('size', 'ASC'))
def generate(size, string):
if not PIL:
return
def background_color(string):
random = Random(string)
r = v = b = 255
# Skip too bright color
while r + v + b > 255 * 2:
r = random.randint(0, 255)
v = random.randint(0, 255)
b = random.randint(0, 255)
return r, v, b
try:
font = ImageFont.truetype(FONT, size=int(0.65 * size))
except ImportError:
return
white = (255, 255, 255)
image = Image.new('RGB', (size, size), background_color(string))
draw = ImageDraw.Draw(image)
letter = string[0].upper() if string else ''
draw.text(
(size / 2, size / 2), letter, fill=white, font=font, anchor='mm')
data = io.BytesIO()
image.save(data, format='jpeg', optimize=True)
return data.getvalue()

10
ir/cache.py Executable file
View File

@@ -0,0 +1,10 @@
# 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 trytond.model import ModelSQL, fields
class Cache(ModelSQL):
"Cache"
__name__ = 'ir.cache'
name = fields.Char('Name', required=True)
timestamp = fields.Timestamp("Timestamp")

67
ir/calendar_.py Executable file
View File

@@ -0,0 +1,67 @@
# 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 trytond.cache import Cache
from trytond.model import ModelSQL, Unique, fields
from trytond.rpc import RPC
from trytond.transaction import Transaction
class _Calendar(ModelSQL):
_order_name = 'index'
index = fields.Integer("Index", required=True)
name = fields.Char("Name", required=True, translate=True)
abbreviation = fields.Char("Abbreviation", required=True, translate=True)
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls.__rpc__.update({
'read': RPC(),
'search': RPC(),
'search_count': RPC(),
'search_read': RPC(),
})
cls.index.domain = [
('index', '>=', cls._min_index),
('index', '<=', cls._max_index),
]
cls._order = [('index', 'ASC')]
cls._sql_constraints = [
('index_unique', Unique(t, t.index),
"The index must by unique.")
]
@classmethod
def locale(cls, language=None, field='name'):
transaction = Transaction()
if language is None:
language = transaction.language
elif isinstance(language, ModelSQL):
language = language.code
key = (language, field)
result = cls._cache_locale.get(key)
if not result:
with transaction.set_context(language=language):
records = cls.search([])
result = [None] * cls._min_index
result += [getattr(r, field) for r in records]
cls._cache_locale.set(key, result)
return result
class Month(_Calendar):
"Month"
__name__ = 'ir.calendar.month'
_min_index = 1
_max_index = 12
_cache_locale = Cache('ir.calendar.month')
class Day(_Calendar):
"Day"
__name__ = 'ir.calendar.day'
_min_index = 0
_max_index = 6
_cache_locale = Cache('ir.calendar.day')

103
ir/calendar_.xml Executable file
View File

@@ -0,0 +1,103 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.calendar.month" id="January">
<field name="index" eval="1"/>
<field name="name">January</field>
<field name="abbreviation">Jan</field>
</record>
<record model="ir.calendar.month" id="February">
<field name="index" eval="2"/>
<field name="name">February</field>
<field name="abbreviation">Feb</field>
</record>
<record model="ir.calendar.month" id="March">
<field name="index" eval="3"/>
<field name="name">March</field>
<field name="abbreviation">Mar</field>
</record>
<record model="ir.calendar.month" id="April">
<field name="index" eval="4"/>
<field name="name">April</field>
<field name="abbreviation">Apr</field>
</record>
<record model="ir.calendar.month" id="May">
<field name="index" eval="5"/>
<field name="name">May</field>
<field name="abbreviation">May</field>
</record>
<record model="ir.calendar.month" id="June">
<field name="index" eval="6"/>
<field name="name">June</field>
<field name="abbreviation">Jun</field>
</record>
<record model="ir.calendar.month" id="July">
<field name="index" eval="7"/>
<field name="name">July</field>
<field name="abbreviation">Jul</field>
</record>
<record model="ir.calendar.month" id="August">
<field name="index" eval="8"/>
<field name="name">August</field>
<field name="abbreviation">Aug</field>
</record>
<record model="ir.calendar.month" id="September">
<field name="index" eval="9"/>
<field name="name">September</field>
<field name="abbreviation">Sep</field>
</record>
<record model="ir.calendar.month" id="October">
<field name="index" eval="10"/>
<field name="name">October</field>
<field name="abbreviation">Oct</field>
</record>
<record model="ir.calendar.month" id="November">
<field name="index" eval="11"/>
<field name="name">November</field>
<field name="abbreviation">Nov</field>
</record>
<record model="ir.calendar.month" id="December">
<field name="index" eval="12"/>
<field name="name">December</field>
<field name="abbreviation">Dec</field>
</record>
<record model="ir.calendar.day" id="Monday">
<field name="index" eval="0"/>
<field name="name">Monday</field>
<field name="abbreviation">Mon</field>
</record>
<record model="ir.calendar.day" id="Tuesday">
<field name="index" eval="1"/>
<field name="name">Tuesday</field>
<field name="abbreviation">Tue</field>
</record>
<record model="ir.calendar.day" id="Wednesday">
<field name="index" eval="2"/>
<field name="name">Wednesday</field>
<field name="abbreviation">Wed</field>
</record>
<record model="ir.calendar.day" id="Thursday">
<field name="index" eval="3"/>
<field name="name">Thursday</field>
<field name="abbreviation">Thu</field>
</record>
<record model="ir.calendar.day" id="Friday">
<field name="index" eval="4"/>
<field name="name">Friday</field>
<field name="abbreviation">Fri</field>
</record>
<record model="ir.calendar.day" id="Saturday">
<field name="index" eval="5"/>
<field name="name">Saturday</field>
<field name="abbreviation">Sat</field>
</record>
<record model="ir.calendar.day" id="Sunday">
<field name="index" eval="6"/>
<field name="name">Sunday</field>
<field name="abbreviation">Sun</field>
</record>
</data>
</tryton>

48
ir/configuration.py Executable file
View File

@@ -0,0 +1,48 @@
# 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 trytond.cache import Cache
from trytond.config import config
from trytond.model import ModelSingleton, ModelSQL, fields
class Configuration(ModelSingleton, ModelSQL):
'Configuration'
__name__ = 'ir.configuration'
language = fields.Char('language')
hostname = fields.Char("Hostname", strip=False)
_get_language_cache = Cache('ir_configuration.get_language')
@staticmethod
def default_language():
return config.get('database', 'language')
@classmethod
def get_language(cls):
language = cls._get_language_cache.get(None)
if language is not None:
return language
language = cls(1).language
if not language:
language = config.get('database', 'language')
cls._get_language_cache.set(None, language)
return language
def check(self):
"Check configuration coherence on pool initialisation"
pass
@classmethod
def create(cls, vlist):
records = super().create(vlist)
cls._get_language_cache.clear()
return records
@classmethod
def write(cls, *args):
super().write(*args)
cls._get_language_cache.clear()
@classmethod
def delete(cls, records):
super().delete(records)
cls._get_language_cache.clear()

217
ir/cron.py Executable file
View File

@@ -0,0 +1,217 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
import logging
import time
from dateutil.relativedelta import relativedelta
from trytond import backend
from trytond.config import config
from trytond.exceptions import UserError, UserWarning
from trytond.model import (
DeactivableMixin, Index, ModelSQL, ModelView, dualmethod, fields)
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.status import processing
from trytond.tools import timezone as tz
from trytond.transaction import Transaction, TransactionError
from trytond.worker import run_task
logger = logging.getLogger(__name__)
class Cron(DeactivableMixin, ModelSQL, ModelView):
"Cron"
__name__ = "ir.cron"
interval_number = fields.Integer('Interval Number', required=True)
interval_type = fields.Selection([
('minutes', 'Minutes'),
('hours', 'Hours'),
('days', 'Days'),
('weeks', 'Weeks'),
('months', 'Months'),
], "Interval Type", sort=False, required=True)
minute = fields.Integer("Minute",
domain=['OR',
('minute', '=', None),
[('minute', '>=', 0), ('minute', '<=', 59)],
],
states={
'invisible': Eval('interval_type').in_(['minutes']),
},
depends=['interval_type'])
hour = fields.Integer("Hour",
domain=['OR',
('hour', '=', None),
[('hour', '>=', 0), ('hour', '<=', 23)],
],
states={
'invisible': Eval('interval_type').in_(['minutes', 'hours']),
},
depends=['interval_type'])
weekday = fields.Many2One(
'ir.calendar.day', "Day of Week",
states={
'invisible': Eval('interval_type').in_(
['minutes', 'hours', 'days']),
},
depends=['interval_type'])
day = fields.Integer("Day",
domain=['OR',
('day', '=', None),
('day', '>=', 0),
],
states={
'invisible': Eval('interval_type').in_(
['minutes', 'hours', 'days', 'weeks']),
},
depends=['interval_type'])
timezone = fields.Function(fields.Char("Timezone"), 'get_timezone')
next_call = fields.DateTime("Next Call")
method = fields.Selection([
('ir.trigger|trigger_time', "Run On Time Triggers"),
('ir.queue|clean', "Clean Task Queue"),
('ir.error|clean', "Clean Errors"),
], "Method", required=True)
@classmethod
def __setup__(cls):
super(Cron, cls).__setup__()
table = cls.__table__()
cls._buttons.update({
'run_once': {
'icon': 'tryton-launch',
},
})
cls._sql_indexes.add(Index(table, (table.next_call, Index.Range())))
@classmethod
def __register__(cls, module_name):
super().__register__(module_name)
table_h = cls.__table_handler__(module_name)
# Migration from 5.0: remove fields
for column in ['name', 'user', 'request_user', 'number_calls',
'repeat_missed', 'model', 'function', 'args']:
table_h.drop_column(column)
# Migration from 5.0: remove required on next_call
table_h.not_null_action('next_call', 'remove')
@classmethod
def default_timezone(cls):
return tz.SERVER.tzname(datetime.datetime.now())
def get_timezone(self, name):
return self.default_timezone()
@classmethod
def check_xml_record(cls, crons, values):
pass
@classmethod
def view_attributes(cls):
return [(
'//label[@id="time_label"]', 'states', {
'invisible': Eval('interval_type') == 'minutes',
}),
]
def compute_next_call(self, now):
return (now.replace(tzinfo=tz.UTC).astimezone(tz.SERVER)
+ relativedelta(**{self.interval_type: self.interval_number})
+ relativedelta(
microsecond=0,
second=0,
minute=(
self.minute
if self.interval_type != 'minutes'
else None),
hour=(
self.hour
if self.interval_type not in {'minutes', 'hours'}
else None),
day=(
self.day
if self.interval_type not in {
'minutes', 'hours', 'days', 'weeks'}
else None),
weekday=(
int(self.weekday.index)
if self.weekday
and self.interval_type not in {'minutes', 'hours', 'days'}
else None))).astimezone(tz.UTC).replace(tzinfo=None)
@dualmethod
@ModelView.button
def run_once(cls, crons):
pool = Pool()
for cron in crons:
model, method = cron.method.split('|')
Model = pool.get(model)
getattr(Model, method)()
@classmethod
def run(cls, db_name):
transaction = Transaction()
logger.info('cron started for "%s"', db_name)
now = datetime.datetime.now()
retry = config.getint('database', 'retry')
with transaction.start(
db_name, 0, context={'_skip_warnings': True},
_lock_tables=[cls._table]):
pool = Pool()
Error = pool.get('ir.error')
crons = cls.search(['OR',
('next_call', '<=', now),
('next_call', '=', None),
])
for cron in crons:
def duration():
return (time.monotonic() - started) * 1000
started = time.monotonic()
name = '<Cron %s@%s %s>' % (cron.id, db_name, cron.method)
transaction_extras = {}
count = 0
while True:
if count:
time.sleep(0.02 * (retry - count))
try:
with processing(name), \
transaction.new_transaction(
**transaction_extras) as cron_trans:
try:
cron.run_once()
cron_trans.commit()
except TransactionError as e:
cron_trans.rollback()
e.fix(transaction_extras)
continue
except backend.DatabaseOperationalError:
if count < retry:
cron_trans.rollback()
count += 1
logger.debug("Retry: %i", count)
continue
else:
raise
except (UserError, UserWarning) as e:
Error.report(cron, e)
logger.info(
"%s failed after %i ms", name, duration())
except Exception:
logger.exception(
"%s failed after %i ms", name, duration())
cron.next_call = cron.compute_next_call(now)
cron.save()
break
logger.info("%s in %i ms", name, duration())
while transaction.tasks:
task_id = transaction.tasks.pop()
run_task(db_name, task_id)
logger.info('cron finished for "%s"', db_name)

51
ir/cron.xml Executable file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<menuitem
name="Scheduler"
parent="menu_administration"
sequence="50"
id="menu_scheduler"/>
<record model="ir.ui.view" id="cron_view_tree">
<field name="model">ir.cron</field>
<field name="type">tree</field>
<field name="name">cron_list</field>
</record>
<record model="ir.ui.view" id="cron_view_form">
<field name="model">ir.cron</field>
<field name="type">form</field>
<field name="name">cron_form</field>
</record>
<record model="ir.action.act_window" id="act_cron_form">
<field name="name">Actions</field>
<field name="res_model">ir.cron</field>
<field name="context"></field>
</record>
<record model="ir.action.act_window.view"
id="act_cron_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="cron_view_tree"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_cron_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="cron_view_form"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<menuitem
parent="ir.menu_scheduler"
action="act_cron_form"
sequence="10"
id="menu_cron_form"/>
<record model="ir.model.button" id="cron_run_once_button">
<field name="model">ir.cron</field>
<field name="name">run_once</field>
<field name="string">Run Once</field>
</record>
</data>
</tryton>

25
ir/date.py Executable file
View File

@@ -0,0 +1,25 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from trytond.model import Model
from trytond.rpc import RPC
class Date(Model):
'Date'
__name__ = 'ir.date'
@classmethod
def __setup__(cls):
super(Date, cls).__setup__()
cls.__rpc__.update({
'today': RPC(),
})
@staticmethod
def today(timezone=None):
'''
Return the current date
'''
return datetime.datetime.now(timezone).date()

99
ir/email.xml Executable file
View File

@@ -0,0 +1,99 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="email_view_form">
<field name="model">ir.email</field>
<field name="type">form</field>
<field name="name">email_form</field>
</record>
<record model="ir.ui.view" id="email_view_list">
<field name="model">ir.email</field>
<field name="type">tree</field>
<field name="name">email_list</field>
</record>
<record model="ir.action.act_window" id="act_email_form">
<field name="name">E-mails</field>
<field name="res_model">ir.email</field>
</record>
<record model="ir.action.act_window.view" id="act_email_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="email_view_list"/>
<field name="act_window" ref="act_email_form"/>
</record>
<record model="ir.action.act_window.view" id="act_email_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="email_view_form"/>
<field name="act_window" ref="act_email_form"/>
</record>
<menuitem
parent="menu_models"
action="act_email_form"
sequence="50"
id="menu_email_form"/>
<record model="ir.model.access" id="access_email">
<field name="model">ir.email</field>
<field name="perm_create" eval="True"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.action.act_window" id="act_email_form_relate">
<field name="name">E-mail Archives</field>
<field name="res_model">ir.email</field>
<field
name="domain"
eval="[If(Eval('active_ids', []) == [Eval('active_id')], ('resource', '=', [Eval('active_model'), Eval('active_id')]), ('resource.id', 'in', Eval('active_ids'), Eval('active_model')))]"
pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_email_form_relate_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="email_view_list"/>
<field name="act_window" ref="act_email_form_relate"/>
</record>
<record model="ir.action.act_window.view" id="act_email_form_relate_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="email_view_form"/>
<field name="act_window" ref="act_email_form_relate"/>
</record>
<record model="ir.action.keyword" id="act_email_form_relate_keyword1">
<field name="keyword">form_relate</field>
<field name="action" ref="act_email_form_relate"/>
</record>
<record model="ir.ui.view" id="email_template_view_form">
<field name="model">ir.email.template</field>
<field name="type">form</field>
<field name="name">email_template_form</field>
</record>
<record model="ir.ui.view" id="email_template_view_list">
<field name="model">ir.email.template</field>
<field name="type">tree</field>
<field name="name">email_template_list</field>
</record>
<record model="ir.action.act_window" id="act_email_template_form">
<field name="name">E-mail Templates</field>
<field name="res_model">ir.email.template</field>
</record>
<record model="ir.action.act_window.view" id="act_email_template_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="email_template_view_list"/>
<field name="act_window" ref="act_email_template_form"/>
</record>
<record model="ir.action.act_window.view" id="act_email_template_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="email_template_view_form"/>
<field name="act_window" ref="act_email_template_form"/>
</record>
<menuitem
parent="menu_action"
action="act_email_template_form"
sequence="50"
id="menu_email_template_form"/>
</data>
</tryton>

588
ir/email_.py Executable file
View File

@@ -0,0 +1,588 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import heapq
import mimetypes
import re
from email.message import EmailMessage
from email.utils import getaddresses
try:
import html2text
except ImportError:
html2text = None
from genshi.template import TextTemplate
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import EvalEnvironment, ModelSQL, ModelView, fields
from trytond.model.exceptions import AccessError, ValidationError
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, PYSONDecoder
from trytond.report import Report
from trytond.rpc import RPC
from trytond.sendmail import send_message_transactional
from trytond.tools import escape_wildcard
from trytond.tools.email_ import (
convert_ascii_email, format_address, set_from_header)
from trytond.tools.string_ import StringMatcher
from trytond.transaction import Transaction
from .resource import ResourceAccessMixin
HTML_EMAIL = """<!DOCTYPE html>
<html>
<head><title>%(subject)s</title></head>
<body>%(body)s<br/>
<hr style="width: 2em; text-align: start; display: inline-block"/><br/>
%(signature)s</body>
</html>"""
specialsre = re.compile(r'[][\\()<>@,:;".]')
escapesre = re.compile(r'[\\"]')
class EmailTemplateError(ValidationError):
pass
def _formataddr(pair):
"Format address without encoding"
name, address = pair
convert_ascii_email(address).encode('ascii')
if name:
quotes = ''
if specialsre.search(name):
quotes = '"'
name = escapesre.sub(r'\\\g<0>', name)
return '%s%s%s <%s>' % (quotes, name, quotes, address)
return address
class Email(ResourceAccessMixin, ModelSQL, ModelView):
"Email"
__name__ = 'ir.email'
user = fields.Function(fields.Char("User"), 'get_user')
at = fields.Function(fields.DateTime("At"), 'get_at')
recipients = fields.Char("Recipients", readonly=True)
recipients_secondary = fields.Char("Secondary Recipients", readonly=True)
recipients_hidden = fields.Char("Hidden Recipients", readonly=True)
addresses = fields.One2Many(
'ir.email.address', 'email', "Addresses", readonly=True)
subject = fields.Char("Subject", readonly=True)
body = fields.Text("Body", readonly=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('create_date', 'DESC'))
cls.__rpc__.update({
'send': RPC(readonly=False, result=int),
'complete': RPC(check_access=False),
})
del cls.__rpc__['create']
def get_user(self, name):
return self.create_uid.rec_name
def get_at(self, name):
return self.create_date.replace(microsecond=0)
@classmethod
def send(cls, to='', cc='', bcc='', subject='', body='',
files=None, record=None, reports=None, attachments=None):
pool = Pool()
User = pool.get('res.user')
ActionReport = pool.get('ir.action.report')
Attachment = pool.get('ir.attachment')
transaction = Transaction()
user = User(transaction.user)
Model = pool.get(record[0])
record = Model(record[1])
msg = EmailMessage()
body_html = HTML_EMAIL % {
'subject': subject,
'body': body,
'signature': user.signature or '',
}
if html2text:
body_text = HTML_EMAIL % {
'subject': subject,
'body': body,
'signature': '',
}
converter = html2text.HTML2Text()
body_text = converter.handle(body_text)
if user.signature:
body_text += '\n-- \n' + converter.handle(user.signature)
msg.add_alternative(body_text, subtype='plain')
if msg.is_multipart():
msg.add_alternative(body_html, subtype='html')
else:
msg.set_content(body_html, subtype='html')
if files or reports or attachments:
if files is None:
files = []
else:
files = list(files)
for report_id in (reports or []):
report = ActionReport(report_id)
Report = pool.get(report.report_name, type='report')
ext, content, _, title = Report.execute(
[record.id], {
'action_id': report.id,
})
name = '%s.%s' % (title, ext)
if isinstance(content, str):
content = content.encode('utf-8')
files.append((name, content))
if attachments:
files += [
(a.name, a.data) for a in Attachment.browse(attachments)]
for name, data in files:
ctype, _ = mimetypes.guess_type(name)
if not ctype:
ctype = 'application/octet-stream'
maintype, subtype = ctype.split('/', 1)
msg.add_attachment(
data,
maintype=maintype,
subtype=subtype,
filename=('utf-8', '', name))
from_ = config.get('email', 'from')
set_from_header(msg, from_, user.email or from_)
msg['To'] = [format_address(a, n) for n, a in getaddresses([to])]
msg['Cc'] = [format_address(a, n) for n, a in getaddresses([cc])]
msg['Bcc'] = [format_address(a, n) for n, a in getaddresses([bcc])]
msg['Subject'] = subject
send_message_transactional(msg, strict=True)
email = cls.from_message(msg, body=body, resource=record)
email.save()
if files:
attachments_ = []
for name, data in files:
attachments_.append(
Attachment(resource=email, name=name, data=data))
Attachment.save(attachments_)
return email
@classmethod
def complete(cls, text, limit):
limit = int(limit)
if not limit > 0:
raise ValueError('limit must be > 0: %r' % (limit,))
emails = getaddresses([text])
if not emails:
return []
name, email = map(str.strip, emails[-1])
if not name and not email:
return []
s = StringMatcher()
try:
s.set_seq2(_formataddr((name, email)))
except UnicodeEncodeError:
return []
def generate(name, email):
for name, email in cls._match(name, email):
try:
address = _formataddr((name, email))
except UnicodeEncodeError:
continue
s.set_seq1(address)
yield (
s.ratio(), address,
', '.join(map(_formataddr, emails[:-1] + [(name, email)])))
return heapq.nlargest(limit, generate(name, email))
@classmethod
def _match(cls, name, email):
pool = Pool()
User = pool.get('res.user')
domain = ['OR']
for field in ['name', 'login', 'email']:
for value in [name, email]:
if value and len(value) >= 3:
domain.append(
(field, 'ilike', '%' + escape_wildcard(value) + '%'))
for user in User.search([
('email', '!=', ''),
domain,
], order=[]):
yield user.name, user.email
@classmethod
def from_message(cls, msg, **values):
to_addrs = [e for _, e in getaddresses(
filter(None, (msg['To'], msg['Cc'], msg['Bcc'])))]
return cls(
recipients=msg['To'],
recipients_secondary=msg['Cc'],
recipients_hidden=msg['Bcc'],
addresses=[{'address': a} for a in to_addrs],
subject=msg['Subject'],
**values)
class EmailAddress(ModelSQL):
"Email Address"
__name__ = 'ir.email.address'
email = fields.Many2One(
'ir.email', "E-mail", required=True, ondelete='CASCADE')
address = fields.Char("Address", required=True)
class EmailTemplate(ModelSQL, ModelView):
"Email Template"
__name__ = 'ir.email.template'
model = fields.Many2One('ir.model', "Model", required=True)
name = fields.Char("Name", required=True, translate=True)
recipients = fields.Many2One(
'ir.model.field', "Recipients",
states={
'invisible': Bool(Eval('recipients_pyson')),
},
depends=['recipients_pyson'],
help="The field that contains the recipient(s).")
recipients_pyson = fields.Char(
"Recipients",
states={
'invisible': Bool(Eval('recipients')),
},
depends=['recipients'],
help="A PYSON expression that generates a list of recipients "
'with the record represented by "self".')
recipients_secondary = fields.Many2One(
'ir.model.field', "Secondary Recipients",
states={
'invisible': Bool(Eval('recipients_secondary_pyson')),
},
depends=['recipients_secondary_pyson'],
help="The field that contains the secondary recipient(s).")
recipients_secondary_pyson = fields.Char(
"Secondary Recipients",
states={
'invisible': Bool(Eval('recipients_secondary')),
},
depends=['recipients_secondary'],
help="A PYSON expression that generates a list "
'of secondary recipients with the record represented by "self".')
recipients_hidden = fields.Many2One(
'ir.model.field', "Hidden Recipients",
states={
'invisible': Bool(Eval('recipients_hidden_pyson')),
},
depends=['recipients_hidden_pyson'],
help="The field that contains the secondary recipient(s).")
recipients_hidden_pyson = fields.Char(
"Hidden Recipients",
states={
'invisible': Bool(Eval('recipients_hidden')),
},
depends=['recipients_hidden'],
help="A PYSON expression that generates a list of hidden recipients "
'with the record represented by "self".')
subject = fields.Char("Subject", translate=True)
body = fields.Text("Body", translate=True)
reports = fields.Many2Many(
'ir.email.template-ir.action.report', 'template', 'report',
"Reports",
domain=[
('model', '=', Eval('model_name')),
],
depends=['model_name'])
model_name = fields.Function(
fields.Char("Model Name"), 'on_change_with_model_name')
@classmethod
def __setup__(cls):
super().__setup__()
for field in [
'recipients',
'recipients_secondary',
'recipients_hidden',
]:
field = getattr(cls, field)
field.domain = [
('model_ref.id', '=', Eval('model', -1)),
['OR',
('relation', 'in', cls.email_models()),
[
('model', 'in', cls.email_models()),
('name', '=', 'id'),
],
]
]
field.depends.add('model')
cls.__rpc__.update({
'get': RPC(instantiate=0),
'get_default': RPC(),
})
@fields.depends('model')
def on_change_with_model_name(self, name=None):
if self.model:
return self.model.model
@classmethod
def validate_fields(cls, templates, field_names):
super().validate_fields(templates, field_names)
cls.check_subject(templates, field_names)
cls.check_body(templates, field_names)
cls.check_fields_pyson(templates, field_names)
@classmethod
def check_subject(cls, templates, field_names=None):
if field_names and 'subject' not in field_names:
return
for template in templates:
if not template.subject:
continue
try:
TextTemplate(template.subject)
except Exception as exception:
raise EmailTemplateError(gettext(
'ir.msg_email_template_invalid_subject',
template=template.rec_name,
exception=exception)) from exception
@classmethod
def check_body(self, templates, field_names=None):
if field_names and 'body' not in field_names:
return
for template in templates:
if not template.body:
continue
try:
TextTemplate(template.body)
except Exception as exception:
raise EmailTemplateError(gettext(
'ir.msg_email_template_invalid_body',
template=template.rec_name,
exception=exception)) from exception
@classmethod
def check_fields_pyson(cls, templates, field_names=None):
pyson_fields = {
'recipients_pyson',
'recipients_secondary_pyson',
'recipients_hidden_pyson',
}
if field_names:
pyson_fields &= field_names
if not pyson_fields:
return
encoder = PYSONDecoder(noeval=True)
for template in templates:
for field in pyson_fields:
value = getattr(template, field)
if not value:
continue
try:
pyson = encoder.decode(value)
except Exception as exception:
raise EmailTemplateError(
gettext('ir.msg_email_template_invalid_field_pyson',
template=template.rec_name,
field=cls.__names__(field)['field'],
exception=exception)) from exception
if not isinstance(pyson, list) and pyson.types() != {list}:
raise EmailTemplateError(gettext(
'ir.msg_email_template_invalid_field_pyson_type',
template=template.rec_name,
field=cls.__names__(field)['field'],
))
def get(self, record):
pool = Pool()
Model = pool.get(self.model.model)
record = Model(int(record))
values = {}
for attr, key in [
('recipients', 'to'),
('recipients_secondary', 'cc'),
('recipients_hidden', 'bcc'),
]:
field = getattr(self, attr)
try:
if field:
if field.name == 'id':
value = record
else:
value = getattr(record, field.name, None)
if value:
values[key] = self.get_addresses(value)
else:
value = getattr(self, attr + '_pyson')
if value:
value = self.eval(record, value)
if value:
values[key] = self.get_addresses(value)
except AccessError:
continue
if self.subject:
try:
values['subject'] = (TextTemplate(self.subject)
.generate(**self.get_context(record))
.render())
except AccessError:
pass
if self.body:
try:
values['body'] = (TextTemplate(self.body)
.generate(**self.get_context(record))
.render())
except AccessError:
pass
if self.reports:
values['reports'] = [r.id for r in self.reports]
return values
def get_context(self, record):
pool = Pool()
User = pool.get('res.user')
return {
'context': Transaction().context,
'user': User(Transaction().user),
'record': record,
'format_date': Report.format_date,
'format_datetime': Report.format_datetime,
'format_timedelta': Report.format_timedelta,
'format_currency': Report.format_currency,
'format_number': Report.format_number,
}
def eval(self, record, pyson, _env=None):
'Evaluate the pyson with the record'
if _env is None:
env = {}
else:
env = _env.copy()
env['context'] = Transaction().context
env['self'] = EvalEnvironment(record, record.__class__)
return PYSONDecoder(env).decode(pyson)
@classmethod
def _get_default_exclude(cls, record):
return ['create_uid', 'write_uid']
@classmethod
def get_default(cls, model, record):
pool = Pool()
Field = pool.get('ir.model.field')
Model = pool.get(model)
record = Model(int(record))
values = {}
fields = Field.search([
('model.model', '=', model),
('name', 'not in', cls._get_default_exclude(record)),
['OR',
('relation', 'in', cls.email_models()),
[
('model.model', 'in', cls.email_models()),
('name', '=', 'id'),
],
],
])
addresses = set()
for field in fields:
try:
if field.name == 'id':
value = record
else:
value = getattr(record, field.name)
addresses.update(cls.get_addresses(value))
except AccessError:
pass
values['to'] = list(addresses)
try:
values['subject'] = '%s: %s' % (
Model.__names__()['model'], record.rec_name)
except AccessError:
pass
return values
@classmethod
def email_models(cls):
return ['res.user']
@classmethod
def get_addresses(cls, value):
if isinstance(value, (list, tuple)):
addresses = (cls._get_address(v) for v in value)
else:
addresses = [cls._get_address(value)]
return [
_formataddr((name, email))
for name, email in filter(None, addresses)
if email]
@classmethod
def _get_address(cls, record):
pool = Pool()
User = pool.get('res.user')
if isinstance(record, str):
return (None, record)
elif isinstance(record, User) and record.email:
return (record.name, record.email)
@classmethod
def get_languages(cls, value):
pool = Pool()
Configuration = pool.get('ir.configuration')
Lang = pool.get('ir.lang')
if isinstance(value, (list, tuple)):
languages = {cls._get_language(v) for v in value}
else:
languages = {cls._get_language(value)}
languages = list(filter(None, languages))
if not languages:
return Lang.search([
('code', '=', Configuration.get_language()),
], limit=1)
return languages
@classmethod
def _get_language(cls, record):
pool = Pool()
User = pool.get('res.user')
if isinstance(record, User) and record.language:
return record.language
@classmethod
def create(cls, vlist):
ModelView._view_toolbar_get_cache.clear()
return super().create(vlist)
@classmethod
def write(cls, *args):
if any({'name', 'model'} & v.keys() for v in args[1:None:2]):
ModelView._view_toolbar_get_cache.clear()
super().write(*args)
@classmethod
def delete(cls, records):
ModelView._view_toolbar_get_cache.clear()
super().delete(records)
class EmailTemplate_Report(ModelSQL):
"Email Template - Report"
__name__ = 'ir.email.template-ir.action.report'
template = fields.Many2One(
'ir.email.template', "Template", required=True, ondelete='CASCADE')
report = fields.Many2One(
'ir.action.report', "Report", required=True, ondelete='CASCADE')

188
ir/error.py Executable file
View File

@@ -0,0 +1,188 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime as dt
import functools
import logging
import warnings
from trytond.config import config
from trytond.exceptions import UserError, UserWarning
from trytond.model import (
Index, ModelSQL, ModelView, Workflow, dualmethod, fields)
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.tools import firstline
from trytond.transaction import Transaction
logger = logging.getLogger(__name__)
clean_days = config.getint('error', 'clean_days', default=90)
def set_user(field):
def decorator(func):
@functools.wraps(func)
def wrapper(cls, records, *args, **kwargs):
result = func(cls, records, *args, **kwargs)
cls.write(
[r for r in records
if not getattr(r, field)], {
field: Transaction().user,
})
return result
return wrapper
return decorator
def reset_user(*fields):
def decorator(func):
@functools.wraps(func)
def wrapper(cls, records, *args, **kwargs):
result = func(cls, records, *args, **kwargs)
cls.write(records, {f: None for f in fields})
return result
return wrapper
return decorator
class Error(Workflow, ModelView, ModelSQL):
"Error"
__name__ = 'ir.error'
origin = fields.Reference("Origin", [
('ir.cron', "Action"),
('ir.queue', "Task"),
], readonly=True)
origin_string = origin.translated('origin')
message = fields.Text("Message", readonly=True)
description = fields.Text("Description", readonly=True)
summary = fields.Function(fields.Char("Summary"), 'on_change_with_summary')
processed_by = fields.Many2One(
'res.user', "Processed by",
states={
'readonly': Eval('state').in_(['processing', 'solved']),
},
depends=['state'])
solved_by = fields.Many2One(
'res.user', "Solved by",
states={
'readonly': Eval('state').in_(['solved']),
},
depends=['state'])
state = fields.Selection([
('open', "Open"),
('processing', "Processing"),
('solved', "Solved"),
], "State", readonly=True, sort=False)
@classmethod
def __setup__(cls):
super().__setup__()
table = cls.__table__()
cls._sql_indexes.add(
Index(
table,
(table.state, Index.Equality()),
where=table.state.in_(['open', 'processing'])))
cls._transitions |= {
('open', 'processing'),
('processing', 'solved'),
('processing', 'open'),
}
cls._buttons.update({
'open': {
'invisible': Eval('state') != 'processing',
'depends': ['state'],
},
'process': {
'invisible': Eval('state') != 'open',
'depends': ['state'],
},
'solve': {
'invisible': Eval('state') != 'processing',
'depends': ['state'],
},
})
@classmethod
def default_state(cls):
return 'open'
@fields.depends('message')
def on_change_with_summary(self, name=None):
return firstline(self.message or '')
def get_rec_name(self, name):
if self.origin:
return "%s - %s" % (self.origin_string, self.origin.rec_name)
return super().get_rec_name(name)
@dualmethod
def log(cls, *args, **kwargs):
# Test if it is a ModelStorage.log call
if len(args) <= 1 or not isinstance(args[1], Exception):
return super().log(*args, **kwargs)
warnings.warn(
"Call report instead of log to store exception",
DeprecationWarning)
cls.report(*args, **kwargs)
@classmethod
def report(cls, origin, exception):
try:
assert isinstance(exception, (UserError, UserWarning))
with Transaction().new_transaction(autocommit=True):
if not cls.search([
('origin', '=', str(origin)),
('message', '=', exception.message),
('description', '=', exception.description),
('state', '!=', 'solved'),
]):
cls.create([{
'origin': str(origin),
'message': exception.message,
'description': exception.description,
}])
except Exception:
logger.critical(
"failed to store exception %s of %s", exception, origin,
exc_info=True)
@classmethod
def clean(cls, date=None):
if date is None:
date = (
dt.datetime.now() - dt.timedelta(days=clean_days))
errors = cls.search([('create_date', '<', date)])
cls.delete(errors)
@classmethod
@ModelView.button
@Workflow.transition('open')
@reset_user('processed_by')
def open(cls, errors):
pass
@classmethod
@ModelView.button
@Workflow.transition('processing')
@set_user('processed_by')
def process(cls, errors):
pass
@classmethod
@ModelView.button
@Workflow.transition('solved')
@set_user('solved_by')
def solve(cls, errors):
pool = Pool()
Cron = pool.get('ir.cron')
Queue = pool.get('ir.queue')
for error in errors:
if isinstance(error.origin, Cron):
Cron.__queue__.run_once([error.origin])
elif isinstance(error.origin, Queue):
task = error.origin
Queue.push(task.name, task.data)

85
ir/error.xml Executable file
View File

@@ -0,0 +1,85 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="error_view_list">
<field name="model">ir.error</field>
<field name="type">tree</field>
<field name="name">error_list</field>
</record>
<record model="ir.ui.view" id="error_view_form">
<field name="model">ir.error</field>
<field name="type">form</field>
<field name="name">error_form</field>
</record>
<record model="ir.action.act_window" id="act_error_form">
<field name="name">Errors</field>
<field name="res_model">ir.error</field>
</record>
<record model="ir.action.act_window.view" id="act_error_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="error_view_list"/>
<field name="act_window" ref="act_error_form"/>
</record>
<record model="ir.action.act_window.view" id="act_error_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="error_view_form"/>
<field name="act_window" ref="act_error_form"/>
</record>
<record model="ir.action.act_window.domain" id="act_error_form_domain_open">
<field name="name">Open</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('state', '=', 'open')]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_error_form"/>
</record>
<record model="ir.action.act_window.domain" id="act_error_form_domain_processing">
<field name="name">Processing</field>
<field name="sequence" eval="20"/>
<field name="domain" eval="[('state', '=', 'processing')]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_error_form"/>
</record>
<record model="ir.action.act_window.domain" id="act_error_form_domain_all">
<field name="name">All</field>
<field name="sequence" eval="9999"/>
<field name="domain"></field>
<field name="act_window" ref="act_error_form"/>
</record>
<menuitem
parent="ir.menu_scheduler"
action="act_error_form"
sequence="50"
id="menu_error_form"/>
<record model="ir.model.button" id="error_open_button">
<field name="model">ir.error</field>
<field name="name">open</field>
<field name="string">Open</field>
</record>
<record model="ir.model.button" id="error_process_button">
<field name="model">ir.error</field>
<field name="name">process</field>
<field name="string">Process</field>
</record>
<record model="ir.model.button" id="error_solve_button">
<field name="model">ir.error</field>
<field name="name">solve</field>
<field name="string">Solve</field>
</record>
</data>
<data noupdate="1">
<record model="ir.cron" id="cron_error_clean">
<field name="method">ir.error|clean</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">months</field>
</record>
</data>
</tryton>

24
ir/exceptions.py Executable file
View File

@@ -0,0 +1,24 @@
# 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 .lang import DateError as LanguageDateError
from .lang import DeleteDefaultError as LanguageDeleteDefaultError
from .lang import GroupingError as LanguageGroupingError
from .lang import TranslatableError as LanguageTranslatableError
from .module import DeactivateDependencyError
from .sequence import AffixError as SequenceAffixError
from .sequence import MissingError as SequenceMissingError
from .translation import OverriddenError as TranslationOverriddenError
from .trigger import ConditionError as TriggerConditionError
__all__ = [
DeactivateDependencyError,
LanguageDateError,
LanguageDeleteDefaultError,
LanguageGroupingError,
LanguageTranslatableError,
SequenceAffixError,
SequenceMissingError,
TranslationOverriddenError,
TriggerConditionError,
]

143
ir/export.py Executable file
View File

@@ -0,0 +1,143 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"Exports"
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.rpc import RPC
from trytond.transaction import Transaction
class _ClearCache(ModelSQL):
@classmethod
def create(cls, vlist):
ModelView._view_toolbar_get_cache.clear()
return super().create(vlist)
@classmethod
def write(cls, *args):
ModelView._view_toolbar_get_cache.clear()
super().write(*args)
@classmethod
def delete(cls, records):
ModelView._view_toolbar_get_cache.clear()
super().delete(records)
class Export(_ClearCache, ModelSQL, ModelView):
"Export"
__name__ = "ir.export"
name = fields.Char('Name')
resource = fields.Char('Resource')
user = fields.Many2One(
'res.user', "User", required=True, ondelete='CASCADE')
header = fields.Boolean(
"Header",
help="Check to include field names on the export.")
records = fields.Selection([
('selected', "Selected"),
('listed', "Listed"),
], "Records",
help="The records on which the export runs.")
export_fields = fields.One2Many('ir.export.line', 'export',
'Fields')
@classmethod
def __setup__(cls):
super().__setup__()
cls.__rpc__.update(
get=RPC(check_access=False),
set=RPC(check_access=False, readonly=False),
update=RPC(check_access=False, readonly=False),
unset=RPC(check_access=False, readonly=False),
)
@classmethod
def __register__(cls, module):
table_h = cls.__table_handler__(module)
table = cls.__table__()
cursor = Transaction().connection.cursor()
user_exists = table_h.column_exist('user')
super().__register__(module)
# Migration from 6.8: add user
if not user_exists:
cursor.execute(*table.update([table.user], [table.create_uid]))
@classmethod
def default_header(cls):
return False
@classmethod
def default_records(cls):
return 'selected'
@classmethod
def get(cls, resource, fields_names):
pool = Pool()
User = pool.get('res.user')
return cls.search_read([
('resource', '=', resource),
['OR',
('groups', 'in', User.get_groups()),
('user', '=', Transaction().user),
],
],
fields_names=fields_names)
@classmethod
def set(cls, values):
export = cls(**values)
export.user = Transaction().user
export.save()
return export.id
@classmethod
def update(cls, id, values, fields):
pool = Pool()
User = pool.get('res.user')
exports = cls.search([
('id', '=', id),
['OR',
('write_groups', 'in', User.get_groups()),
('user', '=', Transaction().user),
],
])
try:
export, = exports
except ValueError:
return
for name, value in values.items():
setattr(export, name, value)
lines = []
for name in fields:
lines.append({'name': name})
export.export_fields = lines
export.save()
@classmethod
def unset(cls, id):
pool = Pool()
User = pool.get('res.user')
cls.delete(cls.search([
('id', '=', id),
['OR',
('write_groups', 'in', User.get_groups()),
('user', '=', Transaction().user),
],
]))
class ExportLine(_ClearCache, ModelSQL, ModelView):
"Export line"
__name__ = 'ir.export.line'
name = fields.Char('Name')
export = fields.Many2One('ir.export', 'Export', required=True,
ondelete='CASCADE')
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('export')

51
ir/export.xml Executable file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="export_view_form">
<field name="model">ir.export</field>
<field name="type">form</field>
<field name="name">export_form</field>
</record>
<record model="ir.ui.view" id="export_view_tree">
<field name="model">ir.export</field>
<field name="type">tree</field>
<field name="name">export_list</field>
</record>
<record model="ir.action.act_window" id="act_export_form">
<field name="name">Exports</field>
<field name="type">ir.action.act_window</field>
<field name="res_model">ir.export</field>
</record>
<record model="ir.action.act_window.view"
id="act_export_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="export_view_tree"/>
<field name="act_window" ref="act_export_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_export_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="export_view_form"/>
<field name="act_window" ref="act_export_form"/>
</record>
<menuitem
parent="ir.menu_models"
action="act_export_form"
sequence="50"
id="menu_export_form"/>
<record model="ir.ui.view" id="export_line_view_form">
<field name="model">ir.export.line</field>
<field name="type">form</field>
<field name="name">export_line_form</field>
</record>
<record model="ir.ui.view" id="export_line_view_tree">
<field name="model">ir.export.line</field>
<field name="type">tree</field>
<field name="name">export_line_list</field>
</record>
</data>
</tryton>

BIN
ir/fonts/karla.ttf Executable file

Binary file not shown.

32
ir/ir.xml Executable file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<!--
lang_en is defined here to be in translation lang selection
-->
<record model="ir.lang" id="lang_en">
<field name="code">en</field>
<field name="name">English</field>
<field name="date">%m/%d/%Y</field>
<field name="am">AM</field>
<field name="pm">PM</field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">.</field>
<field name="thousands_sep">,</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">.</field>
<field name="mon_thousands_sep">,</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="False"/>
<field name="n_sep_by_space" eval="False"/>
</record>
</data>
</tryton>

667
ir/lang.py Executable file
View File

@@ -0,0 +1,667 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from ast import literal_eval
from decimal import Decimal
from itertools import takewhile
from locale import CHAR_MAX
from sql import Table
from trytond.cache import Cache
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.model import (
Check, DeactivableMixin, ModelSQL, ModelView, Unique, fields)
from trytond.modules import create_graph, load_translations
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction, inactive_records
from trytond.wizard import Button, StateTransition, StateView, Wizard
Transaction.cache_keys.add('translate_name')
class GroupingError(UserError):
pass
class DateError(UserError):
pass
class TranslatableError(UserError):
pass
class DeleteDefaultError(UserError):
pass
NO_BREAKING_SPACE = '\u00A0'
def _replace(src, old, new, escape='%'):
"""Return a copy of src with all occurrences of substring old replaced
by new if not escaped using double escape"""
def is_escape(x):
return x == escape
assert old.startswith(escape)
start = 0
while start < len(src):
i = src.find(old, start)
if i < 0:
break
# if there is an odd number of percentage before the
# placeholder then it is not escaped
if len(list(takewhile(is_escape, reversed(src[:i])))) % 2:
start = i + len(old)
else:
src = src[:i] + new + src[i + len(old):]
start = i + len(new)
return src
class Lang(DeactivableMixin, ModelSQL, ModelView):
"Language"
__name__ = "ir.lang"
name = fields.Char('Name', required=True, translate=True)
code = fields.Char('Code', required=True, help="RFC 4646 tag.")
translatable = fields.Boolean('Translatable', readonly=True)
parent = fields.Char("Parent Code", help="Code of the exceptional parent")
direction = fields.Selection([
('ltr', 'Left-to-right'),
('rtl', 'Right-to-left'),
], 'Direction', required=True)
# date
date = fields.Char("Date", required=True, strip=False)
am = fields.Char("AM", strip=False)
pm = fields.Char("PM", strip=False)
# number
grouping = fields.Char('Grouping', required=True)
decimal_point = fields.Char(
"Decimal Separator", required=True, strip=False)
thousands_sep = fields.Char("Thousands Separator", strip=False)
# monetary formatting
mon_grouping = fields.Char('Grouping', required=True)
mon_decimal_point = fields.Char(
"Decimal Separator", required=True, strip=False)
mon_thousands_sep = fields.Char('Thousands Separator', strip=False)
p_sign_posn = fields.Integer('Positive Sign Position', required=True)
n_sign_posn = fields.Integer('Negative Sign Position', required=True)
positive_sign = fields.Char("Positive Sign", strip=False)
negative_sign = fields.Char("Negative Sign", strip=False)
p_cs_precedes = fields.Boolean('Positive Currency Symbol Precedes')
n_cs_precedes = fields.Boolean('Negative Currency Symbol Precedes')
p_sep_by_space = fields.Boolean('Positive Separate by Space')
n_sep_by_space = fields.Boolean('Negative Separate by Space')
pg_text_search = fields.Char(
"PostgreSQL Text Search Configuration", readonly=True)
_lang_cache = Cache('ir.lang')
_code_cache = Cache('ir.lang.code', context=False)
@classmethod
def __setup__(cls):
super(Lang, cls).__setup__()
table = cls.__table__()
cls._sql_constraints += [
('code_unique', Unique(table, table.code),
'ir.msg_language_code_unique'),
('check_decimal_point_thousands_sep',
Check(table, table.decimal_point != table.thousands_sep),
'decimal_point and thousands_sep must be different!'),
]
cls._buttons.update({
'load_translations': {},
'unload_translations': {
'invisible': ~Eval('translatable', False),
},
})
@classmethod
def search_rec_name(cls, name, clause):
langs = cls.search([('code',) + tuple(clause[1:])], order=[])
if langs:
langs += cls.search([('name',) + tuple(clause[1:])], order=[])
return [('id', 'in', [l.id for l in langs])]
return [('name',) + tuple(clause[1:])]
@classmethod
def read(cls, ids, fields_names):
pool = Pool()
Translation = pool.get('ir.translation')
Config = pool.get('ir.configuration')
res = super(Lang, cls).read(ids, fields_names)
if (Transaction().context.get('translate_name')
and (not fields_names or 'name' in fields_names)):
with Transaction().set_context(
language=Config.get_language(),
translate_name=False):
res2 = cls.read(ids, ['id', 'code', 'name'])
for record2 in res2:
for record in res:
if record['id'] == record2['id']:
break
res_trans = Translation.get_ids(cls.__name__ + ',name',
'model', record2['code'], [record2['id']])
record['name'] = (res_trans.get(record2['id'], False)
or record2['name'])
return res
@staticmethod
def default_translatable():
return False
@staticmethod
def default_direction():
return 'ltr'
@staticmethod
def default_date():
return '%m/%d/%Y'
@staticmethod
def default_grouping():
return '[]'
@staticmethod
def default_decimal_point():
return '.'
@staticmethod
def default_thousands_sep():
return ','
@classmethod
def default_mon_grouping(cls):
return '[]'
@classmethod
def default_mon_thousands_sep(cls):
return ','
@classmethod
def default_mon_decimal_point(cls):
return '.'
@classmethod
def default_p_sign_posn(cls):
return 1
@classmethod
def default_n_sign_posn(cls):
return 1
@classmethod
def default_negative_sign(cls):
return '-'
@classmethod
def default_positive_sign(cls):
return ''
@classmethod
def default_p_cs_precedes(cls):
return True
@classmethod
def default_n_cs_precedes(cls):
return True
@classmethod
def default_p_sep_by_space(cls):
return False
@classmethod
def default_n_sep_by_space(cls):
return False
@classmethod
@ModelView.button
def load_translations(cls, languages):
pool = Pool()
Module = pool.get('ir.module')
codes = set()
cls.write(languages, {'translatable': True})
for language in languages:
code = language.code
while code:
codes.add(code)
code = get_parent_language(code)
modules = Module.search([
('state', '=', 'activated'),
])
modules = {m.name for m in modules}
for node in create_graph(modules):
load_translations(pool, node, codes)
@classmethod
@ModelView.button
def unload_translations(cls, languages):
pool = Pool()
Translation = pool.get('ir.translation')
cls.write(languages, {'translatable': False})
languages = [l.code for l in languages]
Translation.delete(Translation.search([
('lang', 'in', languages),
('module', '!=', None),
]))
@classmethod
def validate_fields(cls, languages, field_names):
super().validate_fields(languages, field_names)
cls.check_grouping(languages, field_names)
cls.check_date(languages, field_names)
cls.check_translatable(languages)
@classmethod
def check_grouping(cls, langs, fields_names=None):
'''
Check if grouping is list of numbers
'''
if fields_names and not (fields_names & {'grouping', 'mon_grouping'}):
return
for lang in langs:
for grouping in [lang.grouping, lang.mon_grouping]:
try:
grouping = literal_eval(grouping)
for i in grouping:
if not isinstance(i, int):
raise
except Exception:
raise GroupingError(
gettext('ir.msg_language_invalid_grouping',
grouping=grouping,
language=lang.rec_name))
@classmethod
def check_date(cls, langs, field_names=None):
'''
Check the date format
'''
if field_names and 'date' not in field_names:
return
for lang in langs:
date = lang.date
try:
datetime.datetime.now().strftime(date)
except Exception:
raise DateError(gettext('ir.msg_language_invalid_date',
format=lang.date,
language=lang.rec_name))
if (('%Y' not in lang.date)
or ('%b' not in lang.date
and '%B' not in lang.date
and '%m' not in lang.date
and '%-m' not in lang.date)
or ('%d' not in lang.date
and '%-d' not in lang.date
and '%j' not in lang.date
and '%-j' not in lang.date)
or ('%x' in lang.date
or '%X' in lang.date
or '%c' in lang.date
or '%Z' in lang.date)):
raise DateError(gettext(
'ir.msg_language_invalid_date',
format=lang.date,
language=lang.rec_name))
@classmethod
def check_translatable(cls, langs, field_names=None):
pool = Pool()
Config = pool.get('ir.configuration')
if field_names and 'translatable' not in field_names:
return
# Skip check for root because when languages are created from XML file,
# translatable is not yet set.
if Transaction().user == 0:
return True
for lang in langs:
if (lang.code == Config.get_language()
and not lang.translatable):
raise TranslatableError(
gettext('ir.msg_language_default_translatable',
language=lang.rec_name))
@classmethod
def check_xml_record(cls, langs, values):
pass
@classmethod
def get_translatable_languages(cls):
res = cls._lang_cache.get('translatable_languages')
if res is None:
langs = cls.search([
('translatable', '=', True),
])
res = [x.code for x in langs]
cls._lang_cache.set('translatable_languages', res)
return res
@classmethod
def create(cls, vlist):
pool = Pool()
Translation = pool.get('ir.translation')
# Clear cache
cls._lang_cache.clear()
languages = super(Lang, cls).create(vlist)
Translation._get_language_cache.clear()
_parents.clear()
return languages
@classmethod
def write(cls, langs, values, *args):
pool = Pool()
Translation = pool.get('ir.translation')
# Clear cache
cls._lang_cache.clear()
cls._code_cache.clear()
super(Lang, cls).write(langs, values, *args)
Translation._get_language_cache.clear()
_parents.clear()
@classmethod
def delete(cls, langs):
pool = Pool()
Config = pool.get('ir.configuration')
Translation = pool.get('ir.translation')
for lang in langs:
if lang.code == Config.get_language():
raise DeleteDefaultError(
gettext('ir.msg_language_delete_default',
language=lang.rec_name))
# Clear cache
cls._lang_cache.clear()
cls._code_cache.clear()
super(Lang, cls).delete(langs)
Translation._get_language_cache.clear()
_parents.clear()
@classmethod
@inactive_records
def get(cls, code=None):
"Return language instance for the code or the transaction language"
if code is None:
code = Transaction().language
lang_id = cls._code_cache.get(code)
if not lang_id:
lang, = cls.search([
('code', '=', code),
])
cls._code_cache.set(code, lang.id)
else:
lang = cls(lang_id)
return lang
def _group(self, s, monetary=False):
# Code from _group in locale.py
# Iterate over grouping intervals
def _grouping_intervals(grouping):
last_interval = 0
for interval in grouping:
# if grouping is -1, we are done
if interval == CHAR_MAX:
return
# 0: re-use last group ad infinitum
if interval == 0:
while True:
yield last_interval
yield interval
last_interval = interval
if monetary:
thousands_sep = self.mon_thousands_sep
grouping = literal_eval(self.mon_grouping)
else:
thousands_sep = self.thousands_sep
grouping = literal_eval(self.grouping)
if not grouping:
return (s, 0)
if s[-1] == ' ':
stripped = s.rstrip()
right_spaces = s[len(stripped):]
s = stripped
else:
right_spaces = ''
left_spaces = ''
groups = []
for interval in _grouping_intervals(grouping):
if not s or s[-1] not in "0123456789":
# only non-digit characters remain (sign, spaces)
left_spaces = s
s = ''
break
groups.append(s[-interval:])
s = s[:-interval]
if s:
groups.append(s)
groups.reverse()
return (
left_spaces + thousands_sep.join(groups) + right_spaces,
len(thousands_sep) * (len(groups) - 1)
)
def format(self, percent, value, grouping=False, monetary=False,
*additional):
'''
Returns the lang-aware substitution of a %? specifier (percent).
'''
# Code from format in locale.py
# Strip a given amount of excess padding from the given string
def _strip_padding(s, amount):
lpos = 0
while amount and s[lpos] == ' ':
lpos += 1
amount -= 1
rpos = len(s) - 1
while amount and s[rpos] == ' ':
rpos -= 1
amount -= 1
return s[lpos:rpos + 1]
# this is only for one-percent-specifier strings
# and this should be checked
if percent[0] != '%':
raise ValueError("format() must be given exactly one %char "
"format specifier")
if additional:
formatted = percent % ((value,) + additional)
else:
formatted = percent % value
# floats and decimal ints need special action!
if percent[-1] in 'eEfFgG':
seps = 0
parts = formatted.split('.')
if grouping:
parts[0], seps = self._group(parts[0], monetary=monetary)
if monetary:
decimal_point = self.mon_decimal_point
else:
decimal_point = self.decimal_point
formatted = decimal_point.join(parts)
if seps:
formatted = _strip_padding(formatted, seps)
elif percent[-1] in 'diu':
seps = 0
if grouping:
formatted, seps = self._group(formatted, monetary=monetary)
if seps:
formatted = _strip_padding(formatted, seps)
return formatted.replace(' ', NO_BREAKING_SPACE)
def currency(
self, val, currency, symbol=True, grouping=False, digits=None):
"""
Formats val according to the currency settings in lang.
"""
# Code from currency in locale.py
# check for illegal values
if digits is None:
digits = currency.digits
if digits == 127:
raise ValueError("Currency formatting is not possible using "
"the 'C' locale.")
s = self.format(
'%%.%if' % digits, abs(val), grouping, monetary=True)
# '<' and '>' are markers if the sign must be inserted
# between symbol and value
s = '<' + s + '>'
if symbol:
smb = currency.symbol
precedes = (val < 0 and self.n_cs_precedes
or self.p_cs_precedes)
separated = (val < 0 and self.n_sep_by_space
or self.p_sep_by_space)
if not smb and hasattr(currency, 'code'):
smb = currency.code
separated = True
if precedes:
s = smb + (separated and ' ' or '') + s
else:
s = s + (separated and ' ' or '') + smb
sign_pos = val < 0 and self.n_sign_posn or self.p_sign_posn
sign = val < 0 and self.negative_sign or self.positive_sign
if sign_pos == 0:
s = '(' + s + ')'
elif sign_pos == 1:
s = sign + s
elif sign_pos == 2:
s = s + sign
elif sign_pos == 3:
s = s.replace('<', sign)
elif sign_pos == 4:
s = s.replace('>', sign)
else:
# the default if nothing specified;
# this should be the most fitting sign position
s = sign + s
return (
s.replace('<', '').replace('>', '')
.replace(' ', NO_BREAKING_SPACE))
def strftime(self, value, format=None):
'''
Convert value to a string as specified by the format argument.
'''
pool = Pool()
Month = pool.get('ir.calendar.month')
Day = pool.get('ir.calendar.day')
if format is None:
format = self.date
if isinstance(value, datetime.datetime):
format += ' %H:%M:%S'
if '%x' in format:
format = _replace(format, '%x', self.date)
format = _replace(format, '%X', '%H:%M:%S')
if isinstance(value, datetime.date):
for f, i, klass in (('%A', 6, Day), ('%B', 1, Month)):
for field, f in [('name', f), ('abbreviation', f.lower())]:
if f in format:
locale = klass.locale(self, field=field)
fvalue = locale[value.timetuple()[i]]
format = _replace(format, f, fvalue.replace('%', '%%'))
if '%p' in format:
if isinstance(value, datetime.time):
time = value
else:
try:
time = value.time()
except AttributeError:
time = None
if time:
if time < datetime.time(12):
p = self.am or 'AM'
else:
p = self.pm or 'PM'
format = _replace(format, '%p', p.replace('%', '%%'))
return value.strftime(format).replace(' ', NO_BREAKING_SPACE)
def format_number(self, value, digits=None, grouping=True, monetary=None):
if digits is None:
d = value
if not isinstance(d, Decimal):
d = Decimal(repr(value))
digits = -int(d.as_tuple().exponent)
return self.format(
'%.*f', (digits, value), grouping=grouping, monetary=monetary)
def format_number_symbol(self, value, symbol, digits=None, grouping=True):
symbol, position = symbol.get_symbol(value)
separated = True
s = self.format_number(value, digits, grouping)
if position:
s = s + (separated and ' ' or '') + symbol
else:
s = symbol + (separated and ' ' or '') + s
return s.replace(' ', NO_BREAKING_SPACE)
class LangConfigStart(ModelView):
"Configure languages"
__name__ = 'ir.lang.config.start'
languages = fields.Many2Many('ir.lang', None, None, "Languages")
@classmethod
def default_languages(cls):
pool = Pool()
Lang = pool.get('ir.lang')
return [x.id for x in Lang.search([('translatable', '=', True)])]
class LangConfig(Wizard):
'Configure languages'
__name__ = 'ir.lang.config'
start = StateView('ir.lang.config.start',
'ir.lang_config_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Load', 'load', 'tryton-ok', default=True),
])
load = StateTransition()
def transition_load(self):
pool = Pool()
Lang = pool.get('ir.lang')
Lang.load_translations(list(self.start.languages))
untranslated_languages = Lang.search([
('id', 'not in', [l.id for l in self.start.languages]),
('translatable', '=', True),
])
Lang.unload_translations(untranslated_languages)
return 'end'
def get_parent_language(code):
if code not in _parents:
# Use SQL because it is used by load_module_graph
cursor = Transaction().connection.cursor()
lang = Table('ir_lang')
cursor.execute(*lang.select(lang.code, lang.parent))
_parents.update(cursor)
if _parents.get(code):
return _parents[code]
for sep in ['@', '_']:
if sep in code:
return code.rsplit(sep, 1)[0]
_parents = {}

574
ir/lang.xml Executable file
View File

@@ -0,0 +1,574 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<menuitem
name="Localization"
parent="menu_administration"
sequence="30"
id="menu_localization"/>
<record model="ir.lang" id="lang_bg">
<field name="code">bg</field>
<field name="name">Bulgarian</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_ca">
<field name="code">ca</field>
<field name="name">Català</field>
<field name="date">%d/%m/%Y</field>
<field name="am">a. m.</field>
<field name="pm">p. m.</field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_cs">
<field name="code">cs</field>
<field name="name">Czech</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_de">
<field name="code">de</field>
<field name="name">German</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<!--
lang_en is defined in ir.xml
-->
<record model="ir.lang" id="lang_es">
<field name="code">es</field>
<field name="name">Spanish</field>
<field name="date">%d/%m/%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_es_419">
<field name="code">es_419</field>
<field name="name">Spanish (Latin American)</field>
<field name="date">%d/%m/%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">.</field>
<field name="thousands_sep">,</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">.</field>
<field name="mon_thousands_sep">,</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_et">
<field name="code">et</field>
<field name="name">Estonian</field>
<field name="date">%d.%m.%Y</field>
<field name="grouping">[3, 3]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_fa">
<field name="code">fa</field>
<field name="name">Persian</field>
<field name="direction">rtl</field>
<field name="date">%Y/%m/%d</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 0]</field>
<field name="decimal_point">.</field>
<field name="thousands_sep">,</field>
<field name="mon_grouping">[3, 0]</field>
<field name="mon_decimal_point">٫</field>
<field name="mon_thousands_sep">٬</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_fi">
<field name="code">fi</field>
<field name="name">Finnish</field>
<field name="date">%d.%m.%Y</field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_fr">
<field name="code">fr</field>
<field name="name">French</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_hu">
<field name="code">hu</field>
<field name="name">Hungarian</field>
<field name="date">%Y-%m-%d</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_id">
<field name="code">id</field>
<field name="name">Indonesian</field>
<field name="date">%d/%m/%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="False"/>
<field name="n_sep_by_space" eval="False"/>
</record>
<record model="ir.lang" id="lang_it">
<field name="code">it</field>
<field name="name">Italian</field>
<field name="date">%d/%m/%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"></field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_lo">
<field name="code">lo</field>
<field name="name">Lao</field>
<field name="date">%d/%m/%Y</field>
<field name="am">AM</field>
<field name="pm">PM</field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">.</field>
<field name="thousands_sep">,</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">.</field>
<field name="mon_thousands_sep">,</field>
<field name="p_sign_posn" eval="4"/>
<field name="n_sign_posn" eval="4"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_lt">
<field name="code">lt</field>
<field name="name">Lithuanian</field>
<field name="date">%Y-%m-%d</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_nl">
<field name="code">nl</field>
<field name="name">Dutch</field>
<field name="date">%d-%m-%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"></field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="2"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_pl">
<field name="code">pl</field>
<field name="name">Polish</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 0, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_pt">
<field name="code">pt</field>
<field name="name">Portuguese</field>
<field name="date">%d-%m-%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"></field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_ro">
<field name="code">ro</field>
<field name="name">Romanian</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3,3]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3,3]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_ru">
<field name="code">ru</field>
<field name="name">Russian</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">.</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_sl">
<field name="code">sl</field>
<field name="name">Slovenian</field>
<field name="date">%d.%m.%Y</field>
<field name="am"></field>
<field name="pm"></field>
<field name="grouping">[]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"> </field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"> </field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_tr">
<field name="code">tr</field>
<field name="name">Turkish</field>
<field name="date">%d-%m-%Y</field>
<field name="grouping">[3, 3, 0]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep">.</field>
<field name="mon_grouping">[3, 3, 0]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep">.</field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_uk">
<field name="code">uk</field>
<field name="name">Ukrainian</field>
<field name="date">%d.%m.%Y</field>
<field name="grouping">[3, 3]</field>
<field name="decimal_point">,</field>
<field name="thousands_sep"></field>
<field name="mon_grouping">[3, 3]</field>
<field name="mon_decimal_point">,</field>
<field name="mon_thousands_sep"></field>
<field name="p_sign_posn" eval="1"/>
<field name="n_sign_posn" eval="1"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="False"/>
<field name="n_cs_precedes" eval="False"/>
<field name="p_sep_by_space" eval="True"/>
<field name="n_sep_by_space" eval="True"/>
</record>
<record model="ir.lang" id="lang_zh_CN">
<field name="code">zh_CN</field>
<field name="name">Chinese Simplified</field>
<field name="date">%Y-%m-%d</field>
<field name="am">上午</field>
<field name="pm">下午</field>
<field name="grouping">[3, 0]</field>
<field name="decimal_point">.</field>
<field name="thousands_sep">,</field>
<field name="mon_grouping">[3, 0]</field>
<field name="mon_decimal_point">.</field>
<field name="mon_thousands_sep">,</field>
<field name="p_sign_posn" eval="4"/>
<field name="n_sign_posn" eval="4"/>
<field name="positive_sign"></field>
<field name="negative_sign">-</field>
<field name="p_cs_precedes" eval="True"/>
<field name="n_cs_precedes" eval="True"/>
<field name="p_sep_by_space" eval="False"/>
<field name="n_sep_by_space" eval="False"/>
</record>
<record model="ir.ui.view" id="lang_view_tree">
<field name="model">ir.lang</field>
<field name="type">tree</field>
<field name="name">lang_list</field>
</record>
<record model="ir.ui.view" id="lang_view_form">
<field name="model">ir.lang</field>
<field name="type">form</field>
<field name="name">lang_form</field>
</record>
<record model="ir.action.act_window" id="act_lang_form">
<field name="name">Languages</field>
<field name="res_model">ir.lang</field>
<field name="context"/>
</record>
<record model="ir.action.act_window.view"
id="act_lang_form_view1">
<field name="sequence" eval="1"/>
<field name="view" ref="lang_view_tree"/>
<field name="act_window" ref="act_lang_form"/>
</record>
<record model="ir.action.act_window.view"
id="act_lang_form_view2">
<field name="sequence" eval="2"/>
<field name="view" ref="lang_view_form"/>
<field name="act_window" ref="act_lang_form"/>
</record>
<menuitem
parent="ir.menu_localization"
action="act_lang_form"
sequence="10"
id="menu_lang_form"/>
<record model="ir.model.button" id="lang_load_translations_button">
<field name="model">ir.lang</field>
<field name="name">load_translations</field>
<field name="string">Load translations</field>
<field name="confirm">Are you sure you want to load languages' translations?</field>
</record>
<record model="ir.model.button" id="lang_unload_translations_button">
<field name="model">ir.lang</field>
<field name="name">unload_translations</field>
<field name="string">Unload translations</field>
<field name="confirm">Are you sure you want to remove languages' translations?</field>
</record>
<record model="ir.ui.view" id="lang_config_start_view_form">
<field name="model">ir.lang.config.start</field>
<field name="type">form</field>
<field name="name">lang_config_start_form</field>
</record>
<record model="ir.action.wizard" id="act_lang_config">
<field name="name">Configure Languages</field>
<field name="wiz_name">ir.lang.config</field>
<field name="window" eval="True"/>
</record>
<record model="ir.module.config_wizard.item" id="config_wizard_item_lang">
<field name="action" ref="act_lang_config"/>
</record>
</data>
</tryton>

4618
ir/locale/bg.po Executable file

File diff suppressed because it is too large Load Diff

4288
ir/locale/ca.po Executable file

File diff suppressed because it is too large Load Diff

4454
ir/locale/cs.po Executable file

File diff suppressed because it is too large Load Diff

4309
ir/locale/de.po Executable file

File diff suppressed because it is too large Load Diff

4298
ir/locale/es.po Executable file

File diff suppressed because it is too large Load Diff

4247
ir/locale/es_419.po Executable file

File diff suppressed because it is too large Load Diff

4404
ir/locale/et.po Executable file

File diff suppressed because it is too large Load Diff

4449
ir/locale/fa.po Executable file

File diff suppressed because it is too large Load Diff

4429
ir/locale/fi.po Executable file

File diff suppressed because it is too large Load Diff

4312
ir/locale/fr.po Executable file

File diff suppressed because it is too large Load Diff

4496
ir/locale/hu.po Executable file

File diff suppressed because it is too large Load Diff

4299
ir/locale/id.po Executable file

File diff suppressed because it is too large Load Diff

4566
ir/locale/it.po Executable file

File diff suppressed because it is too large Load Diff

4582
ir/locale/lo.po Executable file

File diff suppressed because it is too large Load Diff

4373
ir/locale/lt.po Executable file

File diff suppressed because it is too large Load Diff

4295
ir/locale/nl.po Executable file

File diff suppressed because it is too large Load Diff

4294
ir/locale/pl.po Executable file

File diff suppressed because it is too large Load Diff

4460
ir/locale/pt.po Executable file

File diff suppressed because it is too large Load Diff

4394
ir/locale/ro.po Executable file

File diff suppressed because it is too large Load Diff

4613
ir/locale/ru.po Executable file

File diff suppressed because it is too large Load Diff

4262
ir/locale/sl.po Executable file

File diff suppressed because it is too large Load Diff

4433
ir/locale/tr.po Executable file

File diff suppressed because it is too large Load Diff

4353
ir/locale/uk.po Executable file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More