backlogops 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backlogops/__init__.py +87 -0
- backlogops/apply_format_rules.py +95 -0
- backlogops/available_teams.py +205 -0
- backlogops/available_teams_config.py +538 -0
- backlogops/available_teams_wizard.py +448 -0
- backlogops/backlog.py +465 -0
- backlogops/backlog_helpers.py +658 -0
- backlogops/backlog_releases.py +299 -0
- backlogops/backlog_releases_io.py +200 -0
- backlogops/console_yes_no_bridge.py +45 -0
- backlogops/date_ranges.py +59 -0
- backlogops/demo_backlog.py +129 -0
- backlogops/estimate_ready_date.py +379 -0
- backlogops/format_rules.py +73 -0
- backlogops/io_config.py +277 -0
- backlogops/key_list_io.py +227 -0
- backlogops/levels.py +165 -0
- backlogops/move_keys_first.py +166 -0
- backlogops/no_text_io.py +100 -0
- backlogops/order_by_dependencies.py +381 -0
- backlogops/person.py +25 -0
- backlogops/py.typed +0 -0
- backlogops/release_backlog_updates.py +239 -0
- backlogops/release_change_io.py +134 -0
- backlogops/releases.py +170 -0
- backlogops/table_create.py +47 -0
- backlogops/table_rows.py +125 -0
- backlogops/team.py +190 -0
- backlogops/work_hours.py +155 -0
- backlogops-0.1.dist-info/METADATA +238 -0
- backlogops-0.1.dist-info/RECORD +34 -0
- backlogops-0.1.dist-info/WHEEL +5 -0
- backlogops-0.1.dist-info/licenses/LICENSE.txt +22 -0
- backlogops-0.1.dist-info/top_level.txt +1 -0
backlogops/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Library with backlog operations.
|
|
3
|
+
|
|
4
|
+
This package provides the data model for a backlog, the available
|
|
5
|
+
workforce and the calendar, together with the operations that build and
|
|
6
|
+
validate them. The names an application programmer is most likely to use
|
|
7
|
+
are re-exported here, so that they can be imported directly from
|
|
8
|
+
``backlogops``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
12
|
+
# MIT License
|
|
13
|
+
|
|
14
|
+
from backlogops.backlog import (
|
|
15
|
+
Backlog, BacklogItem, Status, get_backlog, get_backlog_item,
|
|
16
|
+
check_backlog_consistency, build_dependency_graph, item_dependency_edges,
|
|
17
|
+
event_start, event_finish)
|
|
18
|
+
from backlogops.backlog_helpers import find_cycle
|
|
19
|
+
from backlogops.levels import (
|
|
20
|
+
Level, Levels, DEFAULT_LEVELS, check_levels_consistency,
|
|
21
|
+
level_number_from_name)
|
|
22
|
+
from backlogops.person import Person
|
|
23
|
+
from backlogops.team import FteException, Membership, Team
|
|
24
|
+
from backlogops.releases import Release, Releases, get_release, get_releases
|
|
25
|
+
from backlogops.backlog_releases import BacklogReleases
|
|
26
|
+
from backlogops.demo_backlog import get_demo_backlog
|
|
27
|
+
from backlogops.available_teams import AvailableTeams
|
|
28
|
+
from backlogops.available_teams_config import (
|
|
29
|
+
AvailableTeamsConfig, read_available_teams, write_available_teams,
|
|
30
|
+
get_available_teams)
|
|
31
|
+
from backlogops.order_by_dependencies import (
|
|
32
|
+
order_by_dependencies, DependencyMode)
|
|
33
|
+
from backlogops.estimate_ready_date import (
|
|
34
|
+
estimate_ready_date, set_plan_from_estimate)
|
|
35
|
+
from backlogops.release_backlog_updates import (
|
|
36
|
+
ReleaseChange, ReleaseChanges, ReleaseDateChange, ReleaseDateChanges,
|
|
37
|
+
BacklogReleaseChange, ReleasesAndDateChanges, estimate_release_dates,
|
|
38
|
+
release_plan_on_estimate, adjust_release_content)
|
|
39
|
+
from backlogops.release_change_io import (
|
|
40
|
+
format_content_changes, format_date_changes, write_content_changes,
|
|
41
|
+
write_date_changes)
|
|
42
|
+
from backlogops.io_config import (
|
|
43
|
+
InputFormatConfig, OutputFormatConfig, resolve_input_config,
|
|
44
|
+
resolve_output_config, make_input_config, make_output_config)
|
|
45
|
+
from backlogops.backlog_releases_io import (
|
|
46
|
+
read_backlog_releases, write_backlog_releases)
|
|
47
|
+
from backlogops.table_rows import (
|
|
48
|
+
item_to_row, row_to_item, release_to_row, row_to_release)
|
|
49
|
+
from backlogops.format_rules import FormatRules
|
|
50
|
+
from backlogops.apply_format_rules import format_backlog, format_releases
|
|
51
|
+
from backlogops.move_keys_first import move_keys_first, get_keys_in_order
|
|
52
|
+
from backlogops.key_list_io import read_key_list, write_key_list
|
|
53
|
+
from backlogops.available_teams_wizard import (
|
|
54
|
+
available_teams_wizard, teams_config_wizard, YesNoUiBridge)
|
|
55
|
+
from backlogops.console_yes_no_bridge import ConsoleYesNoUiBridge
|
|
56
|
+
from backlogops.work_hours import (
|
|
57
|
+
WeekDay, ScheduleWorkHours, DEFAULT_WORK_WEEK, ExceptionWorkHours,
|
|
58
|
+
CompanyWorkHours)
|
|
59
|
+
from backlogops.date_ranges import check_date_range, check_no_overlap
|
|
60
|
+
from backlogops.no_text_io import NoTextIO
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
'Backlog', 'BacklogItem', 'Status', 'get_backlog', 'get_backlog_item',
|
|
64
|
+
'check_backlog_consistency', 'build_dependency_graph',
|
|
65
|
+
'item_dependency_edges', 'event_start', 'event_finish', 'find_cycle',
|
|
66
|
+
'Level', 'Levels', 'DEFAULT_LEVELS', 'check_levels_consistency',
|
|
67
|
+
'level_number_from_name', 'Person', 'FteException', 'Membership', 'Team',
|
|
68
|
+
'Release', 'Releases', 'get_release', 'get_releases', 'BacklogReleases',
|
|
69
|
+
'get_demo_backlog',
|
|
70
|
+
'AvailableTeams', 'AvailableTeamsConfig', 'read_available_teams',
|
|
71
|
+
'write_available_teams', 'get_available_teams', 'order_by_dependencies',
|
|
72
|
+
'DependencyMode', 'InputFormatConfig', 'OutputFormatConfig',
|
|
73
|
+
'resolve_input_config', 'resolve_output_config', 'make_input_config',
|
|
74
|
+
'make_output_config', 'read_backlog_releases', 'write_backlog_releases',
|
|
75
|
+
'item_to_row', 'row_to_item', 'release_to_row', 'row_to_release',
|
|
76
|
+
'FormatRules', 'format_backlog', 'format_releases',
|
|
77
|
+
'estimate_ready_date', 'set_plan_from_estimate',
|
|
78
|
+
'ReleaseChange', 'ReleaseChanges', 'ReleaseDateChange',
|
|
79
|
+
'ReleaseDateChanges', 'BacklogReleaseChange', 'ReleasesAndDateChanges',
|
|
80
|
+
'estimate_release_dates', 'release_plan_on_estimate',
|
|
81
|
+
'adjust_release_content', 'format_content_changes', 'format_date_changes',
|
|
82
|
+
'write_content_changes', 'write_date_changes',
|
|
83
|
+
'move_keys_first', 'get_keys_in_order', 'read_key_list', 'write_key_list',
|
|
84
|
+
'available_teams_wizard', 'teams_config_wizard', 'YesNoUiBridge',
|
|
85
|
+
'ConsoleYesNoUiBridge', 'WeekDay',
|
|
86
|
+
'ScheduleWorkHours', 'DEFAULT_WORK_WEEK', 'ExceptionWorkHours',
|
|
87
|
+
'CompanyWorkHours', 'check_date_range', 'check_no_overlap', 'NoTextIO']
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Apply format rules to backlog and release table data."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
from datetime import date
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from tableio import DictData, Fmt, ValueFmt
|
|
10
|
+
from backlogops.backlog import Backlog, BacklogItem
|
|
11
|
+
from backlogops.releases import Release, Releases
|
|
12
|
+
from backlogops.format_rules import FormatRules
|
|
13
|
+
from backlogops.table_rows import item_to_row, release_to_row
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _estimate_format(estimated: Optional[date], planned: Optional[date],
|
|
17
|
+
rules: FormatRules) -> Fmt:
|
|
18
|
+
"""Return the estimate-cell format from estimated versus planned date.
|
|
19
|
+
|
|
20
|
+
A missing estimated or planned date leaves the cell unformatted, as
|
|
21
|
+
there is then nothing to compare.
|
|
22
|
+
"""
|
|
23
|
+
if estimated is None or planned is None:
|
|
24
|
+
return Fmt()
|
|
25
|
+
if estimated > planned:
|
|
26
|
+
return rules.estimate_late
|
|
27
|
+
if estimated < planned:
|
|
28
|
+
return rules.estimate_early
|
|
29
|
+
return rules.estimate_eq_planned
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _item_cell_format(name: str, item: BacklogItem, estimate: Fmt,
|
|
33
|
+
rules: FormatRules) -> Fmt:
|
|
34
|
+
"""Return the format for one backlog cell named by its field."""
|
|
35
|
+
if name == 'status':
|
|
36
|
+
return rules.get_status_format(item.status)
|
|
37
|
+
if name == 'estimated_ready_date':
|
|
38
|
+
return estimate
|
|
39
|
+
return Fmt()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _format_item(item: BacklogItem, rules: FormatRules) -> dict[str, ValueFmt]:
|
|
43
|
+
"""Return one backlog item as a formatted row of cells."""
|
|
44
|
+
estimate = _estimate_format(item.estimated_ready_date,
|
|
45
|
+
item.planned_ready_date, rules)
|
|
46
|
+
return {name: ValueFmt(value=value,
|
|
47
|
+
fmt=_item_cell_format(name, item, estimate, rules))
|
|
48
|
+
for name, value in item_to_row(item).items()}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def format_backlog(backlog: Backlog,
|
|
52
|
+
format_rules: FormatRules) -> DictData[ValueFmt]:
|
|
53
|
+
"""Format the backlog according to the format rules.
|
|
54
|
+
|
|
55
|
+
Each backlog item becomes one row of formatted cells, keyed by the
|
|
56
|
+
internal field name. The status cell is formatted by its status, and
|
|
57
|
+
the estimated-ready-date cell by its relation to the planned-ready
|
|
58
|
+
date; all other cells are left unformatted.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
backlog: The backlog to format.
|
|
62
|
+
format_rules: The format rules to apply.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The formatted backlog rows, ready for TableIO.
|
|
66
|
+
"""
|
|
67
|
+
return [_format_item(item, format_rules) for item in backlog]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _format_release(release: Release,
|
|
71
|
+
rules: FormatRules) -> dict[str, ValueFmt]:
|
|
72
|
+
"""Return one release as a formatted row of cells."""
|
|
73
|
+
estimate = _estimate_format(release.estimated_date, release.planned_date,
|
|
74
|
+
rules)
|
|
75
|
+
return {name: ValueFmt(value=value,
|
|
76
|
+
fmt=estimate if name == 'estimated_date' else Fmt())
|
|
77
|
+
for name, value in release_to_row(release).items()}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def format_releases(releases: Releases,
|
|
81
|
+
format_rules: FormatRules) -> DictData[ValueFmt]:
|
|
82
|
+
"""Format the releases according to the format rules.
|
|
83
|
+
|
|
84
|
+
Each release becomes one row of formatted cells, keyed by the internal
|
|
85
|
+
field name. The estimated-date cell is formatted by its relation to the
|
|
86
|
+
planned date; the other cells are left unformatted.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
releases: The releases to format.
|
|
90
|
+
format_rules: The format rules to apply.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The formatted release rows, ready for TableIO.
|
|
94
|
+
"""
|
|
95
|
+
return [_format_release(release, format_rules) for release in releases]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Define the available workforce: persons and teams."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026, Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import date, timedelta
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
from backlogops.backlog_helpers import check_field_types, report_bad_value
|
|
12
|
+
from backlogops.backlog_helpers import report_unknown_reference
|
|
13
|
+
from backlogops.date_ranges import check_no_overlap
|
|
14
|
+
from backlogops.person import Person
|
|
15
|
+
from backlogops.team import Membership, Team
|
|
16
|
+
from backlogops.work_hours import CompanyWorkHours
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def membership_fte_on(membership: Membership, day: date) -> float:
|
|
20
|
+
"""Return the full-time equivalent of a membership on a given day.
|
|
21
|
+
|
|
22
|
+
Days outside the membership date range give 0.0. A day covered by an
|
|
23
|
+
fte_exception gives that exception's full-time equivalent. Otherwise
|
|
24
|
+
the membership's base full-time equivalent applies.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
membership: The membership to evaluate.
|
|
28
|
+
day: The day to evaluate the membership on.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The full-time equivalent the person gives to the team on the day.
|
|
32
|
+
"""
|
|
33
|
+
if membership.start_date is not None and day < membership.start_date:
|
|
34
|
+
return 0.0
|
|
35
|
+
if membership.end_date is not None and day > membership.end_date:
|
|
36
|
+
return 0.0
|
|
37
|
+
for exception in membership.fte_exceptions:
|
|
38
|
+
if exception.start_date <= day <= exception.end_date:
|
|
39
|
+
return exception.fte
|
|
40
|
+
return membership.fte
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def candidate_days(memberships: list[Membership]) -> set[date]:
|
|
44
|
+
"""Return the days where the summed full-time equivalent can change.
|
|
45
|
+
|
|
46
|
+
The summed full-time equivalent is constant between the start and end
|
|
47
|
+
boundaries of the memberships and their fte_exceptions, so checking
|
|
48
|
+
those boundary days is enough to find its maximum. When there are no
|
|
49
|
+
boundaries (all memberships are fully open) a single day is returned,
|
|
50
|
+
on which every membership contributes its base full-time equivalent.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
memberships: The memberships of one person across all teams.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The set of days on which to evaluate the summed full-time
|
|
57
|
+
equivalent.
|
|
58
|
+
"""
|
|
59
|
+
days: set[date] = set()
|
|
60
|
+
for membership in memberships:
|
|
61
|
+
if membership.start_date is not None:
|
|
62
|
+
days.add(membership.start_date)
|
|
63
|
+
if membership.end_date is not None:
|
|
64
|
+
days.add(membership.end_date + timedelta(days=1))
|
|
65
|
+
for exception in membership.fte_exceptions:
|
|
66
|
+
days.add(exception.start_date)
|
|
67
|
+
days.add(exception.end_date + timedelta(days=1))
|
|
68
|
+
return days or {date.min}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check_person_capacity(person_name: str, memberships: list[Membership],
|
|
72
|
+
stderr_file: TextIO = sys.stderr) -> None:
|
|
73
|
+
"""Check a person is not allocated more than full time on any day.
|
|
74
|
+
|
|
75
|
+
The summed full-time equivalent over all of the person's memberships
|
|
76
|
+
is evaluated on every boundary day and must not exceed 1.0.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
person_name: The name of the person, for error messages.
|
|
80
|
+
memberships: The memberships of the person across all teams.
|
|
81
|
+
stderr_file: The file to report errors to.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If the summed full-time equivalent exceeds 1.0 on any
|
|
85
|
+
day.
|
|
86
|
+
"""
|
|
87
|
+
for day in candidate_days(memberships):
|
|
88
|
+
total: float = sum((membership_fte_on(m, day) for m in memberships),
|
|
89
|
+
0.0)
|
|
90
|
+
if total > 1.0 + 1e-9:
|
|
91
|
+
report_bad_value('fte', total,
|
|
92
|
+
f'person {person_name!r} is allocated {total} '
|
|
93
|
+
f'FTE (more than 1.0) on {day.isoformat()}',
|
|
94
|
+
stderr_file, 'Person')
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class AvailableTeams:
|
|
99
|
+
"""Define the available workforce that can do work.
|
|
100
|
+
|
|
101
|
+
The persons registry holds every person once, keyed by the lower-case
|
|
102
|
+
person name, so that personal availability is entered in a single
|
|
103
|
+
place. Teams reference their members by person name into this
|
|
104
|
+
registry. This lets a person move between teams or split time across
|
|
105
|
+
teams without duplicating the person.
|
|
106
|
+
|
|
107
|
+
Fields:
|
|
108
|
+
persons: The registry of persons, keyed by lower-case person name.
|
|
109
|
+
teams: The list of teams that are available to do work.
|
|
110
|
+
company_work_hours: The company work hours that apply to everyone.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
persons: dict[str, Person]
|
|
114
|
+
teams: list[Team]
|
|
115
|
+
company_work_hours: CompanyWorkHours = field(
|
|
116
|
+
default_factory=CompanyWorkHours)
|
|
117
|
+
|
|
118
|
+
def _check_persons(self, stderr_file: TextIO) -> None:
|
|
119
|
+
"""Check each person's key, name and work hour exceptions."""
|
|
120
|
+
for key, person in self.persons.items():
|
|
121
|
+
check_field_types(person, stderr_file, 'Person')
|
|
122
|
+
if person.name == '':
|
|
123
|
+
report_bad_value('name', person.name, 'must not be empty',
|
|
124
|
+
stderr_file, 'Person')
|
|
125
|
+
if key != person.name.lower():
|
|
126
|
+
report_bad_value('persons', key,
|
|
127
|
+
f'key does not match person name '
|
|
128
|
+
f'{person.name!r}', stderr_file,
|
|
129
|
+
'Available teams')
|
|
130
|
+
for exception in person.exceptions:
|
|
131
|
+
exception.check_consistency(stderr_file)
|
|
132
|
+
check_no_overlap('exceptions',
|
|
133
|
+
[(e.start_date, e.end_date)
|
|
134
|
+
for e in person.exceptions], stderr_file,
|
|
135
|
+
'Person')
|
|
136
|
+
|
|
137
|
+
def _add_team_label(self, label: str, seen_labels: dict[str, str],
|
|
138
|
+
stderr_file: TextIO) -> None:
|
|
139
|
+
"""Add one team label and reject a case-insensitive duplicate."""
|
|
140
|
+
if label == '':
|
|
141
|
+
report_bad_value('aliases', label, 'must not be empty',
|
|
142
|
+
stderr_file, 'Team')
|
|
143
|
+
lowered = label.lower()
|
|
144
|
+
if lowered in seen_labels:
|
|
145
|
+
report_bad_value('name', label,
|
|
146
|
+
f'duplicates team label '
|
|
147
|
+
f'{seen_labels[lowered]!r} (case-insensitive)',
|
|
148
|
+
stderr_file, 'Team')
|
|
149
|
+
seen_labels[lowered] = label
|
|
150
|
+
|
|
151
|
+
def _check_teams(self, stderr_file: TextIO) -> None:
|
|
152
|
+
"""Check every team and that names and aliases are unique."""
|
|
153
|
+
seen_labels: dict[str, str] = {}
|
|
154
|
+
for team in self.teams:
|
|
155
|
+
team.check_consistency(stderr_file)
|
|
156
|
+
for label in [team.name, *team.aliases]:
|
|
157
|
+
self._add_team_label(label, seen_labels, stderr_file)
|
|
158
|
+
|
|
159
|
+
def _check_member_refs(self, stderr_file: TextIO) -> None:
|
|
160
|
+
"""Check each membership references a known person."""
|
|
161
|
+
for team in self.teams:
|
|
162
|
+
for member in team.members:
|
|
163
|
+
if member.person_name.lower() not in self.persons:
|
|
164
|
+
report_unknown_reference('person_name', team.name,
|
|
165
|
+
member.person_name, stderr_file,
|
|
166
|
+
'Team')
|
|
167
|
+
|
|
168
|
+
def _memberships_by_person(self) -> dict[str, list[Membership]]:
|
|
169
|
+
"""Group memberships across all teams by lower-case person name."""
|
|
170
|
+
result: dict[str, list[Membership]] = {}
|
|
171
|
+
for team in self.teams:
|
|
172
|
+
for member in team.members:
|
|
173
|
+
key = member.person_name.lower()
|
|
174
|
+
result.setdefault(key, []).append(member)
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
def _check_capacity(self, stderr_file: TextIO) -> None:
|
|
178
|
+
"""Check no person is allocated more than full time on any day."""
|
|
179
|
+
for person_name, memberships in self._memberships_by_person().items():
|
|
180
|
+
check_person_capacity(person_name, memberships, stderr_file)
|
|
181
|
+
|
|
182
|
+
def check_consistency(self, stderr_file: TextIO = sys.stderr) -> None:
|
|
183
|
+
"""Check the consistency of the available workforce.
|
|
184
|
+
|
|
185
|
+
Field types are verified, the company work hours and every person
|
|
186
|
+
and team are checked, team names and aliases are checked to be
|
|
187
|
+
unique case-insensitively across all teams, every membership is
|
|
188
|
+
checked to reference a known person, and no person is allocated
|
|
189
|
+
more than full time on any day.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
stderr_file: The file to report errors to.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
TypeError: If a field has the wrong type.
|
|
196
|
+
ValueError: If a field value violates a constraint, if team
|
|
197
|
+
labels are not unique, or if a person is over-allocated.
|
|
198
|
+
KeyError: If a membership references an unknown person.
|
|
199
|
+
"""
|
|
200
|
+
check_field_types(self, stderr_file, 'Available teams')
|
|
201
|
+
self.company_work_hours.check_consistency(stderr_file)
|
|
202
|
+
self._check_persons(stderr_file)
|
|
203
|
+
self._check_teams(stderr_file)
|
|
204
|
+
self._check_member_refs(stderr_file)
|
|
205
|
+
self._check_capacity(stderr_file)
|