A: 

You would have to use a couple of cursors and two nested loops. (this may be a little slow)

One to read all the payments - I assume Customer, Amount

Then for each customer create another cursor for the open items.

The first loop will read payments until done

Within that loop open a new cursor for the customer's open items sorted by oldest first.

Loop through each open item and apply the payments as you described

Then get next payment.

Repeat until no more payments.

DJ
I do not suggest using cursors for performace reasons...if you find your self performing business logic in SQL it should go in your business layer of your application...
Miyagi Coder
unless this is a one time thing...
Miyagi Coder
I know - I don't recommend it either - but the OP wants to do it within the DB
DJ
-1: Using cursors for this solution would impose a significant performance penalty.
Jim G.
A: 

Unless this is a one time effort...

It sounds like this is buisness logic and belongs in your business layer of your application.

Miyagi Coder
I know. Somehow I have to run this process nightly, which is not part of the application.
Saif Khan
+2  A: 

Consider the following:

a payment either applies in full to a balance, applies in part to a balance, or overpays a balance.

Now, imagine that we could find, for any balance, the cumulative balance of invoices to date. Rather than imagine that, let's do it:

create view cumulative_balance as
select a.*, 
  (select sum( balance ) 
  from openitems b 
  where b.id = a.id and b.type = a.type and a.daysOpen >= a.daysOpen)
  as cumulative_balance
from openitems a;

Now we can find the first cumulative balance less than or equal to the payment, for any id and type, and store that, and daysOpen, and cumulative balance in server variables.

Then we update all openItems with that id and type, where daysOpen <= the value we got, setting all those balances to zero.

Then we find the first non-zero balance of that id and type, and set its balance to be it's balance - (payment - the cumulative balance we stored). if there's an overpayment, this balance will be correctly negative.

With the correct query, you'll be able to do the lookup and first update in one statement.

There are two problems. One is that you can't determine, of two or more alances with the same id and type and daysOpen, which should be paid first. Adding a unique id to your table would serve as a tie-breaker for those cases.

Second is the need to save the cumulative balance to use it in the query for the second update. if you designed your table correctly, with a column for invoice_amount that wasn't updated by payments, and a payment column that was, this would solve your problem.

An even better refactoring would be to have two tables, one for invoices and one for payment: then a view could just do all the work, by comparing cumulative balances to cumulative payments, producing a list of unpaid balances or overpayments.

In fact, I designed just such a system for a major mortgage guarantee company with the initials FM. It was a bit more complicated than what you have, in that balances were calculated from a number of formulas of amounts and percentages, and multiple payers (actually, insurers, this was for mortgages that had gone into default) had to be invoiced in a prescribed order according to other rules, per defauted mortgage.

All of this was done in views, with a short (100 line or so) stored procedure that essentially did what I've outlined above: used a view that ordered the billing of invoices by these rules, applied payments (in the view), calculating what additional payments to invoice on what date to which insurer. The stored procedure then just generated invoices for the current date (which current date could be set, again using a view, to any date for testing purposes).

(The irony is that I'd taken the job onteh promise I'd get to write C++; the only C++ I wrote used the Oracle and Sybase C APIs to transfer data from the Oracle system to the Sybase one.)

tpdi
Sometimes it drives me nuts trying to implement Business Logic in the data store.
Saif Khan
SQL is about sets; if you think it terms of sets it becomes obvious. Just as, say, Haskell seems weird and convoluted to programmers with a background only in imperative languages, but cleaner and sweeter to Haskell programmers.
tpdi
+1  A: 

This should do it. I've declared a local variable, but you could make that a parameter from a stored procedure. I've also added an invoice_id to the table to uniquely identify invoices since id and date don't seem to be unique.

DECLARE
    @paid_amount DECIMAL(9, 2)

SET @paid_amount = 500

UPDATE
    OI
SET
    balance =
      CASE
       WHEN @paid_amount - SQ.running_total > balance THEN 0
       ELSE balance - (@paid_amount - SQ.running_total)
      END
FROM
    dbo.OpenItems OI
INNER JOIN (
    SELECT
     I1.id,
     I1.invoice_id,
     I1.date,
     ISNULL(SUM(I2.amount), 0) AS running_total
    FROM
     OpenItems I1
    LEFT OUTER JOIN OpenItems I2 ON
     I2.id = I1.id AND
     I2.type = 'INV' AND
     I2.daysopen > I1.daysopen
    GROUP BY
     I1.id,
     I1.invoice_id,
     I1.date
) AS SQ ON
    SQ.id = OI.id AND
    SQ.invoice_id = OI.invoice_id
WHERE
    @paid_amount > SQ.running_total
Tom H.
This didn't work. The @paid_amount - SQ.running_total > balance can't work. The calculation must happen, and the difference must then pass to the next row. I believe a CURSOR might be the best solution. I was trying to avoid that.
Saif Khan
Did you actually test it? I just ran it again on a local database and it gave the exact results that you were looking for. Are you sure that you understand how it works? My guess is not, since you said that it "can't work". If not, I can explain it. You need to think in sets when working with SQL, not procedural code.
Tom H.