-
Notifications
You must be signed in to change notification settings - Fork 8
Sample Project Allocation
This illustrates the allocation pattern:
cd examples/payment_allocation/tests
python add_payment.py
Allocation is a pattern (more examples below), where:
A
Provider
allocates to a list ofRecipients
, creatingAllocation
rows.
The allocation pattern may be unfamiliar,
but experience has shown it is valuable in
about 1/3 of applications.
Some examples are briefly described below.
In an unemployment system, benefits are allocated to a set of time periods. The allocation pattern eliminated hundreds of lines of code that had taken months to develop, was running in minutes instead of seconds, and was getting the wrong answer.
In a state government, chosen projects are allocated budget for an approved project. Moreover, the allocation was "chained" - the budget was allocated to each departments General Ledger. Allocation enabled a project to be completed in a weekend that had been running for months.
To manage emergencies such as wild fires, resources (e.g, personnel, bulldozers, aircraft) were allocated to incidents.
Our favorite - project completes and the department is awarded a bonus; it is allocated to the employees in the department.
A large logistics company allocates bank payments to a set of customer accounts. A project was completed in a weekend that previously required 9 months.
For example, imagine a Customer
has a set of outstanding
Orders
, and pays all/several off with a periodic Payment
.
When the Payment
is inserted, our system must:
- Allocate the
Payment
toOrders
that haveAmountOwed
, oldest first - Keep track of how the
Payment
is allocated, by creating aPaymentAllocation
- As the
Payment
is allocated,- Update the
Order.AmountOwed
, and - Adjust the
Customer.Balance
- Update the
We create the following rules:
Observe that the allocation rule looks very much like our pattern:
RuleExtension.allocate(provider=Payment,
recipients=unpaid_orders,
creating_allocation=PaymentAllocation)
The console log panel at the bottom illustrates how rules log their execution, including the rule and row state. Indention indicates rule chaining - how values changed by 1 rule can trigger other rules, even across tables.
The dotted lines show you can correlate your rules to actual execution. We'll explore the logic execution more below, but it's useful to see how it looks during actual development.
Let's explore examples/payment_allocation/tests/add_payment.py
;
here's the key segment that inserts a payment:
cust_alfki = session.query(models.Customer).filter(models.Customer.Id == "ALFKI").one()
new_payment = models.Payment(Amount=1000)
cust_alfki.PaymentList.append(new_payment)
session.add(new_payment)
session.commit()
The test illustrates allocation logic for our inserted payment, which operates as follows:
- The triggering event is the insertion of a
Payment
, which triggers: - The
allocate
rule (line 28). It performs the allocation:- Obtains the list of recipient orders by calling the function
unpaid_orders
(line 9) - For each recipient (
Order
),- Creates a
PaymentAllocation
, links it to theOrder
andPayment
, - Invokes
while_calling_allocator
, which- Reduces
Payment.AmountUnAllocated
- Inserts the
PaymentAllocation
, which runs the following rules:- r1
PaymentAllocation.AmountAllocated
is derived (formula, line 25); this triggers the next rule... - r2
Order.AmountPaid
is adjusted (sum rule, line 23); that triggers... - r3
Order.AmountOwed
is derived (formula rule, line 22); that triggers - r4
Customer.Balance
is adjusted (sum rule, line 20)
- r1
- Returns whether the
Payment.AmountUnAllocated
has remaining value ( > 0 ).
- Reduces
- Tests the returned result
- If true (allocation remains), the loop continues for the next recipient
- Otherwise, the allocation loop is terminated
- Creates a
- Obtains the list of recipient orders by calling the function
This example does not supply an optional argument:
while_calling_allocator
.
The logic_bank/extensions/allocate.py
provides a default, called while_calling_allocator_default
.
This default presumes the attribute names shown in the code below. If these do not match your attribute names, copy / alter this implementation to your own, and specify it on the constructor (line 15).
def while_calling_allocator_default(self, allocation_logic_row, provider_logic_row) -> bool:
"""
Called for each created allocation, to
* insert the created allocation (triggering rules that compute `Allocation.AmountAllocated`)
* reduce Provider.AmountUnAllocated
* return boolean indicating whether Provider.AmountUnAllocated > 0 (remains)
This uses default names:
* provider.Amount
* provider.AmountUnallocated
* allocation.AmountAllocated
To use your names, copy this code and alter as as required
:param allocation_logic_row: allocation row being created
:param provider_logic_row: provider
:return: provider has AmountUnAllocated remaining
"""
if provider_logic_row.row.AmountUnAllocated is None:
provider_logic_row.row.AmountUnAllocated = provider_logic_row.row.Amount # initialization
allocation_logic_row.insert(reason="Allocate " + provider_logic_row.name) # triggers rules, eg AmountAllocated
provider_logic_row.row.AmountUnAllocated = \
provider_logic_row.row.AmountUnAllocated - allocation_logic_row.row.AmountAllocated
return provider_logic_row.row.AmountUnAllocated > 0 # terminate allocation loop if none left
Logic operation is visible in the log
Note: the test program
examples/payment_allocation/tests/add_payment.py
shows some test data in comments at the end
Logic Phase: BEFORE COMMIT - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
Logic Phase: ROW LOGIC (sqlalchemy before_flush) - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Customer[ALFKI] {Update - client} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance: 1016.00, CreditLimit: 2000.00 row@: 0x10abbea00 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Payment[None] {Insert - client} Id: None, Amount: 1000, AmountUnAllocated: None, CustomerId: None, CreatedOn: None row@: 0x10970f610 - 2020-12-23 05:56:45,682 - logic_logger - DEBUG
..Payment[None] {BEGIN Allocate Rule, creating: PaymentAllocation} Id: None, Amount: 1000, AmountUnAllocated: None, CustomerId: None, CreatedOn: None row@: 0x10970f610 - 2020-12-23 05:56:45,683 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None row@: 0x10abbe700 - 2020-12-23 05:56:45,684 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 100.00, OrderId: None, PaymentId: None row@: 0x10abbe700 - 2020-12-23 05:56:45,684 - logic_logger - DEBUG
......Order[10692] {Update - Adjusting Order} Id: 10692, CustomerId: ALFKI, OrderDate: 2013-10-03, AmountTotal: 878.00, AmountPaid: [778.00-->] 878.00, AmountOwed: 100.00 row@: 0x10ac82370 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
......Order[10692] {Formula AmountOwed} Id: 10692, CustomerId: ALFKI, OrderDate: 2013-10-03, AmountTotal: 878.00, AmountPaid: [778.00-->] 878.00, AmountOwed: [100.00-->] 0.00 row@: 0x10ac82370 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance: [1016.00-->] 916.00, CreditLimit: 2000.00 row@: 0x10abbea00 - 2020-12-23 05:56:45,685 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None row@: 0x10ac6a850 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 330.00, OrderId: None, PaymentId: None row@: 0x10ac6a850 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
......Order[10702] {Update - Adjusting Order} Id: 10702, CustomerId: ALFKI, OrderDate: 2013-10-13, AmountTotal: 330.00, AmountPaid: [0.00-->] 330.00, AmountOwed: 330.00 row@: 0x10ac824f0 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
......Order[10702] {Formula AmountOwed} Id: 10702, CustomerId: ALFKI, OrderDate: 2013-10-13, AmountTotal: 330.00, AmountPaid: [0.00-->] 330.00, AmountOwed: [330.00-->] 0.00 row@: 0x10ac824f0 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance: [916.00-->] 586.00, CreditLimit: 2000.00 row@: 0x10abbea00 - 2020-12-23 05:56:45,686 - logic_logger - DEBUG
....PaymentAllocation[None] {Insert - Allocate Payment} Id: None, AmountAllocated: None, OrderId: None, PaymentId: None row@: 0x10ac6a9d0 - 2020-12-23 05:56:45,687 - logic_logger - DEBUG
....PaymentAllocation[None] {Formula AmountAllocated} Id: None, AmountAllocated: 570.00, OrderId: None, PaymentId: None row@: 0x10ac6a9d0 - 2020-12-23 05:56:45,687 - logic_logger - DEBUG
......Order[10835] {Update - Adjusting Order} Id: 10835, CustomerId: ALFKI, OrderDate: 2014-01-15, AmountTotal: 851.00, AmountPaid: [0.00-->] 570.00, AmountOwed: 851.00 row@: 0x10ac82550 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
......Order[10835] {Formula AmountOwed} Id: 10835, CustomerId: ALFKI, OrderDate: 2014-01-15, AmountTotal: 851.00, AmountPaid: [0.00-->] 570.00, AmountOwed: [851.00-->] 281.00 row@: 0x10ac82550 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
........Customer[ALFKI] {Update - Adjusting Customer} Id: ALFKI, CompanyName: Alfreds Futterkiste, Balance: [586.00-->] 16.00, CreditLimit: 2000.00 row@: 0x10abbea00 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
..Payment[None] {END Allocate Rule, creating: PaymentAllocation} Id: None, Amount: 1000, AmountUnAllocated: 0.00, CustomerId: None, CreatedOn: None row@: 0x10970f610 - 2020-12-23 05:56:45,688 - logic_logger - DEBUG
Logic Phase: COMMIT - 2020-12-23 05:56:45,689 - logic_logger - DEBUG
Logic Phase: FLUSH (sqlalchemy flush processing - 2020-12-23 05:56:45,689 - logic_logger - DEBUG
add_payment, update completed
Allocation illustrates some key points regarding logic.
While Allocation is part of Logic Bank, you could have recognized the pattern yourself, and provided the implementation. This is enabled since Event rules can invoke Python. You can make your Python code generic, using meta data (from SQLAlchemy), parameters, etc.
Note how the created PaymentAllocation
row triggered
the more standard rules such as sums and formulas. This
required no special machinery: rules watch and react to changes in data -
if you change the data, rules will "notice" that, and fire. Automatically.
User Project Operations
Logic Bank Internals