diff --git a/superx_budget/__init__.py b/superx_budget/__init__.py index c92f0d0..2cfdb3d 100644 --- a/superx_budget/__init__.py +++ b/superx_budget/__init__.py @@ -5,6 +5,7 @@ Creating a budget overview from a SuperX export __version__ = "0.0.1" - -from .superx import parse_export_data # noqa: F401 +from .budget import parse_budget_file # noqa: F401 +from .superx import parse_exported_file # noqa: F401 +from .overview import create_overview # noqa: F401 from .exceptions import BudgetParserError, SuperXParserError # noqa: F401 diff --git a/superx_budget/budget.py b/superx_budget/budget.py index ac548e8..79442a1 100644 --- a/superx_budget/budget.py +++ b/superx_budget/budget.py @@ -35,8 +35,6 @@ BudgetData = namedtuple( def _check_table_header(xl_row): fields = [c.strip() for c in xl_row.data[:7]] - print(fields) - print(EXPECTED_TABLE_HEADERS) if fields != EXPECTED_TABLE_HEADERS: raise BudgetParserError(f"unexpected headers: '{xl_row.data}'") diff --git a/superx_budget/helpers.py b/superx_budget/helpers.py index 51ca576..93b83c1 100644 --- a/superx_budget/helpers.py +++ b/superx_budget/helpers.py @@ -3,6 +3,17 @@ import openpyxl +def excel_value_as_number(value): + if value is None: + return 0 + if isinstance(value, str): + try: + return float(value) + except ValueError: + return 0 + return value + + def get_sheet_of_file(excel_file, sheet=None): """ returns a sheet from an excel FileCache diff --git a/superx_budget/overview.py b/superx_budget/overview.py new file mode 100644 index 0000000..5f6694b --- /dev/null +++ b/superx_budget/overview.py @@ -0,0 +1,77 @@ +from collections import namedtuple + +from .helpers import excel_value_as_number + +KIND_OBLIGO = "obligo" +KIND_EXPENSES = "expenses" + +VALID_MATERIAL_IDS = { + "0216 - Sachmittel", + "0227 - Verbrauchsmaterial", + "0843 - Verbrauch/Stoffe", + "2 - Sachmittel", +} + +OverviewBudgetEntry = namedtuple( + "OverviewBudgetEntry", ["description", "kind", "amount"] +) + + +class ProjectOverview: + def __init__(self, budget_data): + """ initializes the class """ + self.budget_data = budget_data + self.entries = [] + + @property + def project(self): + """ returns the project number """ + return self.budget_data.project + + @property + def expenses(self): + """ returns the accumulated expenses """ + numbers = (excel_value_as_number(e.amount) for e in self.entries) + values = (abs(entry) for entry in numbers) + return sum(values) + + @property + def available(self): + """ returns the still available budget """ + return self.budget_data.budget - self.expenses + + def add(self, description, kind, amount): + """ adds an entry that modifies the available budget """ + entry = OverviewBudgetEntry(description, kind, amount) + self.entries.append(entry) + + +def _create_overview_map(budget_list): + """ returns a dictonary with project as key and overview as value """ + map = {} + for budget_data in budget_list: + overview = ProjectOverview(budget_data) + map[str(overview.project)] = overview + return map + + +def _filter_superx_material_expenses(superx_export): + """ filters superx data to only contain material entries """ + return (i for i in superx_export.data if i.kind in VALID_MATERIAL_IDS) + + +def _create_entries_from_superx(overview_map, superx_export_data): + """ adds overview entries from superx data """ + for line in superx_export_data: + if line.project in overview_map: + overview = overview_map[line.project] + overview.add(line.kind, KIND_OBLIGO, line.obligo) + overview.add(line.kind, KIND_EXPENSES, line.expenses) + return overview_map + + +def create_overview(budget_list, superx_export): + """ create a overview map with budget entries from the parsed raw data """ + overview_map = _create_overview_map(budget_list) + material_expenses = _filter_superx_material_expenses(superx_export) + return _create_entries_from_superx(overview_map, material_expenses) diff --git a/tests/conftest.py b/tests/conftest.py index 0ebb54b..d6b32fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,3 +18,34 @@ class MockWorkbookSheet: def example_root(request): root_dir = Path(request.config.rootdir) yield root_dir / "test data" + + +@pytest.fixture +def budget_example_file(example_root): + return example_root / "Verbrauchsmittel-Toto-2020.xlsx" + + +@pytest.fixture +def budget_example_workbook(budget_example_file): + import openpyxl + + yield openpyxl.open(budget_example_file) + + +@pytest.fixture +def budget_example_sheet(budget_example_workbook): + sheets = budget_example_workbook.sheetnames + first = sheets[0] + yield budget_example_workbook[first] + + +@pytest.fixture +def superx_example_file(example_root): + return example_root / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx" + + +@pytest.fixture +def superx_example_workbook(superx_example_file): + import openpyxl + + yield openpyxl.open(superx_example_file) diff --git a/tests/test_budget_parser.py b/tests/test_budget_parser.py index f08b28a..bd80183 100644 --- a/tests/test_budget_parser.py +++ b/tests/test_budget_parser.py @@ -1,25 +1,6 @@ import pytest -@pytest.fixture -def example_file(example_root): - return example_root / "Verbrauchsmittel-Toto-2020.xlsx" - - -@pytest.fixture -def example_workbook(example_file): - import openpyxl - - yield openpyxl.open(example_file) - - -@pytest.fixture -def example_sheet(example_workbook): - sheets = example_workbook.sheetnames - first = sheets[0] - yield example_workbook[first] - - def test_check_table_header_raises_error(): from superx_budget.budget import _check_table_header, ExcelRow from superx_budget.exceptions import BudgetParserError @@ -67,10 +48,10 @@ def test_parse_data_table(): assert last.rest == "z" -def test_parse_budget_data(example_sheet): +def test_parse_budget_data(budget_example_sheet): from superx_budget.budget import parse_budget_data - result = parse_budget_data(example_sheet) + result = parse_budget_data(budget_example_sheet) first, last = result[0], result[-1] assert len(result) == 18 @@ -84,10 +65,10 @@ def test_parse_budget_data(example_sheet): assert last.rest == "=E54-F54" -def test_parse_budget_file(example_file): +def test_parse_budget_file(budget_example_file): from superx_budget.budget import parse_budget_file - result = parse_budget_file(example_file) + result = parse_budget_file(budget_example_file) first = result[0] assert len(result) == 18 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8c36639..0563bad 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -71,3 +71,23 @@ def test_get_sheet_of_file_named(example_file): first_cell = first_row[0] assert first_cell == 1 + + +@pytest.mark.parametrize( + "input,expected", + [ + (None, 0), + ("", 0), + ("a", 0), + ("1", 1.0), + ("2.4", 2.4), + (1, 1), + (2.4, 2.4), + ], +) +def test_excel_value_as_number(input, expected): + from superx_budget.helpers import excel_value_as_number + + result = excel_value_as_number(input) + + assert result == expected diff --git a/tests/test_overview.py b/tests/test_overview.py new file mode 100644 index 0000000..2af3358 --- /dev/null +++ b/tests/test_overview.py @@ -0,0 +1,127 @@ +import pytest + + +@pytest.fixture +def example_budget_data(): + from superx_budget.budget import BudgetData + + example = BudgetData( + 3, + "1.", + "Safegurard I", + "01.08.2019-31.07.2020", + "1100000102", + 5000, + 0, + 0, + ) + + yield example + + +def test_project_overview_init(example_budget_data): + from superx_budget.overview import ProjectOverview + + over = ProjectOverview(example_budget_data) + + assert over.budget_data == example_budget_data + assert over.entries == [] + + +def test_project_overview_project_property(example_budget_data): + from superx_budget.overview import ProjectOverview + + over = ProjectOverview(example_budget_data) + + assert over.project == example_budget_data.project + + +def test_project_overview_add(example_budget_data): + from superx_budget.overview import ProjectOverview, OverviewBudgetEntry + + over = ProjectOverview(example_budget_data) + over.add("Sachmittel", "Obligo", -100) + over.add("Kleinzeugs", "Ausgaben", 200) + + assert len(over.entries) == 2 + assert isinstance(over.entries[0], OverviewBudgetEntry) + assert over.entries[0].description == "Sachmittel" + assert over.entries[0].kind == "Obligo" + assert over.entries[0].amount == -100 + + +def test_project_overview_expenses_property(example_budget_data): + from superx_budget.overview import ProjectOverview + + over = ProjectOverview(example_budget_data) + over.add("Sachmittel", "Obligo", -100) + over.add("Kleinzeugs", "Ausgaben", 200) + + assert over.expenses == 300 + + +def test_project_overview_available_property(example_budget_data): + from superx_budget.overview import ProjectOverview + + over = ProjectOverview(example_budget_data) + over.add("Sachmittel", "Obligo", -100) + over.add("Kleinzeugs", "Ausgaben", 200) + + assert over.available == 5000 - 300 + + +def test_create_overview_map(budget_example_file): + from superx_budget.overview import _create_overview_map + from superx_budget.budget import parse_budget_file + + budget_data = parse_budget_file(budget_example_file) + result = _create_overview_map(budget_data) + + assert len(result) == 18 + assert "2100276501" in result + for key, value in result.items(): + assert key == str(value.project) + + +def test_create_entries_from_export(budget_example_file, superx_example_file): + from superx_budget.overview import ( + _create_entries_from_superx, + _create_overview_map, + ) + from superx_budget.budget import parse_budget_file + from superx_budget.superx import parse_exported_file + + superx_data = parse_exported_file(superx_example_file) + budget_data = parse_budget_file(budget_example_file) + budget_map = _create_overview_map(budget_data) + + _create_entries_from_superx(budget_map, superx_data.data) + + assert budget_map["1100000102"].available == 5000 - 0.01 - 1000 - 1 - 1001 + + +def test_filter_superx_material_expenses(superx_example_file): + from superx_budget.overview import ( + _filter_superx_material_expenses, + VALID_MATERIAL_IDS, + ) + from superx_budget.superx import parse_exported_file + + superx_data = parse_exported_file(superx_example_file) + result = list(_filter_superx_material_expenses(superx_data)) + + assert {item.kind for item in result} == VALID_MATERIAL_IDS + assert result[0].obligo == 1 + assert result[0].expenses == 1001 + + +def test_create_overview(budget_example_file, superx_example_file): + from superx_budget.overview import create_overview + from superx_budget.budget import parse_budget_file + from superx_budget.superx import parse_exported_file + + superx_data = parse_exported_file(superx_example_file) + budget_data = parse_budget_file(budget_example_file) + budget_map = create_overview(budget_data, superx_data) + + assert budget_map["1100000102"].available == 5000 - 1 - 1001 diff --git a/tests/test_superx_parser.py b/tests/test_superx_parser.py index c0b097e..3561a02 100644 --- a/tests/test_superx_parser.py +++ b/tests/test_superx_parser.py @@ -4,18 +4,6 @@ import pytest -@pytest.fixture -def example_file(example_root): - return example_root / "Verwendungsnachweis_und_Kassenstand_SAP_Zahlen.xlsx" - - -@pytest.fixture -def example_workbook(example_file): - import openpyxl - - yield openpyxl.open(example_file) - - def test_check_export_headline(): from superx_budget.superx import _check_export_headline from superx_budget.exceptions import SuperXParserError @@ -109,11 +97,11 @@ def test_parse_data_table(): assert second_value.acutal_value == "z" -def test_parse_export_data(example_workbook): +def test_parse_export_data(superx_example_workbook): from superx_budget.superx import parse_export_data from datetime import datetime - result = parse_export_data(example_workbook.active) + result = parse_export_data(superx_example_workbook.active) assert result.account_year == "2020" assert result.export_date == datetime(2020, 3, 18) @@ -142,10 +130,10 @@ def test_parse_export_data(example_workbook): assert last.acutal_value == 4236 -def test_parse_exported_file(example_file): +def test_parse_exported_file(superx_example_file): from superx_budget.superx import parse_exported_file - result = parse_exported_file(example_file) + result = parse_exported_file(superx_example_file) first = result.data[0] assert len(result.data) == 212