Datenbank #1140

Updated by Alexander Blum about 1 year ago

# Background

- `Invoice.amount/shared_amount` are only helper attributes for testing the invoicing, should be replaced by
`UtilisationIndicators.invoice_amount` = `UtilizationIndicators.administration_fee` + `UtilizationIndicators.distribution_amount`
- relationship see also [database diagram](https://redmine.c3s.cc/projects/collecting_society/wiki/Databasemodels#Invoicing)
- use `erpserver> db-console` for a tryton console to test the functions; maybe write a script with some objects initialized and import it there

>>> Tariff = pool.get('tariff_system.tariff')
>>> tariff = Tariff(1)

# Procedure

1. Write a [wizard](https://github.com/C3S/collecting_society/blob/development/collecting_society.py#L829) to trigger the allocation process
- can be triggered by
- cron job
- manual [action](https://github.com/C3S/collecting_society/blob/development/collecting_society.xml#L800)
- in Declaration Menu (action in entry view or on [selected](https://github.com/C3S/collecting_society/blob/development/collecting_society.py#L3973) items in list view)
- in Utilization Menu (action in entry view or on [selected](https://github.com/C3S/collecting_society/blob/development/collecting_society.py#L3973) items in list view). Maybe we could skip that, but probable it's a valid usecase, like:
- if some utilization (tariff bound!) of a declaration should be payed for and some other has issues to be resolved first
- or a multi day festival event and the utilisations should be split.
- creates allocations with all corresponding utilisations connected, one allocation for each licensee
- loops over different sets of declarations depending on where/how the action was triggered (algorithm should be generic, see next point confirm screen how this is done; not sure, how to handle the same action with different selections though, all other things should be similiar to the existing example):
- declarations list with no selection: all suitable declarations with suitable utilisations (default for the cron job e.g. each day)
- declarations list with selection: all suitable declaration.utilisations for each selected suitable declaration
- declaration entry: all suitable declaration.utilisations for this declaration (if suitable)
- utilisations list with no selection: all suitable selected utilisations
- utilisations list with selection: all suitable utilisations in list
- utilisation entry: this utilisation (if suitable)
- confirm screen (like in [fingerprint_merge](https://github.com/C3S/collecting_society/blob/development/collecting_society.py#L4194), maybe a tree list with declarations -> utilisations)
- list of definitely selected utilisations/declarations
- info on deselection of non suitable utilisations/declarations
2. Write a dataset [triggering](https://github.com/C3S/collecting_society_docker/blob/development/volumes/shared/data/datasets
/device_message_fingerprint_merge.py#L29) the wizard for some Declarations
- ensure, that non complete utilisation datasets for manual tests are still available
3. Implement the Tariff and TariffSystem `formula`s from [database diagram](https://redmine.c3s.cc/projects/collecting_society/wiki/Databasemodels#Invoicing)
- fomulas should have the exact same plain value parameters as stated in the tariff system (check the latest version), so that parametrized calling is possible via `formula = getattr(tariff, 'method_name')`
-

they should **not** receive some tryton object, so they can be used (and tested) standalone
- each formula should be one **static** method method, so that parametrized calling is possible via `formula = getattr(tariff, 'method_name')`
- TariffSystem parameters: tariff system version (dot replaced with underscore); like

@staticmethod
def formula_<tariffsystemversion>(...):
[...]
return invoice_amount
- Tariff parameters: tariff code (includes version already, dot replaced with underscore); one for each tariff; like

@staticmethod
def formula_<tariffcode>(...):
[...]
return base, relevance, adjustments
- maybe better change the tariff_system.version.code, that it is save to use as function names in general and omit the replacements here
- add an **instance** method, to be able to easily fetch method for fetching the corresponding formula from an instance
- TariffSystem

def get_formula(self):
return getattr(self, f"formula_{self.version.replace(',', '_')}")
- Tariff

def get_formula(self):
return getattr(self, f"formula_{self.code.replace(',', '_')}")
4. Implement the **instance** methods
- params: sample = indicator set, maybe also 'all' to calculate all indicator sets, but then the returned tuples have to be e.g. a dict with sample name as key and the tuple as value update = write or just calculate)
- maybe rename `calculate_invoice_amount`, as all values are returned, or maybe better split invoice amount calculation (only invoice_amount returned) from calculating the distribution_amount and administration_fee (but not sure if this is needed by some other process)
- `Allocation.calculate_invoice_amount(self, sample, update=False)`

invoice_amount_sum = 0
distribution_amount_sum = 0
administration_fee_sum = 0
for utilisation self.utilisations:
invoice_amount, distribution_amount, administration_fee = \
utilisation.calculate_invoice_amount(sample, update)
invoice_amount_sum += invoice_amount
distribution_amount_sum += distribution_amount
administration_fee_sum += administration_fee
return invoice_amount, distribution_amount, administration_fee
- `Utilisation.calculate_invoice_amount(self, sample, update=False)`

utilisation = self
invoice_amount = self.tariff.calculate_invoice_amount(utilisation, sample, update)
return invoice_amount, distribution_amount, admnistration_fee
- `Tariff.calculate_invoice_amount(self, utilisation, sample, update=False)`

formula = self.get_formula()

utilisation_indicators = getattr(utilisation, f'{sample}_indicators')
context_indicators = getattr(utilisation.context, f'{sample}_indicators')

# generic way to get all and only indicators needed for all different formulas
import inspect # place import on top of file
formula_indicators = {
field: getattr(context_indicators, field)
for field in inspect.signature(formula).parameters
}

base, relevance, adjustments = formula(**formula_indicators):
invoice_amount, distribution_amount, administration_fee = \
tariff_system.calculate_invoice_amount(base, relevance, adjustments)

if update:
utilisation_indicators.base = base
utilisation_indicators.relevance = relevance
utilisation_indicators.adjustments = adjustments
utilisation_indicators.invoice_amount = invoice_amount
utilisation_indicators.distribution_amount = distribution_amount
utilisation_indicators.administration_fee = administration_fee
utilisation_indicators.save()

return invoice_amount, distribution_amount, administration_fee
- `TariffSystem.calculate_invoice_amount(self, base, relevance, adjustments)`

formula = self.get_formula()

invoice_amount = formula(base, relevance, adjustments)
distribution_amount = invoice_amount * self.administration_share
administration_fee = invoice_amount - distribution_amount
# check if results add up

return invoice_amount, distribution_amount, administration_fee

5. Implement the same for `calculate_utilisation_indicators()`, maybe reuse in `calculate_invoice_amount()`
- `Utilisation.calculate_utilisation_indicators(self, sample, update)`
- `Tariff.calculate_utilisation_indicators(self, utilisation, sample, update)`
6. Implement a wizard to recalculate invoice amounts / utilsation indicators (prevent, if invoice is already issued)
7. Check the wizard to write invoices ([exists](https://github.com/C3S/collecting_society/blob/development/collecting_society.py#L829) already)
8. Write datasets to trigger the invoice action for the Allocations
- ensure, that some allocations are still left to invoice for manual tests

Back