feat(organizations): generate complete test statements
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user