Exemple #1
0
    def urja_to_gridium(
            self, urja_data: UrjanetData) -> GridiumBillingPeriodCollection:
        """Transform urjanet data into Gridium billing periods"""

        # Process the account objects in reverse order by statement date. The main motivation here is corrections;
        # we want to process the most recent billing date first, and ignore earlier data for those same dates.
        ordered_accounts = sorted(urja_data.accounts,
                                  key=lambda x: x.StatementDate,
                                  reverse=True)

        # First, we rough out the billing period dates, by iterating through the ordered accounts and pulling out
        # usage periods
        bill_history = DateIntervalTree()
        for account in ordered_accounts:
            usage_periods = self.get_account_billing_periods(account)
            for ival in sorted(usage_periods.intervals(), reverse=True):
                if bill_history.overlaps(ival.begin, ival.end):
                    log.debug(
                        "Skipping overlapping usage period: account_pk={}, start={}, end={}"
                        .format(account.PK, ival.begin, ival.end))
                else:
                    log.debug("Adding usage period: %s - %s", ival.begin,
                              ival.end)
                    bill_history.add(ival.begin, ival.end,
                                     self.billing_period_class(account))
        # fix periods where start/end are the same
        bill_history = self.shift_endpoints(bill_history)

        # Next, we go through the accounts again and insert relevant charge/usage information into the computed
        # billing periods
        for account in ordered_accounts:
            self.merge_statement_data(bill_history, account)

        # Convert the billing periods into the expected "gridium" format
        gridium_periods = []
        for ival in sorted(bill_history.intervals()):
            period_data = ival.data
            gridium_periods.append(
                GridiumBillingPeriod(
                    start=ival.begin,
                    end=ival.end,
                    statement=period_data.statement(),
                    total_charge=period_data.get_total_charge(),
                    peak_demand=period_data.get_peak_demand(),
                    total_usage=period_data.get_total_usage(),
                    source_urls=period_data.get_source_urls(),
                    line_items=(period_data.utility_charges +
                                period_data.third_party_charges),
                    tariff=period_data.tariff(),
                    service_id=period_data.get_service_id(),
                    utility=period_data.get_utility(),
                    utility_account_id=period_data.get_utility_account_id(),
                ))
        return GridiumBillingPeriodCollection(periods=gridium_periods)
    def urja_to_gridium(
            self, urja_data: UrjanetData) -> GridiumBillingPeriodCollection:
        """Transform Urjanet data for water bills into Gridium billing periods"""

        filtered_accounts = self.filtered_accounts(urja_data)
        ordered_accounts = self.ordered_accounts(filtered_accounts)

        # For each account, create a billing period, taking care to detect overlaps (e.g. in the case that a
        # correction bill in issued)
        bill_history = DateIntervalTree()
        for account in ordered_accounts:
            period_start, period_end = self.get_account_period(account)
            if bill_history.overlaps(period_start, period_end):
                log.debug(
                    "Skipping overlapping billing period: account_pk={}, start={}, end={}"
                    .format(account.PK, period_start, period_end))
            else:
                log.debug(
                    "Adding billing period: account_pk={}, start={}, end={}".
                    format(account.PK, period_start, period_end))
                bill_history.add(period_start, period_end,
                                 self.billing_period(account))

        # Adjust date endpoints to avoid 1-day overlaps
        bill_history = DateIntervalTree.shift_endpoints(bill_history)

        # Log the billing periods we determined
        log_generic_billing_periods(bill_history)

        # Compute the final set of gridium billing periods
        gridium_periods = []
        for ival in sorted(bill_history.intervals()):
            period_data = ival.data
            gridium_periods.append(
                GridiumBillingPeriod(
                    start=ival.begin,
                    end=ival.end,
                    statement=period_data.statement(),
                    total_charge=period_data.get_total_charge(),
                    peak_demand=None,  # No peak demand for water
                    total_usage=period_data.get_total_usage(),
                    source_urls=period_data.get_source_urls(),
                    line_items=list(period_data.iter_charges()),
                    tariff=period_data.tariff(),
                ))
        return GridiumBillingPeriodCollection(periods=gridium_periods)
Exemple #3
0
def log_generic_billing_periods(bill_history: DateIntervalTree) -> None:
    """Helper function for logging data in an interval tree holding bill data"""
    log.debug("Billing periods")
    for ival in sorted(bill_history.intervals()):
        period_data = ival.data
        log.debug(
            "\t{} to {} ({} days)".format(
                ival.begin, ival.end, (ival.end - ival.begin).days
            )
        )
        log.debug("\t\tUtility Charges:")
        for chg in period_data.iter_charges():
            log.debug(
                "\t\t\tAmt=${0}\tName='{1}'\tPK={2}\t{3}\t{4}".format(
                    chg.ChargeAmount,
                    chg.ChargeActualName,
                    chg.PK,
                    chg.IntervalStart,
                    chg.IntervalEnd,
                )
            )
        log.debug("\t\tTotal Charge: ${}".format(period_data.get_total_charge()))
        log.debug("\t\tUsages:")
        for usg in period_data.iter_unique_usages():
            log.debug(
                "\t\t\tAmt={0}{1}\tComponent={2}\tPK={3}\t{4}\t{5}".format(
                    usg.UsageAmount,
                    usg.EnergyUnit,
                    usg.RateComponent,
                    usg.PK,
                    usg.IntervalStart,
                    usg.IntervalEnd,
                )
            )
        log.debug("\t\tTotal Usage: {}".format(period_data.get_total_usage()))
        log.debug("\t\tStatements:")
        log.debug(
            "\t\t\t{0}\tPK={1}".format(
                period_data.account.SourceLink, period_data.account.PK
            )
        )
Exemple #4
0
    def update_date_range_from_charges(account: Account) -> Account:
        """Fix date range for bills that cross the winter/summer boundary.

        When a bill crosses the winter/summary boundary (9/1), charges are reported in two
        batches: the summer portion and the winter portion. The account and meter IntervalStart
        and IntervalEnd may encompass just one of these date ranges; fix if needed.

        Summer/winter example:
        meter oid 1707479190338
        +-----------+---------------+-------------+----------+
        | accountPK | IntervalStart | IntervalEnd | meterPK  |
        +-----------+---------------+-------------+----------+
        |   5494320 | 2015-09-01    | 2015-09-11  | 19729463 |
        |   5498442 | 2015-09-11    | 2015-10-09  | 19740313 |

        PDF (https://sources.o2.urjanet.net/sourcewithhttpbasicauth?id=1e55ab22-7795-d6a4
        -a229-22000b849d83)
        has two two sections for charges:
          - 8/13/15 - 8/31/15 (summer)
          - 9/1/15 - 9/11/15 (winter)
        Meter record has IntervalStart = 9/1/15 and IntervalEnd = 9/1/15
        The Charge records have IntervalStart and IntervalEnd for both date ranges.
        """
        account_range = DateIntervalTree()
        log.debug(
            "account interval range: %s to %s",
            account.IntervalStart,
            account.IntervalEnd,
        )
        if account.IntervalEnd > account.IntervalStart:
            account_range.add(account.IntervalStart, account.IntervalEnd)
        for meter in account.meters:
            meter_range = DateIntervalTree()
            log.debug("meter interval range: %s to %s", meter.IntervalStart,
                      meter.IntervalEnd)
            if meter.IntervalEnd > meter.IntervalStart:
                meter_range.add(meter.IntervalStart, meter.IntervalEnd)
            charge_range = DateIntervalTree()
            for charge in meter.charges:
                # don't create single day periods
                if (charge.IntervalEnd - charge.IntervalStart).days <= 1:
                    continue
                log.debug(
                    "charge %s interval range: %s to %s",
                    charge.PK,
                    charge.IntervalStart,
                    charge.IntervalEnd,
                )
                charge_range.add(charge.IntervalStart, charge.IntervalEnd)
            if len(charge_range.intervals()) > 1:
                min_charge_dt = min(
                    [r.begin for r in charge_range.intervals()])
                max_charge_dt = max([r.end for r in charge_range.intervals()])
                log.debug(
                    "Updating meter date range from charges to %s - %s (was %s - %s)",
                    min(meter.IntervalStart, min_charge_dt),
                    max(account.IntervalEnd, max_charge_dt),
                    meter.IntervalStart,
                    meter.IntervalEnd,
                )
                meter.IntervalStart = min(meter.IntervalStart, min_charge_dt)
                meter.IntervalEnd = max(meter.IntervalEnd, max_charge_dt)
                log.debug(
                    "Updating account date range from charges to %s - %s (was %s - %s)",
                    min(account.IntervalStart, min_charge_dt),
                    max(account.IntervalEnd, max_charge_dt),
                    account.IntervalStart,
                    account.IntervalEnd,
                )
                account.IntervalStart = min(account.IntervalStart,
                                            min_charge_dt)
                account.IntervalEnd = max(account.IntervalEnd, max_charge_dt)
        return account