|
|
|
@ -1,12 +1,14 @@ |
|
|
|
from pandas import DataFrame, merge, to_datetime, NaT |
|
|
|
from pandas import DataFrame, merge, to_datetime, NaT, concat, Series |
|
|
|
from numpy import concatenate |
|
|
|
from numpy import concatenate |
|
|
|
from abc import ABC, abstractmethod |
|
|
|
from abc import ABC, abstractmethod |
|
|
|
from logging import getLogger |
|
|
|
from logging import getLogger |
|
|
|
import re |
|
|
|
import re |
|
|
|
from typing import Literal |
|
|
|
from typing import Literal |
|
|
|
import datetime |
|
|
|
import datetime |
|
|
|
|
|
|
|
from copy import deepcopy |
|
|
|
|
|
|
|
|
|
|
|
from helpers import CN_REGEX |
|
|
|
from helpers import CN_REGEX, drop_unnamed |
|
|
|
|
|
|
|
from memory import get_prev_reconciled |
|
|
|
|
|
|
|
|
|
|
|
logger = getLogger(__name__) |
|
|
|
logger = getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
@ -17,8 +19,11 @@ class HoldReport(ABC): |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, dataframe: DataFrame, reports_config: dict) -> None: |
|
|
|
def __init__(self, dataframe: DataFrame, reports_config: dict) -> None: |
|
|
|
self.config = reports_config |
|
|
|
self.config = reports_config |
|
|
|
|
|
|
|
drop_unnamed(dataframe) |
|
|
|
self.df = dataframe |
|
|
|
self.df = dataframe |
|
|
|
|
|
|
|
self.prev_rec = None |
|
|
|
self._normalize() |
|
|
|
self._normalize() |
|
|
|
|
|
|
|
self._previsouly_resolved() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize(self): |
|
|
|
def _normalize(self): |
|
|
|
@ -45,37 +50,88 @@ class HoldReport(ABC): |
|
|
|
self.df["Source"] = self.source |
|
|
|
self.df["Source"] = self.source |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_no_match(self, other: 'HoldReport'): |
|
|
|
def _previsouly_resolved(self): |
|
|
|
# Merge the two DataFrames using the contract number as the join key |
|
|
|
""" |
|
|
|
outer_merge = merge( |
|
|
|
""" |
|
|
|
self.df, other.df, |
|
|
|
current_contracts: list[str] = self.df["contract_number"] |
|
|
|
how="outer", |
|
|
|
|
|
|
|
on=["contract_number"], |
|
|
|
prev_recd: DataFrame = get_prev_reconciled(contracts=current_contracts) |
|
|
|
suffixes=('_'+self.source, '_'+other.source) |
|
|
|
if not prev_recd: |
|
|
|
) |
|
|
|
logger.info("No previously reconciled!") |
|
|
|
|
|
|
|
self.df = self._add_work_columns(self.df) |
|
|
|
# Filter the merged DataFrame to include only the transactions that do not have a match in both OBT and GPT |
|
|
|
return |
|
|
|
no_match = outer_merge.loc[ |
|
|
|
self.prev_rec = prev_recd |
|
|
|
(outer_merge[f"Source_{self.source}"].isna()) | |
|
|
|
|
|
|
|
(outer_merge[f"Source_{other.source}"].isna()) |
|
|
|
start_size = self.df.shape[0] |
|
|
|
] |
|
|
|
logger.debug(f"Report DF: \n{self.df}") |
|
|
|
|
|
|
|
logger.debug(f"prev_rec: \n{prev_recd}") |
|
|
|
# Fill in missing values and drop unnecessary columns |
|
|
|
|
|
|
|
no_match["Source"] = no_match[f"Source_{self.source}"].fillna("GP") |
|
|
|
source_id = f"ID_{self.source}" |
|
|
|
no_match["onhold_amount"] = no_match[f"onhold_amount_{self.source}"].fillna( |
|
|
|
self.df[source_id] = self.df["ID"] |
|
|
|
no_match[f"onhold_amount_{other.source}"] |
|
|
|
self.df = merge( |
|
|
|
|
|
|
|
self.df, |
|
|
|
|
|
|
|
prev_recd, |
|
|
|
|
|
|
|
how="left", |
|
|
|
|
|
|
|
on= source_id, |
|
|
|
|
|
|
|
suffixes=("_cur", "_prev") |
|
|
|
) |
|
|
|
) |
|
|
|
no_match["vendor_name"] = no_match[f"vendor_name_{self.source}"].fillna( |
|
|
|
#self.df.to_excel(f"merged_df_{self.source}.xlsx") |
|
|
|
no_match[f"vendor_name_{other.source}"] |
|
|
|
|
|
|
|
|
|
|
|
# Drop anything that should be ignored |
|
|
|
|
|
|
|
self.df = self.df[self.df["Hide Next Month"] != True] |
|
|
|
|
|
|
|
logger.info(f"Prev res added:\n{self.df}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
col_to_drop = [] |
|
|
|
|
|
|
|
for c in self.df.keys().to_list(): |
|
|
|
|
|
|
|
logger.debug(f"{c=}") |
|
|
|
|
|
|
|
if "_prev" in c or "ID_" in c: |
|
|
|
|
|
|
|
logger.debug(f"Found '_prev' in {c}") |
|
|
|
|
|
|
|
col_to_drop.append(c) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
logger.debug(f"{c} is a good col!") |
|
|
|
|
|
|
|
#col_to_drop.extend([c for c in self.df.keys().to_list() if '_prev' in c]) |
|
|
|
|
|
|
|
logger.debug(f"{col_to_drop=}") |
|
|
|
|
|
|
|
self.df.drop( |
|
|
|
|
|
|
|
columns= col_to_drop, |
|
|
|
|
|
|
|
inplace=True |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
# Restandardize |
|
|
|
|
|
|
|
self.df.rename(columns={"contract_number_cur": "contract_number"}, inplace=True) |
|
|
|
|
|
|
|
end_size = self.df.shape[0] |
|
|
|
|
|
|
|
logger.info(f"Reduced df by {start_size-end_size}") |
|
|
|
|
|
|
|
|
|
|
|
return no_match |
|
|
|
def _remove_full_matches(self, other: 'HoldReport'): |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
Removes any contracts that match both contract number and hold amount. |
|
|
|
|
|
|
|
These do not need to be reconciled. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This id done 'in place' to both dataframes |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
filter_id_match: DataFrame = self.df[~(self.df["ID"].isin(other.df["ID"]))] |
|
|
|
|
|
|
|
other.df: DataFrame = other.df[~(other.df["ID"].isin(self.df["ID"]))] |
|
|
|
|
|
|
|
self.df = filter_id_match |
|
|
|
|
|
|
|
self.combined_missing: DataFrame = concat([self.df, other.df], ignore_index=True) |
|
|
|
|
|
|
|
self.combined_missing.to_excel("ALL MISSING.xlsx") |
|
|
|
|
|
|
|
logger.debug(f"Combined Missing:\n{self.combined_missing}") |
|
|
|
|
|
|
|
logger.info(f"Payments with errors: {self.combined_missing.shape[0]}") |
|
|
|
|
|
|
|
|
|
|
|
def _get_contract_matches(self, other: 'HoldReport') -> DataFrame: |
|
|
|
@staticmethod |
|
|
|
|
|
|
|
def _created_combined_col(column: str, target_df: DataFrame, sources: tuple[str, str]) -> DataFrame : |
|
|
|
""" |
|
|
|
""" |
|
|
|
|
|
|
|
Creates a new column by filling empty columns of this source, with the matching column from another source |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
this, that = sources |
|
|
|
|
|
|
|
target_df[column] = target_df[f"{column}_{this}"].fillna( |
|
|
|
|
|
|
|
target_df[f"{column}_{that}"] |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
return target_df |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _requires_rec(self, other: 'HoldReport') -> DataFrame: |
|
|
|
""" |
|
|
|
""" |
|
|
|
|
|
|
|
To be run after full matches have been re |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
# Merge the two filtered DataFrames on the contract number |
|
|
|
# Merge the two filtered DataFrames on the contract number |
|
|
|
contract_match = merge( |
|
|
|
contract_match = merge( |
|
|
|
self.df, other.df, |
|
|
|
self.df, other.df, |
|
|
|
@ -84,49 +140,78 @@ class HoldReport(ABC): |
|
|
|
suffixes=('_'+self.source, '_'+other.source) |
|
|
|
suffixes=('_'+self.source, '_'+other.source) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
contract_match["vendor_name"] = contract_match[f"vendor_name_{self.source}"].fillna( |
|
|
|
#contract_match.to_excel("CONTRACT_MATCH.xlsx") |
|
|
|
contract_match[f"vendor_name_{other.source}"] |
|
|
|
|
|
|
|
) |
|
|
|
for col in ["vendor_name", "Resolution", "Notes"]: |
|
|
|
|
|
|
|
self._created_combined_col(col, contract_match, (self.source, other.source)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"_requires_rec | contract_match:\n{contract_match.columns} ({contract_match.shape})") |
|
|
|
|
|
|
|
|
|
|
|
return contract_match |
|
|
|
no_match: DataFrame = self.combined_missing[~( |
|
|
|
|
|
|
|
self.combined_missing["contract_number"].isin( |
|
|
|
|
|
|
|
contract_match["contract_number"] |
|
|
|
|
|
|
|
)) |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
no_match[f"ID_{self.source}"] = no_match.apply(lambda row: |
|
|
|
|
|
|
|
row["ID"] if row["Source"] == self.source else None |
|
|
|
|
|
|
|
, axis=1) |
|
|
|
|
|
|
|
no_match[f"ID_{other.source}"] = no_match.apply(lambda row: |
|
|
|
|
|
|
|
row["ID"] if row["Source"] == other.source else None |
|
|
|
|
|
|
|
, axis=1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"_requires_rec | no_match:\n{no_match.columns} ({no_match.shape})") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return contract_match, no_match |
|
|
|
|
|
|
|
|
|
|
|
@staticmethod |
|
|
|
@staticmethod |
|
|
|
def _add_work_columns(df: DataFrame) -> DataFrame: |
|
|
|
def _add_work_columns(df: DataFrame) -> DataFrame: |
|
|
|
""" |
|
|
|
""" |
|
|
|
Add empty columns to the dataframe to faciliate working through the report. |
|
|
|
Add empty columns to the dataframe to faciliate working through the report. |
|
|
|
""" |
|
|
|
""" |
|
|
|
WORK_COLS = ["Resolution", "Notes"] |
|
|
|
logger.debug("Adding work columns!") |
|
|
|
|
|
|
|
df_cols: list[str] = df.columns.to_list() |
|
|
|
|
|
|
|
WORK_COLS = ["Hide Next Month","Resolution"] |
|
|
|
for col in WORK_COLS: |
|
|
|
for col in WORK_COLS: |
|
|
|
df[col] = '' |
|
|
|
if col not in df_cols: |
|
|
|
|
|
|
|
df[col] = '' |
|
|
|
return df |
|
|
|
return df |
|
|
|
|
|
|
|
|
|
|
|
def reconcile(self, other: 'HoldReport') -> tuple[DataFrame]: |
|
|
|
def reconcile(self, other: 'HoldReport') -> tuple[DataFrame]: |
|
|
|
""" |
|
|
|
""" |
|
|
|
""" |
|
|
|
""" |
|
|
|
no_match: DataFrame = self._get_no_match(other) |
|
|
|
self._remove_full_matches(other) |
|
|
|
no_match.to_excel("NOMATCH.xlsx") |
|
|
|
all_prev_reced = concat([self.prev_rec, other.prev_rec],ignore_index=True) |
|
|
|
logger.debug(f"No_match: {no_match}") |
|
|
|
logger.debug(f"Removed matches:\n{self.df}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
amount_mismatch: DataFrame = self._get_contract_matches(other) |
|
|
|
amount_mismatch, no_match = self._requires_rec(other) |
|
|
|
amount_mismatch.to_excel("AMTMM.xlsx") |
|
|
|
|
|
|
|
logger.debug(f"amt_mismatche: {no_match}") |
|
|
|
logger.debug(f"reconcile | no_match unaltered\n{no_match.columns} ({no_match.shape})") |
|
|
|
|
|
|
|
logger.debug(f"reconcile | am_mm unaltered:\n{amount_mismatch.columns} ({amount_mismatch.shape})") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
columns: list[str] = ["ID_GP", "ID_OB"] |
|
|
|
|
|
|
|
columns.extend(self.config["output_columns"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nm_cols:list[str] = deepcopy(columns) |
|
|
|
|
|
|
|
nm_cols.insert(3,"onhold_amount") |
|
|
|
|
|
|
|
nm_cols.insert(4,"Source") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
columns.insert(3,"onhold_amount_GP") |
|
|
|
|
|
|
|
columns.insert(4, "onhold_amount_OB") |
|
|
|
|
|
|
|
|
|
|
|
# Select and reorder columns |
|
|
|
# Select and reorder columns |
|
|
|
no_match = no_match[ |
|
|
|
no_match = no_match[ |
|
|
|
["Source"].extend(self.config["output_columns"]) |
|
|
|
nm_cols |
|
|
|
] |
|
|
|
] |
|
|
|
no_match = self._add_work_columns(no_match) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
amount_mismatch = amount_mismatch[ |
|
|
|
amount_mismatch = amount_mismatch[ |
|
|
|
self.config["output_columns"] |
|
|
|
columns |
|
|
|
] |
|
|
|
] |
|
|
|
amount_mismatch = self._add_work_columns(amount_mismatch) |
|
|
|
logger.info(f"no_match: {no_match.shape[0]}") |
|
|
|
|
|
|
|
logger.info(f"am_mm: {amount_mismatch.shape[0]}") |
|
|
|
return no_match, amount_mismatch |
|
|
|
return no_match, amount_mismatch |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OnBaseReport(HoldReport): |
|
|
|
class OnBaseReport(HoldReport): |
|
|
|
|
|
|
|
|
|
|
|
source = "OB" |
|
|
|
source = "OB" |
|
|
|
@ -134,16 +219,14 @@ class OnBaseReport(HoldReport): |
|
|
|
def get_overdue(self) -> DataFrame: |
|
|
|
def get_overdue(self) -> DataFrame: |
|
|
|
""" |
|
|
|
""" |
|
|
|
""" |
|
|
|
""" |
|
|
|
self.df["install_date"] = to_datetime(self.df["install_date"]) |
|
|
|
self.df["InstallDate"] = to_datetime(self.df["InstallDate"]) |
|
|
|
self.df["install_date"].fillna(NaT, inplace=True) |
|
|
|
self.df["InstallDate"].fillna(NaT, inplace=True) |
|
|
|
return self.df[self.df["install_date"].dt.date < datetime.date.today()] |
|
|
|
return self.df[self.df["InstallDate"].dt.date < datetime.date.today()] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GreatPlainsReport(HoldReport): |
|
|
|
class GreatPlainsReport(HoldReport): |
|
|
|
|
|
|
|
|
|
|
|
source = "GP" |
|
|
|
source = "GP" |
|
|
|
filted_df: bool = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, dataframe: DataFrame, report_config: dict) -> None: |
|
|
|
def __init__(self, dataframe: DataFrame, report_config: dict) -> None: |
|
|
|
|
|
|
|
|
|
|
|
|