gazpar2haws 0.2.0b1__py3-none-any.whl → 0.3.0__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.
gazpar2haws/pricer.py ADDED
@@ -0,0 +1,579 @@
1
+ import calendar
2
+ from datetime import date, timedelta
3
+ from typing import Optional, Tuple, overload
4
+
5
+ from gazpar2haws.model import (
6
+ BaseUnit,
7
+ ConsumptionPriceArray,
8
+ ConsumptionQuantityArray,
9
+ CostArray,
10
+ EnergyTaxesPriceArray,
11
+ PriceUnit,
12
+ PriceValue,
13
+ Pricing,
14
+ QuantityUnit,
15
+ SubscriptionPriceArray,
16
+ TimeUnit,
17
+ TransportPriceArray,
18
+ Value,
19
+ ValueArray,
20
+ ValueUnit,
21
+ VatRate,
22
+ VatRateArray,
23
+ )
24
+
25
+
26
+ class Pricer:
27
+
28
+ # ----------------------------------
29
+ def __init__(self, pricing: Pricing):
30
+ self._pricing = pricing
31
+
32
+ # ----------------------------------
33
+ def pricing_data(self) -> Pricing:
34
+ return self._pricing
35
+
36
+ # ----------------------------------
37
+ def compute( # pylint: disable=too-many-branches
38
+ self, quantities: ConsumptionQuantityArray, price_unit: PriceUnit
39
+ ) -> CostArray:
40
+
41
+ if quantities is None:
42
+ raise ValueError("quantities is None")
43
+
44
+ if quantities.start_date is None:
45
+ raise ValueError("quantities.start_date is None")
46
+
47
+ start_date = quantities.start_date
48
+
49
+ if quantities.end_date is None:
50
+ raise ValueError("quantities.end_date is None")
51
+
52
+ end_date = quantities.end_date
53
+
54
+ if quantities.value_array is None:
55
+ raise ValueError("quantities.value_array is None")
56
+
57
+ if quantities.value_unit is None:
58
+ raise ValueError("quantities.value_unit is None")
59
+
60
+ if quantities.base_unit is None:
61
+ raise ValueError("quantities.base_unit is None")
62
+
63
+ quantity_array = quantities.value_array
64
+
65
+ # Convert all pricing data to the same unit as the quantities.
66
+ consumption_prices = Pricer.convert(self._pricing.consumption_prices, (price_unit, quantities.value_unit))
67
+
68
+ if self._pricing.subscription_prices is not None and len(self._pricing.subscription_prices) > 0:
69
+ subscription_prices = Pricer.convert(self._pricing.subscription_prices, (price_unit, quantities.base_unit))
70
+ else:
71
+ subscription_prices = None
72
+
73
+ if self._pricing.transport_prices is not None and len(self._pricing.transport_prices) > 0:
74
+ transport_prices = Pricer.convert(self._pricing.transport_prices, (price_unit, quantities.base_unit))
75
+ else:
76
+ transport_prices = None
77
+
78
+ if self._pricing.energy_taxes is not None and len(self._pricing.energy_taxes) > 0:
79
+ energy_taxes = Pricer.convert(self._pricing.energy_taxes, (price_unit, quantities.value_unit))
80
+ else:
81
+ energy_taxes = None
82
+
83
+ # Transform to the vectorized form.
84
+ if self._pricing.vat is not None and len(self._pricing.vat) > 0:
85
+ vat_rate_array_by_id = self.get_vat_rate_array_by_id(
86
+ start_date=start_date, end_date=end_date, vat_rates=self._pricing.vat
87
+ )
88
+ else:
89
+ vat_rate_array_by_id = dict[str, VatRateArray]()
90
+
91
+ consumption_price_array = self.get_consumption_price_array(
92
+ start_date=start_date,
93
+ end_date=end_date,
94
+ consumption_prices=consumption_prices,
95
+ vat_rate_array_by_id=vat_rate_array_by_id,
96
+ )
97
+
98
+ # Subscription price is optional.
99
+ if subscription_prices is not None and len(subscription_prices) > 0:
100
+ subscription_price_array = self.get_subscription_price_array(
101
+ start_date=start_date,
102
+ end_date=end_date,
103
+ subscription_prices=subscription_prices,
104
+ vat_rate_array_by_id=vat_rate_array_by_id,
105
+ )
106
+ else:
107
+ subscription_price_array = SubscriptionPriceArray(
108
+ name="subscription_prices",
109
+ start_date=start_date,
110
+ end_date=end_date,
111
+ value_unit=price_unit,
112
+ base_unit=quantities.base_unit,
113
+ )
114
+
115
+ # Transport price is optional.
116
+ if transport_prices is not None and len(transport_prices) > 0:
117
+ transport_price_array = self.get_transport_price_array(
118
+ start_date=start_date,
119
+ end_date=end_date,
120
+ transport_prices=transport_prices,
121
+ vat_rate_array_by_id=vat_rate_array_by_id,
122
+ )
123
+ else:
124
+ transport_price_array = TransportPriceArray(
125
+ name="transport_prices",
126
+ start_date=start_date,
127
+ end_date=end_date,
128
+ value_unit=price_unit,
129
+ base_unit=quantities.base_unit,
130
+ )
131
+
132
+ # Energy taxes are optional.
133
+ if energy_taxes is not None and len(energy_taxes) > 0:
134
+ energy_taxes_price_array = self.get_energy_taxes_price_array(
135
+ start_date=start_date,
136
+ end_date=end_date,
137
+ energy_taxes_prices=energy_taxes,
138
+ vat_rate_array_by_id=vat_rate_array_by_id,
139
+ )
140
+ else:
141
+ energy_taxes_price_array = EnergyTaxesPriceArray(
142
+ name="energy_taxes",
143
+ start_date=start_date,
144
+ end_date=end_date,
145
+ value_unit=price_unit,
146
+ base_unit=quantities.value_unit,
147
+ )
148
+
149
+ res = CostArray(
150
+ name="costs",
151
+ start_date=start_date,
152
+ end_date=end_date,
153
+ value_unit=price_unit,
154
+ base_unit=quantities.base_unit,
155
+ )
156
+
157
+ # Compute pricing formula
158
+ res.value_array = quantity_array * (consumption_price_array.value_array + energy_taxes_price_array.value_array) + subscription_price_array.value_array + transport_price_array.value_array # type: ignore
159
+
160
+ return res
161
+
162
+ # ----------------------------------
163
+ @classmethod
164
+ def get_vat_rate_array_by_id(
165
+ cls, start_date: date, end_date: date, vat_rates: list[VatRate]
166
+ ) -> dict[str, VatRateArray]:
167
+
168
+ if vat_rates is None or len(vat_rates) == 0:
169
+ raise ValueError("vat_rates is None or empty")
170
+
171
+ res = dict[str, VatRateArray]()
172
+ vat_rate_by_id = dict[str, list[VatRate]]()
173
+ for vat_rate in vat_rates:
174
+ res[vat_rate.id] = VatRateArray(name="vats", id=vat_rate.id, start_date=start_date, end_date=end_date)
175
+ if vat_rate.id not in vat_rate_by_id:
176
+ vat_rate_by_id[vat_rate.id] = list[VatRate]()
177
+ vat_rate_by_id[vat_rate.id].append(vat_rate)
178
+
179
+ for vat_id, vat_rate_list in vat_rate_by_id.items():
180
+ cls._fill_value_array(res[vat_id], vat_rate_list) # type: ignore
181
+
182
+ return res
183
+
184
+ # ----------------------------------
185
+ @classmethod
186
+ def get_consumption_price_array(
187
+ cls,
188
+ start_date: date,
189
+ end_date: date,
190
+ consumption_prices: list[PriceValue[PriceUnit, QuantityUnit]],
191
+ vat_rate_array_by_id: dict[str, VatRateArray],
192
+ ) -> ConsumptionPriceArray:
193
+
194
+ if consumption_prices is None or len(consumption_prices) == 0:
195
+ raise ValueError("consumption_prices is None or empty")
196
+
197
+ first_consumption_price = consumption_prices[0]
198
+
199
+ res = ConsumptionPriceArray(
200
+ name="consumption_prices",
201
+ start_date=start_date,
202
+ end_date=end_date,
203
+ value_unit=first_consumption_price.value_unit,
204
+ base_unit=first_consumption_price.base_unit,
205
+ vat_id=first_consumption_price.vat_id,
206
+ )
207
+
208
+ cls._fill_price_array(res, consumption_prices, vat_rate_array_by_id) # type: ignore
209
+
210
+ return res
211
+
212
+ # ----------------------------------
213
+ @classmethod
214
+ def get_subscription_price_array(
215
+ cls,
216
+ start_date: date,
217
+ end_date: date,
218
+ subscription_prices: list[PriceValue[PriceUnit, TimeUnit]],
219
+ vat_rate_array_by_id: dict[str, VatRateArray],
220
+ ) -> SubscriptionPriceArray:
221
+
222
+ if subscription_prices is None or len(subscription_prices) == 0:
223
+ raise ValueError("subscription_prices is None or empty")
224
+
225
+ first_subscription_price = subscription_prices[0]
226
+
227
+ res = SubscriptionPriceArray(
228
+ name="subscription_prices",
229
+ start_date=start_date,
230
+ end_date=end_date,
231
+ value_unit=first_subscription_price.value_unit,
232
+ base_unit=first_subscription_price.base_unit,
233
+ vat_id=first_subscription_price.vat_id,
234
+ )
235
+
236
+ cls._fill_price_array(res, subscription_prices, vat_rate_array_by_id) # type: ignore
237
+
238
+ return res
239
+
240
+ # ----------------------------------
241
+ @classmethod
242
+ def get_transport_price_array(
243
+ cls,
244
+ start_date: date,
245
+ end_date: date,
246
+ transport_prices: list[PriceValue[PriceUnit, TimeUnit]],
247
+ vat_rate_array_by_id: dict[str, VatRateArray],
248
+ ) -> TransportPriceArray:
249
+
250
+ if transport_prices is None or len(transport_prices) == 0:
251
+ raise ValueError("transport_prices is None or empty")
252
+
253
+ first_transport_price = transport_prices[0]
254
+
255
+ res = TransportPriceArray(
256
+ name="transport_prices",
257
+ start_date=start_date,
258
+ end_date=end_date,
259
+ value_unit=first_transport_price.value_unit,
260
+ base_unit=first_transport_price.base_unit,
261
+ vat_id=first_transport_price.vat_id,
262
+ )
263
+
264
+ cls._fill_price_array(res, transport_prices, vat_rate_array_by_id) # type: ignore
265
+
266
+ return res
267
+
268
+ # ----------------------------------
269
+ @classmethod
270
+ def get_energy_taxes_price_array(
271
+ cls,
272
+ start_date: date,
273
+ end_date: date,
274
+ energy_taxes_prices: list[PriceValue[PriceUnit, QuantityUnit]],
275
+ vat_rate_array_by_id: dict[str, VatRateArray],
276
+ ) -> EnergyTaxesPriceArray:
277
+
278
+ if energy_taxes_prices is None or len(energy_taxes_prices) == 0:
279
+ raise ValueError("energy_taxes_prices is None or empty")
280
+
281
+ first_energy_taxes_price = energy_taxes_prices[0]
282
+
283
+ res = EnergyTaxesPriceArray(
284
+ name="energy_taxes",
285
+ start_date=start_date,
286
+ end_date=end_date,
287
+ value_unit=first_energy_taxes_price.value_unit,
288
+ base_unit=first_energy_taxes_price.base_unit,
289
+ vat_id=first_energy_taxes_price.vat_id,
290
+ )
291
+
292
+ cls._fill_price_array(res, energy_taxes_prices, vat_rate_array_by_id) # type: ignore
293
+
294
+ return res
295
+
296
+ # ----------------------------------
297
+ @classmethod
298
+ def _fill_value_array(cls, out_value_array: ValueArray, in_values: list[Value]) -> None:
299
+
300
+ if out_value_array is None:
301
+ raise ValueError("out_value_array is None")
302
+
303
+ if out_value_array.start_date is None:
304
+ raise ValueError("out_value_array.start_date is None")
305
+
306
+ start_date = out_value_array.start_date
307
+
308
+ if out_value_array.end_date is None:
309
+ raise ValueError("out_value_array.end_date is None")
310
+
311
+ end_date = out_value_array.end_date
312
+
313
+ if out_value_array.value_array is None:
314
+ raise ValueError("out_value_array.value_array is None")
315
+
316
+ value_array = out_value_array.value_array
317
+
318
+ if in_values is None or len(in_values) == 0:
319
+ raise ValueError("in_values is None or empty")
320
+
321
+ first_value = in_values[0]
322
+ last_value = in_values[-1]
323
+
324
+ if first_value.start_date > end_date:
325
+ # Fully before first value period.
326
+ value_array[start_date : end_date + timedelta(1)] = first_value.value # type: ignore
327
+ elif last_value.end_date is not None and last_value.end_date < start_date:
328
+ # Fully after last value period.
329
+ value_array[start_date : end_date + timedelta(1)] = last_value.value # type: ignore
330
+ else:
331
+ if start_date < first_value.start_date:
332
+ # Partially before first value period.
333
+ value_array[start_date : first_value.start_date + timedelta(1)] = first_value.value # type: ignore
334
+ if last_value.end_date is not None and end_date > last_value.end_date:
335
+ # Partially after last value period.
336
+ value_array[last_value.end_date : end_date + timedelta(1)] = last_value.value # type: ignore
337
+ # Inside value periods.
338
+ for value in in_values:
339
+ latest_start = max(value.start_date, start_date)
340
+ earliest_end = min(value.end_date if value.end_date is not None else end_date, end_date)
341
+ current_date = latest_start
342
+ while current_date <= earliest_end:
343
+ value_array[current_date] = value.value
344
+ current_date += timedelta(days=1)
345
+
346
+ # ----------------------------------
347
+ @classmethod
348
+ def _fill_price_array( # pylint: disable=too-many-branches
349
+ cls,
350
+ out_value_array: ValueArray,
351
+ in_values: list[PriceValue],
352
+ vat_rate_array_by_id: dict[str, VatRateArray],
353
+ ) -> None:
354
+
355
+ if out_value_array is None:
356
+ raise ValueError("out_value_array is None")
357
+
358
+ if out_value_array.start_date is None:
359
+ raise ValueError("out_value_array.start_date is None")
360
+
361
+ start_date = out_value_array.start_date
362
+
363
+ if out_value_array.end_date is None:
364
+ raise ValueError("out_value_array.end_date is None")
365
+
366
+ end_date = out_value_array.end_date
367
+
368
+ if out_value_array.value_array is None:
369
+ raise ValueError("out_value_array.value_array is None")
370
+
371
+ value_array = out_value_array.value_array
372
+
373
+ if in_values is None or len(in_values) == 0:
374
+ raise ValueError("in_values is None or empty")
375
+
376
+ first_value = in_values[0]
377
+ last_value = in_values[-1]
378
+
379
+ if first_value.start_date > end_date:
380
+ # Fully before first value period.
381
+ if vat_rate_array_by_id is not None and first_value.vat_id in vat_rate_array_by_id:
382
+ vat_value = vat_rate_array_by_id[first_value.vat_id].value_array[start_date : end_date + timedelta(1)] # type: ignore
383
+ else:
384
+ vat_value = 0.0
385
+ value_array[start_date : end_date + timedelta(1)] = (vat_value + 1) * first_value.value # type: ignore
386
+ elif last_value.end_date is not None and last_value.end_date < start_date:
387
+ # Fully after last value period.
388
+ if vat_rate_array_by_id is not None and last_value.vat_id in vat_rate_array_by_id:
389
+ vat_value = vat_rate_array_by_id[last_value.vat_id].value_array[start_date : end_date + timedelta(1)] # type: ignore
390
+ else:
391
+ vat_value = 0.0
392
+ value_array[start_date : end_date + timedelta(1)] = (vat_value + 1) * last_value.value # type: ignore
393
+ else:
394
+ if start_date < first_value.start_date:
395
+ # Partially before first value period.
396
+ if vat_rate_array_by_id is not None and first_value.vat_id in vat_rate_array_by_id:
397
+ vat_value = vat_rate_array_by_id[first_value.vat_id].value_array[start_date : first_value.start_date + timedelta(1)] # type: ignore
398
+ else:
399
+ vat_value = 0.0
400
+ value_array[start_date : first_value.start_date + timedelta(1)] = (vat_value + 1) * first_value.value # type: ignore
401
+ if last_value.end_date is not None and end_date > last_value.end_date:
402
+ # Partially after last value period.
403
+ if vat_rate_array_by_id is not None and last_value.vat_id in vat_rate_array_by_id:
404
+ vat_value = vat_rate_array_by_id[last_value.vat_id].value_array[last_value.end_date : end_date + timedelta(1)] # type: ignore
405
+ else:
406
+ vat_value = 0.0
407
+ value_array[last_value.end_date : end_date + timedelta(1)] = (vat_value + 1) * last_value.value # type: ignore
408
+ # Inside value periods.
409
+ for value in in_values:
410
+ latest_start = max(value.start_date, start_date)
411
+ earliest_end = min(value.end_date if value.end_date is not None else end_date, end_date)
412
+ current_date = latest_start
413
+ while current_date <= earliest_end:
414
+ if vat_rate_array_by_id is not None and value.vat_id in vat_rate_array_by_id:
415
+ vat_value = vat_rate_array_by_id[value.vat_id].value_array[current_date] # type: ignore
416
+ else:
417
+ vat_value = 0.0
418
+ value_array[current_date] = (vat_value + 1) * value.value # type: ignore
419
+ current_date += timedelta(days=1)
420
+
421
+ # ----------------------------------
422
+ @classmethod
423
+ def get_time_unit_convertion_factor(cls, from_time_unit: TimeUnit, to_time_unit: TimeUnit, dt: date) -> float:
424
+
425
+ if from_time_unit == to_time_unit:
426
+ return 1.0
427
+
428
+ def days_in_month(year: int, month: int) -> int:
429
+ return calendar.monthrange(year, month)[1]
430
+
431
+ def days_in_year(year: int) -> int:
432
+ return 366 if calendar.isleap(year) else 365
433
+
434
+ if TimeUnit.MONTH in (from_time_unit, to_time_unit):
435
+ switcher = {
436
+ TimeUnit.DAY: days_in_month(dt.year, dt.month),
437
+ TimeUnit.WEEK: days_in_month(dt.year, dt.month) / 7.0,
438
+ TimeUnit.MONTH: 1.0,
439
+ TimeUnit.YEAR: 1.0 / 12.0,
440
+ }
441
+ else:
442
+ switcher = {
443
+ TimeUnit.DAY: 1.0,
444
+ TimeUnit.WEEK: 1 / 7.0,
445
+ TimeUnit.MONTH: 1 / days_in_month(dt.year, dt.month),
446
+ TimeUnit.YEAR: 1 / days_in_year(dt.year),
447
+ }
448
+
449
+ if from_time_unit not in switcher:
450
+ raise ValueError(f"Invalid 'from' time unit: {from_time_unit}")
451
+
452
+ if to_time_unit not in switcher:
453
+ raise ValueError(f"Invalid 'to' time unit: {to_time_unit}")
454
+
455
+ return switcher[to_time_unit] / switcher[from_time_unit]
456
+
457
+ # ----------------------------------
458
+ @classmethod
459
+ def get_price_unit_convertion_factor(cls, from_price_unit: PriceUnit, to_price_unit: PriceUnit) -> float:
460
+
461
+ if from_price_unit == to_price_unit:
462
+ return 1.0
463
+
464
+ switcher = {
465
+ PriceUnit.EURO: 1.0,
466
+ PriceUnit.CENT: 100.0,
467
+ }
468
+
469
+ if from_price_unit not in switcher:
470
+ raise ValueError(f"Invalid 'from' price unit: {from_price_unit}")
471
+
472
+ if to_price_unit not in switcher:
473
+ raise ValueError(f"Invalid 'to' price unit: {to_price_unit}")
474
+
475
+ return switcher[to_price_unit] / switcher[from_price_unit]
476
+
477
+ # ----------------------------------
478
+ @classmethod
479
+ def get_quantity_unit_convertion_factor(
480
+ cls, from_quantity_unit: QuantityUnit, to_quantity_unit: QuantityUnit
481
+ ) -> float:
482
+
483
+ if from_quantity_unit == to_quantity_unit:
484
+ return 1.0
485
+
486
+ switcher = {
487
+ QuantityUnit.WH: 1.0,
488
+ QuantityUnit.KWH: 0.001,
489
+ QuantityUnit.MWH: 0.000001,
490
+ }
491
+
492
+ if from_quantity_unit not in switcher:
493
+ raise ValueError(f"Invalid 'from' quantity unit: {from_quantity_unit}")
494
+
495
+ if to_quantity_unit not in switcher:
496
+ raise ValueError(f"Invalid 'to' quantity unit: {to_quantity_unit}")
497
+
498
+ return switcher[to_quantity_unit] / switcher[from_quantity_unit]
499
+
500
+ # ----------------------------------
501
+ @overload
502
+ @classmethod
503
+ def get_convertion_factor(
504
+ cls,
505
+ from_unit: Tuple[PriceUnit, QuantityUnit],
506
+ to_unit: Tuple[PriceUnit, QuantityUnit],
507
+ dt: Optional[date] = None,
508
+ ) -> float: ...
509
+
510
+ @overload
511
+ @classmethod
512
+ def get_convertion_factor(
513
+ cls,
514
+ from_unit: Tuple[PriceUnit, TimeUnit],
515
+ to_unit: Tuple[PriceUnit, TimeUnit],
516
+ dt: Optional[date] = None,
517
+ ) -> float: ...
518
+
519
+ @classmethod
520
+ def get_convertion_factor(cls, from_unit, to_unit, dt: Optional[date] = None) -> float:
521
+ if type(from_unit) is not type(to_unit):
522
+ raise ValueError(f"from_unit {from_unit} and to_unit {to_unit} must be of the same type")
523
+ if (
524
+ isinstance(from_unit, tuple)
525
+ and isinstance(from_unit[0], PriceUnit)
526
+ and isinstance(from_unit[1], QuantityUnit)
527
+ ):
528
+ return cls.get_price_unit_convertion_factor(
529
+ from_unit[0], to_unit[0]
530
+ ) / cls.get_quantity_unit_convertion_factor(from_unit[1], to_unit[1])
531
+ if isinstance(from_unit, tuple) and isinstance(from_unit[0], PriceUnit) and isinstance(from_unit[1], TimeUnit):
532
+ if dt is None:
533
+ raise ValueError(
534
+ f"dt must not be None when from_unit {from_unit} and to_unit {to_unit} are of type Tuple[PriceUnit, TimeUnit]"
535
+ )
536
+ return cls.get_price_unit_convertion_factor(from_unit[0], to_unit[0]) / cls.get_time_unit_convertion_factor(
537
+ from_unit[1], to_unit[1], dt
538
+ )
539
+
540
+ raise ValueError(
541
+ f"from_unit {from_unit} and to_unit {to_unit} must be of type Tuple[PriceUnit, QuantityUnit] or Tuple[PriceUnit, TimeUnit]"
542
+ )
543
+
544
+ # ----------------------------------
545
+ @classmethod
546
+ def convert(
547
+ cls,
548
+ price_values: list[PriceValue[ValueUnit, BaseUnit]],
549
+ to_unit: Tuple[ValueUnit, BaseUnit],
550
+ ) -> list[PriceValue[ValueUnit, BaseUnit]]:
551
+
552
+ if price_values is None or len(price_values) == 0:
553
+ raise ValueError("price_values is None or empty")
554
+
555
+ if to_unit is None:
556
+ raise ValueError("to_unit is None")
557
+
558
+ res = list[PriceValue[ValueUnit, BaseUnit]]()
559
+ for price_value in price_values:
560
+ if price_value.value_unit is None:
561
+ raise ValueError("price_value.value_unit is None")
562
+ if price_value.base_unit is None:
563
+ raise ValueError("price_value.base_unit is None")
564
+
565
+ res.append(
566
+ PriceValue(
567
+ start_date=price_value.start_date,
568
+ end_date=price_value.end_date,
569
+ value=price_value.value
570
+ * cls.get_convertion_factor(
571
+ (price_value.value_unit, price_value.base_unit), to_unit, price_value.start_date # type: ignore
572
+ ),
573
+ value_unit=to_unit[0],
574
+ base_unit=to_unit[1],
575
+ vat_id=price_value.vat_id,
576
+ )
577
+ )
578
+
579
+ return res