Compare commits
4 Commits
023c0913f1
...
6460a2a22d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6460a2a22d | |||
| edd5008120 | |||
| 5107f62e54 | |||
| 780d92cd3d |
+1
-1
@@ -52,7 +52,7 @@
|
|||||||
| [`hr-business-trip`](../src/webparts/requestPortal/forms/BusinessTrip/README.md) | Заявка на командировку | Отдел кадров | да | да | да | можно за другого сотрудника; маршрут лентой (точки = город + объект из справочника + даты, участки с видом транспорта); задание — список задач; смета: статьи с позициями, привязанными к точкам/участкам (суммой или кол-во × цена — всё вручную), итог с долями и по объектам; обязательство по авансовому отчёту — флажок; печать без шапки адресата | ⏳ код готов, списки/тип/маршрут не заведены |
|
| [`hr-business-trip`](../src/webparts/requestPortal/forms/BusinessTrip/README.md) | Заявка на командировку | Отдел кадров | да | да | да | можно за другого сотрудника; маршрут лентой (точки = город + объект из справочника + даты, участки с видом транспорта); задание — список задач; смета: статьи с позициями, привязанными к точкам/участкам (суммой или кол-во × цена — всё вручную), итог с долями и по объектам; обязательство по авансовому отчёту — флажок; печать без шапки адресата | ⏳ код готов, списки/тип/маршрут не заведены |
|
||||||
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
||||||
| [`wh-service-request`](../src/webparts/requestPortal/forms/ServiceRequest/README.md) | Заявка на услуги | Склад / Снабжение | да | нет | да | маршрут по проекту (руководитель проекта/ПТО); табличные части «Приложения» и «Смета» (JSON-колонки); сумма по смете считается на лету (qty × цена), итог дублируется числовой колонкой для отчётов | ✅ |
|
| [`wh-service-request`](../src/webparts/requestPortal/forms/ServiceRequest/README.md) | Заявка на услуги | Склад / Снабжение | да | нет | да | маршрут по проекту (руководитель проекта/ПТО); табличные части «Приложения» и «Смета» (JSON-колонки); сумма по смете считается на лету (qty × цена), итог дублируется числовой колонкой для отчётов | ✅ |
|
||||||
| [`acc-payment-request`](../src/webparts/requestPortal/forms/PaymentRequest/README.md) | Заявка на оплату | Бухгалтерия | да | нет | да | основание — согласованная СТ (сайт СТ) или потребность; счёт PDF → библиотека → OCR → ИИ (реквизиты, суммы, позиции); сверка позиций счёта с СТ; подсветка чужого получателя и превышения общей суммы СТ; справочники 1С | ⏳ код готов, списки/библиотека/тип/маршрут не заведены |
|
| [`acc-payment-request`](../src/webparts/requestPortal/forms/PaymentRequest/README.md) | Заявка на оплату | Бухгалтерия | да | нет | да | основание — согласованная СТ (сайт СТ) или потребность; счёт PDF → библиотека → OCR → ИИ (реквизиты, суммы, позиции); сверка позиций счёта с СТ; подсветка чужого получателя и превышения общей суммы СТ; справочники 1С | ⏳ списки, библиотека, тип (id 24) и маршрут заведены; не проверена на живых данных |
|
||||||
|
|
||||||
Легенда: **Согл.** — есть фаза согласования; **Исп.** — есть фаза исполнения;
|
Легенда: **Согл.** — есть фаза согласования; **Исп.** — есть фаза исполнения;
|
||||||
**Просмотр** — есть режим согласующего (`<Форма>View`).
|
**Просмотр** — есть режим согласующего (`<Форма>View`).
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
**Назначение:** оплата по согласованной СТ (сравнительной таблице) или по потребности — счёт, получатель, суммы и сверка позиций счёта с СТ.
|
**Назначение:** оплата по согласованной СТ (сравнительной таблице) или по потребности — счёт, получатель, суммы и сверка позиций счёта с СТ.
|
||||||
**Категория:** Бухгалтерия
|
**Категория:** Бухгалтерия
|
||||||
**Списки SP:** «Заявка на оплату — данные», «Заявка на оплату — согласование»; библиотека «Счета на оплату» (создаётся вручную, колонка `file_content` — многострочный текст)
|
**Списки SP:** «Заявка на оплату — данные», «Заявка на оплату — согласование»; библиотека «Счета на оплату» (создаётся вручную, колонка `file_content` — многострочный текст)
|
||||||
**RequestTypeId (внешний API маршрутов):** резолвится в рантайме по `pageUrl` (см. `docs/forms.md`) — тип `acc-payment-request` нужно завести в админке маршрутов
|
**RequestTypeId (внешний API маршрутов):** резолвится в рантайме по `pageUrl` (см. `docs/forms.md`); заведён тип 24, раздел «Бухгалтерия», список данных `/sites/requests/Lists/120`
|
||||||
|
|
||||||
## Маршрут
|
## Маршрут
|
||||||
- Фазы: согласование (исполнителей нет)
|
- Фазы: согласование (исполнителей нет)
|
||||||
- Маршрут зависит от проекта (`ProjectId`) — как у «Заявки на услуг»: пока проект не выбран, матрица не строится.
|
- Маршрут зависит от проекта (`ProjectId`) — как у «Заявки на услуги»: пока проект не выбран, матрица не строится.
|
||||||
|
- Маршрут 25 (`byProject: true`, без компании и подразделения — для любого проекта): один шаг согласования, фиксированный человек `gorbenko.pavel@tnsu.kz`. Форма всегда шлёт `projectId`, поэтому маршрута с `byProject: false` нет.
|
||||||
|
|
||||||
## Особое поведение
|
## Особое поведение
|
||||||
- **Основание** — согласованная СТ (`status = Согласовано` в списке «Сравнительная таблица» сайта СТ, `CT_SITE_URL`) либо потребность (`GET /Requests/365`, позиции — `/Requests/{oid}/positions`; тот же источник, что шаг 1 СТ). Выбор подставляет проект, компанию (если известна), получателя, раздел, вид работ, условия, общую сумму и позиции (`basisPatch.ts`); всё остаётся редактируемым.
|
- **Основание** — согласованная СТ (`status = Согласовано` в списке «Сравнительная таблица» сайта СТ, `CT_SITE_URL`) либо потребность (`GET /Requests/365`, позиции — `/Requests/{oid}/positions`; тот же источник, что шаг 1 СТ). Выбор подставляет проект, компанию (если известна), получателя, раздел, вид работ, условия, общую сумму и позиции (`basisPatch.ts`); всё остаётся редактируемым.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './PaymentSections.module.scss';
|
import styles from './PaymentSections.module.scss';
|
||||||
import { FormGrid, FormField, TextInput, TextArea } from '../../../shared/ui';
|
import { FormField, TextInput, TextArea } from '../../../shared/ui';
|
||||||
import { formatAmount, overLimitAmount, previousPaymentsTotal } from '../amounts';
|
import { formatAmount, overLimitAmount, previousPaymentsTotal } from '../amounts';
|
||||||
import { ISectionProps } from '../model';
|
import { ISectionProps } from '../model';
|
||||||
import { AmountInput } from './AmountInput';
|
import { AmountInput } from './AmountInput';
|
||||||
@@ -15,7 +15,6 @@ export const AmountsSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormGrid>
|
|
||||||
<FormField label="Общая сумма" htmlFor="TotalAmount">
|
<FormField label="Общая сумма" htmlFor="TotalAmount">
|
||||||
<AmountInput id="TotalAmount" value={data.TotalAmount} onChange={(v) => set('TotalAmount', v)} />
|
<AmountInput id="TotalAmount" value={data.TotalAmount} onChange={(v) => set('TotalAmount', v)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -42,11 +41,10 @@ export const AmountsSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
<FormField label="Комментарий" htmlFor="Comment" full>
|
<FormField label="Комментарий" htmlFor="Comment" full>
|
||||||
<TextArea id="Comment" value={data.Comment} onChange={(v) => set('Comment', v)} rows={2} />
|
<TextArea id="Comment" value={data.Comment} onChange={(v) => set('Comment', v)} rows={2} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormGrid>
|
|
||||||
|
|
||||||
<h4>Заявки на оплату по этому основанию</h4>
|
<h4 className={styles.fullRow}>Заявки на оплату по этому основанию</h4>
|
||||||
{data.PreviousPayments.length === 0 ? (
|
{data.PreviousPayments.length === 0 ? (
|
||||||
<div className={styles.empty}>Ранее заявок на оплату по этому основанию не было.</div>
|
<div className={`${styles.empty} ${styles.fullRow}`}>Ранее заявок на оплату по этому основанию не было.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.tableWrap}>
|
<div className={styles.tableWrap}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Alert, FormGrid, FormField, Select, ISelectOption } from '../../../shared/ui';
|
import { Alert, FormField, Select, ISelectOption } from '../../../shared/ui';
|
||||||
|
import styles from './PaymentSections.module.scss';
|
||||||
import { ICompany, ProjectPicker, getCompanies } from '../../../shared/dictionary';
|
import { ICompany, ProjectPicker, getCompanies } from '../../../shared/dictionary';
|
||||||
import { BASIS_OPTIONS, BasisKind, ISectionProps } from '../model';
|
import { BASIS_OPTIONS, BasisKind, ISectionProps } from '../model';
|
||||||
import { IBasisSelection } from '../useBasisSelection';
|
import { IBasisSelection } from '../useBasisSelection';
|
||||||
@@ -43,7 +44,6 @@ export const BasisSection: React.FC<IBasisSectionProps> = ({ data, patch, contex
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormGrid>
|
|
||||||
<FormField label="Основание" htmlFor="BasisKind" required>
|
<FormField label="Основание" htmlFor="BasisKind" required>
|
||||||
<Select id="BasisKind" value={data.BasisKind} onChange={handleKindChange} options={KIND_OPTIONS} />
|
<Select id="BasisKind" value={data.BasisKind} onChange={handleKindChange} options={KIND_OPTIONS} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -80,9 +80,8 @@ export const BasisSection: React.FC<IBasisSectionProps> = ({ data, patch, contex
|
|||||||
placeholder="Выберите компанию…"
|
placeholder="Выберите компанию…"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormGrid>
|
{basis.loading && <div className={styles.fullRow}><Alert variant="info">Загружаем данные основания…</Alert></div>}
|
||||||
{basis.loading && <Alert variant="info">Загружаем данные основания…</Alert>}
|
{basis.error && <div className={styles.fullRow}><Alert variant="err">{basis.error}</Alert></div>}
|
||||||
{basis.error && <Alert variant="err">{basis.error}</Alert>}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './PaymentSections.module.scss';
|
import styles from './PaymentSections.module.scss';
|
||||||
import { FormGrid, FormField, TextInput, TextArea } from '../../../shared/ui';
|
import { FormField, TextInput, TextArea } from '../../../shared/ui';
|
||||||
import {
|
import {
|
||||||
DictionaryPicker,
|
DictionaryPicker,
|
||||||
getCashFlowGroups,
|
getCashFlowGroups,
|
||||||
@@ -25,7 +25,7 @@ export const PaymentSection: React.FC<ISectionProps> = ({ data, set, patch, cont
|
|||||||
const accountFilter = { companyId: data.CompanyId, projectId: data.ProjectId };
|
const accountFilter = { companyId: data.CompanyId, projectId: data.ProjectId };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGrid>
|
<>
|
||||||
<FormField label="Банковский счёт" htmlFor="BankAccountName" required>
|
<FormField label="Банковский счёт" htmlFor="BankAccountName" required>
|
||||||
<DictionaryPicker
|
<DictionaryPicker
|
||||||
id="BankAccountName"
|
id="BankAccountName"
|
||||||
@@ -99,6 +99,6 @@ export const PaymentSection: React.FC<ISectionProps> = ({ data, set, patch, cont
|
|||||||
<FormField label="Условия оплаты" htmlFor="PaymentTerms">
|
<FormField label="Условия оплаты" htmlFor="PaymentTerms">
|
||||||
<TextArea id="PaymentTerms" value={data.PaymentTerms} onChange={(v) => set('PaymentTerms', v)} rows={2} />
|
<TextArea id="PaymentTerms" value={data.PaymentTerms} onChange={(v) => set('PaymentTerms', v)} rows={2} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormGrid>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tableWrap {
|
.tableWrap {
|
||||||
|
// Тело секции — flex/grid из ui-kit: без явной ширины блок ужимается по содержимому.
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
grid-column: 1 / -1;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
border-radius: $r-sm;
|
border-radius: $r-sm;
|
||||||
@@ -97,3 +102,9 @@
|
|||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Тело CollapseSection из ui-kit — сетка в две колонки; всё не-поле растягиваем на обе.
|
||||||
|
.fullRow {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
import { APPROVAL_API_CLIENT_ID, DATA_SITE_URL, DICTIONARY_API_BASE_URL } from '../../../shared/config';
|
import { APPROVAL_API_CLIENT_ID, DATA_SITE_URL, DICTIONARY_API_BASE_URL } from '../../../shared/config';
|
||||||
import { PAYMENT_INVOICES_LIBRARY_TITLE } from '../../../shared/listTitles';
|
import { PAYMENT_INVOICES_LIBRARY_TITLE } from '../../../shared/listTitles';
|
||||||
import { getSP } from '../../../shared/pnp/pnpjs';
|
import { getSP } from '../../../shared/pnp/pnpjs';
|
||||||
import { fileStamp, storedFileName } from '../../../shared/chat/chatFileNames';
|
import { fileStamp } from '../../../shared/chat/chatFileNames';
|
||||||
|
|
||||||
/** Колонка библиотеки, куда OCR кладёт распознанный текст счёта. */
|
/** Колонка библиотеки, куда OCR кладёт распознанный текст счёта. */
|
||||||
const FILE_CONTENT_FIELD = 'file_content';
|
const FILE_CONTENT_FIELD = 'file_content';
|
||||||
@@ -37,10 +37,13 @@ export function validateInvoiceFile(file: File): string | undefined {
|
|||||||
/**
|
/**
|
||||||
* Сохранить файл счёта в библиотеку. Делаем это сразу при выборе файла, до
|
* Сохранить файл счёта в библиотеку. Делаем это сразу при выборе файла, до
|
||||||
* сохранения самой заявки: OCR работает с элементом библиотеки, а не с вложением.
|
* сохранения самой заявки: OCR работает с элементом библиотеки, а не с вложением.
|
||||||
* Отметка времени в имени исключает перезапись счёта с тем же именем из другой заявки.
|
* Имя в библиотеке — только латиница и цифры: OCR-сервис получает имя в multipart и
|
||||||
|
* на кириллице теряет расширение («unsupported file type ''»). Исходное имя
|
||||||
|
* хранится в данных заявки (InvoiceFileName), поэтому в библиотеке оно не нужно.
|
||||||
|
* Метка времени и случайный хвост исключают перезапись счёта другой заявки.
|
||||||
*/
|
*/
|
||||||
export async function storeInvoice(file: File): Promise<IStoredInvoice> {
|
export async function storeInvoice(file: File): Promise<IStoredInvoice> {
|
||||||
const storedName = storedFileName(fileStamp(new Date()), 1, file.name);
|
const storedName = `invoice-${fileStamp(new Date())}-${Math.random().toString(36).slice(2, 8)}.pdf`;
|
||||||
const root = getSP().web.lists.getByTitle(PAYMENT_INVOICES_LIBRARY_TITLE).rootFolder;
|
const root = getSP().web.lists.getByTitle(PAYMENT_INVOICES_LIBRARY_TITLE).rootFolder;
|
||||||
const added = await root.files.addUsingPath(storedName, file);
|
const added = await root.files.addUsingPath(storedName, file);
|
||||||
// Id элемента библиотеки нужен OCR, а addUsingPath отдаёт только сведения о файле.
|
// Id элемента библиотеки нужен OCR, а addUsingPath отдаёт только сведения о файле.
|
||||||
|
|||||||
Reference in New Issue
Block a user