[C28-33][@ant] feat: исходящие платежи расходов в реестре (тип EXPENSE: листенер на authexp + payexp при подтверждении кассой, реквизиты и назначение из снимка), протокол 2011 по канону решений совета (кворум/голоса/повестка через decision_id), подтверждение оплаты файлом в реестре, карточка расхода без механики и с № СЗ
Typecheck / desktop (pull_request) Failing after 8m28s
Typecheck / controller (pull_request) Failing after 8m0s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
coopops
2026-06-12 07:58:28 +00:00
parent cb0056f716
commit f9d09a74cc
25 changed files with 634 additions and 150 deletions
@@ -1,23 +1,24 @@
import type { IGenerate, IMetaDocument } from '../../document'
import type { ICommonUser, ICooperativeData, IVars } from '../../model'
import type { IDecisionData, IGenerate, IMetaDocument } from '../../document'
import type { ICooperativeData, IVars } from '../../model'
import type { IExpenseItem, IExpenseProposalHeader } from '../2010.ExpenseProposalStatement'
export const registry_id = 2011
export type ExpenseProposalDecisionKind = 'approve' | 'decline'
/** Резолюция совета по СЗ: утвердить / отказать (+ причина отказа). */
export interface IExpenseProposalDecisionBody {
kind: ExpenseProposalDecisionKind
reason?: string
protocol_number?: string
protocol_date?: string
}
export interface Action extends IGenerate {
proposal_hash: string
/** Идентификатор решения совета (повестка) — источник данных голосования */
decision_id: number
proposal: IExpenseProposalHeader
items: IExpenseItem[]
decision: IExpenseProposalDecisionBody
resolution: IExpenseProposalDecisionBody
}
export type Meta = IMetaDocument & Action
@@ -25,48 +26,98 @@ export type Meta = IMetaDocument & Action
export interface Model {
meta: IMetaDocument
coop: ICooperativeData
user: ICommonUser
vars: IVars
/** Данные собрания совета (кворум, голоса) — канон протоколов решений */
decision: IDecisionData
proposal_hash: string
/** Короткий идентификатор СЗ (первые 16 символов хэша, uppercase) */
proposal_short_hash: string
proposal: IExpenseProposalHeader
items: IExpenseItem[]
decision: IExpenseProposalDecisionBody
resolution: IExpenseProposalDecisionBody
}
export const title = 'Протокол-1: решение по служебной записке о расходах'
export const description = 'Решение председателя/совета об утверждении (или отказе) служебной записки-сметы о расходах. Подписывается председателем поверх документа Creator.'
export const title = 'Протокол решения совета о расходах'
export const description = 'Протокол собрания совета по служебной записке-смете о расходах. Структура — канон протоколов решений совета (кворум, голоса, повестка); меняется только содержание вопроса и принятого решения.'
export const context = `<style>h1{margin:0;text-align:center;}h3{margin:0;padding-top:15px;text-align:center;}.digital-document{padding:20px;white-space:pre-wrap;}table{width:100%;border-collapse:collapse;}th,td{border:1px solid #ccc;padding:8px;text-align:left;word-wrap:break-word;overflow-wrap:break-word;}th{background-color:#f4f4f4;}.summary{margin-top:20px;text-align:right;font-weight:bold;}.decision-block{padding-top:20px;border:2px solid #333;padding:15px;margin-top:20px;}.signature{padding-top:30px;}</style><div class="digital-document"><div style="padding-bottom:30px;"><h1 style="text-align:center">{% trans 'PROTOCOL_TITLE' %}</h1><p style="text-align:center">{{vars.full_abbr_genitive}} «{{vars.name}}»</p><p style="text-align:right;padding-top:20px">{{ coop.city }}, {{ meta.created_at }}</p>{% if decision.protocol_number %}<p style="text-align:center;padding-top:10px;">{% trans 'PROTOCOL_NUMBER' %} {{ decision.protocol_number }} {% trans 'FROM' %} {{ decision.protocol_date }}</p>{% endif %}</div><h3>{% trans 'REFERENCE' %}</h3><p>{% trans 'REFERENCE_BODY' %}</p><p><strong>{% trans 'REFERENCE_HASH' %}:</strong> № {{ proposal_short_hash }}</p><p><strong>{% trans 'PURPOSE' %}:</strong> {{ proposal.description }}</p><h3 style="padding-top:20px;">{% trans 'ITEMS' %}</h3><table><thead><tr><th>№</th><th>{% trans 'ITEM_DESCRIPTION' %}</th><th>{% trans 'ITEM_AMOUNT' %}</th></tr></thead><tbody>{% for item in items %}<tr><td>{{ item.number }}</td><td>{{ item.description }}</td><td>{{ item.amount }}</td></tr>{% endfor %}</tbody></table><p class="summary">{% trans 'TOTAL' %}: {{ proposal.total_amount }}</p><div class="decision-block"><h3>{% trans 'DECISION_HEADER' %}</h3>{% if decision.kind == 'approve' %}<p><strong>{% trans 'DECISION_APPROVE' %}</strong></p>{% else %}<p><strong>{% trans 'DECISION_DECLINE' %}</strong></p>{% if decision.reason %}<p>{% trans 'DECLINE_REASON' %}: {{ decision.reason }}</p>{% endif %}{% endif %}</div><div class="signature"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'CHAIRMAN_SIGNATURE' %}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
// Вёрстка — точная копия канона протоколов решений совета (600.FreeDecision):
// форма/место/дата/время, члены совета, правомочность, повестка, голосование,
// РЕШИЛИ, подпись председателя. Отличается только содержание вопроса повестки
// и принятого решения (смета расходов по СЗ).
export const context = '<style> \nh1 {\nmargin: 0px; \ntext-align:center;\n}\nh3{\nmargin: 0px;\npadding-top: 15px;\n}\n.about {\npadding: 20px;\n}\n.about p{\nmargin: 0px;\n}\n.signature {\npadding-top: 20px;\n}\n.digital-document {\npadding: 20px;\nwhite-space: pre-wrap;\n}\n.subheader {\npadding-bottom: 20px; \n}\ntable {\n width: 100%;\n border-collapse: collapse;\n}\nth, td {\n border: 1px solid #ccc;\n padding: 8px;\n text-align: left;\n word-wrap: break-word; \n overflow-wrap: break-word; \n}\nth {\n background-color: #f4f4f4;\n width: 30%;\n}\n</style>\n\n<div class="digital-document"><h1 class="header">{% trans \'protocol_number\', decision.id %}</h1>\n<p style="text-align:center" class="subheader">{% trans \'council_meeting_name\' %} {{vars.full_abbr_genitive}} "{{vars.name}}"</p>\n<p style="text-align: right"> {{ meta.created_at }}, {{ coop.city }}</p>\n<table class="about">\n<tbody>\n<tr>\n<th>{% trans \'meeting_format\' %}</th>\n<td>{% trans \'meeting_format_value\' %}</td>\n</tr>\n<tr>\n<th>{% trans \'meeting_place\' %}</th>\n<td>{{ coop.full_address }}</td>\n</tr>\n<tr>\n<th>{% trans \'meeting_date\' %}</th>\n<td>{{ decision.date }}</td>\n</tr>\n<tr>\n<th>{% trans \'opening_time\' %}</th>\n<td>{{ decision.time }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'council_members\' %}</h3>\n<table>\n<tbody>\n{% for member in coop.members %}\n<tr>\n<th>{% if member.is_chairman %}{% trans \'chairman_of_the_council\' %}{% else %}{% trans \'member_of_the_council\' %}{% endif %}</th>\n<td>{{ member.last_name }} {{ member.first_name }} {{ member.middle_name }}</td>\n</tr>\n{% endfor %}\n</tbody>\n</table>\n<h3>{% trans \'meeting_legality\' %} </h3>\n<p>{% trans \'voting_results\', decision.voters_percent %} {% trans \'quorum\' %} {% trans \'chairman_of_the_meeting\', coop.chairman.last_name, coop.chairman.first_name, coop.chairman.middle_name %}.</p>\n<h3>{% trans \'agenda\' %}</h3>\n<table>\n<tbody>\n<tr>\n<th>№</th>\n<td>{% trans \'question\' %}</td>\n</tr>\n<tr>\n<th>1</th>\n<td>{% trans \'question_body\', proposal_short_hash, proposal.total_amount %} {% trans \'question_purpose\' %}: {{ proposal.description }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'voting\' %}</h3>\n<p>{% trans \'vote_results\' %} </p><table>\n<tbody>\n<tr>\n<th>{% trans \'votes_for\' %}</th>\n<td>{{ decision.votes_for }}</td>\n</tr>\n<tr>\n<th>{% trans \'votes_against\' %}</th>\n<td>{{ decision.votes_against }}</td>\n</tr>\n<tr>\n<th>{% trans \'votes_abstained\' %}</th>\n<td>{{ decision.votes_abstained }}</td>\n</tr>\n</tbody>\n</table>\n<h3>{% trans \'decision_made\' %}</h3>\n<table>\n<tbody>\n<tr>\n<th>№</th>\n<td>{% trans \'decision\' %}</td>\n</tr>\n<tr>\n<th>1</th>\n<td>{% if resolution.kind == \'approve\' %}<p>{% trans \'decision_approve\', proposal_short_hash, proposal.total_amount %}{% if proposal.deadline %} {% trans \'decision_deadline\', proposal.deadline %}{% endif %}{% trans \'decision_namely\' %}:</p>{% for item in items %}<p>{{ item.number }}. {{ item.description }} — {{ item.amount }} ({% if item.mechanics == \'ADVANCE\' %}{% trans \'mech_advance\' %}{% else %}{% trans \'mech_direct\' %}{% endif %})</p>{% endfor %}{% else %}<p>{% trans \'decision_decline\', proposal_short_hash %}{% if resolution.reason %} {% trans \'decline_reason\' %}: {{ resolution.reason }}{% endif %}</p>{% endif %}</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<p>{% trans \'closing_time\', decision.time %}</p>\n<div class="signature"><p>{% trans \'signature\' %}</p><p>{% trans \'chairman\' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}</p></div></div>\n'
export const translations = {
ru: {
PROTOCOL_TITLE: 'ПРОТОКОЛ-1: РЕШЕНИЕ ПО СЛУЖЕБНОЙ ЗАПИСКЕ О РАСХОДАХ',
PROTOCOL_NUMBER: '',
FROM: 'от',
REFERENCE: 'ОСНОВАНИЕ',
REFERENCE_BODY: 'Рассмотрена служебная записка-смета о расходах:',
REFERENCE_HASH: 'Служебная записка',
PURPOSE: 'Цель',
ITEMS: ОЗИЦИИ РАСХОДА',
ITEM_DESCRIPTION: 'Описание',
ITEM_AMOUNT: 'Сумма',
TOTAL: 'Итого',
DECISION_HEADER: 'РЕШЕНИЕ',
DECISION_APPROVE: 'УТВЕРДИТЬ расход в полном объёме согласно настоящей служебной записке.',
DECISION_DECLINE: 'ОТКАЗАТЬ в утверждении расхода.',
DECLINE_REASON: 'Основание отказа',
CHAIRMAN_SIGNATURE: 'Председатель',
SIGNED_DIGITALLY: 'подписано электронной подписью',
meeting_format: 'Форма',
meeting_date: 'Дата',
meeting_place: 'Место',
opening_time: 'Время открытия',
council_members: 'ЧЛЕНЫ СОВЕТА',
voting_results: 'Количество голосов составляет {0}% от общего числа членов Совета.',
meeting_legality: 'СОБРАНИЕ ПРАВОМОЧНО',
chairman_of_the_meeting: редседатель собрания совета: {0} {1} {2}',
agenda: 'ПОВЕСТКА ДНЯ',
vote_results: 'По первому вопросу повестки дня проголосовали:',
decision_made: 'РЕШИЛИ',
closing_time: 'Время закрытия собрания совета: {0}.',
protocol_number: 'ПРОТОКОЛ № {0}',
council_meeting_name: 'Собрания Совета',
chairman_of_the_council: 'Председатель совета',
signature: 'Документ подписан электронной подписью.',
chairman: 'Председатель',
quorum: 'Кворум для решения поставленных на повестку дня вопросов имеется.',
voting: 'ГОЛОСОВАНИЕ',
meeting_format_value: 'Заочная',
member_of_the_council: 'Член совета',
question: 'Вопрос',
votes_for: 'ЗА',
votes_against: 'ПРОТИВ',
votes_abstained: 'ВОЗДЕРЖАЛСЯ',
decision: 'Решение',
question_body: 'Рассмотрение служебной записки-сметы о расходах № {0} на общую сумму {1}.',
question_purpose: 'Цель расходов',
decision_approve: 'Утвердить расходы по служебной записке № {0} в общей сумме {1}',
decision_deadline: 'со сроком исполнения до {0}',
decision_namely: ', а именно',
mech_advance: 'аванс под отчёт',
mech_direct: 'оплата по счёту',
decision_decline: 'Отказать в утверждении расходов по служебной записке № {0}.',
decline_reason: 'Основание отказа',
},
}
export const exampleData = {
coop: { city: 'Москва' },
meta: { created_at: '03.06.2026 11:00' },
vars: { full_abbr_genitive: 'ПК', name: 'Восход' },
user: { full_name_or_short_name: 'Петров П.П.' },
meta: {
created_at: '03.06.2026 11:00',
},
coop: {
city: 'Москва',
full_address: 'г. Москва, ул. Смольная, д. 3, оф. 84',
chairman: {
last_name: 'Муравьев',
first_name: 'Алексей',
middle_name: 'Николаевич',
},
},
decision: {
time: '11:00',
date: '03.06.2026',
votes_for: '3',
votes_against: '0',
votes_abstained: '0',
voters_percent: '100',
id: '15',
},
member: {
is_chairman: true,
last_name: 'Муравьев',
first_name: 'Алексей',
middle_name: 'Николаевич',
},
vars: {
full_abbr_genitive: 'потребительского кооператива',
name: 'ВОСХОД',
},
proposal_hash: '55c470039a8c53ce1b4b6e842fe8063ab3d5b85ba2ba8ab0ae6e30be3ad328b7',
proposal_short_hash: '55C470039A8C53CE',
proposal: {
@@ -74,14 +125,14 @@ export const exampleData = {
total_amount: '15000.00 RUB',
items_count: 2,
source_wallet: 'w.cap.bla',
deadline: '30.06.2026',
fund_name: 'Фонду хозяйственной деятельности',
},
items: [
{ number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB' },
{ number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB' },
{ number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB', recipient_type: 'ORG', mechanics: 'DIRECT' },
{ number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB', recipient_type: 'SELF', mechanics: 'ADVANCE' },
],
decision: {
resolution: {
kind: 'approve',
protocol_number: '15',
protocol_date: '03.06.2026',
},
}