Open Tradon processes inside Tryton

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-29 12:57:48 +02:00
parent 6441bbb31d
commit 9facb61cae
4 changed files with 267 additions and 3 deletions

View File

@@ -34,6 +34,7 @@ from . import (
backtoback,
service,
invoice,
process_documentation,
routes,
)
@@ -60,6 +61,7 @@ def register():
lc.LCMessage,
lc.CreateLCStart,
global_reporting.GRConfiguration,
process_documentation.ProcessDocumentation,
pricing.ImportPricesStart,
pricing.ImportPricesResult,
module='purchase_trade', type_='model')

View File

@@ -0,0 +1,238 @@
# 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 base64
import html
import json
import mimetypes
import os
import posixpath
import re
from pathlib import Path
from urllib.parse import unquote
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
DOCUMENTATION_ROOT = Path(__file__).with_name('process_documentation')
MARKDOWN_LINK = re.compile(r'(!?)\[([^\]]*)\]\(([^)]+)\)')
class ProcessDocumentation(ModelSingleton, ModelSQL, ModelView):
"Process Documentation"
__name__ = 'purchase_trade.process.documentation'
content = fields.Function(fields.Text('Content'), 'get_content')
def get_content(self, name):
pages = _documentation_pages()
return HTML_TEMPLATE % {
'initial': pages.get('README.md', ''),
'pages': json.dumps(pages).replace('</', '<\\/'),
}
def _documentation_pages():
pages = {}
for path in sorted(DOCUMENTATION_ROOT.rglob('*.md')):
relative_name = _relative_name(path)
markdown = path.read_text(encoding='utf-8')
pages[relative_name] = _markdown_to_html(markdown, relative_name)
return pages
def _relative_name(path):
return path.relative_to(DOCUMENTATION_ROOT.resolve()).as_posix()
def _resolve_link(current_file, target):
if target.startswith(('#', 'http://', 'https://', 'mailto:')):
return target
target = unquote(target).replace('\\', '/')
current_dir = posixpath.dirname(current_file)
return posixpath.normpath(posixpath.join(current_dir, target)).lstrip('/')
def _asset_data_uri(filename):
path = (DOCUMENTATION_ROOT / Path(*filename.split('/'))).resolve()
root = DOCUMENTATION_ROOT.resolve()
common_path = os.path.commonpath([str(root), str(path)])
if path != root and common_path == str(root):
if path.exists() and path.is_file():
mimetype = mimetypes.guess_type(str(path))[0]
mimetype = mimetype or 'application/octet-stream'
data = base64.b64encode(path.read_bytes()).decode('ascii')
return 'data:%s;base64,%s' % (mimetype, data)
def _inline(markdown, current_file):
parts = []
last = 0
for match in MARKDOWN_LINK.finditer(markdown):
parts.append(html.escape(markdown[last:match.start()]))
is_image, label, target = match.groups()
resolved = _resolve_link(current_file, target)
label = html.escape(label)
if is_image:
source = _asset_data_uri(resolved)
if source:
parts.append('<img src="%s" alt="%s"/>' % (
html.escape(source, quote=True), label))
else:
parts.append(label)
elif resolved.endswith('.md'):
parts.append('<a href="#" data-doc="%s">%s</a>' % (
html.escape(resolved, quote=True), label))
elif resolved.startswith(('#', 'http://', 'https://', 'mailto:')):
parts.append('<a href="%s">%s</a>' % (
html.escape(resolved, quote=True), label))
else:
parts.append(html.escape(match.group(0)))
last = match.end()
parts.append(html.escape(markdown[last:]))
return ''.join(parts)
def _markdown_to_html(markdown, current_file):
body = []
table = []
in_list = False
def close_list():
nonlocal in_list
if in_list:
body.append('</ul>')
in_list = False
def flush_table():
nonlocal table
if table:
close_list()
body.append(_render_table(table, current_file))
table = []
for line in markdown.splitlines():
stripped = line.strip()
if stripped.startswith('|') and stripped.endswith('|'):
table.append(stripped)
continue
flush_table()
if not stripped:
close_list()
continue
if stripped.startswith('#'):
close_list()
level = len(stripped) - len(stripped.lstrip('#'))
level = min(level, 6)
title = stripped[level:].strip()
body.append('<h%d>%s</h%d>' % (
level, _inline(title, current_file), level))
elif stripped.startswith('- '):
if not in_list:
body.append('<ul>')
in_list = True
body.append('<li>%s</li>' % _inline(
stripped[2:].strip(), current_file))
else:
close_list()
body.append('<p>%s</p>' % _inline(stripped, current_file))
flush_table()
close_list()
if current_file != 'README.md':
body.insert(
0, '<p><a href="#" data-doc="README.md">Back to index</a></p>')
return '\n'.join(body)
def _render_table(lines, current_file):
rows = [
[cell.strip() for cell in line.strip('|').split('|')]
for line in lines
]
header = rows[0] if rows else []
data = rows[1:]
if data and all(set(cell) <= {'-', ':'} for cell in data[0]):
data = data[1:]
output = ['<table>', '<thead><tr>']
for cell in header:
output.append('<th>%s</th>' % _inline(cell, current_file))
output.append('</tr></thead><tbody>')
for row in data:
output.append('<tr>')
for cell in row:
output.append('<td>%s</td>' % _inline(cell, current_file))
output.append('</tr>')
output.append('</tbody></table>')
return ''.join(output)
HTML_TEMPLATE = '''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style>
body {
color: #1f2933;
font-family: Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 24px;
}
a {
color: #0b63ce;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
table {
border-collapse: collapse;
margin: 16px 0;
width: 100%%;
}
th,
td {
border: 1px solid #d7dde5;
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
th {
background: #f3f6f9;
}
img {
height: auto;
max-width: 100%%;
}
</style>
</head>
<body>
<main id="content">%(initial)s</main>
<script>
const pages = %(pages)s;
const content = document.getElementById('content');
function showDocument(name) {
content.innerHTML = pages[name] || pages['README.md'];
window.scrollTo(0, 0);
}
document.addEventListener('click', function(event) {
const link = event.target.closest('a[data-doc]');
if (!link) {
return;
}
event.preventDefault();
showDocument(link.dataset.doc);
});
showDocument('README.md');
</script>
</body>
</html>
'''

View File

@@ -3,9 +3,21 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.action.url" id="url_tradon_processes">
<record model="ir.ui.view" id="process_documentation_view_form">
<field name="model">purchase_trade.process.documentation</field>
<field name="type">form</field>
<field name="name">process_documentation_form</field>
</record>
<record model="ir.action.act_window" id="act_tradon_processes">
<field name="name">Tradon Processes</field>
<field name="url">/tradon/purchase_trade/processes</field>
<field name="res_model">purchase_trade.process.documentation</field>
</record>
<record model="ir.action.act_window.view"
id="act_tradon_processes_view_form">
<field name="sequence" eval="10"/>
<field name="view" ref="process_documentation_view_form"/>
<field name="act_window" ref="act_tradon_processes"/>
</record>
<menuitem
@@ -17,8 +29,16 @@ this repository contains the full copyright notices and license terms. -->
<menuitem
name="Tradon Processes"
parent="menu_help_processes"
action="url_tradon_processes"
action="act_tradon_processes"
sequence="10"
id="menu_tradon_processes"/>
<record model="ir.model.access" id="access_process_documentation">
<field name="model">purchase_trade.process.documentation</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,4 @@
<form col="1">
<field name="content" widget="html_viewer" height="900"/>
</form>