forked from CITKParus/P8-ExchangeService
Запрос статуса зарегистрированного в АТОЛ-Онлайн чека, канализация алгоритмов подбора функции и сервиса обработчика от версии ФФД
This commit is contained in:
parent
2c6e24f91e
commit
9b42179bde
@ -1,5 +1,21 @@
|
||||
create or replace package UDO_PKG_EXS_ATOL as
|
||||
|
||||
/* Константы - типы функций обработки */
|
||||
SFN_TYPE_REG_BILL constant varchar2(20) := 'REG_BILL'; -- Типовая функция регистрации чека
|
||||
SFN_TYPE_GET_BILL_INF constant varchar2(20) := 'GET_BILL_INF'; -- Типовая функция получения иформации о регистрации чека
|
||||
|
||||
/* Получение рег. номера функции сервиса обмена для регистрации чека по рег. номеру фискального документа */
|
||||
function UTL_FISCDOC_GET_REG_EXSFN
|
||||
(
|
||||
NFISCDOC in number -- Рег. номер фискального документа
|
||||
) return number; -- Рег. номер функции регистрации чека в сервисе АТОЛ-Онлайн
|
||||
|
||||
/* Получение рег. номера функции сервиса обмена для запроса информации о регистрации чека по рег. номеру фискального документа */
|
||||
function UTL_FISCDOC_GET_INF_EXSFN
|
||||
(
|
||||
NFISCDOC in number -- Рег. номер фискального документа
|
||||
) return number; -- Рег. номер функции запроса информации о регистрации чека в сервисе АТОЛ-Онлайн
|
||||
|
||||
/* Îòðàáîòêà îòâåòîâ ÀÒÎË (v4) íà ðåãèñòðàöèþ ÷åêà íà ïðèõîä, ðàñõîä, âîçâðàò (ÔÔÄ 1.05) */
|
||||
procedure V4_FFD105_PROCESS_REG_BILL_SIR
|
||||
(
|
||||
@ -10,38 +26,211 @@ create or replace package UDO_PKG_EXS_ATOL as
|
||||
/* Îòðàáîòêà îòâåòîâ ÀÒÎË (v4) íà çàïðîñ ñâåäåíèé î çàðåãèñòðèðîâàííîì äîêóìåíòå (ÔÔÄ 1.05) */
|
||||
procedure V4_FFD105_PROCESS_GET_BILL_INF
|
||||
(
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
);
|
||||
|
||||
end;
|
||||
/
|
||||
create or replace package body UDO_PKG_EXS_ATOL as
|
||||
|
||||
/* Отработка ответов АТОЛ (v4) на регистрацию чека на приход, расход, возврат (ФФД 1.05) */
|
||||
procedure V4_FFD105_PROCESS_REG_BILL_SIR
|
||||
/* Константы - состояния документа в АТОЛ */
|
||||
SSTATUS_DONE constant varchar2(10) := 'done'; -- Готово
|
||||
SSTATUS_FAIL constant varchar2(10) := 'fail'; -- Ошибка
|
||||
SSTATUS_WAIT constant varchar2(10) := 'wait'; -- Ожидание
|
||||
|
||||
/* Шаблон URL для чека ОФД */
|
||||
SBILL_OFD_UTL constant varchar2(80) := 'https://ofd.ru/rec/<1041>/<1040>/<1077>?format=pdf';
|
||||
|
||||
/* Проверка корректности атрибутов позиции очереди */
|
||||
procedure UTL_EXSQUEUE_CHECK_ATTRS
|
||||
(
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
REXSQUEUE in EXSQUEUE%rowtype -- Проверяемая запись позиции очереди
|
||||
)
|
||||
is
|
||||
REXSQUEUE EXSQUEUE%rowtype; -- Запись позиции очереди
|
||||
CTMP clob; -- Буфер для хранения данных ответа сервера
|
||||
begin
|
||||
/* Считаем запись очереди */
|
||||
REXSQUEUE := GET_EXSQUEUE_ID(NFLAG_SMART => 0, NRN => NEXSQUEUE);
|
||||
/* Проверим что позиция очереди корректна */
|
||||
/* Должна быть организация */
|
||||
if (REXSQUEUE.LNK_COMPANY is null) then
|
||||
P_EXCEPTION(0, 'Для позиции очереди не указана связанная организация.');
|
||||
end if;
|
||||
/* Должна быть связь с документом */
|
||||
if (REXSQUEUE.LNK_DOCUMENT is null) then
|
||||
P_EXCEPTION(0, 'Äëÿ ïîçèöèè î÷åðåäè íå óêàçàí ñâÿçàííûé äîêóìåíò.');
|
||||
end if;
|
||||
/* Должна быть связь с разделом */
|
||||
if (REXSQUEUE.LNK_UNITCODE is null) then
|
||||
P_EXCEPTION(0, 'Äëÿ ïîçèöèè î÷åðåäè íå óêàçàí ñâÿçàííûé ðàçäåë.');
|
||||
end if;
|
||||
/* Должна быть связь именно с разделом "Фискальные документы" */
|
||||
if (REXSQUEUE.LNK_UNITCODE <> 'UDO_FiscalDocuments') then
|
||||
P_EXCEPTION(0,
|
||||
'Ñâÿçàííûé ðàçäåë "%s", óêàçàííûé â ïîçèöèè î÷åðåäè, íå ïîääåðæèâàåòñÿ.',
|
||||
REXSQUEUE.LNK_UNITCODE);
|
||||
end if;
|
||||
end UTL_EXSQUEUE_CHECK_ATTRS;
|
||||
|
||||
/* Считывание записи фискального документа */
|
||||
function UTL_FISCDOC_GET
|
||||
(
|
||||
NFISCDOC in number -- Рег. номер фискального документа
|
||||
) return UDO_V_FISCDOCS%rowtype -- Найденная запись фискального документа
|
||||
is
|
||||
RRES UDO_V_FISCDOCS%rowtype; -- Буфер для результата
|
||||
begin
|
||||
/* Считаем запись */
|
||||
select T.* into RRES from UDO_V_FISCDOCS T where T.NRN = NFISCDOC;
|
||||
/* Вернём результат */
|
||||
return RRES;
|
||||
exception
|
||||
when NO_DATA_FOUND then
|
||||
PKG_MSG.RECORD_NOT_FOUND(NFLAG_SMART => 0, NDOCUMENT => NFISCDOC, SUNIT_TABLE => 'UDO_FISCDOCS');
|
||||
end UTL_FISCDOC_GET;
|
||||
|
||||
/* Получение мнемокода сервиса обмена и мнемокода его функции по типу функции обработки и версии ФФД */
|
||||
procedure UTL_FISCDOC_GET_EXSFN_BY_FFD
|
||||
(
|
||||
SFN_TYPE in varchar2, -- Тип функции обработки (см. константы SFN_TYPE_*)
|
||||
SFFD_VERSION in varchar2, -- Версия ФФД
|
||||
SEXSSERVICE out varchar2, -- Код сервиса-обработчика
|
||||
SEXSSERVICEFN out varchar2 -- Код функции-обработчика
|
||||
)
|
||||
is
|
||||
begin
|
||||
/* Работаем от типовой функции */
|
||||
case SFN_TYPE
|
||||
/* Регистрация чека */
|
||||
when SFN_TYPE_REG_BILL then
|
||||
begin
|
||||
/* Выбираем API обмена в зависимости от версии фискального документа */
|
||||
case SFFD_VERSION
|
||||
/* ФФД 1.05 */
|
||||
when '1.05' then
|
||||
begin
|
||||
SEXSSERVICE := 'АТОЛ_V4_ИСХ';
|
||||
SEXSSERVICEFN := 'V4_ФФД1.05_РегистрацияЧекаРПВ';
|
||||
end;
|
||||
/* Неизвестная версия ФФД */
|
||||
else
|
||||
begin
|
||||
P_EXCEPTION(0,
|
||||
'Версия фискального документа "%s" не поддерживается!',
|
||||
SFFD_VERSION);
|
||||
end;
|
||||
end case;
|
||||
end;
|
||||
/* Получение иформации о регистрации чека */
|
||||
when SFN_TYPE_GET_BILL_INF then
|
||||
begin
|
||||
/* Выбираем API обмена в зависимости от версии фискального документа */
|
||||
case SFFD_VERSION
|
||||
/* ФФД 1.05 */
|
||||
when '1.05' then
|
||||
begin
|
||||
SEXSSERVICE := 'АТОЛ_V4_ИСХ';
|
||||
SEXSSERVICEFN := 'V4_ФФД1.05_РезОбрабЧека';
|
||||
end;
|
||||
/* Неизвестная версия ФФД */
|
||||
else
|
||||
begin
|
||||
P_EXCEPTION(0,
|
||||
'Версия фискального документа "%s" не поддерживается!',
|
||||
SFFD_VERSION);
|
||||
end;
|
||||
end case;
|
||||
end;
|
||||
/* Неизвестная типовая функция */
|
||||
else
|
||||
begin
|
||||
P_EXCEPTION(0, 'Типовая функция "%s" не поддерживается!', SFN_TYPE);
|
||||
end;
|
||||
end case;
|
||||
end UTL_FISCDOC_GET_EXSFN_BY_FFD;
|
||||
|
||||
/* Получение рег. номера функции сервиса обмена для регистрации чека по рег. номеру фискального документа */
|
||||
function UTL_FISCDOC_GET_REG_EXSFN
|
||||
(
|
||||
NFISCDOC in number -- Рег. номер фискального документа
|
||||
) return number -- Рег. номер функции регистрации чека в сервисе АТОЛ-Онлайн
|
||||
is
|
||||
NRES PKG_STD.TREF; -- Буфер для результата
|
||||
RFISCDOC UDO_V_FISCDOCS%rowtype; -- Запись фискального документа
|
||||
NEXSSERVICE EXSSERVICEFN.RN%type; -- Рег. номер сервиса-обработчика
|
||||
SEXSSERVICE EXSSERVICEFN.CODE%type; -- Код сервиса-обработчика
|
||||
SEXSSERVICEFN EXSSERVICEFN.CODE%type; -- Код функции-обработчика
|
||||
begin
|
||||
/* Считаем запись фискального документа */
|
||||
RFISCDOC := UTL_FISCDOC_GET(NFISCDOC => NFISCDOC);
|
||||
/* Определим мнемокоды сервиса и функции для обработки */
|
||||
UTL_FISCDOC_GET_EXSFN_BY_FFD(SFN_TYPE => SFN_TYPE_REG_BILL,
|
||||
SFFD_VERSION => RFISCDOC.STYPE_VERSION,
|
||||
SEXSSERVICE => SEXSSERVICE,
|
||||
SEXSSERVICEFN => SEXSSERVICEFN);
|
||||
/* Находим рег. номер сервиса */
|
||||
FIND_EXSSERVICE_CODE(NFLAG_SMART => 0, NFLAG_OPTION => 0, SCODE => SEXSSERVICE, NRN => NEXSSERVICE);
|
||||
/* Находим рег. номер функции сервиса */
|
||||
FIND_EXSSERVICEFN_CODE(NFLAG_SMART => 0,
|
||||
NFLAG_OPTION => 0,
|
||||
NEXSSERVICE => NEXSSERVICE,
|
||||
SCODE => SEXSSERVICEFN,
|
||||
NRN => NRES);
|
||||
/* Вернём результат */
|
||||
return NRES;
|
||||
end UTL_FISCDOC_GET_REG_EXSFN;
|
||||
|
||||
/* Получение рег. номера функции сервиса обмена для запроса информации о регистрации чека по рег. номеру фискального документа */
|
||||
function UTL_FISCDOC_GET_INF_EXSFN
|
||||
(
|
||||
NFISCDOC in number -- Рег. номер фискального документа
|
||||
) return number -- Рег. номер функции запроса информации о регистрации чека в сервисе АТОЛ-Онлайн
|
||||
is
|
||||
NRES PKG_STD.TREF; -- Буфер для результата
|
||||
RFISCDOC UDO_V_FISCDOCS%rowtype; -- Запись фискального документа
|
||||
NEXSSERVICE EXSSERVICEFN.RN%type; -- Рег. номер сервиса-обработчика
|
||||
SEXSSERVICE EXSSERVICEFN.CODE%type; -- Код сервиса-обработчика
|
||||
SEXSSERVICEFN EXSSERVICEFN.CODE%type; -- Код функции-обработчика
|
||||
begin
|
||||
/* Считаем запись фискального документа */
|
||||
RFISCDOC := UTL_FISCDOC_GET(NFISCDOC => NFISCDOC);
|
||||
/* Определим мнемокоды сервиса и функции для обработки */
|
||||
UTL_FISCDOC_GET_EXSFN_BY_FFD(SFN_TYPE => SFN_TYPE_GET_BILL_INF,
|
||||
SFFD_VERSION => RFISCDOC.STYPE_VERSION,
|
||||
SEXSSERVICE => SEXSSERVICE,
|
||||
SEXSSERVICEFN => SEXSSERVICEFN);
|
||||
/* Находим рег. номер сервиса */
|
||||
FIND_EXSSERVICE_CODE(NFLAG_SMART => 0, NFLAG_OPTION => 0, SCODE => SEXSSERVICE, NRN => NEXSSERVICE);
|
||||
/* Находим рег. номер функции сервиса */
|
||||
FIND_EXSSERVICEFN_CODE(NFLAG_SMART => 0,
|
||||
NFLAG_OPTION => 0,
|
||||
NEXSSERVICE => NEXSSERVICE,
|
||||
SCODE => SEXSSERVICEFN,
|
||||
NRN => NRES);
|
||||
/* Вернём результат */
|
||||
return NRES;
|
||||
end UTL_FISCDOC_GET_INF_EXSFN;
|
||||
|
||||
/* Отработка ответов АТОЛ (v4) на регистрацию чека на приход, расход, возврат (ФФД 1.05) */
|
||||
procedure V4_FFD105_PROCESS_REG_BILL_SIR
|
||||
(
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
)
|
||||
is
|
||||
REXSQUEUE EXSQUEUE%rowtype; -- Запись позиции очереди
|
||||
RFISCDOC UDO_V_FISCDOCS%rowtype; -- Запись фискального документа
|
||||
CTMP clob; -- Буфер для хранения данных ответа сервера
|
||||
begin
|
||||
/* Считаем запись очереди */
|
||||
REXSQUEUE := GET_EXSQUEUE_ID(NFLAG_SMART => 0, NRN => NEXSQUEUE);
|
||||
/* Проверим что позиция очереди корректна */
|
||||
UTL_EXSQUEUE_CHECK_ATTRS(REXSQUEUE => REXSQUEUE);
|
||||
/* Считаем запись фискального документа */
|
||||
RFISCDOC := UTL_FISCDOC_GET(NFISCDOC => REXSQUEUE.LNK_DOCUMENT);
|
||||
/* Проверим, что он верного формата */
|
||||
if (RFISCDOC.STYPE_VERSION <> '1.05') then
|
||||
P_EXCEPTION(0,
|
||||
'Версия формата фискального документа (%s) не поддерживается. Ожидаемая версия - 1.05.',
|
||||
RFISCDOC.STYPE_VERSION);
|
||||
end if;
|
||||
/* Ðàçáèðàåì îòâåò */
|
||||
CTMP := BLOB2CLOB(LBDATA => REXSQUEUE.RESP, SCHARSET => 'UTF8');
|
||||
if (CTMP is null) then
|
||||
@ -60,20 +249,153 @@ create or replace package body UDO_PKG_EXS_ATOL as
|
||||
/* Îòðàáîòêà îòâåòîâ ÀÒÎË (v4) íà çàïðîñ ñâåäåíèé î çàðåãèñòðèðîâàííîì äîêóìåíòå (ÔÔÄ 1.05) */
|
||||
procedure V4_FFD105_PROCESS_GET_BILL_INF
|
||||
(
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
NIDENT in number, -- Идентификатор процесса
|
||||
NEXSQUEUE in number -- Регистрационный номер обрабатываемой позиции очереди обмена
|
||||
)
|
||||
is
|
||||
REXSQUEUE EXSQUEUE%rowtype; -- Запись позиции очереди
|
||||
CTMP clob; -- Буфер для хранения данных ответа сервера
|
||||
REXSQUEUE EXSQUEUE%rowtype; -- Запись позиции очереди
|
||||
RFISCDOC UDO_V_FISCDOCS%rowtype; -- Запись фискального документа
|
||||
RDOC PKG_XPATH.TDOCUMENT; -- Разобранный XML-документ
|
||||
RROOT_NODE PKG_XPATH.TNODE; -- Корневой тэг XML-документа
|
||||
SSTATUS PKG_STD.TSTRING; -- Буфер для значения "Статус обработки документа"
|
||||
STIMESTAMP PKG_STD.TSTRING; -- Буфер для значения "Дата и время документа внешней системы" (строковое представление)
|
||||
DTIMESTAMP PKG_STD.TLDATE; -- Буфер для значения "Дата и время документа внешней системы"
|
||||
STAG1012 PKG_STD.TSTRING; -- Буфер для значения "Дата и время документа из ФН" (тэг 1012)
|
||||
STAG1040 PKG_STD.TSTRING; -- Буфер для значения "Фискальный номер документа" (тэг 1040)
|
||||
STAG1041 PKG_STD.TSTRING; -- Буфер для значения "Номер ФН" (тэг 1041)
|
||||
STAG1077 PKG_STD.TSTRING; -- Буфер для значения "Фискальный признак документа" (тэг 1077)
|
||||
SERR_CODE PKG_STD.TSTRING; -- Буфер для значения "Код ошибки"
|
||||
SERR_TEXT PKG_STD.TSTRING; -- Буфер для значения "Текст ошибки"
|
||||
begin
|
||||
/* Ñ÷èòàåì çàïèñü î÷åðåäè */
|
||||
REXSQUEUE := GET_EXSQUEUE_ID(NFLAG_SMART => 0, NRN => NEXSQUEUE);
|
||||
/* Разбираем ответ */
|
||||
CTMP := BLOB2CLOB(LBDATA => REXSQUEUE.RESP, SCHARSET => 'UTF8');
|
||||
if (CTMP is null) then
|
||||
P_EXCEPTION(0, 'Нет ответа от сервера.');
|
||||
/* Проверим что позиция очереди корректна */
|
||||
UTL_EXSQUEUE_CHECK_ATTRS(REXSQUEUE => REXSQUEUE);
|
||||
/* Считаем запись фискального документа */
|
||||
RFISCDOC := UTL_FISCDOC_GET(NFISCDOC => REXSQUEUE.LNK_DOCUMENT);
|
||||
/* Проверим, что он верного формата */
|
||||
if (RFISCDOC.STYPE_VERSION <> '1.05') then
|
||||
P_EXCEPTION(0,
|
||||
'Версия формата фискального документа (%s) не поддерживается. Ожидаемая версия - 1.05.',
|
||||
RFISCDOC.STYPE_VERSION);
|
||||
end if;
|
||||
/* Разбираем ответ */
|
||||
begin
|
||||
RDOC := PKG_XPATH.PARSE_FROM_BLOB(LBXML => REXSQUEUE.RESP, SCHARSET => 'UTF8');
|
||||
exception
|
||||
when others then
|
||||
P_EXCEPTION(0, 'Ошибка разбора XML - неожиданный ответ сервера.');
|
||||
end;
|
||||
/* Находим корневой элемент */
|
||||
RROOT_NODE := PKG_XPATH.ROOT_NODE(RDOCUMENT => RDOC);
|
||||
/* Забираем значения документа */
|
||||
SSTATUS := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE, SPATTERN => '/RESP/STATUS'));
|
||||
STIMESTAMP := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE,
|
||||
SPATTERN => '/RESP/TIMESTAMP'));
|
||||
STAG1012 := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE, SPATTERN => '/RESP/TAG1012'));
|
||||
STAG1040 := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE, SPATTERN => '/RESP/TAG1040'));
|
||||
STAG1041 := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE, SPATTERN => '/RESP/TAG1041'));
|
||||
STAG1077 := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE, SPATTERN => '/RESP/TAG1077'));
|
||||
SERR_CODE := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE,
|
||||
SPATTERN => '/RESP/ERROR/CODE'));
|
||||
SERR_TEXT := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RROOT_NODE,
|
||||
SPATTERN => '/RESP/ERROR/TEXT'));
|
||||
/* Освобождаем память */
|
||||
PKG_XPATH.FREE(RDOCUMENT => RDOC);
|
||||
/* Проверим, что указан статус документа */
|
||||
if (SSTATUS is null) then
|
||||
P_EXCEPTION(0, 'Не указан статус обработки документа.');
|
||||
end if;
|
||||
/* Обрабатываем ответ в зависимости от статуса */
|
||||
case SSTATUS
|
||||
/* Обрабатывается */
|
||||
when SSTATUS_WAIT then
|
||||
begin
|
||||
/* Документ ещё не обработан, ожидаем результатов, поэтому пока ничего не делаем */
|
||||
null;
|
||||
end;
|
||||
/* Готов */
|
||||
when SSTATUS_DONE then
|
||||
begin
|
||||
/* Проверим наличие данных в тэгах и дату ответа АТОЛ */
|
||||
if (STIMESTAMP is null) then
|
||||
P_EXCEPTION(0,
|
||||
'Документ в статусе "%s", но не указано значение "Дата и время документа внешней системы".',
|
||||
SSTATUS);
|
||||
end if;
|
||||
if (STAG1040 is null) then
|
||||
P_EXCEPTION(0,
|
||||
'Документ в статусе "%s", но не указано значение "Фискальный номер документа" (тэг 1040).',
|
||||
SSTATUS);
|
||||
end if;
|
||||
if (STAG1041 is null) then
|
||||
P_EXCEPTION(0,
|
||||
'Документ в статусе "%s", но не указано значение "Номер ФН" (тэг 1041).',
|
||||
SSTATUS);
|
||||
end if;
|
||||
if (STAG1077 is null) then
|
||||
P_EXCEPTION(0,
|
||||
'Документ в статусе "%s", но не указано значение "Фискальный признак документа" (тэг 1077).',
|
||||
SSTATUS);
|
||||
end if;
|
||||
/* Проверяем корректность даты подтверждения */
|
||||
begin
|
||||
DTIMESTAMP := TO_DATE(STIMESTAMP, 'dd.mm.yyyy hh24:mi:ss');
|
||||
exception
|
||||
when others then
|
||||
P_EXCEPTION(0,
|
||||
'Значение поля "Дата и время документа внешней системы" (%s) не является датой в формате "ДД.ММ.ГГГГ ЧЧ:МИ:CC"',
|
||||
STIMESTAMP);
|
||||
end;
|
||||
/* Выставляем значение "Дата подтверждения" и "Ссылка на фискальный документ в ОФД" для фискального документа */
|
||||
update UDO_FISCDOCS T
|
||||
set T.CONFIRM_DATE = DTIMESTAMP,
|
||||
T.DOC_URL = replace(replace(replace(SBILL_OFD_UTL, '<1040>', STAG1040), '<1041>', STAG1041),
|
||||
'<1077>',
|
||||
STAG1077)
|
||||
where T.RN = RFISCDOC.NRN;
|
||||
/* Устанавливаем значения тэгов */
|
||||
begin
|
||||
UDO_P_FISCDOCSPROP_SET_VAL(NPRN => RFISCDOC.NRN,
|
||||
NCOMPANY => RFISCDOC.NCOMPANY,
|
||||
SATTRIBUTE => '1040',
|
||||
NVAL_NUMB => TO_NUMBER(STAG1040));
|
||||
UDO_P_FISCDOCSPROP_SET_VAL(NPRN => RFISCDOC.NRN,
|
||||
NCOMPANY => RFISCDOC.NCOMPANY,
|
||||
SATTRIBUTE => '1041',
|
||||
SVAL_STR => STAG1041);
|
||||
UDO_P_FISCDOCSPROP_SET_VAL(NPRN => RFISCDOC.NRN,
|
||||
NCOMPANY => RFISCDOC.NCOMPANY,
|
||||
SATTRIBUTE => '1077',
|
||||
SVAL_STR => STAG1077);
|
||||
exception
|
||||
when others then
|
||||
P_EXCEPTION(0,
|
||||
'Ошибка установки значения атрибута фискального документа: %s',
|
||||
sqlerrm);
|
||||
end;
|
||||
end;
|
||||
/* Ошибка обработки */
|
||||
when SSTATUS_FAIL then
|
||||
begin
|
||||
/* Проверим, что пришли код и текст ошибки */
|
||||
if ((SERR_CODE is null) or (SERR_TEXT is null)) then
|
||||
P_EXCEPTION(0,
|
||||
'Документ в статусе "%s", но не указан код или текст ошибки.',
|
||||
SSTATUS);
|
||||
end if;
|
||||
/* Выставим код и текст в фискальном документе */
|
||||
update UDO_FISCDOCS T
|
||||
set T.SEND_ERROR = SUBSTR(SERR_CODE || ': ' ||
|
||||
REGEXP_REPLACE(replace(SERR_TEXT, CHR(10), ''), '[[:space:]]+', ' '),
|
||||
1,
|
||||
4000)
|
||||
where T.RN = RFISCDOC.NRN;
|
||||
end;
|
||||
/* Неизвестный статус */
|
||||
else
|
||||
P_EXCEPTION(0, 'Cтатус докуента "%s" не поддерживается.', SSTATUS);
|
||||
end case;
|
||||
/* Âñ¸ ïðîøëî óñïåøíî */
|
||||
PKG_EXS.PRC_RESP_RESULT_SET(NIDENT => NIDENT);
|
||||
exception
|
||||
@ -81,5 +403,6 @@ create or replace package body UDO_PKG_EXS_ATOL as
|
||||
/* Âåðí¸ì îøèáêó */
|
||||
PKG_EXS.PRC_RESP_RESULT_SET(NIDENT => NIDENT, SRESULT => PKG_EXS.SPRC_RESP_RESULT_ERR, SMSG => sqlerrm);
|
||||
end V4_FFD105_PROCESS_GET_BILL_INF;
|
||||
|
||||
end;
|
||||
/
|
||||
|
71
db/UDO_P_FISCDOCS_BASE_SEND.prc
Normal file
71
db/UDO_P_FISCDOCS_BASE_SEND.prc
Normal file
@ -0,0 +1,71 @@
|
||||
create or replace procedure UDO_P_FISCDOCS_BASE_SEND
|
||||
(
|
||||
NRN in number, -- Ðåãèñòðàöèîííûé íîìåð
|
||||
NCOMPANY in number -- Îðãàíèçàöèÿ
|
||||
)
|
||||
as
|
||||
NEXSQUEUE PKG_STD.TREF; -- Ðåãèñòðàöèîííûé íîìåð çàïèñè î÷åðåäè îáìåíà
|
||||
SPRC_NAME varchar2(60); -- Íàèìåíîâàíèå ïðîöåäóðû ïðîâåðêè çíà÷åíèé
|
||||
PRMS PKG_CONTPRMLOC.TCONTAINER; -- Êîíòåéíåð äëÿ ïàðàìåòðîâ ïðîöåäóðû îáðàáîòêè
|
||||
begin
|
||||
/* Ïðîâåðêà çàïîëíåíèÿ îáÿçàòåëüíûõ àòðèáóòîâ */
|
||||
UDO_P_FISCDOCSPROP_CHECK_REQ(NCOMPANY => NCOMPANY, NPRN => NRN);
|
||||
|
||||
/* Îïðåäåëåíèå ïðîöåäóðû ïðîâåðêè çíà÷åíèé */
|
||||
for REC in (select V.PKG_CHECK,
|
||||
V.PRC_CHECK
|
||||
from UDO_FISCDOCS T,
|
||||
UDO_FDKNDVERS V
|
||||
where T.RN = NRN
|
||||
and T.COMPANY = NCOMPANY
|
||||
and T.TYPE_VERSION = V.RN)
|
||||
loop
|
||||
/* Íàèìåíîâàíèå ïðîöåäóðû */
|
||||
SPRC_NAME := NULLIF(REC.PKG_CHECK || '.', '.') || REC.PRC_CHECK;
|
||||
end loop;
|
||||
|
||||
/* Åñëè åñòü çàïîëíåíà ïðîöåäóðà ïðîâåðêè */
|
||||
if (SPRC_NAME is not null) then
|
||||
/* Óñòàíîâêà çíà÷åíèé ôèêñèðîâàííûõ âõîäíûõ ïàðàìåòðîâ */
|
||||
PKG_CONTPRMLOC.APPENDN(RCONTAINER => PRMS, SNAME => 'NRN', NVALUE => NRN, NIN_OUT => PKG_STD.IPARAM_TYPE_IN);
|
||||
PKG_CONTPRMLOC.APPENDN(RCONTAINER => PRMS,
|
||||
SNAME => 'NCOMPANY',
|
||||
NVALUE => NCOMPANY,
|
||||
NIN_OUT => PKG_STD.IPARAM_TYPE_IN);
|
||||
/* Âûïîëíåíèå ïðîöåäóðû */
|
||||
begin
|
||||
PKG_SQL_CALL.EXECUTE_STORED(SSTORED_NAME => SPRC_NAME, RPARAM_CONTAINER => PRMS);
|
||||
exception
|
||||
when others then
|
||||
P_EXCEPTION(0,
|
||||
'Îøèáêà âûïîëíåíèÿ ïðîöåäóðû "%s".' || CR || 'Òåêñò îøèáêè: %s',
|
||||
SPRC_NAME,
|
||||
sqlerrm);
|
||||
end;
|
||||
end if;
|
||||
|
||||
/* Ôîðìèðîâàíèå è îòïðàâêà ñîîáùåíèÿ äëÿ ÀÒÎË-Îíëàéí */
|
||||
UDO_P_FISCDOCS_MAKE_MSG_ATOL(NCOMPANY => NCOMPANY, NFISCDOC => NRN, NEXSQUEUE => NEXSQUEUE);
|
||||
|
||||
/* Ñîçäàíèå ñâÿçè ìåæäó ÷åêîì è î÷åðåäüþ îáìåíà */
|
||||
PKG_DOCLINKS.LINK(NFLAG_SMART => 0,
|
||||
NCOMPANY => NCOMPANY,
|
||||
SIN_UNITCODE => 'UDO_FiscalDocuments',
|
||||
NIN_DOCUMENT => NRN,
|
||||
SOUT_UNITCODE => 'EXSQueue',
|
||||
NOUT_DOCUMENT => NEXSQUEUE);
|
||||
|
||||
/* Èçìåíåíèå ñòàòóñà */
|
||||
UDO_P_FISCDOCS_BASE_SET_STATUS(NRN => NRN, NCOMPANY => NCOMPANY, NSTATUS => 1);
|
||||
|
||||
/* Óñòàíîâêà äàòû è âðåìåíè îòïðàâêè */
|
||||
update UDO_FISCDOCS
|
||||
set SEND_TIME = sysdate
|
||||
where RN = NRN
|
||||
and COMPANY = NCOMPANY;
|
||||
|
||||
if (sql%notfound) then
|
||||
PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'UDO_FiscalDocuments');
|
||||
end if;
|
||||
end;
|
||||
/
|
27
db/UDO_P_FISCDOCS_GET_STATE_ATOL.prc
Normal file
27
db/UDO_P_FISCDOCS_GET_STATE_ATOL.prc
Normal file
@ -0,0 +1,27 @@
|
||||
create or replace procedure UDO_P_FISCDOCS_GET_STATE_ATOL
|
||||
as
|
||||
NEXSQUEUE PKG_STD.TREF; -- Ðåãèñòðàöèîííûé íîìåð äîáàâëåííîé ïîçèöèè î÷åðåäè îáìåíà
|
||||
begin
|
||||
/* Îáõîäèì ôèñêàëüíûå äîêóìåíòû, óñïåøíî îòïðàâëåííûå â ÀÒÎË, íî ïî êîòîðûì åù¸ íåò ïðîñòàâëåííîãî ñòàòóñà */
|
||||
for C in (select T.COMPANY,
|
||||
T.RN,
|
||||
Q.RESP BUUID
|
||||
from UDO_FISCDOCS T,
|
||||
EXSQUEUE Q
|
||||
where ((T.SEND_ERROR is null) and (T.CONFIRM_DATE is null))
|
||||
and Q.LNK_COMPANY = T.COMPANY
|
||||
and Q.LNK_UNITCODE = 'UDO_FiscalDocuments'
|
||||
and Q.LNK_DOCUMENT = T.RN
|
||||
and Q.EXEC_STATE = PKG_EXS.NQUEUE_EXEC_STATE_OK
|
||||
and Q.EXSSERVICEFN = UDO_PKG_EXS_ATOL.UTL_FISCDOC_GET_REG_EXSFN(T.RN))
|
||||
loop
|
||||
/* Ñòàâèì çàïðîñ íà ïîëó÷åíèå ñòàòóñà äîêóìåíòà â î÷åðåäü */
|
||||
PKG_EXS.QUEUE_PUT(NEXSSERVICEFN => UDO_PKG_EXS_ATOL.UTL_FISCDOC_GET_INF_EXSFN(NFISCDOC => C.RN),
|
||||
BMSG => C.BUUID,
|
||||
NLNK_COMPANY => C.COMPANY,
|
||||
NLNK_DOCUMENT => C.RN,
|
||||
SLNK_UNITCODE => 'UDO_FiscalDocuments',
|
||||
NNEW_EXSQUEUE => NEXSQUEUE);
|
||||
end loop;
|
||||
end;
|
||||
/
|
@ -1,20 +1,18 @@
|
||||
create or replace procedure UDO_P_FISCDOCS_MAKE_MSG_ATOL
|
||||
(
|
||||
NCOMPANY in number, -- Организация
|
||||
NFISCDOC in number, -- Регистрационный номер фискального документа
|
||||
NEXSQUEUE out number -- Регистрационный номер добавленной позиции очереди обмена
|
||||
NCOMPANY in number, -- Îðãàíèçàöèÿ
|
||||
NFISCDOC in number, -- Ðåãèñòðàöèîííûé íîìåð ôèñêàëüíîãî äîêóìåíòà
|
||||
NEXSQUEUE out number -- Ðåãèñòðàöèîííûé íîìåð äîáàâëåííîé ïîçèöèè î÷åðåäè îáìåíà
|
||||
)
|
||||
as
|
||||
/* Ëîêàëüíûå ïåðåìåííûå */
|
||||
CDATA clob; -- Буфер для XML-посылки
|
||||
SEXSSERVICE EXSSERVICEFN.CODE%type; -- Код сервиса-обработчика
|
||||
SEXSSERVICEFN EXSSERVICEFN.CODE%type; -- Код функции отправки
|
||||
NTMP_RN PKG_STD.TREF; -- Буфер для вычислений
|
||||
CDATA clob; -- Áóôåð äëÿ XML-ïîñûëêè
|
||||
NTMP_RN PKG_STD.TREF; -- Áóôåð äëÿ âû÷èñëåíèé
|
||||
|
||||
/* Äîáàâëåíèå ïóñòîé îòêðûòîé âåòêè */
|
||||
procedure NODE
|
||||
(
|
||||
SNAME varchar2 -- Имя ветки
|
||||
SNAME varchar2 -- Èìÿ âåòêè
|
||||
)
|
||||
as
|
||||
begin
|
||||
@ -27,8 +25,8 @@ as
|
||||
/* Äîáàâëåíèå âåòêè ñî çíà÷åíèåì (ñòðîêà) */
|
||||
procedure NODE
|
||||
(
|
||||
SNAME varchar2, -- Имя ветки
|
||||
SVALUE varchar2 -- Значение ветки
|
||||
SNAME varchar2, -- Èìÿ âåòêè
|
||||
SVALUE varchar2 -- Çíà÷åíèå âåòêè (ñòðîêà)
|
||||
)
|
||||
as
|
||||
begin
|
||||
@ -45,8 +43,8 @@ as
|
||||
/* Äîáàâëåíèå âåòêè ñî çíà÷åíèåì (÷èñëî) */
|
||||
procedure NODE
|
||||
(
|
||||
SNAME varchar2, -- Имя ветки
|
||||
NVALUE number -- Значение ветки
|
||||
SNAME varchar2, -- Èìÿ âåòêè
|
||||
NVALUE number -- Çíà÷åíèå âåòêè (÷èñëî)
|
||||
)
|
||||
as
|
||||
begin
|
||||
@ -63,8 +61,8 @@ as
|
||||
/* Äîáàâëåíèå âåòêè ñî çíà÷åíèåì (äàòà) */
|
||||
procedure NODE
|
||||
(
|
||||
SNAME varchar2, -- Имя ветки
|
||||
DVALUE date -- Значение ветки
|
||||
SNAME varchar2, -- Èìÿ âåòêè
|
||||
DVALUE date -- Çíà÷åíèå âåòêè (äàòà)
|
||||
)
|
||||
as
|
||||
begin
|
||||
@ -92,23 +90,7 @@ begin
|
||||
from UDO_V_FISCDOCS T
|
||||
where T.NRN = UDO_P_FISCDOCS_MAKE_MSG_ATOL.NFISCDOC
|
||||
and T.NCOMPANY = UDO_P_FISCDOCS_MAKE_MSG_ATOL.NCOMPANY)
|
||||
loop
|
||||
/* Выбираем API обмена в зависимости от версии фискального документа */
|
||||
case D.STYPE_VERSION
|
||||
/* ФФД 1.05 */
|
||||
when '1.05' then
|
||||
begin
|
||||
SEXSSERVICE := 'АТОЛ_V4_ИСХ';
|
||||
SEXSSERVICEFN := 'V4_ФФД1.05_РегистрацияЧекаРПВ';
|
||||
end;
|
||||
/* Неизвестная версия ФФД */
|
||||
else
|
||||
begin
|
||||
P_EXCEPTION(0,
|
||||
'Версия фискального документа "%s" не поддерживается!',
|
||||
D.STYPE_VERSION);
|
||||
end;
|
||||
end case;
|
||||
loop
|
||||
/* Äàííûå çàãîëîâêà ôèñêàëüíîãî äîêóìåíòà */
|
||||
NODE(SNAME => 'NRN', NVALUE => D.NRN);
|
||||
NODE(SNAME => 'NCOMPANY', NVALUE => D.NCOMPANY);
|
||||
@ -187,8 +169,7 @@ begin
|
||||
/* Ôèíàëèçèðóåì ñáîðêó XML-äîêóìåíòà */
|
||||
PKG_XMLFAST.EPILOGUE(LDATA => CDATA);
|
||||
/* Îòïðàâëÿåì ñôîðìèðîâàííûé äîêóìåíò */
|
||||
PKG_EXS.QUEUE_PUT(SEXSSERVICE => SEXSSERVICE,
|
||||
SEXSSERVICEFN => SEXSSERVICEFN,
|
||||
PKG_EXS.QUEUE_PUT(NEXSSERVICEFN => UDO_PKG_EXS_ATOL.UTL_FISCDOC_GET_REG_EXSFN(NFISCDOC => NFISCDOC),
|
||||
BMSG => CLOB2BLOB(LCDATA => CDATA, SCHARSET => 'UTF8'),
|
||||
NLNK_COMPANY => NCOMPANY,
|
||||
NLNK_DOCUMENT => NFISCDOC,
|
||||
|
@ -104,12 +104,10 @@
|
||||
// Подключение библиотек
|
||||
//----------------------
|
||||
|
||||
const util = require("util"); //Встроенные вспомогательные утилиты
|
||||
const parseString = require("xml2js").parseString; //Конвертация XML в JSON
|
||||
const js2xmlparser = require("js2xmlparser"); //Конвертация JSON в XML
|
||||
const _ = require("lodash"); //Работа с массивами и коллекциями
|
||||
const rqp = require("request-promise"); //Работа с HTTP/HTTPS запросами
|
||||
const { buildURL } = require("@core/utils"); //Вспомогательные функции
|
||||
const { NFN_TYPE_LOGIN } = require("@models/obj_service_function");
|
||||
|
||||
//---------------------
|
||||
// Глобальные константы
|
||||
@ -118,6 +116,11 @@ const { NFN_TYPE_LOGIN } = require("@models/obj_service_function");
|
||||
//Код круппы ККТ
|
||||
const SGROUP_CODE = "v4-online-atol-ru_4179";
|
||||
|
||||
//Статусы документов АТОЛ-онлайн
|
||||
const SSTATUS_DONE = "done"; //Готово
|
||||
const SSTATUS_FAIL = "fail"; //Ошибка
|
||||
const SSTATUS_WAIT = "wait"; //Ожидание
|
||||
|
||||
//Словарь - Признак способа расчёта
|
||||
const paymentMethod = {
|
||||
sName: "Признак способа расчёта",
|
||||
@ -474,24 +477,34 @@ const beforeRegBillSIR = async prms => {
|
||||
|
||||
//Обработчик "После" отправки запроса на регистрацию чека (приход, расход, возврат) серверу "АТОЛ-Онлайн"
|
||||
const afterRegBillSIR = async prms => {
|
||||
//Буфер для данных ответа сервера
|
||||
let resp = null;
|
||||
//Если есть данные от сервера АТОЛ
|
||||
if (prms.queue.blResp) {
|
||||
//Пытаемся их разбирать
|
||||
try {
|
||||
resp = JSON.parse(prms.queue.blResp.toString());
|
||||
} catch (e) {
|
||||
//Пришел не JSON
|
||||
throw new Error(`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
//Данных от сервера нет
|
||||
throw new Error(`Сервер АТОЛ-Онлайн не вернул ответ`);
|
||||
}
|
||||
//Данные есть и нам удалось их разобрать, проверяем на наличие ошибок
|
||||
if (resp.error === null) {
|
||||
//Ошибок нет - забираем идентификатор документа в системе АТОЛ и кладём в тело ответа - он это то что нам нужно
|
||||
return {
|
||||
blResp: new Buffer(resp.uuid)
|
||||
};
|
||||
} else {
|
||||
//Есть ошибки, посмотрим что это, может быть аутентификация (кончился токен)
|
||||
if (resp.error.code === 10 || resp.error.code === 11) {
|
||||
//да, это была она - сигнализируем серверу приложений - надо переподключаться
|
||||
return { bUnAuth: true };
|
||||
} else {
|
||||
//прочие ошибки - фиксируем в журнале
|
||||
throw new Error(`Сервер АТОЛ-Онлайн вернул ошибку: ${resp.error.text}`);
|
||||
}
|
||||
}
|
||||
@ -529,8 +542,107 @@ const beforeGetBillInfo = async prms => {
|
||||
|
||||
//Обработчик "После" отправки запроса на получение информации о чеке серверу "АТОЛ-Онлайн"
|
||||
const afterGetBillInfo = async prms => {
|
||||
if (prms.queue.blResp) console.log(prms.queue.blResp.toString());
|
||||
else console.log("Сервер не вернул ответ");
|
||||
//if (prms.queue.blResp) console.log(prms.queue.blResp.toString());
|
||||
//else console.log("Сервер не вернул ответ");
|
||||
//Буфер для результата работы обработчика
|
||||
let res = null;
|
||||
//Буфер для данных ответа сервера
|
||||
let resp = null;
|
||||
//Если есть данные от сервера АТОЛ
|
||||
if (prms.queue.blResp) {
|
||||
//Пытаемся их разбирать
|
||||
try {
|
||||
resp = JSON.parse(prms.queue.blResp.toString());
|
||||
} catch (e) {
|
||||
//Пришел не JSON
|
||||
throw new Error(`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
//Данных от сервера нет
|
||||
throw new Error(`Сервер АТОЛ-Онлайн не вернул ответ`);
|
||||
}
|
||||
//Данные есть и нам удалось их разобрать - проверим, что нет ошибок аутентификации
|
||||
//Есть ошибки, посмотрим что это, может быть аутентификация (кончился токен)
|
||||
if (resp.error !== null && (resp.error.code === 10 || resp.error.code === 11)) {
|
||||
//да, это была она - сигнализируем серверу приложений - надо переподключаться и дальше не работаем
|
||||
return { bUnAuth: true };
|
||||
}
|
||||
//Ошибок атуентификации нет - проверяем состояние документа
|
||||
if (resp.status) {
|
||||
//Проверям, может быть документ зарегистрирован
|
||||
if (resp.status === SSTATUS_DONE) {
|
||||
//Документ обработан, проверим наличие данных фискализации
|
||||
if (resp.payload) {
|
||||
//Ошибок нет - забираем данные фискализации и кладём в тело ответа - это то что нам нужно
|
||||
res = {
|
||||
//Статус обработки документа
|
||||
STATUS: resp.status,
|
||||
//Дата и время документа внешней системы
|
||||
TIMESTAMP: resp.timestamp,
|
||||
//Дата и время документа из ФН
|
||||
TAG1012: resp.payload.receipt_datetime,
|
||||
//Фискальный номер документа
|
||||
TAG1040: resp.payload.fiscal_document_number,
|
||||
//Номер ФН
|
||||
TAG1041: resp.payload.fn_number,
|
||||
//Фискальный признак документа
|
||||
TAG1077: resp.payload.fiscal_document_attribute
|
||||
};
|
||||
} else {
|
||||
//В ответе сервера нет ни данных фискализации, при этом документ отработан - это неожиданный ответ
|
||||
throw new Error(
|
||||
`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: документ в статусе "Готов" но в ответе сервера нет данных фискализации`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//Документ не зарегистрирован, может быть ещё обрабатывается
|
||||
if (resp.status === SSTATUS_WAIT) {
|
||||
//Скажем и об этом
|
||||
res = {
|
||||
//Статус обработки документа
|
||||
STATUS: resp.status
|
||||
};
|
||||
} else {
|
||||
//Документ не готов и не обрабатывается - очевидно ошибка при регистрации
|
||||
if (resp.status === SSTATUS_FAIL) {
|
||||
if (resp.error) {
|
||||
res = {
|
||||
//Статус обработки документа
|
||||
STATUS: resp.status,
|
||||
//Ошибка
|
||||
ERROR: {
|
||||
//Код ошибки
|
||||
CODE: resp.error.code,
|
||||
//Текст ошибки
|
||||
TEXT: resp.error.text
|
||||
}
|
||||
};
|
||||
} else {
|
||||
//Статус - ошибка, но ошибки нет, это неожиданный ответ
|
||||
throw new Error(
|
||||
`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: документ в статусе "Ошибка" но в ответе сервера нет об ошибке`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//Других статусов быть не должно - это неожиданный ответ
|
||||
throw new Error(
|
||||
`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: неизвестный статус документа - ${
|
||||
resp.status
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Нет данных о статусе документа - это неожиданный ответ
|
||||
throw new Error(
|
||||
`Неожиданный ответ сервера АТОЛ-Онлайн. Ошибка интерпретации: статус документа в ответе не определён`
|
||||
);
|
||||
}
|
||||
//Вернём сформированный ответ
|
||||
return {
|
||||
blResp: new Buffer(js2xmlparser.parse("RESP", res))
|
||||
};
|
||||
};
|
||||
|
||||
//-----------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user