Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
93
mklab_dms_document/README.md
Normal file
93
mklab_dms_document/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# mklab_dms_document
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
## Add your files
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://gitlab.inf-centre.ru/mklab-base/documents/mklab_dms_document.git
|
||||
git branch -M master
|
||||
git push -uf origin master
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://gitlab.inf-centre.ru/mklab-base/documents/mklab_dms_document/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
2
mklab_dms_document/__init__.py
Normal file
2
mklab_dms_document/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import models
|
||||
30
mklab_dms_document/__manifest__.py
Normal file
30
mklab_dms_document/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'DMS — Документы (входящие/исходящие/внутренние)',
|
||||
'summary': 'Учёт входящих, исходящих и внутренних документов с хранением в DMS',
|
||||
'description': """
|
||||
DMS — Документы
|
||||
===============
|
||||
|
||||
Учёт входящих, исходящих и внутренних документов организации:
|
||||
шаблоны текста, формирование PDF, запись в хранилище DMS,
|
||||
цепочки документов и многоступенчатая валидация (tier validation).
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Document Management',
|
||||
'version': '19.0.2026.09.17',
|
||||
'depends': ['base', 'dms', 'base_tier_validation'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/data.xml',
|
||||
'views/views.xml',
|
||||
'views/report.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
2
mklab_dms_document/controllers/__init__.py
Normal file
2
mklab_dms_document/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import controllers
|
||||
20
mklab_dms_document/controllers/controllers.py
Normal file
20
mklab_dms_document/controllers/controllers.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# from odoo import http
|
||||
|
||||
|
||||
# class MklabDmsDocument(http.Controller):
|
||||
# @http.route('/mklab_dms_document/mklab_dms_document/', auth='public')
|
||||
# def index(self, **kw):
|
||||
# return "Hello, world"
|
||||
|
||||
# @http.route('/mklab_dms_document/mklab_dms_document/objects/', auth='public')
|
||||
# def list(self, **kw):
|
||||
# return http.request.render('mklab_dms_document.listing', {
|
||||
# 'root': '/mklab_dms_document/mklab_dms_document',
|
||||
# 'objects': http.request.env['mklab_dms_document.mklab_dms_document'].search([]),
|
||||
# })
|
||||
|
||||
# @http.route('/mklab_dms_document/mklab_dms_document/objects/<model("mklab_dms_document.mklab_dms_document"):obj>/', auth='public')
|
||||
# def object(self, obj, **kw):
|
||||
# return http.request.render('mklab_dms_document.object', {
|
||||
# 'object': obj
|
||||
# })
|
||||
25
mklab_dms_document/data/data.xml
Normal file
25
mklab_dms_document/data/data.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<odoo>
|
||||
<record id="seq_ir_dms_document" model="ir.sequence">
|
||||
<field name="name">Входящий документ</field>
|
||||
<field name="code">dms.document_number</field>
|
||||
<field name="prefix">InDoc</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_ir_dms_document_out" model="ir.sequence">
|
||||
<field name="name">Исходящий документ</field>
|
||||
<field name="code">dms.document_number_out</field>
|
||||
<field name="prefix">OutDoc</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_ir_dms_document_internal" model="ir.sequence">
|
||||
<field name="name">Внутренний документ документ</field>
|
||||
<field name="code">dms.document_number_internal</field>
|
||||
<field name="prefix">InternalDoc</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
55
mklab_dms_document/demo/demo.xml
Normal file
55
mklab_dms_document/demo/demo.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Шаблон документа -->
|
||||
<record id="demo_dms_template_letter" model="dms.template">
|
||||
<field name="name">Шаблон исходящего письма</field>
|
||||
<field name="text"><p>Уважаемые коллеги,</p><p>Настоящим сообщаем вам о ...</p><p>С уважением,<br/>${user.name}</p></field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_dms_template_request" model="dms.template">
|
||||
<field name="name">Шаблон запроса документов</field>
|
||||
<field name="text"><p>Просим предоставить следующие документы:</p><ul><li>Счёт-фактура</li><li>Товарная накладная</li></ul></field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Входящий документ -->
|
||||
<record id="demo_dms_document_incoming" model="dms.document">
|
||||
<field name="type_document">incoming</field>
|
||||
<field name="name">ВХ-2026-001</field>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="date">2026-01-10</field>
|
||||
<field name="text"><p>Входящее письмо от поставщика с коммерческим предложением.</p></field>
|
||||
<field name="incoming_file_type">pdf</field>
|
||||
<field name="state">draft</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Исходящий документ -->
|
||||
<record id="demo_dms_document_outgoing" model="dms.document">
|
||||
<field name="type_document">outgoing</field>
|
||||
<field name="name">ИСХ-2026-001</field>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="date">2026-01-12</field>
|
||||
<field name="text"><p>Исходящее письмо покупателю с подтверждением заказа.</p></field>
|
||||
<field name="incoming_file_type">pdf</field>
|
||||
<field name="state">draft</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
<!-- Внутренний документ -->
|
||||
<record id="demo_dms_document_internal" model="dms.document">
|
||||
<field name="type_document">internal</field>
|
||||
<field name="name">ВН-2026-001</field>
|
||||
<field name="partner_id" ref="base.main_partner"/>
|
||||
<field name="date">2026-01-15</field>
|
||||
<field name="text"><p>Внутренняя служебная записка о порядке работы с документами.</p></field>
|
||||
<field name="incoming_file_type">pdf</field>
|
||||
<field name="state">draft</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
5
mklab_dms_document/models/__init__.py
Normal file
5
mklab_dms_document/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
from . import res_partner
|
||||
from . import document
|
||||
from . import tier_definition
|
||||
from . import tier_dms
|
||||
291
mklab_dms_document/models/document.py
Normal file
291
mklab_dms_document/models/document.py
Normal file
@@ -0,0 +1,291 @@
|
||||
import logging
|
||||
from odoo import api, fields, models, tools, exceptions
|
||||
import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from werkzeug import urls
|
||||
import functools
|
||||
import base64
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
mako_template_env = SandboxedEnvironment(
|
||||
block_start_string="<%",
|
||||
block_end_string="%>",
|
||||
variable_start_string="${",
|
||||
variable_end_string="}",
|
||||
comment_start_string="<%doc>",
|
||||
comment_end_string="</%doc>",
|
||||
line_statement_prefix="%",
|
||||
line_comment_prefix="##",
|
||||
trim_blocks=True, # do not output newline after blocks
|
||||
autoescape=True, # XML/HTML automatic escaping
|
||||
)
|
||||
mako_template_env.globals.update({
|
||||
'str': str,
|
||||
'quote': urls.url_quote,
|
||||
'urlencode': urls.url_encode,
|
||||
'datetime': datetime,
|
||||
'len': len,
|
||||
'abs': abs,
|
||||
'min': min,
|
||||
'max': max,
|
||||
'sum': sum,
|
||||
'filter': filter,
|
||||
'reduce': functools.reduce,
|
||||
'map': map,
|
||||
'round': round,
|
||||
'relativedelta': lambda *a, **kw: relativedelta(*a, **kw),
|
||||
})
|
||||
except ImportError:
|
||||
_logger.warning("jinja2 not available, templating features will not work!")
|
||||
|
||||
class DmsDocumentChoiseTemplate(models.TransientModel):
|
||||
_name = 'dms.choise_template'
|
||||
_description = 'mklab_dms_document_choise_template'
|
||||
|
||||
temp_id = fields.Many2one(comodel_name='dms.template', string='Шаблон', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
doc_id = fields.Many2one(comodel_name='dms.document', string='Документ')
|
||||
company_id = fields.Many2one(comodel_name='res.company', string='Компания', required=True, index=True, default=lambda self: self.env.company)
|
||||
|
||||
def get_choise(self):
|
||||
for s in self:
|
||||
if s.temp_id:
|
||||
s.doc_id.text = s.temp_id.text
|
||||
|
||||
class DmsDocumentTemplate(models.Model):
|
||||
_name = 'dms.template'
|
||||
_description = 'mklab_dms_document_template'
|
||||
|
||||
name = fields.Char(string='Наименование', required=True, tracking=True)
|
||||
text = fields.Html(string='Текст', required=True, tracking=True)
|
||||
company_id = fields.Many2one(comodel_name='res.company', string='Компания', required=True, index=True, default=lambda self: self.env.company, tracking=True)
|
||||
|
||||
class DmsDocument(models.Model):
|
||||
_name = 'dms.document'
|
||||
_description = 'Документ DMS'
|
||||
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def create_pdf(self):
|
||||
report_action = self.env.ref('mklab_dms_document.action_report_dms_document').sudo()
|
||||
for s in self:
|
||||
if s.type_document=='outgoing':
|
||||
pdf, _ = report_action._render_qweb_pdf(report_action.report_name, [s.id])
|
||||
s.incoming_file = base64.b64encode(pdf)
|
||||
s.incoming_file_type = 'pdf'
|
||||
if not s.incoming_file and s.incoming_file_type == 'pdf':
|
||||
raise exceptions.UserError('Не указан файл для сохранения')
|
||||
if not s.incoming_file_other and s.incoming_file_type != 'pdf':
|
||||
raise exceptions.UserError('Не указан файл для сохранения')
|
||||
if not s.directory_id:
|
||||
raise exceptions.UserError('Не указана директория для сохранения файла')
|
||||
content = s.incoming_file if s.incoming_file_type == 'pdf' else s.incoming_file_other
|
||||
save_file = self.env['dms.file'].sudo().create({
|
||||
'name': s.name,
|
||||
'directory_id': s.directory_id.id,
|
||||
'content': content,
|
||||
})
|
||||
s.file = save_file
|
||||
s.state = 'done'
|
||||
|
||||
def open_choise_template(self):
|
||||
company_id = False
|
||||
if self.company_id:
|
||||
company_id = self.company_id.id
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': "Выберите шаблон",
|
||||
'res_model': 'dms.choise_template',
|
||||
'target': 'new',
|
||||
'view_mode': 'form',
|
||||
'context': {'default_doc_id': self.id, 'company_id': company_id},
|
||||
}
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_done(self):
|
||||
# Штатный механизм запрета удаления (вместо raise прямо в unlink):
|
||||
# корректно уживается с каскадами ondelete и групповыми операциями.
|
||||
for s in self:
|
||||
if s.state == 'done':
|
||||
raise exceptions.ValidationError('Нельзя удалить документ, записанный в хранилище')
|
||||
|
||||
# Соответствие типа документа коду последовательности нумерации.
|
||||
_SEQUENCE_BY_TYPE = {
|
||||
'incoming': 'dms.document_number',
|
||||
'outgoing': 'dms.document_number_out',
|
||||
'internal': 'dms.document_number_internal',
|
||||
}
|
||||
|
||||
def _next_document_number(self, type_document):
|
||||
"""Следующий номер по типу документа из соответствующей последовательности."""
|
||||
code = self._SEQUENCE_BY_TYPE.get(type_document)
|
||||
if not code:
|
||||
return False
|
||||
return self.env['ir.sequence'].next_by_code(code)
|
||||
|
||||
type_document = fields.Selection(
|
||||
[('incoming', 'Входящий документ'),
|
||||
('outgoing', 'Исходящий документ'),
|
||||
('internal', 'Внутренний документ')],
|
||||
string='Тип', required=True, tracking=True)
|
||||
name = fields.Char(string='Номер', copy=False, tracking=True)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name='res.partner', string='Контакт', required=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
tracking=True)
|
||||
date = fields.Date(
|
||||
string='Дата документа', default=fields.Date.context_today,
|
||||
required=True, tracking=True)
|
||||
text = fields.Html(string='Текст')
|
||||
incoming_file = fields.Binary(string='Входящий файл (PDF)')
|
||||
incoming_file_other = fields.Binary(string='Входящий файл')
|
||||
incoming_file_type = fields.Selection(
|
||||
[('pdf', 'PDF'), ('other', 'Другой')],
|
||||
string='Тип входящего файла', default='pdf', tracking=True)
|
||||
text_str = fields.Text(string='Текст шапки')
|
||||
text_render = fields.Html(string='Обработанный текст')
|
||||
file = fields.Many2one(
|
||||
comodel_name='dms.file', string='Связанный файл',
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
tracking=True)
|
||||
link_model = fields.Many2one(
|
||||
comodel_name='ir.model', string='Связанная модель', tracking=True)
|
||||
res_id = fields.Integer(
|
||||
string='ID документа в связанной модели', tracking=True)
|
||||
print_head = fields.Boolean(
|
||||
string='Печатать шапку', default=False, tracking=True)
|
||||
state = fields.Selection(
|
||||
[('draft', 'Черновик'), ('done', 'Записано в хранилище')],
|
||||
string='Статус', default='draft', tracking=True)
|
||||
parent_id = fields.Many2one(
|
||||
comodel_name='dms.document', string='Родительский документ',
|
||||
domain="[('id', '!=', id), '|', ('partner_id', '=', partner_id),"
|
||||
" ('partner_id.parent_id', '=', partner_id),"
|
||||
" '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
tracking=True)
|
||||
child_ids = fields.One2many(
|
||||
comodel_name='dms.document', inverse_name='parent_id',
|
||||
string='Дочерние документы')
|
||||
subordinate_ids = fields.One2many(
|
||||
comodel_name='dms.document', string='Все связанные документы',
|
||||
compute='_compute_subordinates', compute_sudo=True)
|
||||
company_id = fields.Many2one(comodel_name='res.company', string='Компания', required=True, index=True, default=lambda self: self.env.company, tracking=True)
|
||||
directory_id = fields.Many2one(
|
||||
comodel_name="dms.directory",
|
||||
string="Директория хранения",
|
||||
domain="[('permission_create', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
context={'dms_directory_show_path': True},
|
||||
ondelete="restrict",
|
||||
auto_join=True,
|
||||
index=True,
|
||||
tracking=True
|
||||
#domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"
|
||||
)
|
||||
|
||||
child_all_count = fields.Integer(
|
||||
string='Количество связанных документов',
|
||||
compute='_compute_subordinates', store=False, compute_sudo=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name'):
|
||||
type_document = vals.get('type_document') \
|
||||
or self.env.context.get('default_type_document')
|
||||
number = self._next_document_number(type_document)
|
||||
if number:
|
||||
vals['name'] = number
|
||||
return super().create(vals_list)
|
||||
|
||||
def _get_subordinates(self, parents=None):
|
||||
if not parents:
|
||||
parents = self.env[self._name]
|
||||
|
||||
indirect_subordinates = self.env[self._name]
|
||||
parents |= self
|
||||
direct_subordinates = self.child_ids - parents
|
||||
for child in direct_subordinates:
|
||||
child_subordinate = child._get_subordinates(parents=parents)
|
||||
indirect_subordinates |= child_subordinate
|
||||
return indirect_subordinates | direct_subordinates
|
||||
|
||||
|
||||
@api.depends('child_ids', 'child_ids.child_all_count')
|
||||
def _compute_subordinates(self):
|
||||
for doc in self:
|
||||
doc.subordinate_ids = doc._get_subordinates()
|
||||
doc.child_all_count = len(doc.subordinate_ids)
|
||||
|
||||
|
||||
def render_template(self):
|
||||
for s in self:
|
||||
template = s.text
|
||||
template_new = s.text
|
||||
model = s.link_model.model
|
||||
res_id = s.res_id
|
||||
render_result = 'Не удалось обработать текст'
|
||||
substrstart = "${"
|
||||
substrend = "}"
|
||||
substr_new_start = "<"
|
||||
substr_new_end = ">"
|
||||
flag = True
|
||||
start = 0
|
||||
s.text_str = ''
|
||||
end = len(template)-1
|
||||
while flag:
|
||||
try: #search for the item
|
||||
index = template.index(substrstart,start,end)
|
||||
start = index+len(substrstart)
|
||||
indexend = template.index(substrend,start,end)
|
||||
substr = template[index:indexend+1]
|
||||
substr_new_str = substr
|
||||
#s.text_str += str(substr)+ '; '
|
||||
if substr.find(substr_new_start)>0 and substr.find(substr_new_end)>0:
|
||||
new_flag = True
|
||||
start_new = 0
|
||||
end_new = len(substr)-1
|
||||
while new_flag:
|
||||
try: #search for the item
|
||||
index_new = substr.index(substr_new_start,start_new,end_new)
|
||||
start_new = index_new+len(substr_new_start)
|
||||
indexend_new = substr.index(substr_new_end,start_new,end_new)
|
||||
substr_new = substr[index_new:indexend_new+1]
|
||||
#s.text_str += str(index_new) + '-' + str(indexend_new) +':'+str(substr_new)+ '; '
|
||||
substr_new_str = substr_new_str.replace(substr_new,'')
|
||||
except ValueError:
|
||||
new_flag=False
|
||||
template_new = template_new.replace(substr,substr_new_str)
|
||||
except ValueError:
|
||||
flag = False
|
||||
parent = ''
|
||||
if s.parent_id:
|
||||
type_doc_parent = dict(self._fields['type_document'].selection).get(s.parent_id.type_document)
|
||||
parent = ' В ответ на {} {} от {}.'.format(type_doc_parent, s.parent_id.name,s.parent_id.date.strftime('%d.%m.%Y'))
|
||||
type_doc = dict(self._fields['type_document'].selection).get(s.type_document)
|
||||
s.text_str = '{} {} от {}.{}'.format(type_doc, s.name,s.date.strftime('%d.%m.%Y'), parent)
|
||||
template = template_new
|
||||
#res = re.findall(r"(${})", template)
|
||||
try:
|
||||
template = mako_template_env.from_string(tools.ustr(template))
|
||||
except:
|
||||
template = mako_template_env.from_string(tools.ustr(''))
|
||||
user = self.env.user
|
||||
variables = {
|
||||
'user': user
|
||||
}
|
||||
if model and res_id:
|
||||
record = self.env[model].browse(res_id)
|
||||
variables['object'] = record
|
||||
|
||||
try:
|
||||
render_result = template.render(variables)
|
||||
except Exception:
|
||||
_logger.error("Failed to render template %r using values %r" % (template, variables))
|
||||
render_result = ""
|
||||
if render_result == "False":
|
||||
render_result = ""
|
||||
s.text_render = render_result
|
||||
return render_result
|
||||
|
||||
25
mklab_dms_document/models/res_partner.py
Normal file
25
mklab_dms_document/models/res_partner.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class DmsResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def action_show_document_incoming(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id(
|
||||
'mklab_dms_document.action_window_incoming')
|
||||
action['domain'] = [('partner_id', '=', self.id),
|
||||
('type_document', '=', 'incoming')]
|
||||
action['context'] = {'default_partner_id': self.id,
|
||||
'default_type_document': 'incoming'}
|
||||
return action
|
||||
|
||||
def action_show_document_outgoing(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id(
|
||||
'mklab_dms_document.action_window_outgoing')
|
||||
action['domain'] = [('partner_id', '=', self.id),
|
||||
('type_document', '=', 'outgoing')]
|
||||
action['context'] = {'default_partner_id': self.id,
|
||||
'default_type_document': 'outgoing'}
|
||||
return action
|
||||
11
mklab_dms_document/models/tier_definition.py
Normal file
11
mklab_dms_document/models/tier_definition.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class TierDefinition(models.Model):
|
||||
_inherit = "tier.definition"
|
||||
|
||||
@api.model
|
||||
def _get_tier_validation_model_names(self):
|
||||
res = super()._get_tier_validation_model_names()
|
||||
res.append("dms.document")
|
||||
return res
|
||||
9
mklab_dms_document/models/tier_dms.py
Normal file
9
mklab_dms_document/models/tier_dms.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class TirerDmsDocument(models.Model):
|
||||
_name = "dms.document"
|
||||
_inherit = ["dms.document", "tier.validation"]
|
||||
_state_field = "state"
|
||||
_state_from = ["draft"]
|
||||
_state_to = ['done']
|
||||
5
mklab_dms_document/security/ir.model.access.csv
Normal file
5
mklab_dms_document/security/ir.model.access.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_dms_document,dms.document,model_dms_document,base.group_user,1,1,1,1
|
||||
access_dms_template_admin,dms_template_admin,model_dms_template,dms.group_dms_manager,1,1,1,1
|
||||
access_dms_template,dms_template,model_dms_template,base.group_user,1,0,0,0
|
||||
access_dms_choise_template,dms_choise_template,model_dms_choise_template,base.group_user,1,1,1,1
|
||||
|
1
mklab_dms_document/tests/__init__.py
Normal file
1
mklab_dms_document/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_dms_document
|
||||
113
mklab_dms_document/tests/test_dms_document.py
Normal file
113
mklab_dms_document/tests/test_dms_document.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Tests for mklab_dms_document — DMS document lifecycle, PDF creation,
|
||||
and template rendering.
|
||||
|
||||
Validates: Requirements 18.1, 18.2, 18.3, 18.4
|
||||
"""
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_partner(env):
|
||||
"""Create a minimal company partner."""
|
||||
return env['res.partner'].create({
|
||||
'name': 'Test DMS Partner',
|
||||
'is_company': True,
|
||||
})
|
||||
|
||||
|
||||
def _make_document(env, partner, state='draft', text=None):
|
||||
"""Create a minimal dms.document record."""
|
||||
vals = {
|
||||
'type_document': 'incoming',
|
||||
'partner_id': partner.id,
|
||||
'name': 'TEST-DOC-001',
|
||||
'state': state,
|
||||
}
|
||||
if text is not None:
|
||||
vals['text'] = text
|
||||
return env['dms.document'].create(vals)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestDmsDocument
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDmsDocument(TransactionCase):
|
||||
"""
|
||||
Tests for dms.document: unlink restrictions, create_pdf validation,
|
||||
and render_template output.
|
||||
|
||||
Validates: Requirements 18.1, 18.2, 18.3, 18.4
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = _make_partner(self.env)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Requirement 18.1 — unlink of a 'done' document raises ValidationError
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_unlink_done_raises(self):
|
||||
"""
|
||||
Req 18.1 — deleting a document in state 'done' must raise
|
||||
ValidationError.
|
||||
"""
|
||||
doc = _make_document(self.env, self.partner, state='done')
|
||||
with self.assertRaises(ValidationError):
|
||||
doc.unlink()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Requirement 18.2 — unlink of a draft document succeeds
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_unlink_draft_ok(self):
|
||||
"""
|
||||
Req 18.2 — deleting a document in state 'draft' must succeed
|
||||
without raising any exception.
|
||||
"""
|
||||
doc = _make_document(self.env, self.partner, state='draft')
|
||||
# Should not raise
|
||||
doc.unlink()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Requirement 18.3 — create_pdf without directory raises UserError
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_create_pdf_no_directory_raises(self):
|
||||
"""
|
||||
Req 18.3 — calling create_pdf on a document with no directory_id
|
||||
set must raise UserError.
|
||||
"""
|
||||
doc = _make_document(self.env, self.partner, state='draft')
|
||||
# Ensure no directory is set
|
||||
doc.directory_id = False
|
||||
with self.assertRaises(UserError):
|
||||
doc.create_pdf()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Requirement 18.4 — render_template with a simple template returns
|
||||
# a non-empty string
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_render_template_basic(self):
|
||||
"""
|
||||
Req 18.4 — render_template on a document with a simple Mako/Jinja2
|
||||
template must return a non-empty string.
|
||||
"""
|
||||
doc = _make_document(
|
||||
self.env,
|
||||
self.partner,
|
||||
state='draft',
|
||||
text='<p>Hello World</p>',
|
||||
)
|
||||
result = doc.render_template()
|
||||
self.assertTrue(
|
||||
result and len(result) > 0,
|
||||
"render_template should return a non-empty string for a simple template",
|
||||
)
|
||||
53
mklab_dms_document/views/report.xml
Normal file
53
mklab_dms_document/views/report.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="report_dms_document">
|
||||
<t t-call="web.basic_layout">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-set="company" t-value="o.company_id or res_company"/>
|
||||
<div class="header">
|
||||
<table class="table" style="width:100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<t t-set="render" t-value="o.render_template()"/>
|
||||
<td>
|
||||
<t t-if="o.print_head">
|
||||
<span t-out="o.text_str"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page">
|
||||
<span t-out="o.text_render"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<record id="paperformat_a4" model="report.paperformat">
|
||||
<field name="name">DMS Документ A4</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">15</field>
|
||||
<field name="margin_bottom">20</field>
|
||||
<field name="margin_left">7</field>
|
||||
<field name="margin_right">7</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">10</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<record id="action_report_dms_document" model="ir.actions.report">
|
||||
<field name="name">DMS Документ</field>
|
||||
<field name="model">dms.document</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mklab_dms_document.report_dms_document</field>
|
||||
<field name="report_file">mklab_dms_document.report_dms_document</field>
|
||||
<field name="print_report_name">'Документ - %s' % (object.name)</field>
|
||||
<field name="paperformat_id" ref="paperformat_a4"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
292
mklab_dms_document/views/views.xml
Normal file
292
mklab_dms_document/views/views.xml
Normal file
@@ -0,0 +1,292 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Кнопки документов в карточке контакта -->
|
||||
<record id="res_partner_inherit" model="ir.ui.view">
|
||||
<field name="name">mklab_dms_document.res_partner_inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button" type="object" icon="fa-inbox"
|
||||
name="action_show_document_incoming"
|
||||
string="Входящие"/>
|
||||
<button class="oe_stat_button" type="object" icon="fa-paper-plane"
|
||||
name="action_show_document_outgoing"
|
||||
string="Исходящие"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Мультикомпанийное правило -->
|
||||
<record id="rule_company_document" model="ir.rule">
|
||||
<field name="name">Документы: мультикомпания</field>
|
||||
<field name="model_id" ref="model_dms_document"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Визард выбора шаблона -->
|
||||
<record model="ir.ui.view" id="form_template_wiz">
|
||||
<field name="name">dms.choise_template.form</field>
|
||||
<field name="model">dms.choise_template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Выбор шаблона">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="temp_id"/>
|
||||
<field name="doc_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Выбрать" name="get_choise" type="object"
|
||||
class="btn-primary"/>
|
||||
<button string="Отмена" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Шаблоны: список и форма -->
|
||||
<record model="ir.ui.view" id="list_template">
|
||||
<field name="name">dms.template.list</field>
|
||||
<field name="model">dms.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Шаблоны документов">
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="form_template">
|
||||
<field name="name">dms.template.form</field>
|
||||
<field name="model">dms.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Шаблон документа">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="Наименование шаблона"/></h1>
|
||||
</div>
|
||||
<field name="text"/>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Документы: список -->
|
||||
<record model="ir.ui.view" id="list">
|
||||
<field name="name">dms.document.list</field>
|
||||
<field name="model">dms.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Документы"
|
||||
decoration-muted="state == 'done'">
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="type_document"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-success="state == 'done'"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Документы: поиск -->
|
||||
<record model="ir.ui.view" id="search_document">
|
||||
<field name="name">dms.document.search</field>
|
||||
<field name="model">dms.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Поиск документов">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<filter name="draft" string="Черновики"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="done" string="В хранилище"
|
||||
domain="[('state', '=', 'done')]"/>
|
||||
<separator/>
|
||||
<filter name="incoming" string="Входящие"
|
||||
domain="[('type_document', '=', 'incoming')]"/>
|
||||
<filter name="outgoing" string="Исходящие"
|
||||
domain="[('type_document', '=', 'outgoing')]"/>
|
||||
<filter name="internal" string="Внутренние"
|
||||
domain="[('type_document', '=', 'internal')]"/>
|
||||
<separator/>
|
||||
<filter name="group_type" string="Тип"
|
||||
context="{'group_by': 'type_document'}"/>
|
||||
<filter name="group_partner" string="Контакт"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter name="group_state" string="Статус"
|
||||
context="{'group_by': 'state'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Документы: форма -->
|
||||
<record model="ir.ui.view" id="form">
|
||||
<field name="name">dms.document.form</field>
|
||||
<field name="model">dms.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Документ">
|
||||
<header>
|
||||
<button name="open_choise_template" type="object"
|
||||
string="Текст из шаблона" class="btn-secondary"
|
||||
invisible="type_document != 'outgoing' or state == 'done'"/>
|
||||
<button name="create_pdf" type="object"
|
||||
string="Сформировать PDF и записать в хранилище"
|
||||
class="btn-primary"
|
||||
invisible="type_document != 'outgoing' or state == 'done'"/>
|
||||
<button name="create_pdf" type="object"
|
||||
string="Записать в хранилище" class="btn-primary"
|
||||
invisible="type_document == 'outgoing' or state == 'done'"/>
|
||||
<button name="request_validation" type="object"
|
||||
string="Запросить согласование"
|
||||
invisible="need_validation != True or rejected == True or state not in ['draft']"/>
|
||||
<button name="restart_validation" type="object"
|
||||
string="Перезапустить согласование"
|
||||
invisible="not review_ids or state not in ['draft']"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,done"/>
|
||||
</header>
|
||||
<field name="need_validation" invisible="1"/>
|
||||
<field name="validated" invisible="1"/>
|
||||
<field name="rejected" invisible="1"/>
|
||||
<field name="review_ids" invisible="1"/>
|
||||
<field name="can_review" invisible="1"/>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
invisible="validated == True or state not in ['draft'] or rejected == True or not review_ids">
|
||||
<span><i class="fa fa-info-circle" title="Информация"/> Требуется согласование.</span>
|
||||
<button name="validate_tier" string="Согласовать"
|
||||
invisible="can_review == False" type="object"
|
||||
class="oe_inline btn-success" icon="fa-thumbs-up"/>
|
||||
<button name="reject_tier" string="Отклонить"
|
||||
invisible="can_review == False" type="object"
|
||||
class="oe_inline btn-danger" icon="fa-thumbs-down"/>
|
||||
</div>
|
||||
<div class="alert alert-success" role="alert"
|
||||
invisible="validated != True or state not in ['draft'] or not review_ids">
|
||||
<span><i class="fa fa-thumbs-up" title="Согласовано"/> <b>Согласование пройдено.</b></span>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert"
|
||||
invisible="rejected != True or state not in ['draft'] or not review_ids">
|
||||
<span><i class="fa fa-thumbs-down" title="Отклонено"/> Согласование <b>отклонено.</b></span>
|
||||
</div>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" readonly="state == 'done'"
|
||||
placeholder="Присвоится автоматически"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Основное">
|
||||
<field name="type_document" readonly="state != 'draft'"/>
|
||||
<field name="date" readonly="state == 'done'"/>
|
||||
<field name="partner_id" readonly="state == 'done'"/>
|
||||
<field name="print_head" readonly="state == 'done'"/>
|
||||
<field name="company_id" groups="base.group_multi_company"
|
||||
readonly="state == 'done'"/>
|
||||
</group>
|
||||
<group string="Хранение и связи">
|
||||
<field name="directory_id" readonly="state == 'done'"/>
|
||||
<field name="file" readonly="state == 'done'"/>
|
||||
<field name="parent_id" readonly="state == 'done'"/>
|
||||
<field name="link_model" readonly="state == 'done'"/>
|
||||
<field name="res_id" readonly="state == 'done'"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="page_document" string="Документ">
|
||||
<field name="text"
|
||||
invisible="type_document != 'outgoing'"
|
||||
readonly="state == 'done'"/>
|
||||
<field name="text_str" invisible="1"/>
|
||||
<field name="text_render" invisible="1"/>
|
||||
<group>
|
||||
<field name="incoming_file_type"
|
||||
invisible="type_document == 'outgoing'"
|
||||
readonly="state == 'done'"/>
|
||||
<field name="incoming_file"
|
||||
invisible="type_document == 'outgoing' or incoming_file_type != 'pdf'"
|
||||
readonly="state == 'done'" widget="pdf_viewer"/>
|
||||
<field name="incoming_file" string="Сформированный документ"
|
||||
invisible="type_document != 'outgoing' or state != 'done'"
|
||||
readonly="1" widget="pdf_viewer"/>
|
||||
<field name="incoming_file_other"
|
||||
invisible="type_document == 'outgoing' or incoming_file_type == 'pdf'"
|
||||
readonly="state == 'done'"/>
|
||||
</group>
|
||||
</page>
|
||||
<page name="page_chain_document" string="Цепочка документов">
|
||||
<field name="child_ids" readonly="state == 'done'">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="type_document"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="review_ids" widget="tier_validation"
|
||||
invisible="not review_ids"/>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Действия -->
|
||||
<record model="ir.actions.act_window" id="action_window_template">
|
||||
<field name="name">Шаблоны</field>
|
||||
<field name="res_model">dms.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_window_incoming">
|
||||
<field name="name">Входящие</field>
|
||||
<field name="res_model">dms.document</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="search_document"/>
|
||||
<field name="context">{'default_type_document': 'incoming'}</field>
|
||||
<field name="domain">[('type_document', '=', 'incoming')]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_window_outgoing">
|
||||
<field name="name">Исходящие</field>
|
||||
<field name="res_model">dms.document</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="search_document"/>
|
||||
<field name="context">{'default_type_document': 'outgoing'}</field>
|
||||
<field name="domain">[('type_document', '=', 'outgoing')]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_window_internal">
|
||||
<field name="name">Внутренние</field>
|
||||
<field name="res_model">dms.document</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="search_document"/>
|
||||
<field name="context">{'default_type_document': 'internal'}</field>
|
||||
<field name="domain">[('type_document', '=', 'internal')]</field>
|
||||
</record>
|
||||
|
||||
<!-- Меню -->
|
||||
<menuitem name="Документы" id="documents" parent="dms.main_menu_dms"/>
|
||||
<menuitem name="Входящие" id="incoming" parent="documents"
|
||||
action="action_window_incoming" sequence="10"/>
|
||||
<menuitem name="Исходящие" id="outgoing" parent="documents"
|
||||
action="action_window_outgoing" sequence="20"/>
|
||||
<menuitem name="Внутренние" id="internal" parent="documents"
|
||||
action="action_window_internal" sequence="30"/>
|
||||
<menuitem name="Шаблоны" id="template_menu" parent="documents"
|
||||
action="action_window_template" sequence="40"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user