Compare commits
No commits in common. "0ac121af1cc1624b633f78c1a3deafae0ae2cc9b" and "15a00ddafc16f1ae38ead00640477cdfc88efc9f" have entirely different histories.
0ac121af1c
...
15a00ddafc
1
.gitignore
vendored
1
.gitignore
vendored
@ -330,7 +330,6 @@ docker-compose.yml
|
|||||||
.vscode
|
.vscode
|
||||||
my_project_env
|
my_project_env
|
||||||
.cursorignore
|
.cursorignore
|
||||||
/test-scripts
|
|
||||||
|
|
||||||
# Graphify: generated graphs, caches, skill and local integration
|
# Graphify: generated graphs, caches, skill and local integration
|
||||||
graphify-out/
|
graphify-out/
|
||||||
|
|||||||
@ -1,27 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pydantic import AliasChoices, Field
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
from src.core.config import settings
|
||||||
from src.models import *
|
from src.models import *
|
||||||
|
|
||||||
|
|
||||||
class MigrationSettings(BaseSettings):
|
|
||||||
database_url: str = Field(validation_alias=AliasChoices(
|
|
||||||
"OPENBAO__DATABASE__DATABASE_URL",
|
|
||||||
"OPENBAO__SETTINGS__DATABASE_URL",
|
|
||||||
))
|
|
||||||
model_config = SettingsConfigDict(
|
|
||||||
env_file=Path(__file__).resolve().parents[1] / ".env",
|
|
||||||
extra="ignore",
|
|
||||||
)
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
@ -31,7 +16,7 @@ config = context.config
|
|||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
config.set_main_option("sqlalchemy.url", MigrationSettings().database_url.replace("%", "%%"))
|
config.set_main_option("sqlalchemy.url", settings.OPENBAO.DATABASE.DATABASE_URL)
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
@ -60,7 +45,6 @@ def do_run_migrations(connection) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.execute('CREATE SCHEMA IF NOT EXISTS "dkb"')
|
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
@ -87,7 +71,6 @@ def run_migrations_offline() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.execute('CREATE SCHEMA IF NOT EXISTS "dkb"')
|
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
@ -113,9 +96,7 @@ async def run_migrations_online() -> None:
|
|||||||
await connectable.dispose()
|
await connectable.dispose()
|
||||||
|
|
||||||
|
|
||||||
if config.attributes.get("connection") is not None:
|
if context.is_offline_mode():
|
||||||
do_run_migrations(config.attributes["connection"])
|
|
||||||
elif context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
run_migrations_offline()
|
||||||
else:
|
else:
|
||||||
asyncio.run(run_migrations_online())
|
asyncio.run(run_migrations_online())
|
||||||
|
|||||||
@ -1,137 +0,0 @@
|
|||||||
"""add osv entries for group function
|
|
||||||
|
|
||||||
Revision ID: d614b414a32f
|
|
||||||
Revises: c89d082850bd
|
|
||||||
Create Date: 2026-09-17 17:15:16.421048
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'd614b414a32f'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = 'c89d082850bd'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute("""
|
|
||||||
CREATE OR REPLACE FUNCTION dkb.get_osv_entries_for_group(
|
|
||||||
p_company_group_id uuid,
|
|
||||||
p_period_year integer,
|
|
||||||
p_period_quarter text
|
|
||||||
)
|
|
||||||
RETURNS TABLE (
|
|
||||||
osv_entry_id uuid,
|
|
||||||
recognition_job_id uuid,
|
|
||||||
legal_entity_id uuid,
|
|
||||||
stored_file_id uuid,
|
|
||||||
account_code varchar,
|
|
||||||
account_id uuid,
|
|
||||||
account_root varchar,
|
|
||||||
counterparty_name varchar,
|
|
||||||
counterparty_inn varchar,
|
|
||||||
counterparty_id uuid,
|
|
||||||
opening_debit numeric,
|
|
||||||
opening_credit numeric,
|
|
||||||
turnover_debit numeric,
|
|
||||||
turnover_credit numeric,
|
|
||||||
closing_debit numeric,
|
|
||||||
closing_credit numeric,
|
|
||||||
legal_entity_name varchar,
|
|
||||||
company_group_id uuid,
|
|
||||||
company_group_name varchar,
|
|
||||||
original_name varchar,
|
|
||||||
file_status varchar,
|
|
||||||
period_year smallint,
|
|
||||||
period_quarter varchar,
|
|
||||||
uploaded_at timestamptz,
|
|
||||||
uploaded_by varchar,
|
|
||||||
finished_at timestamptz,
|
|
||||||
is_gsk boolean,
|
|
||||||
is_gk boolean,
|
|
||||||
entity_group_flag text
|
|
||||||
)
|
|
||||||
LANGUAGE sql
|
|
||||||
STABLE
|
|
||||||
STRICT
|
|
||||||
AS $function$
|
|
||||||
WITH classified_entries AS (
|
|
||||||
SELECT
|
|
||||||
oe.id AS osv_entry_id,
|
|
||||||
oe.recognition_job_id,
|
|
||||||
oe.legal_entity_id,
|
|
||||||
oe.stored_file_id,
|
|
||||||
CASE
|
|
||||||
WHEN oe.account_id IS NOT NULL THEN coa.code
|
|
||||||
ELSE oe.account_code
|
|
||||||
END AS account_code,
|
|
||||||
oe.account_id,
|
|
||||||
oe.account_root,
|
|
||||||
oe.counterparty_name,
|
|
||||||
oe.counterparty_inn,
|
|
||||||
oe.counterparty_id,
|
|
||||||
oe.opening_debit,
|
|
||||||
oe.opening_credit,
|
|
||||||
oe.turnover_debit,
|
|
||||||
oe.turnover_credit,
|
|
||||||
oe.closing_debit,
|
|
||||||
oe.closing_credit,
|
|
||||||
le.name AS legal_entity_name,
|
|
||||||
le.company_group_id,
|
|
||||||
cg.name AS company_group_name,
|
|
||||||
sf.original_name,
|
|
||||||
sf.status AS file_status,
|
|
||||||
sf.period_year,
|
|
||||||
sf.period_quarter,
|
|
||||||
sf.uploaded_at,
|
|
||||||
sf.uploaded_by,
|
|
||||||
rj.finished_at,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM dkb.related_group_member AS member
|
|
||||||
JOIN dkb.related_group AS rg
|
|
||||||
ON rg.id = member.related_group_id
|
|
||||||
WHERE member.legal_entity_id = oe.legal_entity_id
|
|
||||||
AND member.is_current IS TRUE
|
|
||||||
AND rg.company_group_id = p_company_group_id
|
|
||||||
AND rg.kind = 'GSK'
|
|
||||||
) AS is_gsk,
|
|
||||||
le.company_group_id = p_company_group_id AS is_gk
|
|
||||||
FROM dkb.osv_entry AS oe
|
|
||||||
LEFT JOIN dkb.chart_of_account AS coa ON coa.id = oe.account_id
|
|
||||||
JOIN dkb.stored_file AS sf ON sf.id = oe.stored_file_id
|
|
||||||
JOIN dkb.legal_entity AS le ON le.id = oe.legal_entity_id
|
|
||||||
JOIN dkb.company_group AS cg ON cg.id = le.company_group_id
|
|
||||||
LEFT JOIN dkb.recognition_job AS rj
|
|
||||||
ON rj.id = oe.recognition_job_id
|
|
||||||
AND rj.file_id = sf.id
|
|
||||||
WHERE sf.status = 'UPLOADED'
|
|
||||||
AND sf.period_year = p_period_year
|
|
||||||
AND sf.period_quarter = p_period_quarter
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1 FROM dkb.company_group AS selected_group
|
|
||||||
WHERE selected_group.id = p_company_group_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
ce.*,
|
|
||||||
CASE
|
|
||||||
WHEN ce.is_gsk THEN 'ГСК'
|
|
||||||
WHEN ce.is_gk THEN 'ГК'
|
|
||||||
ELSE 'Внешний'
|
|
||||||
END AS entity_group_flag
|
|
||||||
FROM classified_entries AS ce;
|
|
||||||
$function$;
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.execute("""
|
|
||||||
DROP FUNCTION IF EXISTS
|
|
||||||
dkb.get_osv_entries_for_group(uuid, integer, text)
|
|
||||||
""")
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from src.app import app
|
from src import app, get_settings
|
||||||
from src.core.config import get_settings
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
@ -10,3 +9,4 @@ if __name__ == "__main__":
|
|||||||
port=settings.server.PORT,
|
port=settings.server.PORT,
|
||||||
lifespan="on",
|
lifespan="on",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,7 @@
|
|||||||
"""API package. Import application entry points explicitly from src.app."""
|
from .app import app
|
||||||
|
from .core.config import get_settings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'app',
|
||||||
|
'get_settings',
|
||||||
|
]
|
||||||
@ -1,28 +1,11 @@
|
|||||||
from fastapi import APIRouter, File, HTTPException, UploadFile, Form, Depends, Query
|
from fastapi import APIRouter, File, HTTPException, UploadFile, Form, Depends
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from src.api.dependencies.services import get_osv_service, OsvService
|
from src.api.dependencies.services import get_osv_service, OsvService
|
||||||
from src.schemas.enums import PeriodQuarter
|
from src.schemas.enums import PeriodQuarter
|
||||||
from src.schemas.osv import OsvGroupEntryResponse
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
router = APIRouter(prefix='/osv', tags=['osv'])
|
router = APIRouter(prefix='/osv', tags=['osv'])
|
||||||
|
|
||||||
|
|
||||||
@router.get('/by-company-group', response_model=list[OsvGroupEntryResponse])
|
|
||||||
async def get_osv_by_company_group(
|
|
||||||
company_group_id: uuid.UUID,
|
|
||||||
quarter: PeriodQuarter,
|
|
||||||
year: int = Query(..., ge=1, le=9999),
|
|
||||||
service: OsvService = Depends(get_osv_service),
|
|
||||||
):
|
|
||||||
"""Все ОСВ за период из UPLOADED-файлов с флагом владельца относительно ГК.
|
|
||||||
|
|
||||||
Пустая выборка (в том числе неизвестная ГК) возвращается как [].
|
|
||||||
"""
|
|
||||||
return await service.get_by_company_group(
|
|
||||||
company_group_id=company_group_id, year=year, quarter=quarter
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get('/by-file-id')
|
@router.get('/by-file-id')
|
||||||
async def get_osv_by_file(file_id: uuid.UUID, service: OsvService = Depends(get_osv_service)):
|
async def get_osv_by_file(file_id: uuid.UUID, service: OsvService = Depends(get_osv_service)):
|
||||||
result = await service.get_by_file_id(file_id)
|
result = await service.get_by_file_id(file_id)
|
||||||
|
|||||||
139
api/src/app.py
139
api/src/app.py
@ -24,8 +24,8 @@ from src.core.clients import S3Storage, PollWorker
|
|||||||
from src.core.async_cache import AsyncInMemoryCache
|
from src.core.async_cache import AsyncInMemoryCache
|
||||||
# from src.core.logger import setup_logger
|
# from src.core.logger import setup_logger
|
||||||
from nats.aio.client import Client as NATS
|
from nats.aio.client import Client as NATS
|
||||||
import base64
|
|
||||||
import nkeys
|
import nkeys
|
||||||
|
import base64
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -42,6 +42,20 @@ VERSION = "1.0.0"
|
|||||||
UP_TIME = datetime.now(timezone.utc)
|
UP_TIME = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def jwt_cb():
|
||||||
|
return settings.OPENBAO.NATS.NATS_JWT.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def sig_cb(nonce):
|
||||||
|
if (isinstance(nonce, str)):
|
||||||
|
nonce = nonce.encode('utf-8')
|
||||||
|
signed = kp.sign(nonce)
|
||||||
|
return base64.b64encode(signed)
|
||||||
|
|
||||||
|
|
||||||
async def sub_cb(msg):
|
async def sub_cb(msg):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -80,75 +94,80 @@ async def lifespan(app):
|
|||||||
app.state.nats_subs = []
|
app.state.nats_subs = []
|
||||||
app.state.storage = None
|
app.state.storage = None
|
||||||
app.state.nats = None
|
app.state.nats = None
|
||||||
app.state.poller = None
|
|
||||||
app.state.pdf_poller = None
|
|
||||||
s3_client = None
|
|
||||||
smb_registered = False
|
|
||||||
try:
|
try:
|
||||||
minio = settings.OPENBAO.MINIO
|
smbclient.register_session(settings.OPENBAO.SMB.SMB_HOST, settings.OPENBAO.SMB.SMB_LOGIN, settings.OPENBAO.SMB.SMB_PASSWORD)
|
||||||
if minio is not None:
|
except:
|
||||||
s3_client = Minio(
|
logger.exception("Ошибка при регистрации сессии SMB")
|
||||||
minio.MINIO_URL,
|
s3_client = Minio(settings.OPENBAO.MINIO.MINIO_URL, access_key=settings.OPENBAO.MINIO.MINIO_ADMIN_LOGIN, secret_key=settings.OPENBAO.MINIO.MINIO_ADMIN_PASSWORD, cert_check=False)
|
||||||
access_key=minio.MINIO_ADMIN_LOGIN,
|
try:
|
||||||
secret_key=minio.MINIO_ADMIN_PASSWORD,
|
bucket_exists = await s3_client.bucket_exists(settings.OPENBAO.MINIO.MINIO_BUCKET)
|
||||||
secure=minio.MINIO_SECURE,
|
logger.info("Бакет проверен. bucket_exists={bucket_exists}", bucket_exists=bucket_exists)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception(
|
||||||
|
"Не удалось проверить наличие бакетa. error_category=s3_error, error_type={error_type}, error={error}",
|
||||||
|
error_type=type(exc).__name__,
|
||||||
|
error=repr(exc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
storage = S3Storage(client=s3_client, bucket_name=settings.OPENBAO.MINIO.MINIO_BUCKET)
|
||||||
|
app.state.storage = storage
|
||||||
|
cache = AsyncInMemoryCache()
|
||||||
|
nc = NATS()
|
||||||
|
if not settings.DEBUG:
|
||||||
|
try:
|
||||||
|
await nc.connect(settings.OPENBAO.NATS.NATS_URL, user_jwt_cb=jwt_cb, signature_cb=sig_cb,
|
||||||
|
max_reconnect_attempts=2)
|
||||||
|
logger.info("Подключение к NATS произошло")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception(
|
||||||
|
"Не удалось проверить наличие бакетa. error_category=nats_error, error_type={error_type}, error={error}",
|
||||||
|
error_type=type(exc).__name__,
|
||||||
|
error=repr(exc),
|
||||||
)
|
)
|
||||||
if not await s3_client.bucket_exists(minio.MINIO_BUCKET):
|
return
|
||||||
raise RuntimeError(f"Бакет MinIO {minio.MINIO_BUCKET!r} не создан")
|
poller = PollWorker(nc, cache, storage)
|
||||||
app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET)
|
await poller.start()
|
||||||
logger.info("MinIO подключён, бакет: {}", minio.MINIO_BUCKET)
|
pdf_poller = PDFPollWorker(cache, storage)
|
||||||
|
await pdf_poller.start()
|
||||||
smb = settings.OPENBAO.SMB
|
app.state.nats = nc
|
||||||
if smb is not None:
|
app.state.poller = poller
|
||||||
smbclient.register_session(smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD,
|
app.state.pdf_poller = pdf_poller
|
||||||
port=smb.SMB_PORT)
|
if not settings.DEBUG:
|
||||||
smb_registered = True
|
handler_cb = build_file_handler(nc, storage)
|
||||||
logger.info("SMB подключён")
|
|
||||||
|
|
||||||
nats_settings = settings.OPENBAO.NATS
|
|
||||||
if nats_settings is not None:
|
|
||||||
nc = NATS()
|
|
||||||
app.state.nats = nc
|
|
||||||
if not settings.DEBUG:
|
|
||||||
keypair = nkeys.from_seed(nats_settings.NATS_SEED.encode("utf-8"))
|
|
||||||
|
|
||||||
def user_jwt_cb():
|
|
||||||
return nats_settings.NATS_JWT.encode("utf-8")
|
|
||||||
|
|
||||||
def signature_cb(nonce):
|
|
||||||
return base64.b64encode(keypair.sign(nonce.encode("utf-8")))
|
|
||||||
|
|
||||||
await nc.connect(nats_settings.NATS_URL, user_jwt_cb=user_jwt_cb,
|
|
||||||
signature_cb=signature_cb, max_reconnect_attempts=2)
|
|
||||||
logger.info("NATS подключён")
|
|
||||||
|
|
||||||
missing = [name for name in ("MINIO", "NATS", "SMB")
|
|
||||||
if getattr(settings.OPENBAO, name) is None]
|
|
||||||
if missing:
|
|
||||||
logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing))
|
|
||||||
else:
|
|
||||||
cache = AsyncInMemoryCache()
|
|
||||||
app.state.poller = PollWorker(nc, cache, app.state.storage)
|
|
||||||
await app.state.poller.start()
|
|
||||||
app.state.pdf_poller = PDFPollWorker(cache, app.state.storage)
|
|
||||||
await app.state.pdf_poller.start()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# sub_xls = await nc.subscribe('files.tabular_parsed', cb=handler_cb)
|
||||||
|
# sub_img = await nc.subscribe('files.img_parsed', cb=handler_cb)
|
||||||
|
#
|
||||||
|
# app.state.nats_subs = [sub_xls, sub_img]
|
||||||
|
logger.info("Подписка на топики завершена")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Не удалось подписаться на тему. error_category=nats_error, error_type={error_type}, error={error}",
|
||||||
|
error_type=type(exc).__name__,
|
||||||
|
error=repr(exc),
|
||||||
|
)
|
||||||
|
try:
|
||||||
logger.info("Приложение запущено")
|
logger.info("Приложение запущено")
|
||||||
yield
|
yield
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("ОШИБКА ПРИ РАБОТЕ С СЕКРЕТАМИ error={error}", error=repr(exc))
|
||||||
finally:
|
finally:
|
||||||
for worker in (app.state.poller, app.state.pdf_poller):
|
logger.info("Приложение закрывается")
|
||||||
if worker is not None:
|
smbclient.reset_connection_cache()
|
||||||
await worker.stop()
|
|
||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
for sub in app.state.nats_subs:
|
for sub in app.state.nats_subs:
|
||||||
|
logger.info("Отписываемся от топика")
|
||||||
await sub.unsubscribe()
|
await sub.unsubscribe()
|
||||||
if app.state.nats is not None:
|
if app.state.nats:
|
||||||
await app.state.nats.close()
|
await app.state.nats.close()
|
||||||
if s3_client is not None:
|
if app.state.poller:
|
||||||
await s3_client.close_session()
|
await app.state.poller.stop()
|
||||||
if smb_registered:
|
if app.state.storage:
|
||||||
smbclient.reset_connection_cache()
|
await app.state.storage.client.close_session()
|
||||||
logger.info("Приложение закрывается")
|
if app.state.pdf_poller:
|
||||||
|
await app.state.pdf_poller.stop()
|
||||||
|
logger.info("CLOSING APP AT ALL")
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Fast API",
|
title="Fast API",
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class PDFPollWorker:
|
|||||||
logger.exception("Не получилось достать данные из кэша")
|
logger.exception("Не получилось достать данные из кэша")
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
||||||
all_files = smbclient.listdir(self.smb_path, port=settings.OPENBAO.SMB.SMB_PORT)
|
all_files = smbclient.listdir(self.smb_path)
|
||||||
|
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
search_fname = f"{job.file_id}"
|
search_fname = f"{job.file_id}"
|
||||||
@ -85,8 +85,8 @@ class PDFPollWorker:
|
|||||||
|
|
||||||
logger.info("Начинаю обработку файла по пути smb_dir={smb_dir}", smb_dir=new_dir)
|
logger.info("Начинаю обработку файла по пути smb_dir={smb_dir}", smb_dir=new_dir)
|
||||||
files = []
|
files = []
|
||||||
for fname in smbclient.listdir(new_dir, port=settings.OPENBAO.SMB.SMB_PORT):
|
for fname in smbclient.listdir(new_dir):
|
||||||
with smbclient.open_file(f'{new_dir}/{fname}', 'r', port=settings.OPENBAO.SMB.SMB_PORT) as f:
|
with smbclient.open_file(f'{new_dir}/{fname}', 'r') as f:
|
||||||
files.append(json.loads(f.read()))
|
files.append(json.loads(f.read()))
|
||||||
s3_json_dict = {
|
s3_json_dict = {
|
||||||
'file_id': job.file_id,
|
'file_id': job.file_id,
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class PollWorker:
|
|||||||
|
|
||||||
if row['subject'] == 'file.img_uploaded':
|
if row['subject'] == 'file.img_uploaded':
|
||||||
last_part = row['row_payload']['s3_key'].split('/')[-1]
|
last_part = row['row_payload']['s3_key'].split('/')[-1]
|
||||||
with smbclient.open_file(f'{settings.OPENBAO.SMB.SMB_PATH}/Файлы/{last_part}', 'wb', port=settings.OPENBAO.SMB.SMB_PORT) as f:
|
with smbclient.open_file(f'{settings.OPENBAO.SMB.SMB_PATH}/Файлы/{last_part}', 'wb') as f:
|
||||||
data = await self.storage.get_object(row['row_payload']['s3_key'])
|
data = await self.storage.get_object(row['row_payload']['s3_key'])
|
||||||
content = await data.read()
|
content = await data.read()
|
||||||
f.write(content)
|
f.write(content)
|
||||||
@ -178,4 +178,3 @@ class PollWorker:
|
|||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
await session.execute(file_stmt)
|
await session.execute(file_stmt)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|||||||
@ -1,37 +1,31 @@
|
|||||||
from pydantic import Field
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
|
|
||||||
class DBSettings(BaseSettings):
|
class DBSettings(BaseSettings):
|
||||||
DATABASE_URL: str
|
DATABASE_URL: str
|
||||||
|
|
||||||
class MinioSettings(BaseSettings):
|
class MinioSettings(BaseSettings):
|
||||||
MINIO_SECURE: bool = True
|
|
||||||
MINIO_URL: str
|
MINIO_URL: str
|
||||||
MINIO_ADMIN_LOGIN: str
|
MINIO_ADMIN_LOGIN: str
|
||||||
MINIO_ADMIN_PASSWORD: str
|
MINIO_ADMIN_PASSWORD: str
|
||||||
MINIO_BUCKET: str
|
MINIO_BUCKET: str
|
||||||
MINIO_SECURE: bool = True
|
|
||||||
|
|
||||||
class NATSSettings(BaseSettings):
|
class NATSSettings(BaseSettings):
|
||||||
NATS_URL: str
|
NATS_URL: str
|
||||||
NATS_JWT: str = Field(min_length=1)
|
NATS_SEED: str
|
||||||
NATS_SEED: str = Field(min_length=1)
|
NATS_JWT: str
|
||||||
|
|
||||||
class SMBSettings(BaseSettings):
|
class SMBSettings(BaseSettings):
|
||||||
SMB_PORT: int = Field(default=445, ge=1, le=65535)
|
|
||||||
SMB_HOST: str
|
SMB_HOST: str
|
||||||
SMB_PORT: int = 445
|
|
||||||
SMB_PATH: str
|
SMB_PATH: str
|
||||||
SMB_LOGIN: str
|
SMB_LOGIN: str
|
||||||
SMB_PASSWORD: str
|
SMB_PASSWORD: str
|
||||||
|
|
||||||
class OpenBAOSettings(BaseSettings):
|
class OpenBAOSettings(BaseSettings):
|
||||||
DATABASE: DBSettings
|
DATABASE: DBSettings
|
||||||
MINIO: MinioSettings | None = None
|
MINIO: MinioSettings
|
||||||
NATS: NATSSettings | None = None
|
NATS: NATSSettings
|
||||||
SMB: SMBSettings | None = None
|
SMB: SMBSettings
|
||||||
|
|
||||||
class ServerSettings(BaseSettings):
|
class ServerSettings(BaseSettings):
|
||||||
HOST: str = "0.0.0.0"
|
HOST: str = "0.0.0.0"
|
||||||
@ -45,7 +39,6 @@ class Settings(BaseSettings):
|
|||||||
VERSION: str = '1.0.0'
|
VERSION: str = '1.0.0'
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
case_sensitive = True
|
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
env_nested_delimiter = "__"
|
env_nested_delimiter = "__"
|
||||||
|
|||||||
@ -1,25 +1,10 @@
|
|||||||
from .base_repository import BaseRepository
|
from .base_repository import BaseRepository
|
||||||
from src.models import OsvEntry
|
from src.models import OsvEntry
|
||||||
from sqlalchemy import insert, select, text
|
from sqlalchemy import insert, select
|
||||||
|
|
||||||
class OsvRepository(BaseRepository):
|
class OsvRepository(BaseRepository):
|
||||||
model = OsvEntry
|
model = OsvEntry
|
||||||
|
|
||||||
async def get_by_company_group(self, company_group_id, year, quarter):
|
|
||||||
stmt = text("""
|
|
||||||
SELECT * FROM dkb.get_osv_entries_for_group(
|
|
||||||
CAST(:company_group_id AS uuid),
|
|
||||||
CAST(:year AS integer),
|
|
||||||
CAST(:quarter AS text)
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
result = await self.session.execute(stmt, {
|
|
||||||
"company_group_id": company_group_id,
|
|
||||||
"year": year,
|
|
||||||
"quarter": quarter,
|
|
||||||
})
|
|
||||||
return [dict(row) for row in result.mappings().all()]
|
|
||||||
|
|
||||||
async def get_by_file_id(self, file_id):
|
async def get_by_file_id(self, file_id):
|
||||||
stmt = select(self.model).where(self.model.stored_file_id == file_id)
|
stmt = select(self.model).where(self.model.stored_file_id == file_id)
|
||||||
result = await self.session.execute(stmt)
|
result = await self.session.execute(stmt)
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Literal
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from src.schemas.enums import PeriodQuarter
|
|
||||||
|
|
||||||
|
|
||||||
class OsvGroupEntryResponse(BaseModel):
|
|
||||||
"""Строка get_osv_entries_for_group; денежные суммы в JSON — точные строки."""
|
|
||||||
|
|
||||||
osv_entry_id: UUID
|
|
||||||
recognition_job_id: UUID
|
|
||||||
legal_entity_id: UUID
|
|
||||||
stored_file_id: UUID
|
|
||||||
account_code: str | None
|
|
||||||
account_id: UUID | None
|
|
||||||
account_root: str
|
|
||||||
counterparty_name: str
|
|
||||||
counterparty_inn: str
|
|
||||||
counterparty_id: UUID | None
|
|
||||||
opening_debit: Decimal
|
|
||||||
opening_credit: Decimal
|
|
||||||
turnover_debit: Decimal
|
|
||||||
turnover_credit: Decimal
|
|
||||||
closing_debit: Decimal
|
|
||||||
closing_credit: Decimal
|
|
||||||
legal_entity_name: str
|
|
||||||
company_group_id: UUID
|
|
||||||
company_group_name: str
|
|
||||||
original_name: str
|
|
||||||
file_status: str
|
|
||||||
period_year: int
|
|
||||||
period_quarter: PeriodQuarter
|
|
||||||
uploaded_at: datetime | None
|
|
||||||
uploaded_by: str | None
|
|
||||||
finished_at: datetime | None
|
|
||||||
is_gsk: bool
|
|
||||||
is_gk: bool
|
|
||||||
entity_group_flag: Literal['ГСК', 'ГК', 'Внешний']
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import HTTPException, UploadFile
|
from fastapi import UploadFile
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.inspection import inspect
|
from sqlalchemy.inspection import inspect
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -21,8 +21,6 @@ class FileService:
|
|||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
async def save_file(self, file: UploadFile, legal_entity_id: uuid.UUID, quarter: str, year: int):
|
async def save_file(self, file: UploadFile, legal_entity_id: uuid.UUID, quarter: str, year: int):
|
||||||
if self.storage is None:
|
|
||||||
raise HTTPException(status_code=503, detail="Обработка файлов не настроена")
|
|
||||||
try:
|
try:
|
||||||
db_payload = {
|
db_payload = {
|
||||||
'original_name': file.filename,
|
'original_name': file.filename,
|
||||||
@ -72,8 +70,6 @@ class FileService:
|
|||||||
return [self._get_transformed_file_job_info(new_file) for new_file in result]
|
return [self._get_transformed_file_job_info(new_file) for new_file in result]
|
||||||
|
|
||||||
async def get_file_bytes(self, file_id):
|
async def get_file_bytes(self, file_id):
|
||||||
if self.storage is None:
|
|
||||||
raise HTTPException(status_code=503, detail="Файловое хранилище не настроено")
|
|
||||||
result = await self.file_repo.get_by_id(file_id)
|
result = await self.file_repo.get_by_id(file_id)
|
||||||
return (await self.storage.get_object(result.s3_key))
|
return (await self.storage.get_object(result.s3_key))
|
||||||
|
|
||||||
@ -86,3 +82,4 @@ class FileService:
|
|||||||
'original_name': stored_file_db_obj.original_name,
|
'original_name': stored_file_db_obj.original_name,
|
||||||
'document_type': stored_file_db_obj.document_type
|
'document_type': stored_file_db_obj.document_type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,22 +4,12 @@ from fastapi import UploadFile
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from src.common.exceptions import NotFoundError
|
from src.common.exceptions import NotFoundError
|
||||||
from src.repositories.osv_repository import OsvEntry, OsvRepository
|
from src.repositories.osv_repository import OsvEntry, OsvRepository
|
||||||
from src.schemas.enums import PeriodQuarter
|
|
||||||
from src.schemas.osv import OsvGroupEntryResponse
|
|
||||||
|
|
||||||
class OsvService:
|
class OsvService:
|
||||||
def __init__(self, session: AsyncSession, osv_repo: OsvRepository):
|
def __init__(self, session: AsyncSession, osv_repo: OsvRepository):
|
||||||
self.osv_repo = osv_repo
|
self.osv_repo = osv_repo
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
async def get_by_company_group(
|
|
||||||
self, company_group_id: uuid.UUID, year: int, quarter: PeriodQuarter
|
|
||||||
) -> list[OsvGroupEntryResponse]:
|
|
||||||
entries = await self.osv_repo.get_by_company_group(
|
|
||||||
company_group_id=company_group_id, year=year, quarter=quarter.value
|
|
||||||
)
|
|
||||||
return [OsvGroupEntryResponse.model_validate(entry) for entry in entries]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_by_file_id(self, file_id: uuid.UUID):
|
async def get_by_file_id(self, file_id: uuid.UUID):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -1,287 +0,0 @@
|
|||||||
# ER-диаграмма схемы `dkb`
|
|
||||||
|
|
||||||
Диаграмма построена по SQLAlchemy-моделям и Alembic-миграциям. `PK` — первичный ключ, `FK` — внешний ключ, `UK` — уникальное поле. Все таблицы находятся в PostgreSQL-схеме `dkb`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
CHART_OF_ACCOUNT {
|
|
||||||
uuid id PK
|
|
||||||
string code UK
|
|
||||||
uuid parent_id FK "nullable"
|
|
||||||
string name
|
|
||||||
enum kind "A | P | AP"
|
|
||||||
}
|
|
||||||
|
|
||||||
REGIONAL_OFFICE {
|
|
||||||
uuid id PK
|
|
||||||
string name
|
|
||||||
boolean is_active
|
|
||||||
}
|
|
||||||
|
|
||||||
COMPANY_GROUP {
|
|
||||||
uuid id PK
|
|
||||||
string crm_id UK
|
|
||||||
string name
|
|
||||||
uuid base_regional_office_id FK
|
|
||||||
datetime valid_from
|
|
||||||
string updated_by
|
|
||||||
datetime created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
COMPANY_GROUP_HISTORY {
|
|
||||||
uuid id PK
|
|
||||||
uuid company_group_id FK
|
|
||||||
string crm_id UK
|
|
||||||
string name
|
|
||||||
uuid base_regional_office_id "snapshot, not FK"
|
|
||||||
datetime valid_from
|
|
||||||
datetime valid_to
|
|
||||||
string updated_by
|
|
||||||
}
|
|
||||||
|
|
||||||
LEGAL_ENTITY {
|
|
||||||
uuid id PK
|
|
||||||
string inn UK
|
|
||||||
enum entity_type "IP | UL"
|
|
||||||
string name
|
|
||||||
uuid company_group_id FK
|
|
||||||
uuid regional_office_id FK
|
|
||||||
date close_date
|
|
||||||
string updated_by
|
|
||||||
datetime created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
LEGAL_ENTITY_HISTORY {
|
|
||||||
uuid id PK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
string inn UK
|
|
||||||
enum entity_type "IP | UL"
|
|
||||||
string name
|
|
||||||
uuid company_group_id FK
|
|
||||||
uuid regional_office_id FK
|
|
||||||
date close_date
|
|
||||||
datetime valid_from
|
|
||||||
datetime valid_to
|
|
||||||
string changed_by
|
|
||||||
}
|
|
||||||
|
|
||||||
LEGAL_ENTITY_ALIAS {
|
|
||||||
uuid id PK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
string alias
|
|
||||||
string normalized_alias
|
|
||||||
string created_by
|
|
||||||
datetime created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
RELATED_GROUP {
|
|
||||||
uuid id PK
|
|
||||||
string code UK
|
|
||||||
uuid company_group_id FK
|
|
||||||
enum kind "GSK | GSZ"
|
|
||||||
string name
|
|
||||||
date created_date
|
|
||||||
}
|
|
||||||
|
|
||||||
RELATED_GROUP_MEMBER {
|
|
||||||
uuid id PK
|
|
||||||
uuid related_group_id FK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
enum kind "GSK | GSZ"
|
|
||||||
datetime valid_from
|
|
||||||
datetime valid_to
|
|
||||||
boolean is_current
|
|
||||||
string created_by
|
|
||||||
}
|
|
||||||
|
|
||||||
CONSOLIDATION {
|
|
||||||
uuid id PK
|
|
||||||
uuid related_group_id FK
|
|
||||||
smallint year
|
|
||||||
enum quarter "Q1 | Q2 | Q3 | Q4"
|
|
||||||
datetime created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
CONSOLIDATION_VERSION {
|
|
||||||
uuid id PK
|
|
||||||
uuid consolidation_id FK
|
|
||||||
string version_no
|
|
||||||
date consolidation_date
|
|
||||||
string initiator
|
|
||||||
string approved_in_rf
|
|
||||||
string verified_in_go
|
|
||||||
boolean kfo_confirmation_enabled
|
|
||||||
boolean is_in_work
|
|
||||||
datetime created_at
|
|
||||||
string created_by
|
|
||||||
}
|
|
||||||
|
|
||||||
CONSOLIDATION_MEMBER {
|
|
||||||
uuid id PK
|
|
||||||
uuid consolidation_version_id FK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
}
|
|
||||||
|
|
||||||
CORRECTION {
|
|
||||||
uuid id PK
|
|
||||||
uuid consolidation_version_id FK
|
|
||||||
uuid correction_id FK "nullable; parent correction"
|
|
||||||
uuid legal_entity_id FK "nullable"
|
|
||||||
string debit_line_code
|
|
||||||
string debit_account_code
|
|
||||||
numeric debit_adjustment
|
|
||||||
string credit_line_code
|
|
||||||
string credit_account_code
|
|
||||||
numeric credit_adjustment
|
|
||||||
numeric deviation_amount
|
|
||||||
numeric deviation_percent
|
|
||||||
text comment
|
|
||||||
string created_by
|
|
||||||
datetime created_at
|
|
||||||
string uploaded_by
|
|
||||||
datetime uploaded_at
|
|
||||||
}
|
|
||||||
|
|
||||||
STORED_FILE {
|
|
||||||
uuid id PK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
enum document_type "nullable"
|
|
||||||
enum file_type "XLSX | XLS | PDF | PNG | JPG"
|
|
||||||
string original_name
|
|
||||||
string s3_key UK "nullable"
|
|
||||||
string content_hash "nullable"
|
|
||||||
bigint size_bytes
|
|
||||||
smallint period_year
|
|
||||||
enum period_quarter "nullable"
|
|
||||||
enum status "nullable"
|
|
||||||
uuid current_recognition_job_id FK "nullable"
|
|
||||||
string uploaded_by "nullable"
|
|
||||||
datetime uploaded_at "nullable"
|
|
||||||
}
|
|
||||||
|
|
||||||
RECOGNITION_JOB {
|
|
||||||
uuid id PK
|
|
||||||
uuid file_id FK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
enum pipeline "nullable"
|
|
||||||
string service_version "nullable"
|
|
||||||
enum status "nullable"
|
|
||||||
smallint attempt
|
|
||||||
string queue_msg_id "nullable"
|
|
||||||
datetime requested_at "nullable"
|
|
||||||
datetime started_at "nullable"
|
|
||||||
datetime finished_at "nullable"
|
|
||||||
text error_message "nullable"
|
|
||||||
string raw_result_s3_key "nullable"
|
|
||||||
}
|
|
||||||
|
|
||||||
FINANCIAL_STATEMENT_META {
|
|
||||||
uuid id PK
|
|
||||||
uuid stored_file_id FK
|
|
||||||
enum statement_type "BALANCE | INCOME"
|
|
||||||
string inn
|
|
||||||
boolean inn_verified
|
|
||||||
date report_date "nullable"
|
|
||||||
boolean report_date_verified
|
|
||||||
string units
|
|
||||||
boolean units_verified
|
|
||||||
}
|
|
||||||
|
|
||||||
FINANCIAL_STATEMENT_LINE {
|
|
||||||
uuid id PK
|
|
||||||
uuid recognition_job_id FK
|
|
||||||
uuid meta_id FK "nullable"
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
string line_code
|
|
||||||
boolean line_code_recognized
|
|
||||||
string line_name
|
|
||||||
boolean line_name_recognized
|
|
||||||
enum row_kind
|
|
||||||
bigint ordinal
|
|
||||||
}
|
|
||||||
|
|
||||||
FINANCIAL_STATEMENT_VALUE {
|
|
||||||
uuid id PK
|
|
||||||
uuid line_id FK
|
|
||||||
numeric amount "nullable"
|
|
||||||
string string_value "nullable"
|
|
||||||
smallint column_index
|
|
||||||
boolean is_recognized
|
|
||||||
}
|
|
||||||
|
|
||||||
OSV_ENTRY {
|
|
||||||
uuid id PK
|
|
||||||
uuid recognition_job_id FK
|
|
||||||
uuid legal_entity_id FK
|
|
||||||
uuid stored_file_id FK
|
|
||||||
string account_code
|
|
||||||
uuid account_id FK
|
|
||||||
string account_root
|
|
||||||
string counterparty_name
|
|
||||||
string counterparty_inn
|
|
||||||
uuid counterparty_id FK "nullable"
|
|
||||||
numeric opening_debit
|
|
||||||
numeric opening_credit
|
|
||||||
numeric turnover_debit
|
|
||||||
numeric turnover_credit
|
|
||||||
numeric closing_debit
|
|
||||||
numeric closing_credit
|
|
||||||
}
|
|
||||||
|
|
||||||
CHART_OF_ACCOUNT o|--o{ CHART_OF_ACCOUNT : "parent_id"
|
|
||||||
REGIONAL_OFFICE ||--o{ COMPANY_GROUP : "base office"
|
|
||||||
COMPANY_GROUP ||--o{ COMPANY_GROUP_HISTORY : "history"
|
|
||||||
COMPANY_GROUP ||--o{ LEGAL_ENTITY : "contains"
|
|
||||||
REGIONAL_OFFICE ||--o{ LEGAL_ENTITY : "serves"
|
|
||||||
LEGAL_ENTITY ||--o{ LEGAL_ENTITY_ALIAS : "aliases"
|
|
||||||
LEGAL_ENTITY ||--o{ LEGAL_ENTITY_HISTORY : "history"
|
|
||||||
COMPANY_GROUP ||--o{ LEGAL_ENTITY_HISTORY : "group snapshot"
|
|
||||||
REGIONAL_OFFICE ||--o{ LEGAL_ENTITY_HISTORY : "office snapshot"
|
|
||||||
|
|
||||||
COMPANY_GROUP ||--o{ RELATED_GROUP : "owns"
|
|
||||||
RELATED_GROUP ||--o{ RELATED_GROUP_MEMBER : "memberships"
|
|
||||||
LEGAL_ENTITY ||--o{ RELATED_GROUP_MEMBER : "participates"
|
|
||||||
RELATED_GROUP ||--o{ CONSOLIDATION : "periods"
|
|
||||||
CONSOLIDATION ||--o{ CONSOLIDATION_VERSION : "versions"
|
|
||||||
CONSOLIDATION_VERSION ||--o{ CONSOLIDATION_MEMBER : "members"
|
|
||||||
LEGAL_ENTITY ||--o{ CONSOLIDATION_MEMBER : "included"
|
|
||||||
CONSOLIDATION_VERSION ||--o{ CORRECTION : "corrections"
|
|
||||||
LEGAL_ENTITY o|--o{ CORRECTION : "entity correction"
|
|
||||||
CORRECTION o|--o{ CORRECTION : "parent correction"
|
|
||||||
|
|
||||||
LEGAL_ENTITY ||--o{ STORED_FILE : "files"
|
|
||||||
STORED_FILE ||--o{ RECOGNITION_JOB : "recognition attempts"
|
|
||||||
LEGAL_ENTITY ||--o{ RECOGNITION_JOB : "recognized for"
|
|
||||||
RECOGNITION_JOB o|--o{ STORED_FILE : "current job"
|
|
||||||
STORED_FILE ||--o{ FINANCIAL_STATEMENT_META : "metadata"
|
|
||||||
FINANCIAL_STATEMENT_META o|--o{ FINANCIAL_STATEMENT_LINE : "lines"
|
|
||||||
RECOGNITION_JOB ||--o{ FINANCIAL_STATEMENT_LINE : "produces"
|
|
||||||
LEGAL_ENTITY ||--o{ FINANCIAL_STATEMENT_LINE : "owns"
|
|
||||||
FINANCIAL_STATEMENT_LINE ||--o{ FINANCIAL_STATEMENT_VALUE : "values"
|
|
||||||
|
|
||||||
RECOGNITION_JOB ||--o{ OSV_ENTRY : "produces"
|
|
||||||
STORED_FILE ||--o{ OSV_ENTRY : "contains"
|
|
||||||
LEGAL_ENTITY ||--o{ OSV_ENTRY : "owner"
|
|
||||||
LEGAL_ENTITY o|--o{ OSV_ENTRY : "counterparty"
|
|
||||||
CHART_OF_ACCOUNT ||--o{ OSV_ENTRY : "account"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Важные ограничения
|
|
||||||
|
|
||||||
- `consolidation`: уникальна комбинация (`related_group_id`, `year`, `quarter`).
|
|
||||||
- `consolidation_version`: уникальна пара (`consolidation_id`, `version_no`).
|
|
||||||
- `consolidation_member`: уникальна пара (`consolidation_version_id`, `legal_entity_id`).
|
|
||||||
- `legal_entity_history`: уникальна пара (`legal_entity_id`, `valid_from`).
|
|
||||||
- `related_group_member`: действуют частичные уникальные индексы для текущих участников.
|
|
||||||
- `stored_file` и `recognition_job` связаны циклически: файл хранит текущую попытку, а каждая попытка ссылается на файл.
|
|
||||||
|
|
||||||
## Представление
|
|
||||||
|
|
||||||
`dkb.v_osv_entry_with_file_flags` объединяет `osv_entry`, `stored_file`, `recognition_job`, `legal_entity`, `related_group_member` и `related_group`. Оно добавляет вычисляемый тип контрагента: `ГСК`, `ГК` или `Внешний`.
|
|
||||||
|
|
||||||
## Источники
|
|
||||||
|
|
||||||
- `api/src/models/*.py`
|
|
||||||
- `api/alembic/versions/1c4e0a7d9612_first_migration.py`
|
|
||||||
- `api/alembic/versions/c89d082850bd_add_default_and_nullability.py`
|
|
||||||
- `api/scripts/create_osv_entry_view.sql`
|
|
||||||
2832
web/package-lock.json
generated
2832
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user