feat(organizations): generate complete test statements
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 59s
CI/CD Pipeline / Build and Push Images (push) Successful in 17s
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 29s

This commit is contained in:
2026-08-10 14:25:43 +02:00
parent 2a1f29983e
commit d079c74ecf
2 changed files with 407 additions and 46 deletions

View File

@@ -37,6 +37,76 @@ TEST_FINANCIAL_HISTORY_YEARS = 4
TEST_COMPANY_NAMESPACE = UUID("59b36ae9-bcf8-4b7c-b77a-77578f485a01")
TEST_RECORD_PREFIX = "mostovik-test-company"
TEST_BALANCE_LINE_NAMES = {
"1110": "Нематериальные активы",
"1120": "Результаты исследований и разработок",
"1130": "Нематериальные поисковые активы",
"1140": "Материальные поисковые активы",
"1150": "Основные средства",
"1160": "Доходные вложения в материальные ценности",
"1170": "Финансовые вложения",
"1180": "Отложенные налоговые активы",
"1190": "Прочие внеоборотные активы",
"1100": "Итого по разделу I",
"1210": "Запасы",
"1220": "НДС по приобретенным ценностям",
"1230": "Дебиторская задолженность",
"1240": "Финансовые вложения (за исключением денежных эквивалентов)",
"1250": "Денежные средства и денежные эквиваленты",
"1260": "Прочие оборотные активы",
"1200": "Итого по разделу II",
"1600": "БАЛАНС (актив)",
"1310": "Уставный капитал",
"1320": "Собственные акции, выкупленные у акционеров",
"1340": "Переоценка внеоборотных активов",
"1350": "Добавочный капитал (без переоценки)",
"1360": "Резервный капитал",
"1370": "Нераспределенная прибыль (непокрытый убыток)",
"1300": "Итого по разделу III",
"1410": "Заемные средства",
"1420": "Отложенные налоговые обязательства",
"1430": "Оценочные обязательства",
"1450": "Прочие обязательства",
"1400": "Итого по разделу IV",
"1510": "Заемные средства",
"1520": "Кредиторская задолженность",
"1530": "Доходы будущих периодов",
"1540": "Оценочные обязательства",
"1550": "Прочие обязательства",
"1500": "Итого по разделу V",
"1700": "БАЛАНС (пассив)",
}
TEST_PROFIT_LOSS_LINE_NAMES = {
"2110": "Выручка",
"2120": "Себестоимость продаж",
"2100": "Валовая прибыль (убыток)",
"2210": "Коммерческие расходы",
"2220": "Управленческие расходы",
"2200": "Прибыль (убыток) от продаж",
"2310": "Доходы от участия в других организациях",
"2320": "Проценты к получению",
"2330": "Проценты к уплате",
"2340": "Прочие доходы",
"2350": "Прочие расходы",
"2300": "Прибыль (убыток) до налогообложения",
"2410": "Налог на прибыль",
"2411": "Текущий налог на прибыль",
"2412": "Отложенный налог на прибыль",
"2460": "Прочее",
"2400": "Чистая прибыль (убыток)",
"2510": "Результат от переоценки внеоборотных активов",
"2520": "Результат от прочих операций",
"2530": "Налог на прибыль от операций вне чистой прибыли",
"2500": "Совокупный финансовый результат периода",
"2900": "Базовая прибыль (убыток) на акцию",
"2910": "Разводненная прибыль (убыток) на акцию",
}
TEST_FINANCIAL_LINES_PER_YEAR = len(TEST_BALANCE_LINE_NAMES) + len(
TEST_PROFIT_LOSS_LINE_NAMES
)
@dataclass(frozen=True)
class TestCompanyDatasetResult:
@@ -452,7 +522,9 @@ class TestCompanyDatasetService:
"file_hash": sha256(
f"financial:{organization.ogrn}".encode()
).hexdigest(),
"lines_count": 4,
"lines_count": (
TEST_FINANCIAL_LINES_PER_YEAR * TEST_FINANCIAL_HISTORY_YEARS
),
},
},
ParserLoadLog.Source.PROCUREMENTS: cls._procurement_values(
@@ -670,57 +742,215 @@ class TestCompanyDatasetService:
}
@staticmethod
def _refresh_financial_lines(*, record, legacy_report, index: int) -> None:
def _test_amount(
*,
base: int,
index: int,
year: int,
per_company: int,
annual_growth: int,
) -> int:
"""Return a stable amount for the same company and calendar year."""
return base + index * per_company + (year - 2020) * annual_growth
@classmethod
def _balance_values(cls, *, index: int, year: int) -> dict[str, int]:
"""Build a balanced form 0710001 in thousands of rubles."""
def amount(base: int, per_company: int, annual_growth: int) -> int:
return cls._test_amount(
base=base,
index=index,
year=year,
per_company=per_company,
annual_growth=annual_growth,
)
values = {
"1110": amount(350, 7, 25),
"1120": amount(220, 5, 18),
"1130": amount(80, 2, 6),
"1140": amount(70, 2, 5),
"1150": amount(4_800, 60, 240),
"1160": amount(300, 8, 22),
"1170": amount(900, 18, 70),
"1180": amount(180, 4, 12),
"1190": amount(140, 3, 10),
"1210": amount(3_400, 45, 190),
"1220": amount(240, 5, 12),
"1230": amount(3_800, 55, 220),
"1240": amount(1_100, 20, 90),
"1250": amount(1_500, 30, 120),
"1260": amount(260, 6, 16),
"1310": amount(3_500, 30, 100),
"1320": -amount(120, 2, 5),
"1340": amount(320, 4, 15),
"1350": amount(280, 3, 12),
"1360": amount(240, 3, 10),
"1410": amount(2_300, 32, 110),
"1420": amount(220, 4, 12),
"1430": amount(180, 3, 9),
"1450": amount(240, 4, 11),
"1510": amount(1_600, 25, 80),
"1520": amount(3_200, 40, 150),
"1530": amount(120, 2, 6),
"1540": amount(250, 4, 12),
"1550": amount(320, 5, 15),
}
values["1100"] = sum(
values[code]
for code in (
"1110",
"1120",
"1130",
"1140",
"1150",
"1160",
"1170",
"1180",
"1190",
)
)
values["1200"] = sum(
values[code] for code in ("1210", "1220", "1230", "1240", "1250", "1260")
)
values["1600"] = values["1100"] + values["1200"]
values["1400"] = sum(values[code] for code in ("1410", "1420", "1430", "1450"))
values["1500"] = sum(
values[code] for code in ("1510", "1520", "1530", "1540", "1550")
)
equity_without_retained = sum(
values[code] for code in ("1310", "1320", "1340", "1350", "1360")
)
values["1370"] = (
values["1600"] - equity_without_retained - values["1400"] - values["1500"]
)
values["1300"] = equity_without_retained + values["1370"]
values["1700"] = values["1300"] + values["1400"] + values["1500"]
return values
@classmethod
def _profit_loss_values(cls, *, index: int, year: int) -> dict[str, int]:
"""Build a form 0710002 whose calculated rows reconcile."""
def amount(base: int, per_company: int, annual_growth: int) -> int:
return cls._test_amount(
base=base,
index=index,
year=year,
per_company=per_company,
annual_growth=annual_growth,
)
revenue = amount(32_000, 800, 2_400)
cost_of_sales = revenue * 61 // 100
gross_profit = revenue - cost_of_sales
commercial_expenses = revenue * 5 // 100
management_expenses = revenue * 8 // 100
sales_profit = gross_profit - commercial_expenses - management_expenses
participation_income = amount(180, 6, 14)
interest_income = amount(220, 5, 16)
interest_expense = amount(310, 7, 20)
other_income = amount(760, 15, 45)
other_expense = amount(620, 12, 38)
profit_before_tax = (
sales_profit
+ participation_income
+ interest_income
- interest_expense
+ other_income
- other_expense
)
current_tax = profit_before_tax * 19 // 100
deferred_tax = profit_before_tax // 100
income_tax = current_tax + deferred_tax
other_tax_effects = amount(25, 1, 2)
net_profit = profit_before_tax - income_tax - other_tax_effects
revaluation_result = amount(90, 3, 7)
other_operations_result = amount(55, 2, 5)
other_operations_tax = amount(18, 1, 1)
comprehensive_result = (
net_profit
+ revaluation_result
+ other_operations_result
- other_operations_tax
)
basic_eps = max(1, net_profit // 1_000)
diluted_eps = max(1, basic_eps - 1)
return {
"2110": revenue,
"2120": cost_of_sales,
"2100": gross_profit,
"2210": commercial_expenses,
"2220": management_expenses,
"2200": sales_profit,
"2310": participation_income,
"2320": interest_income,
"2330": interest_expense,
"2340": other_income,
"2350": other_expense,
"2300": profit_before_tax,
"2410": income_tax,
"2411": current_tax,
"2412": deferred_tax,
"2460": other_tax_effects,
"2400": net_profit,
"2510": revaluation_result,
"2520": other_operations_result,
"2530": other_operations_tax,
"2500": comprehensive_result,
"2900": basic_eps,
"2910": diluted_eps,
}
@classmethod
def _financial_line_definitions(cls, *, index: int, year: int):
forms = (
(
"1",
TEST_BALANCE_LINE_NAMES,
cls._balance_values,
),
(
"2",
TEST_PROFIT_LOSS_LINE_NAMES,
cls._profit_loss_values,
),
)
for form_code, names, value_factory in forms:
period_start_values = value_factory(index=index, year=year - 1)
period_end_values = value_factory(index=index, year=year)
for line_code, line_name in names.items():
yield (
form_code,
line_code,
line_name,
period_start_values[line_code],
period_end_values[line_code],
)
@classmethod
def _refresh_financial_lines(cls, *, record, legacy_report, index: int) -> None:
current_year = timezone.localdate().year
report_years = range(
current_year - TEST_FINANCIAL_HISTORY_YEARS + 1,
current_year + 1,
)
expected = (
(
"1",
"1600",
"Баланс (актив)",
10_000_000 + index * 100_000,
300_000 + index * 2_000,
),
(
"1",
"1300",
"Капитал и резервы",
4_000_000 + index * 50_000,
120_000 + index * 1_000,
),
(
"2",
"2110",
"Выручка",
20_000_000 + index * 200_000,
750_000 + index * 5_000,
),
(
"2",
"2400",
"Чистая прибыль",
2_000_000 + index * 25_000,
80_000 + index * 500,
),
)
expected_keys = set()
for report_year in report_years:
years_before_current = current_year - report_year
for (
form_code,
line_code,
line_name,
current_period_end,
annual_change,
) in expected:
period_end = current_period_end - annual_change * years_before_current
period_start,
period_end,
) in cls._financial_line_definitions(index=index, year=report_year):
expected_keys.add((form_code, line_code, report_year))
defaults = {
"line_name": line_name,
"period_start": period_end - annual_change,
"period_start": period_start,
"period_end": period_end,
}
OrganizationSourceFinancialLine.objects.update_or_create(

View File

@@ -25,7 +25,10 @@ from organizations.models import (
SourceGroup,
)
from organizations.test_companies import (
TEST_BALANCE_LINE_NAMES,
TEST_FINANCIAL_HISTORY_YEARS,
TEST_FINANCIAL_LINES_PER_YEAR,
TEST_PROFIT_LOSS_LINE_NAMES,
TEST_RECORD_PREFIX,
)
@@ -92,7 +95,7 @@ class TestCompaniesCommandsTest(TestCase):
OrganizationSourceFinancialLine.objects.filter(
source_record__extension__organization__in=companies
).count(),
20 * 4 * TEST_FINANCIAL_HISTORY_YEARS,
20 * TEST_FINANCIAL_LINES_PER_YEAR * TEST_FINANCIAL_HISTORY_YEARS,
)
current_year = timezone.localdate().year
expected_financial_years = set(
@@ -124,7 +127,7 @@ class TestCompaniesCommandsTest(TestCase):
)
self.assertEqual(
FinancialReportLine.objects.count(),
20 * 4 * TEST_FINANCIAL_HISTORY_YEARS,
20 * TEST_FINANCIAL_LINES_PER_YEAR * TEST_FINANCIAL_HISTORY_YEARS,
)
self.assertEqual(
set(FinancialReportLine.objects.values_list("year", flat=True)),
@@ -250,14 +253,10 @@ class TestCompaniesCommandsTest(TestCase):
self.assertEqual(len(reports), 20)
self.assertEqual(
sum(len(report["lines"]) for report in reports),
20 * 4 * TEST_FINANCIAL_HISTORY_YEARS,
20 * TEST_FINANCIAL_LINES_PER_YEAR * TEST_FINANCIAL_HISTORY_YEARS,
)
self.assertEqual(
{
line["year"]
for report in reports
for line in report["lines"]
},
{line["year"] for report in reports for line in report["lines"]},
set(
range(
timezone.localdate().year - TEST_FINANCIAL_HISTORY_YEARS + 1,
@@ -266,6 +265,138 @@ class TestCompaniesCommandsTest(TestCase):
),
)
def test_financial_statements_are_complete_balanced_and_periodic(self):
call_command("create_test_companies", stdout=StringIO())
company = Organization.objects.get(name="Тестовая компания 1")
record = OrganizationSourceRecord.objects.get(
extension__organization=company,
source=ParserLoadLog.Source.FNS_REPORTS,
)
lines = list(
OrganizationSourceFinancialLine.objects.filter(
source_record=record
).order_by("year", "form_code", "line_code")
)
self.assertEqual(
len(lines),
TEST_FINANCIAL_LINES_PER_YEAR * TEST_FINANCIAL_HISTORY_YEARS,
)
self.assertTrue(
all(
line.period_start is not None and line.period_end is not None
for line in lines
)
)
lines_by_year = {}
for line in lines:
lines_by_year.setdefault(line.year, {})[
(line.form_code, line.line_code)
] = line
expected_codes = {
*(("1", code) for code in TEST_BALANCE_LINE_NAMES),
*(("2", code) for code in TEST_PROFIT_LOSS_LINE_NAMES),
}
ordered_years = sorted(lines_by_year)
for year in ordered_years:
year_lines = lines_by_year[year]
self.assertEqual(set(year_lines), expected_codes)
values = {key: line.period_end for key, line in year_lines.items()}
self.assertEqual(
values[("1", "1100")],
sum(
values[("1", code)]
for code in (
"1110",
"1120",
"1130",
"1140",
"1150",
"1160",
"1170",
"1180",
"1190",
)
),
)
self.assertEqual(
values[("1", "1200")],
sum(
values[("1", code)]
for code in ("1210", "1220", "1230", "1240", "1250", "1260")
),
)
self.assertEqual(
values[("1", "1600")],
values[("1", "1100")] + values[("1", "1200")],
)
self.assertEqual(
values[("1", "1300")],
sum(
values[("1", code)]
for code in ("1310", "1320", "1340", "1350", "1360", "1370")
),
)
self.assertEqual(
values[("1", "1400")],
sum(values[("1", code)] for code in ("1410", "1420", "1430", "1450")),
)
self.assertEqual(
values[("1", "1500")],
sum(
values[("1", code)]
for code in ("1510", "1520", "1530", "1540", "1550")
),
)
self.assertEqual(values[("1", "1600")], values[("1", "1700")])
self.assertEqual(
values[("2", "2100")],
values[("2", "2110")] - values[("2", "2120")],
)
self.assertEqual(
values[("2", "2200")],
values[("2", "2100")] - values[("2", "2210")] - values[("2", "2220")],
)
self.assertEqual(
values[("2", "2300")],
values[("2", "2200")]
+ values[("2", "2310")]
+ values[("2", "2320")]
- values[("2", "2330")]
+ values[("2", "2340")]
- values[("2", "2350")],
)
self.assertEqual(
values[("2", "2410")],
values[("2", "2411")] + values[("2", "2412")],
)
self.assertEqual(
values[("2", "2400")],
values[("2", "2300")] - values[("2", "2410")] - values[("2", "2460")],
)
self.assertEqual(
values[("2", "2500")],
values[("2", "2400")]
+ values[("2", "2510")]
+ values[("2", "2520")]
- values[("2", "2530")],
)
for previous_year, current_year in zip(
ordered_years,
ordered_years[1:],
strict=False,
):
for key, current_line in lines_by_year[current_year].items():
self.assertEqual(
current_line.period_start,
lines_by_year[previous_year][key].period_end,
)
def test_delete_removes_only_the_fixed_twenty_companies(self):
untouched = Organization.objects.create(
name="Тестовая компания 99",