Merge pull request 'summary-osv' (#2) from summary-osv into main
Reviewed-on: #2
This commit is contained in:
commit
55ac236e41
1
.gitignore
vendored
1
.gitignore
vendored
@ -330,6 +330,7 @@ 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/
|
||||||
|
|||||||
@ -87,7 +87,7 @@ def run_migrations_offline() -> None:
|
|||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
literal_binds=True,
|
literal_binds=True,
|
||||||
dialect_opts={"paramstyle": "named"},
|
dialect_opts={"paramstyle": "named"},
|
||||||
version_table_schema="dkb",
|
version_table_schema=db_schema,
|
||||||
include_schemas=True,
|
include_schemas=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,150 @@
|
|||||||
|
"""Add OSV entries for group with counterparty alias resolution.
|
||||||
|
|
||||||
|
Revision ID: c94eb1503a26
|
||||||
|
Revises: c89d082850bd
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from src.core.config import settings
|
||||||
|
|
||||||
|
revision = "c94eb1503a26"
|
||||||
|
down_revision = "c89d082850bd"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _quoted_db_schema() -> str:
|
||||||
|
return op.get_bind().dialect.identifier_preparer.quote_schema(
|
||||||
|
settings.OPENBAO.DATABASE.DATABASE_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
schema = _quoted_db_schema()
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE OR REPLACE FUNCTION {schema}.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,
|
||||||
|
counterparty_resolved_by_alias boolean,
|
||||||
|
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,
|
||||||
|
COALESCE(oe.counterparty_id, counterparty_by_inn.id, counterparty_by_alias.legal_entity_id) AS counterparty_id,
|
||||||
|
(oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.id IS NULL
|
||||||
|
AND counterparty_by_alias.legal_entity_id IS NOT NULL)
|
||||||
|
AS counterparty_resolved_by_alias,
|
||||||
|
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 {schema}.related_group_member AS member
|
||||||
|
JOIN {schema}.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 {schema}.osv_entry AS oe
|
||||||
|
LEFT JOIN {schema}.legal_entity AS counterparty_by_inn
|
||||||
|
ON oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.inn = NULLIF(BTRIM(oe.counterparty_inn), '')
|
||||||
|
LEFT JOIN {schema}.legal_entity_alias AS counterparty_by_alias
|
||||||
|
ON oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.id IS NULL
|
||||||
|
AND counterparty_by_alias.normalized_alias = NULLIF(BTRIM(oe.counterparty_name), '')
|
||||||
|
LEFT JOIN {schema}.chart_of_account AS coa ON coa.id = oe.account_id
|
||||||
|
JOIN {schema}.stored_file AS sf ON sf.id = oe.stored_file_id
|
||||||
|
JOIN {schema}.legal_entity AS le ON le.id = oe.legal_entity_id
|
||||||
|
JOIN {schema}.company_group AS cg ON cg.id = le.company_group_id
|
||||||
|
LEFT JOIN {schema}.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 {schema}.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:
|
||||||
|
schema = _quoted_db_schema()
|
||||||
|
op.execute(
|
||||||
|
f"DROP FUNCTION IF EXISTS {schema}.get_osv_entries_for_group(uuid, integer, text)"
|
||||||
|
)
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
"""Add OSV entries scoped to a consolidation version's members and period.
|
||||||
|
|
||||||
|
Revision ID: e28f709a6b31
|
||||||
|
Revises: c94eb1503a26
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from src.core.config import settings
|
||||||
|
|
||||||
|
revision = "e28f709a6b31"
|
||||||
|
down_revision = "c94eb1503a26"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _quoted_db_schema() -> str:
|
||||||
|
return op.get_bind().dialect.identifier_preparer.quote_schema(
|
||||||
|
settings.OPENBAO.DATABASE.DATABASE_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
schema = _quoted_db_schema()
|
||||||
|
op.execute(f"""
|
||||||
|
CREATE OR REPLACE FUNCTION {schema}.get_osv_entries_for_consolidation_version(
|
||||||
|
p_consolidation_version_id uuid
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
counterparty_resolved_by_alias boolean,
|
||||||
|
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 consolidation_info AS (
|
||||||
|
SELECT cv.id AS consolidation_version_id,
|
||||||
|
c.year AS period_year,
|
||||||
|
c.quarter AS period_quarter,
|
||||||
|
rg.company_group_id
|
||||||
|
FROM {schema}.consolidation_version AS cv
|
||||||
|
JOIN {schema}.consolidation AS c ON c.id = cv.consolidation_id
|
||||||
|
JOIN {schema}.related_group AS rg ON rg.id = c.related_group_id
|
||||||
|
WHERE cv.id = p_consolidation_version_id
|
||||||
|
), 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,
|
||||||
|
COALESCE(oe.counterparty_id, counterparty_by_inn.id, counterparty_by_alias.legal_entity_id) AS counterparty_id,
|
||||||
|
(oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.id IS NULL
|
||||||
|
AND counterparty_by_alias.legal_entity_id IS NOT NULL)
|
||||||
|
AS counterparty_resolved_by_alias,
|
||||||
|
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 {schema}.related_group_member AS member
|
||||||
|
JOIN {schema}.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 = ci.company_group_id
|
||||||
|
AND rg.kind = 'GSK'
|
||||||
|
) AS is_gsk,
|
||||||
|
le.company_group_id = ci.company_group_id AS is_gk
|
||||||
|
FROM consolidation_info AS ci
|
||||||
|
JOIN {schema}.consolidation_member AS cm
|
||||||
|
ON cm.consolidation_version_id = ci.consolidation_version_id
|
||||||
|
JOIN {schema}.osv_entry AS oe ON oe.legal_entity_id = cm.legal_entity_id
|
||||||
|
LEFT JOIN {schema}.legal_entity AS counterparty_by_inn
|
||||||
|
ON oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.inn = NULLIF(BTRIM(oe.counterparty_inn), '')
|
||||||
|
LEFT JOIN {schema}.legal_entity_alias AS counterparty_by_alias
|
||||||
|
ON oe.counterparty_id IS NULL
|
||||||
|
AND counterparty_by_inn.id IS NULL
|
||||||
|
AND counterparty_by_alias.normalized_alias = NULLIF(BTRIM(oe.counterparty_name), '')
|
||||||
|
LEFT JOIN {schema}.chart_of_account AS coa ON coa.id = oe.account_id
|
||||||
|
JOIN {schema}.stored_file AS sf ON sf.id = oe.stored_file_id
|
||||||
|
JOIN {schema}.legal_entity AS le ON le.id = oe.legal_entity_id
|
||||||
|
JOIN {schema}.company_group AS cg ON cg.id = le.company_group_id
|
||||||
|
LEFT JOIN {schema}.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 = ci.period_year
|
||||||
|
AND sf.period_quarter = ci.period_quarter
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
schema = _quoted_db_schema()
|
||||||
|
op.execute(
|
||||||
|
f"DROP FUNCTION {schema}.get_osv_entries_for_consolidation_version(uuid)"
|
||||||
|
)
|
||||||
@ -1,11 +1,43 @@
|
|||||||
from fastapi import APIRouter, File, HTTPException, UploadFile, Form, Depends
|
from fastapi import APIRouter, File, HTTPException, UploadFile, Form, Depends, Query
|
||||||
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-consolidation-version', response_model=list[OsvGroupEntryResponse])
|
||||||
|
async def get_osv_by_consolidation_version(
|
||||||
|
consolidation_version_id: uuid.UUID,
|
||||||
|
service: OsvService = Depends(get_osv_service),
|
||||||
|
):
|
||||||
|
"""ОСВ за версию консолидации.
|
||||||
|
|
||||||
|
Пустая выборка (в том числе неизвестная версия) возвращается как [].
|
||||||
|
"""
|
||||||
|
return await service.get_by_consolidation_version(
|
||||||
|
consolidation_version_id=consolidation_version_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@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)
|
||||||
|
|||||||
@ -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):
|
for fname in smbclient.listdir(new_dir, port=settings.OPENBAO.SMB.SMB_PORT):
|
||||||
with smbclient.open_file(f'{new_dir}/{fname}', 'r') as f:
|
with smbclient.open_file(f'{new_dir}/{fname}', 'r', port=settings.OPENBAO.SMB.SMB_PORT) as f:
|
||||||
files.append(json.loads(f.read()))
|
files.append(json.loads(f.read()))
|
||||||
logger.info("В рамках job_id={job_id} файлов в SMB найдено: {files_count}", job_id=job.job_id, files_count=len(files))
|
logger.info("В рамках job_id={job_id} файлов в SMB найдено: {files_count}", job_id=job.job_id, files_count=len(files))
|
||||||
s3_json_dict = {
|
s3_json_dict = {
|
||||||
|
|||||||
@ -108,7 +108,8 @@ 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}/Input/{last_part}', 'wb') as f:
|
with smbclient.open_file(f'{settings.OPENBAO.SMB.SMB_PATH}/Файлы/{last_part}', 'wb', port=settings.OPENBAO.SMB.SMB_PORT) 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)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ class DBSettings(BaseSettings):
|
|||||||
DATABASE_SCHEMA: str
|
DATABASE_SCHEMA: 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
|
||||||
@ -17,6 +19,7 @@ class NATSSettings(BaseSettings):
|
|||||||
NATS_JWT: str
|
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_PATH: str
|
SMB_PATH: str
|
||||||
SMB_LOGIN: str
|
SMB_LOGIN: str
|
||||||
|
|||||||
@ -1,10 +1,36 @@
|
|||||||
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
|
from sqlalchemy import insert, select, text
|
||||||
|
|
||||||
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_consolidation_version(self, consolidation_version_id):
|
||||||
|
stmt = text("""
|
||||||
|
SELECT * FROM dkb.get_osv_entries_for_consolidation_version(
|
||||||
|
CAST(:consolidation_version_id AS uuid)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
result = await self.session.execute(stmt, {
|
||||||
|
"consolidation_version_id": consolidation_version_id,
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
|||||||
43
api/src/schemas/osv.py
Normal file
43
api/src/schemas/osv.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
|
counterparty_resolved_by_alias: bool
|
||||||
|
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,15 +1,73 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import ValidationError
|
||||||
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
|
||||||
|
)
|
||||||
|
|
||||||
|
result: list[OsvGroupEntryResponse] = []
|
||||||
|
for row_index, entry in enumerate(entries):
|
||||||
|
try:
|
||||||
|
result.append(OsvGroupEntryResponse.model_validate(entry))
|
||||||
|
except ValidationError as exc:
|
||||||
|
logger.error(
|
||||||
|
"Некорректный результат get_osv_entries_for_group: "
|
||||||
|
"company_group_id={company_group_id}, year={year}, quarter={quarter}, "
|
||||||
|
"row_index={row_index}, osv_entry_id={osv_entry_id}, "
|
||||||
|
"validation_errors={validation_errors}",
|
||||||
|
company_group_id=company_group_id,
|
||||||
|
year=year,
|
||||||
|
quarter=quarter.value,
|
||||||
|
row_index=row_index,
|
||||||
|
osv_entry_id=entry.get("osv_entry_id"),
|
||||||
|
validation_errors=exc.errors(include_input=False),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_by_consolidation_version(
|
||||||
|
self, consolidation_version_id: uuid.UUID
|
||||||
|
) -> list[OsvGroupEntryResponse]:
|
||||||
|
entries = await self.osv_repo.get_by_consolidation_version(
|
||||||
|
consolidation_version_id=consolidation_version_id
|
||||||
|
)
|
||||||
|
|
||||||
|
result: list[OsvGroupEntryResponse] = []
|
||||||
|
for row_index, entry in enumerate(entries):
|
||||||
|
try:
|
||||||
|
result.append(OsvGroupEntryResponse.model_validate(entry))
|
||||||
|
except ValidationError as exc:
|
||||||
|
logger.error(
|
||||||
|
"Некорректный результат get_osv_entries_for_consolidation_version: "
|
||||||
|
"consolidation_version_id={consolidation_version_id}, "
|
||||||
|
"row_index={row_index}, osv_entry_id={osv_entry_id}, "
|
||||||
|
"validation_errors={validation_errors}",
|
||||||
|
consolidation_version_id=consolidation_version_id,
|
||||||
|
row_index=row_index,
|
||||||
|
osv_entry_id=entry.get("osv_entry_id"),
|
||||||
|
validation_errors=exc.errors(include_input=False),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def get_by_file_id(self, file_id: uuid.UUID):
|
async def get_by_file_id(self, file_id: uuid.UUID):
|
||||||
try:
|
try:
|
||||||
|
|||||||
287
docs/database-er-diagram-17-09-2026.md
Normal file
287
docs/database-er-diagram-17-09-2026.md
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# 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`
|
||||||
Loading…
x
Reference in New Issue
Block a user