Initial import from Docker volume
This commit is contained in:
108
ir/__init__.py
Executable file
108
ir/__init__.py
Executable 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')
|
||||
BIN
ir/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
ir/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/action.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/action.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/action.cpython-311.pyc
Executable file
BIN
ir/__pycache__/action.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/attachment.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/attachment.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/attachment.cpython-311.pyc
Executable file
BIN
ir/__pycache__/attachment.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/avatar.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/avatar.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/avatar.cpython-311.pyc
Executable file
BIN
ir/__pycache__/avatar.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/cache.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/cache.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/cache.cpython-311.pyc
Executable file
BIN
ir/__pycache__/cache.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/calendar_.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/calendar_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/calendar_.cpython-311.pyc
Executable file
BIN
ir/__pycache__/calendar_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/configuration.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/configuration.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/configuration.cpython-311.pyc
Executable file
BIN
ir/__pycache__/configuration.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/cron.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/cron.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/cron.cpython-311.pyc
Executable file
BIN
ir/__pycache__/cron.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/date.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/date.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/date.cpython-311.pyc
Executable file
BIN
ir/__pycache__/date.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/email_.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/email_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/email_.cpython-311.pyc
Executable file
BIN
ir/__pycache__/email_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/error.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/error.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/error.cpython-311.pyc
Executable file
BIN
ir/__pycache__/error.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/exceptions.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/exceptions.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/exceptions.cpython-311.pyc
Executable file
BIN
ir/__pycache__/exceptions.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/export.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/export.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/export.cpython-311.pyc
Executable file
BIN
ir/__pycache__/export.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/lang.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/lang.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/lang.cpython-311.pyc
Executable file
BIN
ir/__pycache__/lang.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/message.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/message.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/message.cpython-311.pyc
Executable file
BIN
ir/__pycache__/message.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/model.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/model.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/model.cpython-311.pyc
Executable file
BIN
ir/__pycache__/model.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/module.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/module.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/module.cpython-311.pyc
Executable file
BIN
ir/__pycache__/module.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/note.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/note.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/note.cpython-311.pyc
Executable file
BIN
ir/__pycache__/note.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/queue_.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/queue_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/queue_.cpython-311.pyc
Executable file
BIN
ir/__pycache__/queue_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/resource.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/resource.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/resource.cpython-311.pyc
Executable file
BIN
ir/__pycache__/resource.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/routes.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/routes.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/routes.cpython-311.pyc
Executable file
BIN
ir/__pycache__/routes.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/rule.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/rule.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/rule.cpython-311.pyc
Executable file
BIN
ir/__pycache__/rule.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/sequence.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/sequence.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/sequence.cpython-311.pyc
Executable file
BIN
ir/__pycache__/sequence.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/session.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/session.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/session.cpython-311.pyc
Executable file
BIN
ir/__pycache__/session.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/stock.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/stock.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/translation.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/translation.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/translation.cpython-311.pyc
Executable file
BIN
ir/__pycache__/translation.cpython-311.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/trigger.cpython-311.opt-1.pyc
Executable file
BIN
ir/__pycache__/trigger.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
ir/__pycache__/trigger.cpython-311.pyc
Executable file
BIN
ir/__pycache__/trigger.cpython-311.pyc
Executable file
Binary file not shown.
1129
ir/action.py
Executable file
1129
ir/action.py
Executable file
File diff suppressed because it is too large
Load Diff
237
ir/action.xml
Executable file
237
ir/action.xml
Executable 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
86
ir/attachment.py
Executable 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
45
ir/attachment.xml
Executable 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
214
ir/avatar.py
Executable 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
10
ir/cache.py
Executable 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
67
ir/calendar_.py
Executable 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
103
ir/calendar_.xml
Executable 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
48
ir/configuration.py
Executable 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
217
ir/cron.py
Executable 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
51
ir/cron.xml
Executable 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
25
ir/date.py
Executable 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
99
ir/email.xml
Executable 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
588
ir/email_.py
Executable 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
188
ir/error.py
Executable 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
85
ir/error.xml
Executable 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
24
ir/exceptions.py
Executable 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
143
ir/export.py
Executable 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
51
ir/export.xml
Executable 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
BIN
ir/fonts/karla.ttf
Executable file
Binary file not shown.
32
ir/ir.xml
Executable file
32
ir/ir.xml
Executable 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
667
ir/lang.py
Executable 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
574
ir/lang.xml
Executable 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
4618
ir/locale/bg.po
Executable file
File diff suppressed because it is too large
Load Diff
4288
ir/locale/ca.po
Executable file
4288
ir/locale/ca.po
Executable file
File diff suppressed because it is too large
Load Diff
4454
ir/locale/cs.po
Executable file
4454
ir/locale/cs.po
Executable file
File diff suppressed because it is too large
Load Diff
4309
ir/locale/de.po
Executable file
4309
ir/locale/de.po
Executable file
File diff suppressed because it is too large
Load Diff
4298
ir/locale/es.po
Executable file
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
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
4404
ir/locale/et.po
Executable file
File diff suppressed because it is too large
Load Diff
4449
ir/locale/fa.po
Executable file
4449
ir/locale/fa.po
Executable file
File diff suppressed because it is too large
Load Diff
4429
ir/locale/fi.po
Executable file
4429
ir/locale/fi.po
Executable file
File diff suppressed because it is too large
Load Diff
4312
ir/locale/fr.po
Executable file
4312
ir/locale/fr.po
Executable file
File diff suppressed because it is too large
Load Diff
4496
ir/locale/hu.po
Executable file
4496
ir/locale/hu.po
Executable file
File diff suppressed because it is too large
Load Diff
4299
ir/locale/id.po
Executable file
4299
ir/locale/id.po
Executable file
File diff suppressed because it is too large
Load Diff
4566
ir/locale/it.po
Executable file
4566
ir/locale/it.po
Executable file
File diff suppressed because it is too large
Load Diff
4582
ir/locale/lo.po
Executable file
4582
ir/locale/lo.po
Executable file
File diff suppressed because it is too large
Load Diff
4373
ir/locale/lt.po
Executable file
4373
ir/locale/lt.po
Executable file
File diff suppressed because it is too large
Load Diff
4295
ir/locale/nl.po
Executable file
4295
ir/locale/nl.po
Executable file
File diff suppressed because it is too large
Load Diff
4294
ir/locale/pl.po
Executable file
4294
ir/locale/pl.po
Executable file
File diff suppressed because it is too large
Load Diff
4460
ir/locale/pt.po
Executable file
4460
ir/locale/pt.po
Executable file
File diff suppressed because it is too large
Load Diff
4394
ir/locale/ro.po
Executable file
4394
ir/locale/ro.po
Executable file
File diff suppressed because it is too large
Load Diff
4613
ir/locale/ru.po
Executable file
4613
ir/locale/ru.po
Executable file
File diff suppressed because it is too large
Load Diff
4262
ir/locale/sl.po
Executable file
4262
ir/locale/sl.po
Executable file
File diff suppressed because it is too large
Load Diff
4433
ir/locale/tr.po
Executable file
4433
ir/locale/tr.po
Executable file
File diff suppressed because it is too large
Load Diff
4353
ir/locale/uk.po
Executable file
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
Reference in New Issue
Block a user