[C28-33][@ant] feat: срок «в срок до» и фонд списания в СЗ 2010 + инвариант механики оплаты — пайщику только аванс под отчёт, организации только прямая оплата (форма/бэкенд/контракт); фикс 422 на пустом recipient организации
This commit is contained in:
@@ -91,6 +91,22 @@ void expense::createexp(name coopname, name username,
|
||||
"Неизвестная mechanics item (0 = ADVANCE, 1 = DIRECT)");
|
||||
eosio::check(it.recipient_type <= static_cast<uint8_t>(D::RecipientType::ORG),
|
||||
"Неизвестный recipient_type item");
|
||||
|
||||
// Инвариант: пайщик (SELF/MEMBER) получает только аванс под отчёт на личные
|
||||
// реквизиты (расходом станет после его отчёта чеком); организация — только
|
||||
// прямую оплату по выставленным реквизитам, аванс ей не выдаётся.
|
||||
if (it.recipient_type == static_cast<uint8_t>(D::RecipientType::ORG)) {
|
||||
eosio::check(it.mechanics == static_cast<uint8_t>(D::Mechanics::DIRECT),
|
||||
"Организации доступна только прямая оплата (DIRECT)");
|
||||
} else {
|
||||
eosio::check(it.mechanics == static_cast<uint8_t>(D::Mechanics::ADVANCE),
|
||||
"Пайщику средства выдаются только авансом под отчёт (ADVANCE)");
|
||||
eosio::check(it.recipient != name(),
|
||||
"recipient обязателен для получателя-пайщика");
|
||||
eosio::check(is_account(it.recipient),
|
||||
"Аккаунт получателя-пайщика не существует");
|
||||
}
|
||||
|
||||
it.status = static_cast<uint8_t>(D::ItemStatus::APPROVED);
|
||||
it.actual_amount = zero_like(it.planned_amount);
|
||||
}
|
||||
|
||||
+13
@@ -77,6 +77,19 @@ class ExpenseProposalHeaderInputDTO implements HeaderAction {
|
||||
@Field({ description: 'Кошелёк-источник' })
|
||||
@IsString()
|
||||
source_wallet!: string;
|
||||
|
||||
@Field({ description: 'Срок исполнения («в срок до»), формат DD.MM.YYYY', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deadline?: string;
|
||||
|
||||
@Field({
|
||||
description: 'Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно',
|
||||
nullable: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
fund_name?: string;
|
||||
}
|
||||
|
||||
@InputType('BaseExpenseProposalStatementMetaDocumentInput')
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export class ProgramExpensesManagementService {
|
||||
itemHash: it.item_hash,
|
||||
recipient: it.recipient,
|
||||
isOrganization: it.recipient_type === ExpenseRecipientType.ORG,
|
||||
mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 'DIRECT' : 'ADVANCE',
|
||||
paymentMethodId: it.payment_method_id,
|
||||
requisites: it.requisites,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||
import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum';
|
||||
import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum';
|
||||
|
||||
@@ -18,7 +18,10 @@ export class ExpenseItemInputDTO {
|
||||
@IsEnum(ExpenseRecipientType)
|
||||
recipient_type!: ExpenseRecipientType;
|
||||
|
||||
@Field(() => String, { description: 'Получатель (username / eosio::name организации).' })
|
||||
@Field(() => String, {
|
||||
description: 'Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет).',
|
||||
})
|
||||
@ValidateIf((o) => o.recipient_type !== ExpenseRecipientType.ORG)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
recipient!: string;
|
||||
|
||||
+14
@@ -55,6 +55,20 @@ export class ExpenseRequisiteSnapshotsService {
|
||||
): Promise<ExpenseRequisiteSnapshotTypeormEntity[]> {
|
||||
return Promise.all(
|
||||
items.map(async (it) => {
|
||||
// Инвариант шасси: пайщику — только аванс под отчёт (средства на личные
|
||||
// реквизиты, расходом станут после отчёта чеком); организации — только
|
||||
// прямая оплата по выставленным реквизитам.
|
||||
if (it.isOrganization && it.mechanics !== 'DIRECT') {
|
||||
throw new BadRequestException(
|
||||
'Организации доступна только прямая оплата по реквизитам (DIRECT) — аванс под отчёт ей не выдаётся'
|
||||
)
|
||||
}
|
||||
if (!it.isOrganization && it.mechanics !== 'ADVANCE') {
|
||||
throw new BadRequestException(
|
||||
`Пайщику ${it.recipient} средства передаются только авансом под отчёт (ADVANCE) — прямая оплата доступна только организации`
|
||||
)
|
||||
}
|
||||
|
||||
const snapshot = this.repository.create({
|
||||
coopname,
|
||||
proposal_hash: it.proposalHash.toLowerCase(),
|
||||
|
||||
+5
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../domain/interfaces/expenses-blockchain.port'
|
||||
import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'
|
||||
import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'
|
||||
import { EXPENSES_CHASSIS_CONFIG } from '../../domain/expenses-chassis.config'
|
||||
import { ExpenseRequisiteSnapshotsService } from './expense-requisite-snapshots.service'
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,9 @@ export class ExpensesMutationsService {
|
||||
): Promise<DocumentDomainEntity> {
|
||||
data.registry_id = Cooperative.Registry.ExpenseProposalStatement.registry_id
|
||||
|
||||
// Фонд списания — параметр шасси, фронт его не передаёт
|
||||
data.proposal = { ...data.proposal, fund_name: EXPENSES_CHASSIS_CONFIG.fundNameDative }
|
||||
|
||||
// Полные реквизиты получателей подставляет сервер: фронт оперирует только
|
||||
// идентификатором платёжного метода (и видит лишь сокращённое представление).
|
||||
// Служебные поля (payment_method_id / recipient_username) в meta документа
|
||||
@@ -177,6 +181,7 @@ function toRequisiteItems(proposalHash: string, items: ExpenseItemInputDTO[]): I
|
||||
itemHash: it.item_hash,
|
||||
recipient: it.recipient,
|
||||
isOrganization: it.recipient_type === ExpenseRecipientType.ORG,
|
||||
mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 'DIRECT' : 'ADVANCE',
|
||||
paymentMethodId: it.payment_method_id,
|
||||
requisites: it.requisites,
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Параметры шасси расходов.
|
||||
*
|
||||
* `fundNameDative` — фонд, по которому совет согласует списание затрат
|
||||
* (строка в служебной записке 2010: «списание затрат по …»). Хранится
|
||||
* в дательном падеже, т.к. подставляется после предлога «по». Дефолт —
|
||||
* фонд хозяйственной деятельности; другой фонд (развития кооперации,
|
||||
* материальной помощи и т.п.) задаётся через env EXPENSES_FUND_NAME.
|
||||
*/
|
||||
export const EXPENSES_CHASSIS_CONFIG = {
|
||||
fundNameDative: process.env.EXPENSES_FUND_NAME ?? 'Фонду хозяйственной деятельности',
|
||||
};
|
||||
@@ -5284,7 +5284,7 @@ export type ValueTypes = {
|
||||
payment_method_id?: string | undefined | null | Variable<any, string>,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string | Variable<any, string>,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string | Variable<any, string>,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>,
|
||||
@@ -5394,8 +5394,12 @@ export type ValueTypes = {
|
||||
requisites?: string | undefined | null | Variable<any, string>
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null | Variable<any, string>,
|
||||
/** Описание цели расходов */
|
||||
description: string | Variable<any, string>,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null | Variable<any, string>,
|
||||
/** Количество позиций */
|
||||
items_count: number | Variable<any, string>,
|
||||
/** Кошелёк-источник */
|
||||
@@ -14310,7 +14314,7 @@ export type ResolverInputTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ResolverInputTypes["ExpenseRecipientType"],
|
||||
@@ -14419,8 +14423,12 @@ export type ResolverInputTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
@@ -23077,7 +23085,7 @@ export type ModelTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ModelTypes["ExpenseRecipientType"],
|
||||
@@ -23183,8 +23191,12 @@ export type ModelTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
@@ -32631,7 +32643,7 @@ export type GraphQLTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: GraphQLTypes["ExpenseRecipientType"],
|
||||
@@ -32741,8 +32753,12 @@ export type GraphQLTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Описание цели расходов */
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
|
||||
+11
-2
@@ -21,6 +21,10 @@ export interface IExpenseProposalHeader {
|
||||
total_amount: string
|
||||
items_count: number
|
||||
source_wallet: string
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY — передаётся при создании расхода */
|
||||
deadline?: string
|
||||
/** Фонд списания в дательном падеже («по …») — подставляет сервер из параметров шасси расходов */
|
||||
fund_name?: string
|
||||
}
|
||||
|
||||
export interface Action extends IGenerate {
|
||||
@@ -48,7 +52,7 @@ export const description = 'Заявка-смета председателю к
|
||||
// дата слева, центрированный заголовок с идентификатором (= proposal_hash,
|
||||
// по нему записку можно найти), позиции — нумерованным списком с полными
|
||||
// реквизитами получателей. Кошелёк-источник в документе не указывается.
|
||||
export const context = `<style>h1{margin:0;text-align:center;}.digital-document{padding:20px;}.doc-header{text-align:right;padding-bottom:25px;}.doc-header p{margin:2px 0;}.doc-id{text-align:center;font-size:12px;word-break:break-all;padding-top:6px;padding-bottom:25px;}ol.items{padding-left:25px;margin:10px 0;}ol.items li{padding-bottom:10px;}.signature{padding-top:40px;}</style><div class="digital-document"><div class="doc-header"><p>{% trans 'TO_COUNCIL' %} {{vars.full_abbr_genitive}} «{{vars.name}}»</p><p>{% trans 'FROM_MEMBER' %} {{ user.full_name_or_short_name }}</p></div><p>{% trans 'DATE' %}: {{ meta.created_at }}</p><h1 style="padding-top:25px;">{% trans 'PROPOSAL_TITLE' %}</h1><p class="doc-id">№ {{ proposal_hash }}</p><p>{% trans 'BODY_INTRO' %} {{ proposal.total_amount }}, {% trans 'BODY_NAMELY' %}:</p><ol class="items">{% for item in items %}<li>{{ item.description }} — {{ item.amount }} ({% if item.mechanics == 'ADVANCE' %}{% trans 'MECH_ADVANCE' %}{% else %}{% trans 'MECH_DIRECT' %}{% endif %}). {% trans 'ITEM_RECIPIENT' %}: {% if item.recipient_type == 'SELF' %}{{ user.full_name_or_short_name }}{% else %}{{ item.recipient_name }}{% endif %}{% if item.requisites %}. {% trans 'ITEM_REQUISITES' %}: {{ item.requisites }}{% endif %}</li>{% endfor %}</ol><p>{% trans 'PURPOSE' %}: {{ proposal.description }}</p><div class="signature"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
|
||||
export const context = `<style>h1{margin:0;text-align:center;}.digital-document{padding:20px;}.doc-header{text-align:right;padding-bottom:25px;}.doc-header p{margin:2px 0;}.doc-id{text-align:center;font-size:12px;word-break:break-all;padding-top:6px;padding-bottom:25px;}ol.items{padding-left:25px;margin:10px 0;}ol.items li{padding-bottom:10px;}.signature{padding-top:40px;}</style><div class="digital-document"><div class="doc-header"><p>{% trans 'TO_COUNCIL' %} {{vars.full_abbr_genitive}} «{{vars.name}}»</p><p>{% trans 'FROM_MEMBER' %} {{ user.full_name_or_short_name }}</p></div><p>{% trans 'DATE' %}: {{ meta.created_at }}</p><h1 style="padding-top:25px;">{% trans 'PROPOSAL_TITLE' %}</h1><p class="doc-id">№ {{ proposal_hash }}</p><p>{% trans 'BODY_INTRO' %}{% if proposal.fund_name %} {% trans 'BY_FUND' %} {{ proposal.fund_name }}{% endif %} {% trans 'IN_TOTAL' %} {{ proposal.total_amount }}{% if proposal.deadline %} {% trans 'DUE_BY' %} {{ proposal.deadline }}{% endif %}, {% trans 'BODY_NAMELY' %}:</p><ol class="items">{% for item in items %}<li>{{ item.description }} — {{ item.amount }} ({% if item.mechanics == 'ADVANCE' %}{% trans 'MECH_ADVANCE' %}{% else %}{% trans 'MECH_DIRECT' %}{% endif %}). {% trans 'ITEM_RECIPIENT' %}: {% if item.recipient_type == 'SELF' %}{{ user.full_name_or_short_name }}{% else %}{{ item.recipient_name }}{% endif %}{% if item.requisites %}. {% trans 'ITEM_REQUISITES' %}: {{ item.requisites }}{% endif %}</li>{% endfor %}</ol><p>{% trans 'PURPOSE' %}: {{ proposal.description }}</p><div class="signature"><p>{{ user.full_name_or_short_name }}</p><p>{% trans 'SIGNED_DIGITALLY' %}</p></div></div>`
|
||||
|
||||
export const translations = {
|
||||
ru: {
|
||||
@@ -56,7 +60,10 @@ export const translations = {
|
||||
FROM_MEMBER: 'от пайщика',
|
||||
DATE: 'Дата',
|
||||
PROPOSAL_TITLE: 'СЛУЖЕБНАЯ ЗАПИСКА',
|
||||
BODY_INTRO: 'Прошу согласовать списание затрат в общей сумме',
|
||||
BODY_INTRO: 'Прошу согласовать списание затрат',
|
||||
BY_FUND: 'по',
|
||||
IN_TOTAL: 'в общей сумме',
|
||||
DUE_BY: 'в срок до',
|
||||
BODY_NAMELY: 'а именно',
|
||||
MECH_ADVANCE: 'аванс под отчёт',
|
||||
MECH_DIRECT: 'прямая оплата',
|
||||
@@ -78,6 +85,8 @@ export const exampleData = {
|
||||
total_amount: '15000.00 RUB',
|
||||
items_count: 2,
|
||||
source_wallet: 'w.cap.bla',
|
||||
deadline: '30.06.2026',
|
||||
fund_name: 'Фонду хозяйственной деятельности',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
|
||||
+9
@@ -27,6 +27,8 @@ export interface ICreateProgramExpenseDraftItem {
|
||||
|
||||
export interface ICreateProgramExpenseDraft {
|
||||
description: string;
|
||||
/** Срок исполнения («в срок до»), значение date-input `YYYY-MM-DD`. */
|
||||
deadline: string;
|
||||
items: ICreateProgramExpenseDraftItem[];
|
||||
}
|
||||
|
||||
@@ -91,6 +93,7 @@ export function useCreateProgramExpense() {
|
||||
total_amount,
|
||||
items_count: draft.items.length,
|
||||
source_wallet: PROGRAM_EXPENSE_SOURCE_WALLET,
|
||||
deadline: formatDeadline(draft.deadline),
|
||||
},
|
||||
items: itemsForDoc,
|
||||
};
|
||||
@@ -134,3 +137,9 @@ export function useCreateProgramExpense() {
|
||||
function generateItemHash(expense_hash: string, idx: number): string {
|
||||
return `${expense_hash.slice(0, 56)}${idx.toString(16).padStart(8, '0')}`;
|
||||
}
|
||||
|
||||
/** `YYYY-MM-DD` (date-input) → `DD.MM.YYYY` (документ). */
|
||||
function formatDeadline(value: string): string {
|
||||
const [year, month, day] = value.split('-');
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
+25
-2
@@ -14,6 +14,12 @@ BaseDialog(
|
||||
placeholder='Например: «Закупка хостинга и канцелярии на июнь»',
|
||||
required
|
||||
)
|
||||
BaseInput(
|
||||
v-model='form.deadline',
|
||||
type='date',
|
||||
label='Срок исполнения (в срок до)',
|
||||
required
|
||||
)
|
||||
|
||||
.field-group
|
||||
.field-group__head
|
||||
@@ -44,14 +50,15 @@ BaseDialog(
|
||||
v-model='item.recipient_type',
|
||||
:options='recipientTypeOptions',
|
||||
label='Тип получателя',
|
||||
@update:model-value='item.payment_method_id = null'
|
||||
@update:model-value='onRecipientTypeChange(item)'
|
||||
)
|
||||
.col-12.col-md-6
|
||||
BaseSelect(
|
||||
v-model='item.mechanics',
|
||||
:options='mechanicsOptions',
|
||||
label='Способ оплаты',
|
||||
hint='Аванс — под отчёт получателю; прямая — оплата по реквизитам'
|
||||
disabled,
|
||||
hint='Пайщику — только аванс под отчёт; организации — только прямая оплата'
|
||||
)
|
||||
.col-12.col-md-6(v-if='item.recipient_type === Zeus.ExpenseRecipientType.MEMBER')
|
||||
BaseInput(
|
||||
@@ -148,6 +155,7 @@ const amountPlaceholder = computed(() => {
|
||||
|
||||
const form = reactive({
|
||||
description: '',
|
||||
deadline: '',
|
||||
items: [] as ICreateProgramExpenseDraftItem[],
|
||||
});
|
||||
|
||||
@@ -164,9 +172,23 @@ const recipientTypeOptions = [
|
||||
{ label: 'Организация', value: Zeus.ExpenseRecipientType.ORG },
|
||||
];
|
||||
|
||||
// Пайщик получает только аванс под отчёт (личные реквизиты, отчитается чеком);
|
||||
// организация — только прямую оплату по выставленным реквизитам.
|
||||
function mechanicsFor(recipientType: Zeus.ExpenseRecipientType): Zeus.ExpenseMechanics {
|
||||
return recipientType === Zeus.ExpenseRecipientType.ORG
|
||||
? Zeus.ExpenseMechanics.DIRECT
|
||||
: Zeus.ExpenseMechanics.ADVANCE;
|
||||
}
|
||||
|
||||
function onRecipientTypeChange(item: ICreateProgramExpenseDraftItem): void {
|
||||
item.payment_method_id = null;
|
||||
item.mechanics = mechanicsFor(item.recipient_type);
|
||||
}
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
form.description.trim().length > 0 &&
|
||||
form.deadline.trim().length > 0 &&
|
||||
form.items.length > 0 &&
|
||||
form.items.every(
|
||||
(i) =>
|
||||
@@ -206,6 +228,7 @@ async function submit(): Promise<void> {
|
||||
submitting.value = true;
|
||||
await submitProgramExpense({
|
||||
description: form.description,
|
||||
deadline: form.deadline,
|
||||
items: form.items,
|
||||
});
|
||||
SuccessAlert('Программный расход подан — заявление подписано и передано в шасси расходов');
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface ICreateProposalDraftItem {
|
||||
export interface ICreateProposalDraft {
|
||||
source_wallet: string;
|
||||
description: string;
|
||||
/** Срок исполнения («в срок до»), значение date-input `YYYY-MM-DD`. */
|
||||
deadline: string;
|
||||
items: ICreateProposalDraftItem[];
|
||||
}
|
||||
|
||||
@@ -51,6 +53,7 @@ export function useExpenseProposalActions() {
|
||||
total_amount,
|
||||
items_count: draft.items.length,
|
||||
source_wallet: draft.source_wallet,
|
||||
deadline: formatDeadline(draft.deadline),
|
||||
};
|
||||
|
||||
const generated = await generateExpenseProposalStatementDocument({
|
||||
@@ -95,3 +98,9 @@ export function useExpenseProposalActions() {
|
||||
function generateItemHash(proposal_hash: string, idx: number): string {
|
||||
return `${proposal_hash.slice(0, 56)}${idx.toString(16).padStart(8, '0')}`;
|
||||
}
|
||||
|
||||
/** `YYYY-MM-DD` (date-input) → `DD.MM.YYYY` (документ). */
|
||||
function formatDeadline(value: string): string {
|
||||
const [year, month, day] = value.split('-');
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ BaseDialog(
|
||||
placeholder='w.cap.blago / w.cap.gen / …',
|
||||
required
|
||||
)
|
||||
BaseInput(
|
||||
v-model='form.deadline',
|
||||
type='date',
|
||||
label='Срок исполнения (в срок до)',
|
||||
required
|
||||
)
|
||||
|
||||
.field-group
|
||||
.field-group__head
|
||||
@@ -58,7 +64,8 @@ BaseDialog(
|
||||
outlined,
|
||||
dense,
|
||||
emit-value,
|
||||
map-options
|
||||
map-options,
|
||||
@update:model-value='onRecipientTypeChange(item)'
|
||||
)
|
||||
.col-12.col-md-6
|
||||
q-select(
|
||||
@@ -68,7 +75,9 @@ BaseDialog(
|
||||
outlined,
|
||||
dense,
|
||||
emit-value,
|
||||
map-options
|
||||
map-options,
|
||||
disable,
|
||||
hint='Пайщику — только аванс под отчёт; организации — только прямая оплата'
|
||||
)
|
||||
.col-12.col-md-6
|
||||
BaseInput(
|
||||
@@ -133,6 +142,7 @@ const { submitProposal } = useExpenseProposalActions();
|
||||
const form = reactive({
|
||||
description: '',
|
||||
source_wallet: 'w.cap.blago',
|
||||
deadline: '',
|
||||
items: [] as ICreateProposalDraftItem[],
|
||||
});
|
||||
|
||||
@@ -149,10 +159,16 @@ const mechanicsOptions = [
|
||||
{ label: 'Прямая оплата', value: 'DIRECT' as const },
|
||||
];
|
||||
|
||||
// Пайщик получает только аванс под отчёт; организация — только прямую оплату.
|
||||
function onRecipientTypeChange(item: ICreateProposalDraftItem): void {
|
||||
item.mechanics = item.recipient_type === 'ORG' ? 'DIRECT' : 'ADVANCE';
|
||||
}
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
form.description.trim().length > 0 &&
|
||||
form.source_wallet.trim().length > 0 &&
|
||||
form.deadline.trim().length > 0 &&
|
||||
form.items.length > 0 &&
|
||||
form.items.every((i) => i.amount.trim() && i.description.trim()),
|
||||
);
|
||||
@@ -184,6 +200,7 @@ async function submit(): Promise<void> {
|
||||
await submitProposal({
|
||||
description: form.description,
|
||||
source_wallet: form.source_wallet,
|
||||
deadline: form.deadline,
|
||||
items: form.items,
|
||||
});
|
||||
SuccessAlert('Служебная записка подана — заявление подписано и отправлено в блокчейн');
|
||||
|
||||
@@ -90,6 +90,12 @@ export interface InterExpenseRequisiteItemInput {
|
||||
recipient: string;
|
||||
/** true — получатель-организация: реквизиты приходят строкой `requisites`. */
|
||||
isOrganization: boolean;
|
||||
/**
|
||||
* Способ оплаты строки. Инвариант шасси: пайщик (SELF/MEMBER) получает только
|
||||
* ADVANCE (аванс под отчёт на личные реквизиты, расходом станет после отчёта
|
||||
* чеком), организация — только DIRECT (прямая оплата по выставленным реквизитам).
|
||||
*/
|
||||
mechanics: 'ADVANCE' | 'DIRECT';
|
||||
/** Идентификатор платёжного метода получателя-пайщика. */
|
||||
paymentMethodId?: string;
|
||||
/** Реквизиты строкой (организации — ручной ввод из формы). */
|
||||
|
||||
@@ -5284,7 +5284,7 @@ export type ValueTypes = {
|
||||
payment_method_id?: string | undefined | null | Variable<any, string>,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string | Variable<any, string>,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string | Variable<any, string>,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ValueTypes["ExpenseRecipientType"] | Variable<any, string>,
|
||||
@@ -5394,8 +5394,12 @@ export type ValueTypes = {
|
||||
requisites?: string | undefined | null | Variable<any, string>
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null | Variable<any, string>,
|
||||
/** Описание цели расходов */
|
||||
description: string | Variable<any, string>,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null | Variable<any, string>,
|
||||
/** Количество позиций */
|
||||
items_count: number | Variable<any, string>,
|
||||
/** Кошелёк-источник */
|
||||
@@ -14310,7 +14314,7 @@ export type ResolverInputTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ResolverInputTypes["ExpenseRecipientType"],
|
||||
@@ -14419,8 +14423,12 @@ export type ResolverInputTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
@@ -23077,7 +23085,7 @@ export type ModelTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: ModelTypes["ExpenseRecipientType"],
|
||||
@@ -23183,8 +23191,12 @@ export type ModelTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
@@ -32631,7 +32643,7 @@ export type GraphQLTypes = {
|
||||
payment_method_id?: string | undefined | null,
|
||||
/** Планируемая сумма (asset, eg "1000.0000 RUB"). */
|
||||
planned_amount: string,
|
||||
/** Получатель (username / eosio::name организации). */
|
||||
/** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */
|
||||
recipient: string,
|
||||
/** Тип получателя. */
|
||||
recipient_type: GraphQLTypes["ExpenseRecipientType"],
|
||||
@@ -32741,8 +32753,12 @@ export type GraphQLTypes = {
|
||||
requisites?: string | undefined | null
|
||||
};
|
||||
["ExpenseProposalHeaderInput"]: {
|
||||
/** Описание цели расходов */
|
||||
/** Срок исполнения («в срок до»), формат DD.MM.YYYY */
|
||||
deadline?: string | undefined | null,
|
||||
/** Описание цели расходов */
|
||||
description: string,
|
||||
/** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */
|
||||
fund_name?: string | undefined | null,
|
||||
/** Количество позиций */
|
||||
items_count: number,
|
||||
/** Кошелёк-источник */
|
||||
|
||||
Reference in New Issue
Block a user