Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import res_company
from . import res_config_settings
from . import hr_employee
from . import ruodoo_rsbu_mil_order
from . import ruodoo_rsbu_mil_report

View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""Данные воинского учёта на карточке сотрудника.
Минимальный набор признаков, достаточный для агрегатов форм воинского
учёта (6, 18, 19): отношение к воинской обязанности, состав (офицеры /
прочие), пребывание в запасе, бронирование и мобпредписание, а также
категория должности для группировки в формах.
Это ОБРАЗЕЦ: состав полей и справочники уточняются у специалиста по ВУ.
"""
from odoo import fields, models
# Категория должности для группировки в формах 6 и 19.
MIL_JOB_CATEGORY = [
('manager', 'Руководители'),
('specialist', 'Специалисты'),
('clerk', 'Служащие'),
('worker', 'Рабочие'),
]
# Состав граждан, пребывающих в запасе (деление в формах 6/18).
MIL_RANK_GROUP = [
('officer', 'Офицеры (и генералы)'),
('soldier', 'Прапорщики, мичманы, сержанты, старшины, солдаты и матросы'),
]
class HrEmployee(models.Model):
_inherit = 'hr.employee'
rsbu_mil_liable = fields.Boolean(
string='Военнообязанный',
help='Сотрудник подлежит воинскому учёту (военнообязанный или '
'призывник).')
rsbu_mil_in_reserve = fields.Boolean(
string='Пребывает в запасе',
help='Гражданин РФ, пребывающий в запасе (для форм 6/18/19).')
rsbu_mil_rank_group = fields.Selection(
selection=MIL_RANK_GROUP, string='Состав',
help='Состав для деления в отчётах: офицеры либо '
'прапорщики/сержанты/солдаты.')
rsbu_mil_conscript = fields.Boolean(
string='Подлежит призыву',
help='Гражданин, подлежащий призыву на военную службу (не в запасе).')
rsbu_mil_booked = fields.Boolean(
string='Забронирован',
help='Гражданин, пребывающий в запасе, забронированный за '
'организацией на период мобилизации и военное время.')
rsbu_mil_mobilization_order = fields.Boolean(
string='Имеет мобпредписание',
help='Наличие мобилизационного предписания в военном билете.')
rsbu_mil_limited_fit = fields.Boolean(
string='Ограниченно годен',
help='Ограниченно годен к военной службе (категория годности «В»).')
rsbu_mil_job_category = fields.Selection(
selection=MIL_JOB_CATEGORY, string='Категория должности (ВУ)',
help='Категория должности для группировки в формах 6 и 19 '
'(руководители/специалисты/служащие/рабочие).')
rsbu_mil_vus = fields.Char(
string='ВУС',
help='Военно-учётная специальность (код по военному билету).')
rsbu_mil_rank = fields.Char(
string='Воинское звание',
help='Воинское звание по военному билету.')
rsbu_mil_fitness = fields.Selection(
selection=[
('A', 'А — годен'),
('B', 'Б — годен с незначительными ограничениями'),
('V', 'В — ограниченно годен'),
('G', 'Г — временно не годен'),
('D', 'Д — не годен'),
],
string='Категория годности',
help='Категория годности к военной службе.')
rsbu_mil_commissariat = fields.Char(
string='Военный комиссариат',
help='Наименование военкомата, в котором состоит на учёте.')
rsbu_mil_is_driver = fields.Boolean(
string='Водитель (ВУ)',
help='Отметка «из них водители» для формы 19.')

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""Реквизиты организации для воинского учёта (формы 6/18/19).
Дополняют реквизиты, уже имеющиеся в rsbu_reporting_base (ОГРН, ОКВЭД)
и в l10n_ru (ИНН/КПП). Это ОБРАЗЕЦ — состав уточняется у специалиста ВУ.
"""
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
rsbu_mil_responsible = fields.Char(
string='Ответственный за ВУ',
help='Ф.И.О. работника, ответственного за воинский учёт и '
'бронирование (форма 18).')
rsbu_mil_responsible_phone = fields.Char(
string='Телефон ответственного за ВУ')
rsbu_mil_does_booking = fields.Boolean(
string='Ведёт бронирование',
help='Организация ведёт бронирование граждан, пребывающих в запасе '
'(пункт 11 формы 18).')
rsbu_mil_booking_list_section = fields.Char(
string='Номер и раздел перечня бронирования',
help='Номер и раздел перечня должностей и профессий, по которым '
'бронируются граждане РФ (пункт 12 формы 18).')
rsbu_mil_reg_number = fields.Char(
string='Рег. номер (районная комиссия)',
help='Регистрационный номер по учёту районной комиссии (форма 18).')
rsbu_mil_parent_org = fields.Char(
string='Вышестоящая организация',
help='Вышестоящая организация (пункт 8 формы 18).')
rsbu_mil_jurisdiction = fields.Char(
string='В сфере ведения',
help='В сфере ведения субъекта РФ или ФОИВ (пункт 13 формы 18).')
# Коды организации для карточки (форма 18).
rsbu_okato = fields.Char(
string='ОКАТО',
help='Код административно-территориального деления (ОКАТО).')
# ОКПО переиспользуется из l10n_ru (поле res.company.okpo).
rsbu_okopf = fields.Char(
string='ОКОПФ',
help='Организационно-правовая форма (ОКОПФ, текстовая расшифровка).')
rsbu_okfs = fields.Char(
string='ОКФС',
help='Форма собственности (ОКФС, текстовая расшифровка).')

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""Реквизиты воинского учёта организации в настройках."""
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
rsbu_mil_responsible = fields.Char(
related='company_id.rsbu_mil_responsible', readonly=False,
string='Ответственный за ВУ')
rsbu_mil_responsible_phone = fields.Char(
related='company_id.rsbu_mil_responsible_phone', readonly=False,
string='Телефон ответственного за ВУ')
rsbu_mil_does_booking = fields.Boolean(
related='company_id.rsbu_mil_does_booking', readonly=False,
string='Ведёт бронирование')
rsbu_mil_booking_list_section = fields.Char(
related='company_id.rsbu_mil_booking_list_section', readonly=False,
string='Номер и раздел перечня бронирования')
rsbu_mil_reg_number = fields.Char(
related='company_id.rsbu_mil_reg_number', readonly=False,
string='Рег. номер (районная комиссия)')
rsbu_mil_parent_org = fields.Char(
related='company_id.rsbu_mil_parent_org', readonly=False,
string='Вышестоящая организация')
rsbu_mil_jurisdiction = fields.Char(
related='company_id.rsbu_mil_jurisdiction', readonly=False,
string='В сфере ведения')
rsbu_okato = fields.Char(
related='company_id.rsbu_okato', readonly=False, string='ОКАТО')
rsbu_okopf = fields.Char(
related='company_id.rsbu_okopf', readonly=False, string='ОКОПФ')
rsbu_okfs = fields.Char(
related='company_id.rsbu_okfs', readonly=False, string='ОКФС')

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""Приказы по воинскому учёту.
Организационные приказы ВУ (об организации воинского учёта, о назначении
ответственного за ВУ и бронирование и т.п.). Документ-шаблон: заполняется
реквизитами и выгружается в DOCX через docx_report.
Это ОБРАЗЕЦ: типовые формулировки приказов задаются по умолчанию и
правятся пользователем.
"""
from odoo import api, fields, models
from odoo.exceptions import UserError
# Виды приказов по ВУ.
ORDER_KIND = [
('organization', 'Об организации воинского учёта'),
('responsible', 'О назначении ответственного за ВУ и бронирование'),
('plan', 'Об утверждении плана работы по ВУ'),
('other', 'Иной приказ по ВУ'),
]
class RuodooRsbuMilOrder(models.Model):
_name = 'ruodoo.rsbu.mil.order'
_description = 'Приказ по воинскому учёту'
_order = 'order_date desc, id desc'
name = fields.Char(
string='Номер приказа', required=True, copy=False, default='б/н')
kind = fields.Selection(
selection=ORDER_KIND, string='Вид приказа', required=True,
default='organization')
company_id = fields.Many2one(
'res.company', string='Организация', required=True, index=True,
default=lambda self: self.env.company)
order_date = fields.Date(
string='Дата приказа', required=True,
default=fields.Date.context_today)
title = fields.Char(
string='Заголовок', compute='_compute_title', store=True,
readonly=False,
help='Заголовок приказа. По умолчанию формируется по виду приказа.')
body = fields.Html(
string='Текст приказа',
help='Основной текст (приказываю: ...). Можно править.')
responsible_employee_id = fields.Many2one(
'hr.employee', string='Назначаемый ответственный',
help='Сотрудник, назначаемый ответственным за ВУ (для приказа о '
'назначении).')
signatory_name = fields.Char(
string='Подписант', compute='_compute_signatory', store=True,
readonly=False)
signatory_title = fields.Char(
string='Должность подписанта', compute='_compute_signatory',
store=True, readonly=False)
@api.depends('kind')
def _compute_title(self):
labels = dict(ORDER_KIND)
for rec in self:
if not rec.title:
rec.title = labels.get(rec.kind, 'Приказ по воинскому учёту')
@api.depends('company_id')
def _compute_signatory(self):
for rec in self:
signatory = rec.company_id.rsbu_signatory_id
if signatory and not rec.signatory_name:
rec.signatory_name = signatory.partner_id.name
if not rec.signatory_title:
rec.signatory_title = (
rec.company_id.rsbu_signatory_title or 'Руководитель')
def action_print_docx(self):
"""Сформировать DOCX приказа по шаблону."""
self.ensure_one()
template = self.env.ref(
'ruodoo_rsbu_mil.docx_template_mil_order',
raise_if_not_found=False)
if not template:
raise UserError(
'Не найден шаблон DOCX приказа по воинскому учёту.')
return template.report_id.report_action(self)

View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
"""Отчёты по воинскому учёту: формы 6, 18, 19.
Собирают численности из карточек сотрудников (hr.employee, поля
воинского учёта) и выгружаются в DOCX через docx_report.
* Форма 6 — отчёт о численности работающих и забронированных граждан,
пребывающих в запасе (строки по категориям должности).
* Форма 18 — карточка учёта организации (реквизиты + сводные числа).
* Форма 19 — сведения об обеспеченности трудовыми ресурсами (строки по
категориям должности с потребностью/некомплектом).
Это ОБРАЗЕЦ: методика подсчёта упрощена и уточняется у специалиста ВУ.
"""
from odoo import api, fields, models
from odoo.exceptions import UserError
from .hr_employee import MIL_JOB_CATEGORY
REPORT_KIND = [
('form6', 'Форма 6 — численность работающих и забронированных в запасе'),
('form18', 'Форма 18 — карточка учёта организации'),
('form19', 'Форма 19 — обеспеченность трудовыми ресурсами'),
]
# Шаблоны docx_report (xmlid docx.template → по виду формы).
_REPORT_XMLID = {
'form6': 'ruodoo_rsbu_mil.docx_template_mil_form6',
'form18': 'ruodoo_rsbu_mil.docx_template_mil_form18',
'form19': 'ruodoo_rsbu_mil.docx_template_mil_form19',
}
class RuodooRsbuMilReport(models.Model):
_name = 'ruodoo.rsbu.mil.report'
_description = 'Отчёт по воинскому учёту'
_order = 'report_date desc, id desc'
name = fields.Char(string='Наименование', compute='_compute_name',
store=True)
kind = fields.Selection(
selection=REPORT_KIND, string='Форма', required=True, default='form6')
company_id = fields.Many2one(
'res.company', string='Организация', required=True, index=True,
default=lambda self: self.env.company)
report_date = fields.Date(
string='По состоянию на', required=True,
default=fields.Date.context_today,
help='Дата, по состоянию на которую формируется отчёт.')
year = fields.Integer(
string='Отчётный год', default=lambda self: fields.Date.today().year)
state = fields.Selection(
selection=[('draft', 'Черновик'), ('computed', 'Рассчитано')],
default='draft', string='Состояние', copy=False)
line_ids = fields.One2many(
'ruodoo.rsbu.mil.report.line', 'report_id', string='Строки')
# --- Сводные числа (используются формой 18 и итогами). ---
total_employees = fields.Integer(
string='Всего работающих', compute='_compute_totals', store=True)
total_reserve = fields.Integer(
string='В запасе, всего', compute='_compute_totals', store=True)
total_reserve_officer = fields.Integer(
string='В запасе, офицеры', compute='_compute_totals', store=True)
total_reserve_soldier = fields.Integer(
string='В запасе, прочие', compute='_compute_totals', store=True)
total_booked = fields.Integer(
string='Забронировано, всего', compute='_compute_totals', store=True)
total_booked_officer = fields.Integer(
string='Забронировано, офицеры', compute='_compute_totals',
store=True)
total_booked_soldier = fields.Integer(
string='Забронировано, прочие', compute='_compute_totals', store=True)
total_conscript = fields.Integer(
string='Подлежат призыву', compute='_compute_totals', store=True)
total_limited_fit = fields.Integer(
string='Ограниченно годных', compute='_compute_totals', store=True)
total_no_order = fields.Integer(
string='В запасе без мобпредписаний', compute='_compute_totals',
store=True)
total_with_order = fields.Integer(
string='В запасе с мобпредписаниями', compute='_compute_totals',
store=True)
@api.depends('kind', 'report_date', 'year')
def _compute_name(self):
labels = dict(REPORT_KIND)
for rec in self:
short = (labels.get(rec.kind, '?') or '').split('—')[0].strip()
dt = rec.report_date and rec.report_date.strftime('%d.%m.%Y') or '?'
rec.name = '%s на %s' % (short, dt)
# ------------------------------------------------------------------
def _mil_employees(self):
"""Военнообязанные сотрудники организации."""
self.ensure_one()
return self.env['hr.employee'].search([
('company_id', '=', self.company_id.id),
('rsbu_mil_liable', '=', True),
])
@api.depends('company_id')
def _compute_totals(self):
for rec in self:
emps = rec._mil_employees()
reserve = emps.filtered('rsbu_mil_in_reserve')
booked = reserve.filtered('rsbu_mil_booked')
rec.total_employees = len(emps)
rec.total_reserve = len(reserve)
rec.total_reserve_officer = len(reserve.filtered(
lambda e: e.rsbu_mil_rank_group == 'officer'))
rec.total_reserve_soldier = len(reserve.filtered(
lambda e: e.rsbu_mil_rank_group == 'soldier'))
rec.total_booked = len(booked)
rec.total_booked_officer = len(booked.filtered(
lambda e: e.rsbu_mil_rank_group == 'officer'))
rec.total_booked_soldier = len(booked.filtered(
lambda e: e.rsbu_mil_rank_group == 'soldier'))
rec.total_conscript = len(emps.filtered('rsbu_mil_conscript'))
rec.total_limited_fit = len(reserve.filtered('rsbu_mil_limited_fit'))
not_booked = reserve.filtered(lambda e: not e.rsbu_mil_booked)
rec.total_no_order = len(not_booked.filtered(
lambda e: not e.rsbu_mil_mobilization_order))
rec.total_with_order = len(not_booked.filtered(
'rsbu_mil_mobilization_order'))
# ------------------------------------------------------------------
def action_compute(self):
"""Рассчитать строки отчёта по категориям должности."""
Line = self.env['ruodoo.rsbu.mil.report.line']
for rec in self:
rec.line_ids.unlink()
emps = rec._mil_employees()
vals = []
for code, label in MIL_JOB_CATEGORY:
cat = emps.filtered(lambda e: e.rsbu_mil_job_category == code)
reserve = cat.filtered('rsbu_mil_in_reserve')
booked = reserve.filtered('rsbu_mil_booked')
conscript = cat.filtered('rsbu_mil_conscript')
vals.append({
'report_id': rec.id,
'category': code,
'name': label,
'total': len(cat),
'reserve_total': len(reserve),
'reserve_officer': len(reserve.filtered(
lambda e: e.rsbu_mil_rank_group == 'officer')),
'reserve_soldier': len(reserve.filtered(
lambda e: e.rsbu_mil_rank_group == 'soldier')),
'booked_total': len(booked),
'booked_officer': len(booked.filtered(
lambda e: e.rsbu_mil_rank_group == 'officer')),
'booked_soldier': len(booked.filtered(
lambda e: e.rsbu_mil_rank_group == 'soldier')),
'conscript_total': len(conscript),
})
if vals:
Line.create(vals)
rec.state = 'computed'
return True
def action_print_docx(self):
"""Сформировать DOCX отчёта по шаблону, соответствующему форме."""
self.ensure_one()
if self.state == 'draft':
self.action_compute()
xmlid = _REPORT_XMLID.get(self.kind)
template = self.env.ref(xmlid, raise_if_not_found=False) if xmlid else None
if not template:
raise UserError(
'Не найден шаблон DOCX для выбранной формы воинского учёта.')
return template.report_id.report_action(self)
class RuodooRsbuMilReportLine(models.Model):
_name = 'ruodoo.rsbu.mil.report.line'
_description = 'Строка отчёта по воинскому учёту'
_order = 'report_id, sequence, id'
report_id = fields.Many2one(
'ruodoo.rsbu.mil.report', string='Отчёт', required=True,
ondelete='cascade', index=True)
sequence = fields.Integer(string='Порядок', default=10)
category = fields.Selection(
selection=MIL_JOB_CATEGORY, string='Категория должности')
name = fields.Char(string='Наименование категории')
total = fields.Integer(string='Всего работающих')
reserve_total = fields.Integer(string='В запасе, всего')
reserve_officer = fields.Integer(string='В запасе, офицеры')
reserve_soldier = fields.Integer(string='В запасе, прочие')
booked_total = fields.Integer(string='Забронировано, всего')
booked_officer = fields.Integer(string='Забронировано, офицеры')
booked_soldier = fields.Integer(string='Забронировано, прочие')
conscript_total = fields.Integer(string='Подлежат призыву')
note = fields.Char(string='Примечание')