feat(отчёт): строка на каждый приём + столбец «Ф.И.О. нанимаемого сотрудника»
На одну заявку на подбор приходится несколько заявок на приём («Разнорабочий, 4 вакансии» → 4 кандидата), и раньше они схлопывались в одну строку: ФИО не было вовсе, а «Дата выхода» показывала только САМУЮ ПОЗДНЮЮ из четырёх. Кто именно вышел и когда — из отчёта было не узнать. Теперь вакансия занимает столько строк, сколько по ней приёмов. Построчные колонки — «Дата выхода», новая «Ф.И.О. нанимаемого сотрудника» и «Вместо сотрудника Ф.И.О.» (у каждого приёма своё «взамен кого»); все остальные общие и объединяются по вертикали. Модель: VacancyReportRow.Lines — именно СТРОКИ ЛИСТА, а не «приёмы». У вакансии без приёмов строка всё равно одна (иначе она пропала бы из отчёта), с пустыми ФИО и датой, но с «Вместо сотрудника» из причины открытия вакансии — прежнее поведение сохранено. Реальное число приёмов осталось в ClosedCount. Отклонённые приёмы теперь отбрасываются целиком, а не только из счётчика: отдельная строка с ФИО отклонённого кандидата утверждала бы «вот кого приняли». Объединение заводится только при lineCount > 1 — область 1×1 Excel считает некорректной и портит файл. Значение общих колонок пишется ТОЛЬКО в первую строку: у объединённой области Excel хранит его в верхней левой ячейке, а дубли сломали бы SUM в итогах (посчитал бы вакансию по разу на каждый приём) — на это есть отдельный тест. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+85
-13
@@ -21,7 +21,8 @@ public sealed class VacancyReportWorkbookTests
|
||||
DateOnly? submitDate = null,
|
||||
DateOnly? closingDate = null,
|
||||
DateOnly? startDate = null,
|
||||
string? recruiter = "Сергеева Надежда Ивановна") =>
|
||||
string? recruiter = "Сергеева Надежда Ивановна",
|
||||
IReadOnlyList<VacancyReportLine>? lines = null) =>
|
||||
new(
|
||||
position,
|
||||
"офис",
|
||||
@@ -29,11 +30,16 @@ public sealed class VacancyReportWorkbookTests
|
||||
closed,
|
||||
submitDate ?? new DateOnly(2026, 7, 8),
|
||||
closingDate,
|
||||
startDate,
|
||||
"Ким Н.Ю.",
|
||||
null,
|
||||
closed == 0 ? VacancyClosedFlags.No : VacancyClosedFlags.Yes,
|
||||
recruiter);
|
||||
recruiter,
|
||||
// По умолчанию одна строка — вакансия без приёмов либо с одним;
|
||||
// тесты многострочности передают lines явно.
|
||||
lines ?? [new VacancyReportLine(null, startDate, null)]);
|
||||
|
||||
/// <summary>Строка листа в пределах вакансии — то, что различается между приёмами.</summary>
|
||||
private static VacancyReportLine Line(string? candidate, DateOnly? startDate = null, string? replaced = null) =>
|
||||
new(candidate, startDate, replaced);
|
||||
|
||||
private static XSSFWorkbook BuildWorkbook(DateOnly from, DateOnly to, params VacancyReportGroup[] groups)
|
||||
{
|
||||
@@ -72,7 +78,7 @@ public sealed class VacancyReportWorkbookTests
|
||||
return cell?.CellType == CellType.Formula ? cell.CellFormula : "";
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Шапка — шестнадцать колонок в порядке образца отчёта")]
|
||||
[Fact(DisplayName = "Шапка — семнадцать колонок в порядке образца отчёта")]
|
||||
public void Header_MatchesSample()
|
||||
{
|
||||
var sheet = BuildListSheet(new VacancyReportGroup("", [Row("Логист")]));
|
||||
@@ -84,10 +90,11 @@ public sealed class VacancyReportWorkbookTests
|
||||
StringAt(sheet, 1, 8).ShouldBe("Дата подачи заявки");
|
||||
StringAt(sheet, 1, 9).ShouldBe("Дата закрытия вакансии");
|
||||
StringAt(sheet, 1, 11).ShouldBe("Дата выхода");
|
||||
StringAt(sheet, 1, 12).ShouldBe("Инициатор заявки");
|
||||
StringAt(sheet, 1, 14).ShouldBe("Комментарии");
|
||||
StringAt(sheet, 1, 16).ShouldBe("Рекрутер");
|
||||
IsEmptyAt(sheet, 1, 17).ShouldBeTrue();
|
||||
StringAt(sheet, 1, 12).ShouldBe("Ф.И.О. нанимаемого сотрудника");
|
||||
StringAt(sheet, 1, 13).ShouldBe("Инициатор заявки");
|
||||
StringAt(sheet, 1, 15).ShouldBe("Комментарии");
|
||||
StringAt(sheet, 1, 17).ShouldBe("Рекрутер");
|
||||
IsEmptyAt(sheet, 1, 18).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Шапка колонок с датами закрашена так же, как остальная шапка")]
|
||||
@@ -124,11 +131,76 @@ public sealed class VacancyReportWorkbookTests
|
||||
DateAt(sheet, 2, 8).ShouldBe(new DateTime(2026, 3, 30));
|
||||
DateAt(sheet, 2, 9).ShouldBe(new DateTime(2026, 7, 8));
|
||||
DateAt(sheet, 2, 11).ShouldBe(new DateTime(2026, 8, 17));
|
||||
StringAt(sheet, 2, 12).ShouldBe("Ким Н.Ю.");
|
||||
StringAt(sheet, 2, 13).ShouldBe("Ким Н.Ю.");
|
||||
// «Комментарии» намеренно пусты — в заявках этих пометок пока нет.
|
||||
IsEmptyAt(sheet, 2, 14).ShouldBeTrue();
|
||||
StringAt(sheet, 2, 15).ShouldBe("да");
|
||||
StringAt(sheet, 2, 16).ShouldBe("Сергеева Надежда Ивановна");
|
||||
IsEmptyAt(sheet, 2, 15).ShouldBeTrue();
|
||||
StringAt(sheet, 2, 16).ShouldBe("да");
|
||||
StringAt(sheet, 2, 17).ShouldBe("Сергеева Надежда Ивановна");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Несколько приёмов: своя строка на каждого, общие поля объединены по вертикали")]
|
||||
public void SeveralHires_OneRowEachWithMergedCommonCells()
|
||||
{
|
||||
var sheet = BuildListSheet(new VacancyReportGroup("", [
|
||||
Row("Разнорабочий", vacancies: 4, closed: 4,
|
||||
submitDate: new DateOnly(2026, 7, 10),
|
||||
lines: [
|
||||
Line("Дулин Евгений Петрович", new DateOnly(2026, 8, 19)),
|
||||
Line("Телятников Игорь Сергеевич", new DateOnly(2026, 8, 21)),
|
||||
Line("Жабаев Нурлан Ахметович", new DateOnly(2026, 8, 25), replaced: "Оспанов К.")
|
||||
])
|
||||
]));
|
||||
|
||||
// Построчные колонки — своё значение в каждой строке.
|
||||
StringAt(sheet, 2, 12).ShouldBe("Дулин Евгений Петрович");
|
||||
StringAt(sheet, 3, 12).ShouldBe("Телятников Игорь Сергеевич");
|
||||
StringAt(sheet, 4, 12).ShouldBe("Жабаев Нурлан Ахметович");
|
||||
DateAt(sheet, 2, 11).ShouldBe(new DateTime(2026, 8, 19));
|
||||
DateAt(sheet, 3, 11).ShouldBe(new DateTime(2026, 8, 21));
|
||||
DateAt(sheet, 4, 11).ShouldBe(new DateTime(2026, 8, 25));
|
||||
StringAt(sheet, 4, 14).ShouldBe("Оспанов К.");
|
||||
|
||||
// Общие — значение ТОЛЬКО в первой строке: у объединённой области Excel
|
||||
// хранит его в верхней левой ячейке, и дубли сломали бы SUM в итогах.
|
||||
StringAt(sheet, 2, 3).ShouldBe("Разнорабочий");
|
||||
IsEmptyAt(sheet, 3, 3).ShouldBeTrue();
|
||||
IsEmptyAt(sheet, 4, 3).ShouldBeTrue();
|
||||
IntAt(sheet, 2, 5).ShouldBe(4);
|
||||
IsEmptyAt(sheet, 3, 5).ShouldBeTrue();
|
||||
|
||||
// И сами объединения заведены — иначе продолжения выглядели бы пустыми.
|
||||
var merged = Enumerable.Range(0, sheet.NumMergedRegions).Select(sheet.GetMergedRegion).ToList();
|
||||
merged.ShouldContain(r => r.FirstRow == 1 && r.LastRow == 3 && r.FirstColumn == 2 && r.LastColumn == 2);
|
||||
// Построчные колонки объединять нельзя — иначе ФИО второго и третьего пропали бы.
|
||||
merged.ShouldNotContain(r => r.FirstColumn == 11); // 0-based: ColCandidateName
|
||||
merged.ShouldNotContain(r => r.FirstColumn == 10); // ColStartDate
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Одна строка у вакансии — объединений не создаётся (область 1×1 портит файл)")]
|
||||
public void SingleLine_NoMergedRegions()
|
||||
{
|
||||
var sheet = BuildListSheet(new VacancyReportGroup("", [Row("Логист")]));
|
||||
|
||||
sheet.NumMergedRegions.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Итоги суммируют вакансии по одному разу, а не по числу приёмов")]
|
||||
public void Totals_CountVacancyOnce_NotPerHire()
|
||||
{
|
||||
var sheet = BuildListSheet(new VacancyReportGroup("", [
|
||||
Row("Разнорабочий", vacancies: 4, closed: 4, lines: [
|
||||
Line("Первый", new DateOnly(2026, 8, 19)),
|
||||
Line("Второй", new DateOnly(2026, 8, 21))
|
||||
]),
|
||||
Row("Логист", vacancies: 1, closed: 0)
|
||||
]));
|
||||
|
||||
// Строки: 2-3 «Разнорабочий» (две), 4 «Логист» → итоги в 5-й.
|
||||
FormulaAt(sheet, 5, 5).ShouldBe("SUM(E2:E4)");
|
||||
// Значение «Кол-во вакансий» лежит только в E2 и E4 — E3 пустая (объединение),
|
||||
// поэтому SUM даёт 4+1=5, а не 4+4+1.
|
||||
IsEmptyAt(sheet, 3, 5).ShouldBeTrue();
|
||||
IntAt(sheet, 4, 5).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "«В работе» и «срок отработки» — формулы, а не посчитанные числа")]
|
||||
|
||||
Reference in New Issue
Block a user