-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools_functions.py
More file actions
367 lines (302 loc) · 12.4 KB
/
tools_functions.py
File metadata and controls
367 lines (302 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
from datetime import datetime, timedelta
from typing import Union
import pandas as pd
from sqlalchemy import text
from db import DB_ENGINE
def create_transaction(
item_name: str,
transaction_type: str,
quantity: int,
price: float,
date: Union[str, datetime],
) -> int:
"""
This function records a transaction of type 'stock_orders' or 'sales' with a specified
item name, quantity, total price, and transaction date into the 'transactions' table of the database.
Args:
item_name (str): The name of the item involved in the transaction.
transaction_type (str): Either 'stock_orders' or 'sales'.
quantity (int): Number of units involved in the transaction.
price (float): Total price of the transaction.
date (str or datetime): Date of the transaction in ISO 8601 format.
Returns:
int: The ID of the newly inserted transaction.
Raises:
ValueError: If `transaction_type` is not 'stock_orders' or 'sales'.
Exception: For other database or execution errors.
"""
try:
# Convert datetime to ISO string if necessary
date_str = date.isoformat() if isinstance(date, datetime) else date
# Validate transaction type
if transaction_type not in {"stock_orders", "sales"}:
raise ValueError("Transaction type must be 'stock_orders' or 'sales'")
# Prepare transaction record as a single-row DataFrame
transaction = pd.DataFrame(
[
{
"item_name": item_name,
"transaction_type": transaction_type,
"units": quantity,
"price": price,
"transaction_date": date_str,
}
]
)
# Insert the record into the database
transaction.to_sql("transactions", DB_ENGINE, if_exists="append", index=False)
# Fetch and return the ID of the inserted row
result = pd.read_sql("SELECT last_insert_rowid() as id", DB_ENGINE)
return int(result.iloc[0]["id"])
except Exception as e:
print(f"Error creating transaction: {e}")
raise
def get_all_inventory(as_of_date: str) -> dict[str, int]:
"""
Retrieve a snapshot of available inventory as of a specific date.
This function calculates the net quantity of each item by summing
all stock orders and subtracting all sales up to and including the given date.
Only items with positive stock are included in the result.
Args:
as_of_date (str): ISO-formatted date string (YYYY-MM-DD) representing the inventory cutoff.
Returns:
dict[str, int]: A dictionary mapping item names to their current stock levels.
"""
# SQL query to compute stock levels per item as of the given date
query = """
SELECT
item_name,
SUM(CASE
WHEN transaction_type = 'stock_orders' THEN units
WHEN transaction_type = 'sales' THEN -units
ELSE 0
END) as stock
FROM transactions
WHERE item_name IS NOT NULL
AND transaction_date <= :as_of_date
GROUP BY item_name
HAVING stock > 0
"""
# Execute the query with the date parameter
result = pd.read_sql(query, DB_ENGINE, params={"as_of_date": as_of_date})
# Convert the result into a dictionary {item_name: stock}
return dict(zip(result["item_name"], result["stock"]))
def get_stock_level(item_name: str, as_of_date: Union[str, datetime]) -> pd.DataFrame:
"""
Retrieve the stock level of a specific item as of a given date.
This function calculates the net stock by summing all 'stock_orders' and
subtracting all 'sales' transactions for the specified item up to the given date.
Args:
item_name (str): The name of the item to look up.
as_of_date (str or datetime): The cutoff date (inclusive) for calculating stock.
Returns:
pd.DataFrame: A single-row DataFrame with columns 'item_name' and 'current_stock'.
"""
# Convert date to ISO string format if it's a datetime object
if isinstance(as_of_date, datetime):
as_of_date = as_of_date.isoformat()
# SQL query to compute net stock level for the item
stock_query = """
SELECT
item_name,
COALESCE(SUM(CASE
WHEN transaction_type = 'stock_orders' THEN units
WHEN transaction_type = 'sales' THEN -units
ELSE 0
END), 0) AS current_stock
FROM transactions
WHERE item_name = :item_name
AND transaction_date <= :as_of_date
"""
# Execute query and return result as a DataFrame
return pd.read_sql(
stock_query,
DB_ENGINE,
params={"item_name": item_name, "as_of_date": as_of_date},
)
def get_supplier_delivery_date(input_date_str: str, quantity: int) -> str:
"""
Estimate the supplier delivery date based on the requested order quantity and a starting date.
Delivery lead time increases with order size:
- ≤10 units: same day
- 11–100 units: 1 day
- 101–1000 units: 4 days
- >1000 units: 7 days
Args:
input_date_str (str): The starting date in ISO format (YYYY-MM-DD).
quantity (int): The number of units in the order.
Returns:
str: Estimated delivery date in ISO format (YYYY-MM-DD).
"""
# Debug log (comment out in production if needed)
print(
f"FUNC (get_supplier_delivery_date): Calculating for qty {quantity} from date string '{input_date_str}'"
)
# Attempt to parse the input date
try:
input_date_dt = datetime.fromisoformat(input_date_str.split("T")[0])
except (ValueError, TypeError):
# Fallback to current date on format error
print(
f"WARN (get_supplier_delivery_date): Invalid date format '{input_date_str}', using today as base."
)
input_date_dt = datetime.now()
# Determine delivery delay based on quantity
if quantity <= 10:
days = 0
elif quantity <= 100:
days = 1
elif quantity <= 1000:
days = 4
else:
days = 7
# Add delivery days to the starting date
delivery_date_dt = input_date_dt + timedelta(days=days)
# Return formatted delivery date
return delivery_date_dt.strftime("%Y-%m-%d")
def get_cash_balance(as_of_date: Union[str, datetime]) -> float:
"""
Calculate the current cash balance as of a specified date.
The balance is computed by subtracting total stock purchase costs ('stock_orders')
from total revenue ('sales') recorded in the transactions table up to the given date.
Args:
as_of_date (str or datetime): The cutoff date (inclusive) in ISO format or as a datetime object.
Returns:
float: Net cash balance as of the given date. Returns 0.0 if no transactions exist or an error occurs.
"""
try:
# Convert date to ISO format if it's a datetime object
if isinstance(as_of_date, datetime):
as_of_date = as_of_date.isoformat()
# Query all transactions on or before the specified date
transactions = pd.read_sql(
"SELECT * FROM transactions WHERE transaction_date <= :as_of_date",
DB_ENGINE,
params={"as_of_date": as_of_date},
)
# Compute the difference between sales and stock purchases
if not transactions.empty:
total_sales = transactions.loc[
transactions["transaction_type"] == "sales", "price"
].sum()
total_purchases = transactions.loc[
transactions["transaction_type"] == "stock_orders", "price"
].sum()
return float(total_sales - total_purchases)
return 0.0
except Exception as e:
print(f"Error getting cash balance: {e}")
return 0.0
def generate_financial_report(as_of_date: Union[str, datetime]) -> dict:
"""
Generate a complete financial report for the company as of a specific date.
This includes:
- Cash balance
- Inventory valuation
- Combined asset total
- Itemized inventory breakdown
- Top 5 best-selling products
Args:
as_of_date (str or datetime): The date (inclusive) for which to generate the report.
Returns:
dict: A dictionary containing the financial report fields:
- 'as_of_date': The date of the report
- 'cash_balance': Total cash available
- 'inventory_value': Total value of inventory
- 'total_assets': Combined cash and inventory value
- 'inventory_summary': list of items with stock and valuation details
- 'top_selling_products': list of top 5 products by revenue
"""
# Normalize date input
if isinstance(as_of_date, datetime):
as_of_date = as_of_date.isoformat()
# Get current cash balance
cash = get_cash_balance(as_of_date)
# Get current inventory snapshot
inventory_df = pd.read_sql("SELECT * FROM inventory", DB_ENGINE)
inventory_value = 0.0
inventory_summary = []
# Compute total inventory value and summary by item
for _, item in inventory_df.iterrows():
stock_info = get_stock_level(item["item_name"], as_of_date)
stock = stock_info["current_stock"].iloc[0]
item_value = stock * item["unit_price"]
inventory_value += item_value
inventory_summary.append(
{
"item_name": item["item_name"],
"stock": stock,
"unit_price": item["unit_price"],
"value": item_value,
}
)
# Identify top-selling products by revenue
top_sales_query = """
SELECT item_name, SUM(units) as total_units, SUM(price) as total_revenue
FROM transactions
WHERE transaction_type = 'sales' AND transaction_date <= :date
GROUP BY item_name
ORDER BY total_revenue DESC
LIMIT 5
"""
top_sales = pd.read_sql(top_sales_query, DB_ENGINE, params={"date": as_of_date})
top_selling_products = top_sales.to_dict(orient="records")
return {
"as_of_date": as_of_date,
"cash_balance": cash,
"inventory_value": inventory_value,
"total_assets": cash + inventory_value,
"inventory_summary": inventory_summary,
"top_selling_products": top_selling_products,
}
def search_quote_history(search_terms: list[str], limit: int = 5) -> list[dict]:
"""
Retrieve a list of historical quotes that match any of the provided search terms.
The function searches both the original customer request (from `quote_requests`) and
the explanation for the quote (from `quotes`) for each keyword. Results are sorted by
most recent order date and limited by the `limit` parameter.
Args:
search_terms (list[str]): list of terms to match against customer requests and explanations.
limit (int, optional): Maximum number of quote records to return. Default is 5.
Returns:
list[dict]: A list of matching quotes, each represented as a dictionary with fields:
- original_request
- total_amount
- quote_explanation
- job_type
- order_size
- event_type
- order_date
"""
conditions = []
params = {}
# Build SQL WHERE clause using LIKE filters for each search term
for i, term in enumerate(search_terms):
param_name = f"term_{i}"
conditions.append(
f"(LOWER(qr.response) LIKE :{param_name} OR "
f"LOWER(q.quote_explanation) LIKE :{param_name})"
)
params[param_name] = f"%{term.lower()}%"
# Combine conditions; fallback to always-true if no terms provided
where_clause = " AND ".join(conditions) if conditions else "1=1"
# Final SQL query to join quotes with quote_requests
query = f"""
SELECT
qr.response AS original_request,
q.total_amount,
q.quote_explanation,
q.job_type,
q.order_size,
q.event_type,
q.order_date
FROM quotes q
JOIN quote_requests qr ON q.request_id = qr.id
WHERE {where_clause}
ORDER BY q.order_date DESC
LIMIT {limit}
"""
# Execute parameterized query
with DB_ENGINE.connect() as conn:
result = conn.execute(text(query), params)
return [dict(row) for row in result]