Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC

This commit is contained in:
CI Publish Bot
2026-05-10 21:19:11 +00:00
commit cbf9e6e6d6
1213 changed files with 183945 additions and 0 deletions

View 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.

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
{
'name': "mklab_dms_document",
'summary': """
Работа с входящими и исходящими документами и их хранение""",
'description': """
Работа с входящими и исходящими документами и их хранение
""",
'author': "MK.Lab",
'website': "http://www.inf-centre.ru",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/13.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '13.20230927',
# any module necessary for this one to work correctly
'depends': ['base','dms', 'utm', 'base_tier_validation'],
# always loaded
'data': [
'security/ir.model.access.csv',
'data/data.xml',
'views/views.xml',
'views/report.xml',
],
'demo': [
'demo/demo.xml',
],
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# 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
# })

View 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>

View 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">&lt;p&gt;Уважаемые коллеги,&lt;/p&gt;&lt;p&gt;Настоящим сообщаем вам о ...&lt;/p&gt;&lt;p&gt;С уважением,&lt;br/&gt;${user.name}&lt;/p&gt;</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">&lt;p&gt;Просим предоставить следующие документы:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Счёт-фактура&lt;/li&gt;&lt;li&gt;Товарная накладная&lt;/li&gt;&lt;/ul&gt;</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">&lt;p&gt;Входящее письмо от поставщика с коммерческим предложением.&lt;/p&gt;</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">&lt;p&gt;Исходящее письмо покупателю с подтверждением заказа.&lt;/p&gt;</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">&lt;p&gt;Внутренняя служебная записка о порядке работы с документами.&lt;/p&gt;</field>
<field name="incoming_file_type">pdf</field>
<field name="state">draft</field>
<field name="company_id" ref="base.main_company"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import res_partner
from . import document
from . import tier_definition
from . import tier_dms

View File

@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
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='1', tracking=True) # Номер — для исходящих с использованием нумератора, для входящих вносится вручную
text = fields.Html(string='Текст', required='1', tracking=True) # Текст — hmtl
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 = 'Документы'
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'utm.mixin']
_description = 'mklab_dms_document'
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},
}
def unlink(self):
for s in self:
if s.state=='done':
raise exceptions.ValidationError('Нельзя удалить документ, записанный в хранилище')
res = super().unlink()
return res
def get_number(self):
type_document = str(self.env.context.get('default_type_document'))
if self.type_document:
type_document = self.type_document
if type_document == 'incoming':
self.name = self.env['ir.sequence'].next_by_code('dms.document_number')
return self.env['ir.sequence'].next_by_code('dms.document_number')
elif type_document == 'outgoing':
self.name = self.env['ir.sequence'].next_by_code('dms.document_number_out')
return self.env['ir.sequence'].next_by_code('dms.document_number_out')
elif type_document == 'internal':
self.name = self.env['ir.sequence'].next_by_code('dms.document_number_internal')
return self.env['ir.sequence'].next_by_code('dms.document_number_internal')
type_document = fields.Selection([('incoming','Входящий документ'),('outgoing','Исходящий документ'),('internal','Внутренний документ')], string='Тип', required="1", tracking=True) # Тип — список выбора (входящее, исходящее, внутреннее)
name = fields.Char(string='Номер', default=lambda r: r.get_number(), tracking=True) # Номер — для исходящих с использованием нумератора, для входящих вносится вручную
partner_id = fields.Many2one(comodel_name='res.partner', string='Контакт', required="1", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) # Контакт — от или для кого
date = fields.Date(string='Дата документа', default=lambda r: datetime.datetime.today(), required="1", tracking=True) # Дата документа
text = fields.Html(string='Текст') # Текст — hmtl
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='Текст') # Текст — hmtl
text_render = fields.Html(string='Текст render') # Текст — hmtl
file = fields.Many2one(comodel_name='dms.file', string='Связанный файл', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) # Связанный dms.file
link_model = fields.Many2one(comodel_name='ir.model', string='Связанная модель', tracking=True) # Связанная модель (не обязательно)
res_id = fields.Integer(string='ID документа в связанной модели', tracking=True) # ID документа в связанной модели (res_id, не обязательно)
# Эти поля потребуются только для заполнения переменных внутри текста
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='Direct subordinates')
subordinate_ids = fields.One2many(comodel_name='dms.document', string='Subordinates', 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='Indirect Surbordinates Count', compute='_compute_subordinates', store=False, compute_sudo=True)
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 = u""
if render_result == u"False":
render_result = u""
s.text_render = render_result
return render_result
# @api.model
# def create(self, vals):
# if 'name' in vals and 'type_document' in vals:
# if vals.get('name', ('New')) == '/' or vals['name'] == '' or vals['name'] == False:
# if vals['type_document'] == 'incoming':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number')
# if vals['type_document'] == 'outgoing':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number_out')
# if vals['type_document'] == 'internal':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number_internal')
# for s in self:
# if not s.name:
# if s.type_document == 'incoming':
# raise exceptions.ValidationError(self.env['ir.sequence'].next_by_code('dms.document_number'))
# s.name = self.env['ir.sequence'].next_by_code('dms.document_number')
# if s.type_document == 'outgoing':
# s.name = self.env['ir.sequence'].next_by_code('dms.document_number_out')
# if s.type_document == 'internal':
# s.name = self.env['ir.sequence'].next_by_code('dms.document_number_internal')
# res = super(mklab_dms_document, self).create(vals)
# return res
# def write(self, vals):
# if 'name' in vals and 'type_document' in vals:
# if vals.get('name', ('New')) == '/' or vals['name'] == '' or vals['name'] == False:
# if vals['type_document'] == 'incoming':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number')
# if vals['type_document'] == 'outgoing':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number_out')
# if vals['type_document'] == 'internal':
# vals['name'] = self.env['ir.sequence'].next_by_code('dms.document_number_internal')
# if not self.name:
# if self.type_document == 'incoming':
# raise exceptions.ValidationError(self.env['ir.sequence'].next_by_code('dms.document_number'))
# self.name = self.env['ir.sequence'].next_by_code('dms.document_number')
# if self.type_document == 'outgoing':
# self.name = self.env['ir.sequence'].next_by_code('dms.document_number_out')
# if self.type_document == 'internal':
# self.name = self.env['ir.sequence'].next_by_code('dms.document_number_internal')
# res = super(mklab_dms_document, self).write(vals)
# return res

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from odoo import models
class DmsResPartner(models.Model):
_inherit = 'res.partner'
def action_show_document_incoming(self):
self.ensure_one()
document_action = self.env['ir.actions.act_window'].for_xml_id('mklab_dms_document','action_window_incoming')
document_action['domain'] = str([('partner_id','=',self.id),('type_document','=','incoming')])
return document_action
def action_show_document_outgoing(self):
self.ensure_one()
document_action = self.env['ir.actions.act_window'].for_xml_id('mklab_dms_document','action_window_outgoing')
document_action['domain'] = str([('partner_id','=',self.id),('type_document','=','outgoing')])
return document_action

View 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

View 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']

View 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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_dms_document dms.document model_dms_document base.group_user 1 1 1 1
3 access_dms_template_admin dms_template_admin model_dms_template dms.group_dms_manager 1 1 1 1
4 access_dms_template dms_template model_dms_template base.group_user 1 0 0 0
5 access_dms_choise_template dms_choise_template model_dms_choise_template base.group_user 1 1 1 1

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_dms_document

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
"""
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",
)

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="mklab_dms_document.report_dms_document">
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="o">
<t t-if="o and 'company_id' in o">
<t t-set="company" t-value="o.company_id"/>
</t>
<t t-if="not o or not 'company_id' in o">
<t t-set="company" t-value="res_company"/>
</t>
<t t-set="context" t-value="o._context"/>
<div class="header">
<STYLE TYPE="text/css">
body {background: #ffffff; margin: 0; font-family: Times new roman; font-size: 10pt; font-style:
normal;}
tr.R0{height: 15px;}
tr.R0 td.R0C0{ font-family: Times new roman; font-size: 10pt; font-style: normal; text-align:
left; vertical-align: medium; font-weight: normal;}
tr.R0 td.R0C1{ font-family: Times new roman; font-size: 10pt; font-style: normal; text-align:
right; vertical-align: medium; font-weight: normal;}
</STYLE>
<table border="0" cellpadding="0" cellspacing="0" style="width:100%">
<tbody>
<tr class="R0">
<t t-set="render" t-value="o.render_template()"/>
<td class="R0C0">
<t t-if='o.print_head'>
<t t-esc="o.text_str"/>
</t>
</td>
</tr>
</tbody>
</table>
</div>
<div class="page">
<span>
<t t-raw="o.text_render"/>
</span>
</div>
</t>
</t>
</template>
<record id="mklab_dms_document.paperformat_a4" model="report.paperformat">
<field name="name">A4</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</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>
<report id="mklab_dms_document.action_report_dms_document"
string="Документ"
model="dms.document"
report_type="qweb-pdf"
file="mklab_dms_document.report_dms_document"
name="mklab_dms_document.report_dms_document"
print_report_name="'Документ - %s' % (object.name)"
/>
<record id="mklab_dms_document.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="paperformat_id" ref="mklab_dms_document.paperformat_a4"/>
</record>
</odoo>

View File

@ -0,0 +1,272 @@
<odoo>
<data>
<record id="mklab_dms_document.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-book" name="action_show_document_incoming"
string="Входящие документы"/>
<button class="oe_stat_button" type="object" icon="fa-book" name="action_show_document_outgoing"
string="Исходящие документы"/>
</xpath>
</field>
</record>
<record id="mklab_dms_document.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="mklab_dms_document.form_template_wiz">
<field name="name">mklab_dms_document form_template_wiz</field>
<field name="model">dms.choise_template</field>
<field name="arch" type="xml">
<form>
<header>
</header>
<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"/>
<button string="Отмена" special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="mklab_dms_document.list_template">
<field name="name">mklab_dms_document list_template</field>
<field name="model">dms.template</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="company_id"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="mklab_dms_document.list">
<field name="name">mklab_dms_document list</field>
<field name="model">dms.document</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="date"/>
<field name="partner_id"/>
<field name="state"/>
<field name="company_id"/>
</list>
</field>
</record>
<record model="ir.ui.view" id="mklab_dms_document.form_template">
<field name="name">mklab_dms_document form_template</field>
<field name="model">dms.template</field>
<field name="arch" type="xml">
<form>
<header>
</header>
<sheet>
<h1>
<field name="name"/>
</h1>
<field name="text"/>
<group>
<field name="company_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="mklab_dms_document.form">
<field name="name">mklab_dms_document form</field>
<field name="model">dms.document</field>
<field name="arch" type="xml">
<form>
<header>
<!--button name='render_template' type='object' string='render'/-->
<button name='open_choise_template' type='object' string='Создать текст из шаблона'
invisible="type_document != 'outgoing' or state =='done'"/>
<button name='create_pdf' type='object' string='Сформировать PDF и записать в хранилище'
invisible="type_document != 'outgoing' or state == 'done'"/>
<button name='create_pdf' type='object' string='Записать в хранилище'
invisible="type_document != 'incoming' or state == 'done'"/>
<button
name="request_validation"
string="Request Validation"
invisible="need_validation != True or rejected == True or state not in ['draft']"
type="object"
/>
<button
name="restart_validation"
string="Restart Validation"
invisible="not review_ids or state not in ['draft']"
type="object"
/>
<field name="state" widget="statusbar"/>
</header>
<field name="need_validation" invisible="1"/>
<field name="validated" invisible="1"/>
<field name="rejected" invisible="1"/>
<div
class="alert alert-warning"
role="alert"
invisible="validated == True or state not in ['draft'] or rejected == True or not review_ids"
style="margin-bottom:0px;"
>
<p><i class="fa fa-info-circle"/>Требуется валидация
<field name="can_review" invisible="1"/>
<button
name="validate_tier"
string="Validate"
invisible="can_review == False"
type="object"
class="oe_inline oe_button btn-success"
icon="fa-thumbs-up"
/>
<button
name="reject_tier"
string="Reject"
invisible="can_review == False"
type="object"
class="btn-icon btn-danger"
icon="fa-thumbs-down"
/>
</p>
</div>
<div
class="alert alert-success"
role="alert"
invisible="validated != True or state not in ['draft'] or not review_ids"
style="margin-bottom:0px;"
>
<p>
<i class="fa fa-thumbs-up"/>
<b>Валидация прошла успешно</b>!
</p>
</div>
<div
class="alert alert-danger"
role="alert"
invisible="rejected != True or state not in ['draft'] or not review_ids"
style="margin-bottom:0px;"
>
<p>
<i class="fa fa-thumbs-down"/>
Валидация <b>отклонена</b>.
</p>
</div>
<sheet>
<h1>
<field name="name" readonly="state == 'done'"/>
</h1>
<group>
<group>
<field name="date" readonly="state == 'done'"/>
<field name="partner_id" readonly="state == 'done'"/>
<field name="type_document" readonly='True'/>
<field name="print_head" readonly="state == 'done'"/>
<field name="company_id" readonly="state == 'done'"/>
</group>
<group>
<field name="file" readonly="state == 'done'"/>
<field name="directory_id" readonly="state == 'done'"/>
<field name="link_model" readonly="state == 'done'"/>
<field name="res_id" readonly="state == 'done'"/>
<field name="parent_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'/>
<field name="text_render" invisible='1'/>
<group>
<field name="incoming_file_type"
invisible="type_document != 'incoming'" readonly="state == 'done'"/>
<field name="incoming_file"
invisible="type_document != 'incoming' or incoming_file_type != 'pdf'" readonly="state == 'done'"
widget="pdf_viewer"/>
<field name="incoming_file" string="Сформированный документ"
invisible="type_document == 'incoming' and state != 'done'" readonly="state == 'done'"
widget="pdf_viewer"/>
<field name="incoming_file_other"
invisible="type_document == 'incoming' or state != 'done'" readonly="state == 'done'"/>
</group>
</page>
<page name='page_chain_document' string='Цепочка документов'>
<field name="child_ids" readonly="state == 'done'"/>
</page>
</notebook>
<label for="create_uid" string="Автор документа: "/>
<field name="create_uid"/>
</sheet>
<field
name="review_ids"
widget="tier_validation"
invisible="not review_ids"
/>
<div class="oe_chatter"/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="mklab_dms_document.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="mklab_dms_document.action_window_incoming">
<field name="name">Входящие</field>
<field name="res_model">dms.document</field>
<field name="view_mode">list,form</field>
<field name="context">{'default_type_document':'incoming'}</field>
<field name="domain">[('type_document', '=', 'incoming')]</field>
</record>
<record model="ir.actions.act_window" id="mklab_dms_document.action_window_outgoing">
<field name="name">Исходящие</field>
<field name="res_model">dms.document</field>
<field name="view_mode">list,form</field>
<field name="context">{'default_type_document':'outgoing'}</field>
<field name="domain">[('type_document', '=', 'outgoing')]</field>
</record>
<record model="ir.actions.act_window" id="mklab_dms_document.action_window_internal">
<field name="name">Внутренние</field>
<field name="res_model">dms.document</field>
<field name="view_mode">list,form</field>
<field name="context">{'default_type_document':'internal'}</field>
<field name="domain">[('type_document', '=', 'internal')]</field>
</record>
<menuitem name="Документы" id="mklab_dms_document.documents" parent="dms.main_menu_dms"/>
<menuitem name="Шаблоны" id="mklab_dms_document.template_menu" parent="mklab_dms_document.documents"
action="mklab_dms_document.action_window_template"/>
<menuitem name="Входящие" id="mklab_dms_document.incoming" parent="mklab_dms_document.documents"
action="mklab_dms_document.action_window_incoming"/>
<menuitem name="Исходящие" id="mklab_dms_document.outgoing" parent="mklab_dms_document.documents"
action="mklab_dms_document.action_window_outgoing"/>
<!--menuitem name="Внутренние" id="mklab_dms_document.internal" parent="mklab_dms_document.documents" action="mklab_dms_document.action_window_internal"/-->
</data>
</odoo>