Compare commits

..

257 Commits

Author SHA1 Message Date
abijah
10f74d90ba Fixed a bug with 2 digit years
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@1003 97e9348a-65ac-dc4b-aefc-98561f571b83
2011-01-08 01:26:04 +00:00
abijah
381d14a6f6 Modified the grid search to support standard date formats, with a bit of a kludge by applying it to fields that end with _date or stamp.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@1002 97e9348a-65ac-dc4b-aefc-98561f571b83
2011-01-08 00:56:15 +00:00
abijah
5b911c856b Changed lease status to LATE after the customer is actually late, instead of when late charges start to apply.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@1001 97e9348a-65ac-dc4b-aefc-98561f571b83
2011-01-08 00:53:40 +00:00
abijah
74eb6d29e1 Fixed a bug with customer merge. Common contacts were being deleted when selected as a source contact to keep.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@1000 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-12 21:48:42 +00:00
abijah
d8dfa0ca25 Removed the debug aid of sub-totaling statement entries. I'll add the right solution someday...
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@998 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 22:19:55 +00:00
abijah
fccaed681d Modified a couple reports to have url control over the period
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@997 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 22:05:24 +00:00
abijah
3495a3d760 Updated to include total leases / past leases
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@996 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 21:43:34 +00:00
abijah
cbdbbdf394 Modified customer merge functionality to leave everything from the destination customer in-tact, and simply add on contacts from the source customer, if requested. This simplifies things significantly, and also the paradigm shift of starting the operation from the destination customer allows us to merge multiple customers into one with ease (no need to dig around for each source customer before clicking merge)
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@995 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 20:52:00 +00:00
abijah
d83b72b2a3 Added the front end code to perform a customer merge. Seems to work, although I don't fully trust it yet. Now that implementation is done, I realize I don't care for the interface. Really, the destination customer should keep its primary contact and all other contacts. The user should only be given the choice of adding zero to all contacts from the source customer. A change in primary contact could always be handled after the merge.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@994 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 19:36:39 +00:00
abijah
2e3f168036 First pass at a customer merge function. Preliminary tests seem to be successful.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@993 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 16:29:06 +00:00
abijah
465d9c0c5f Minor change to the display and sort order of unit sizes
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@992 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 13:38:38 +00:00
abijah
9bc572c590 Minor update to support the 'IN' search type, avoid wrapping floating point in CDATA, and to support the grid request for multiple pages.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@991 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-07-02 13:27:35 +00:00
abijah
59e23794c0 Added override function for counting the results. This is needed because the 'Unpaid Charges' report was returning a crazy large number of entries. Although I know this quick and dirty hack works for that report, I haven't checked or tested to see if this might cause issues in other areas.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@985 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-04-14 04:24:13 +00:00
abijah
fbaf23e7df Added possible fix for this problem with sandbox/devbox themes that use a font size so large, it creates usability issues.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@984 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-04-14 03:34:01 +00:00
abijah
8d8ac0a60b Merge in bugfix from v0.2_work
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@982 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-04-02 13:08:41 +00:00
abijah
65b6d582d9 Changed the invoice page to redirect back to the customer view, which is typically more of interest than the lease view
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@980 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-04-02 12:24:34 +00:00
abijah
45ed9bd7a3 Yet another bug from haste... let's hope this one fixes it.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@979 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-31 20:10:21 +00:00
abijah
9adb37a8e2 Fixed copy/paste bug introduced at the last minute on the prior checkin.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@978 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-31 20:07:29 +00:00
abijah
078497cca8 Added outstanding charges and outstanding credits to the customer page, and reorded fields in order of presumed importance. Removed items from Unit/Lease which really shouldn't have been there. They were added for convenience, but the redundancy was confusing. Added a couple report links to make it easy to see outstanding charges and collected rents.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@977 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-31 19:49:56 +00:00
abijah
c688586fcb On move-out, redirecting back to the customer instead of the closed lease
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@976 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-31 19:45:45 +00:00
abijah
b032dbd137 Fixed a sorting issue (depending on point of view). When sorting by date, or effective date, I find it useful to have the next order of sort be entry ID, so they can be viewed in the order actually entered. Ideally, we'll get to a 4 date system: charge-from date, charge-to date, effective date (whatever date the user feels is the right date for the transaction), and _actual_ date, which is the true and correct entry date/time and _not_ user settable. The names of each may need to be different, but that's where we need to head, in which case, we would sort on actual date instead of entry-id.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@975 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-30 16:02:43 +00:00
abijah
e813849c86 Added ability to see the customer when listing transactions
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@974 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-30 15:57:54 +00:00
abijah
3e68839706 Merge in from v0.2.3, a housekeeping release
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@970 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 04:38:59 +00:00
abijah
1b940d2adf Merge in v0.2.2 fixes
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@966 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 04:05:00 +00:00
abijah
69224ef661 Fixed the monthly charges report to only reflect income accounts (not things like security deposits). Also, by restricting to charges, the report was neglecting things like reversals, leaving the totals skewed. Thefore, the 'CHARGES' restriction was removed and replaced with the much more appropriate chargeDisbursementFields() function, which dumps fields that tabulate correctly.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@962 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 02:21:57 +00:00
abijah
eea66c6932 Changed the monthly charge report to limit itself to 11 months (plus the current month), and to sort in reverse order, keeping the most relevant month at the top.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@961 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 02:02:42 +00:00
abijah
df367d8316 Added a lease up report
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@960 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 01:51:32 +00:00
abijah
9848a2919e Forgot to add the report views for the last two checkins :-/
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@959 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 01:31:05 +00:00
abijah
63511709a8 Added Monthly Charges report. Also, added a new area to the menu specifically for reports.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@958 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-12 00:38:34 +00:00
abijah
1f831551a1 Added a Unit overview action. It's a bit quick and dirty, but it's intended for admins only.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@957 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-11 23:40:47 +00:00
abijah
f89ca2ab93 Renamed the work branch, since it will be used for bugfix revs as well.
git-svn-id: file:///svn-source/pmgr/branches/v0.3_work/site@944 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-02 18:04:47 +00:00
abijah
4562bc8314 Branching for 0.3.0 work
git-svn-id: file:///svn-source/pmgr/branches/v0.3.0_work/site@941 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-02 03:30:17 +00:00
abijah
cf9be800e5 Merge in the v0.2.0 work
git-svn-id: file:///svn-source/pmgr/trunk/site@938 97e9348a-65ac-dc4b-aefc-98561f571b83
2010-03-02 03:25:52 +00:00
abijah
9312f8d49c Merge in from the v0.1.0 tag, which is a bit of a botch. The intention was to make the change to trunk, then re-label as v0.1.1. However, due to a mixup, this was put directly as the v0.1.0 tag. It isn't good, but the change is small enough we'll live with it this time.
git-svn-id: file:///svn-source/pmgr/trunk/site@864 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-10-02 15:54:00 +00:00
abijah
cc12d5ff93 Merge in from pre_0.1 branch
git-svn-id: file:///svn-source/pmgr/trunk/site@847 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-09-15 02:38:28 +00:00
abijah
7b3707ef89 Pseudo merge-in, which is a wholesale copy from the branch directly to trunk. This is fine since trunk has always been empty up to this point.
git-svn-id: file:///svn-source/pmgr/trunk/site@732 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-19 21:09:19 +00:00
abijah
4d615d8028 More cleanup of lingering jqgrid remnants
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@620 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 15:48:59 +00:00
abijah
04dc17409e Upgraded to jqGrid 3.5.2, which has a couple bugfixes. Fixed a bug in our code preventing the grid query from being shown when in development mode.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@619 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 15:33:45 +00:00
abijah
eb39495fa4 Removed the development function for fixing the ACH deposit items.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@618 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 07:17:21 +00:00
abijah
2a28adaca7 Added tooltips to the sitemap. At some point, we hope to incorporate a jquery plugin for nicer presentation, but this gets the basics.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@616 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 06:41:49 +00:00
abijah
2866145e66 disabled logging when assessing late charges
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@615 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 04:00:17 +00:00
abijah
7049542e59 Modified the grid such that the account is right next to the dollar amount. It was too distracting for the eyes to have to span columns putting charges and amounts together.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@614 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 03:59:51 +00:00
abijah
3488774481 Fixed special routing to support a top level url.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@613 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 03:58:24 +00:00
abijah
480170b944 Fixed bug in the age() function that was preventing the ' ago' text. Of course, there are places it may not be desired, so I added a param to control the suffix.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@612 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 01:33:38 +00:00
abijah
3a86a0f689 Added comment field back in (not sure when/why/how it went missing).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@611 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 01:32:28 +00:00
abijah
4ca6c8bf6c Disabled logging again
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@610 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-18 01:32:09 +00:00
abijah
2b8fa33f06 Added a column to Tender that identifies the exact ledger entry which was used to deposit the tender. It was necessary due to the kludgy way that I've set ACH items to auto-deposit, which uses a ledger entry that has nothing to do with the transaction account. This would probably allow us to eliminate the deposit_transaction_id, but I'd like to break as little as possible at the moment. I'll come back and clean this up in the future. Also, fixed a stupid bug that was causing major database thrash whenever a transaction was entered without a customer id. I could have fixed the Customer::update() function, but it was designed to accept null so that we could update all the customers, something definitely useful while developing.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@609 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 23:55:51 +00:00
abijah
8167cc6a6a Added an assert whenever we lookup an account that doesn't exist.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@608 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 23:51:09 +00:00
abijah
1c82d55c1c First pass at making ACH items auto-deposit. Things are really set up for a separate deposit transaction though, and I should just bite the bullet and do that instead. I don't want them to show up as Deposits though, but perhaps it would be easiest just to make a new type 'AUTO_DEPOSIT' or something.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@607 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 21:16:16 +00:00
abijah
ff37597b81 Added a grid load error indication, since it was lost when we disabled the debug output.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@606 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 18:41:17 +00:00
abijah
3ba1e8b97a Fixed a bug when transitioning to a move-in receipt, and changed the invoice/receipt grids to only contain current customers / active leases. A non-current customer or closed lease can still have a receipt/invoice, the user will just have to go directly to the customer / lease first. OK, so I just checked and that's not true. Perhaps it should be...
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@605 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 17:23:50 +00:00
abijah
5abe0770b3 Modified the customer update() function to update all customers if passed null. This would not be a typcial scenario, but it useful at the moment during development.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@603 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 06:32:32 +00:00
abijah
287d9f8f11 Fixed a bug with charge through / paid through, which was not taking into account reversed charges. I fear there are other conditions not being accounted for, not only here but throughout. One at a time for now...
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@602 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 06:31:42 +00:00
abijah
d3d5b54b39 Eliminated (at least temporarily) the single ledger entry view, redirecting to the double entry view instead.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@601 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 06:30:01 +00:00
abijah
4f6f478a9a Hopeful fix for the bug affecting customer security deposit balance. It was introduced when we added the Customer Credit account, since Security Deposit no longer _directly_ pays charges like it used to. Now, it's converted to customer credit, and _that_ account is the one that pays. The security deposit situation is a bit shaky, but seems to be working again.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@600 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 05:48:54 +00:00
abijah
b8d0e18014 Fixed bug when fetching data for editing a customer (the details function was deleted sometime back and might be worth putting back in). Also, added a tiny helper feature to update cached items while things are still somewhat unstable. Any time customer edit is clicked, the customer (and associated leases) will all be updated. This should allow an easy customer workaround in case there is a bug in the field.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@599 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 05:43:01 +00:00
abijah
4c44794212 Added confirmation page, and more importantly date and comment settings, when reversing a charge.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@598 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 05:37:29 +00:00
abijah
a167b8a885 Added code to make jqGrid development aware
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@597 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 03:44:09 +00:00
abijah
18d384ecb3 Merge in support for jqGrid 3.5 (plus a change to layout.css after the merge).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@596 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 03:39:29 +00:00
abijah
77db0c2ed9 Fixed the test for delinquency from 15 days (used while debugging) to 10 days
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@595 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 03:38:07 +00:00
abijah
ebd80a8313 Turned off debugging code that was left on.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@594 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 02:52:47 +00:00
abijah
23cdd0e497 Merge in from surplus_account_20090815 r592
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@593 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-17 02:22:54 +00:00
abijah
893fc74c0a A couple bugfixes and some more tweaks to how reversals are handled.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@566 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-15 16:47:55 +00:00
abijah
e76ad897a4 Getting closer on the reversal issue. There is definitely more testing to do, and some tweaks as well, but this may be approximately what we will finally settle on.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@565 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-15 03:11:16 +00:00
abijah
e9d92494c6 Fixed negative numbers
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@564 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-15 01:22:16 +00:00
abijah
d40f43c617 Cleaned up several places regarding redirect and rendering during development for debug purposes. Also, added a link to the intended redirect target when we've rendered instead. These changes haven't been tested, hopefully they are benign.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@563 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:49:42 +00:00
abijah
ac6a830d27 Change to how reversals are handled. In the process, I've tried to solidify _exactly_ what addTransaction will do, since it was becoming a house of cards of sorts. It was using special logic to decide things like whether to add ledger entries, statement entries, or both, whether to assignCredits afterwards, whether the generated receipt was to be considered a credit, and so on. Consequently, modifications to any calling function (addInvoice, addWaiver, etc) would often require addTransaction modifications, which would turn around and break all of the other calling functions. So, that embedded logic has been removed from addTransaction, and the rules of what addTransaction should do are now defined by the callers. This change is DEFINTELY not complete, as it probably has several bugs, and it DOES NOT YET WORK for reversals. I need a clean baseline to move forward from though, and this checkin approximates where we need to go.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@562 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:31:56 +00:00
abijah
83e552211f Moved the INTERNAL_ERROR calls to use the class function instead of the global one.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@561 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:21:21 +00:00
abijah
fe1b0626c2 Modified INTERNAL_ERROR to support inclusion of the blank layout, since all the javascript is lost otherwise. This should only matter for development. Also, fixed a bug with rendering when redirect is called but headers have already been output.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@560 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:10:16 +00:00
abijah
68df688f12 Minor tweak to grid caption and display of the entry id instead of the transaction id.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@559 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:07:50 +00:00
abijah
61a00f475a Allow the dollar sign in the input box, since it will get stripped off later anyway.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@558 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:07:02 +00:00
abijah
0c3f4c3e18 Fixed the currency function to always return in dollar amounts, and never in just cents, as the core currency function does.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@557 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:06:17 +00:00
abijah
90bf9bac0a Fixed url generation since it was not propogating the admin/dev routing items. Also modified the invoice at move in to exclude the customer list, and to transition directly to the receipt page afterwards.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@556 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:05:36 +00:00
abijah
4472a124f5 Dropped the security level for viewing statement and ledger entries
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@555 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:01:25 +00:00
abijah
09e44995f3 Fixed bug with statement entry counts when statement_entry_id is set
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@554 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 21:00:21 +00:00
abijah
e42c83c585 Added support for deleting (destroying) a transaction. This is strictly development/super-admin type functionality.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@553 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 20:58:50 +00:00
abijah
c31c3a3cdc Added the http request to the internal error box, since it will be necessary to help track down any reported errors.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@552 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 18:13:12 +00:00
abijah
469962dd40 Fixed a couple URL problems, including redirect, which was previously entirely unable to utilize our AppHelper for the url generation. Added a couple menu items into the Admin section. Changed debug to be OFF for any route other than the 'dev' one.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@551 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-14 18:11:33 +00:00
abijah
4d0108c6f8 Fixed the datetime bug
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@550 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 22:23:45 +00:00
abijah
f1d852aea6 Moved paid-through from the detail box, which is semi-static information, to the info box, which holds data that changes with time (i.e. the so called pertinent information).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@549 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 22:09:27 +00:00
abijah
8f6dd40b9a Modified the formatted date/age results to be span encapsulated for later css formatting.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@548 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 22:08:43 +00:00
abijah
1fee7646b6 Got rid of LATE as a unit status, as it did not represent an physical condition. The logic was moved to Lease, where it is a much better fit. The sitemap still presents LATE units, as it is a useful view, but the underlying logic is driven from Lease, not Unit. This checkin also includes a small feature change to how late charges are assessed, as well as a menu item to kick off the charge assessments (both accidentally wedged in to this changeset).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@547 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 20:55:19 +00:00
abijah
97d16bd712 Changed the default of the debug flag when generating grid data. The original intention was to have it debug by default, so that debugging would be on if the url were manually typed in. That has never been needed though, and this change not only results in cleaner logic, it makes sure we have no issues with routing.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@546 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 20:07:49 +00:00
abijah
9d716532bb Changed the custom route variables, so that the dev route could set the admin flag and not screw up the routing. The AppController now checks the routing params to set the dev/admin flags.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@545 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 16:43:07 +00:00
abijah
4be4224742 Added admin and development routes, which should help while in transition to user security.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@544 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 16:09:45 +00:00
abijah
b741bb6b01 Added cached fields for charged_through and paid_through dates of a lease. Also, added the ability to dynamically determine whether a unit is late or not. In reality, we really need this to be part of the lease, not one of the status types for unit. The sitemap, however, is driven from unit information, so it's not clear whether we should move that information or not.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@543 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-13 02:37:37 +00:00
abijah
af935ffd39 Changed naming to match that on the lease page
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@542 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-12 23:16:36 +00:00
abijah
a15466df50 Fixed bug causing a $0.00 security deposit charge on the move-in invoice
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@541 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-12 23:16:17 +00:00
abijah
f9ff27cad4 Many bug fixes, found while entering real facility data. Most are quite minor, although there was a functionality change to assignCredits, to support concessions without assigning them to anything other than rent. I've found that there is a glaring problem with charge reversals, which don't work correctly with the collected report. Of course, we're deleting prior disbursements, which obviously needs to be rectified.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@540 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-12 09:37:49 +00:00
abijah
e33741f579 Made viewing the deposit slip the default action when viewing the list of deposit transactions.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@539 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 18:25:40 +00:00
abijah
6b3266f022 Implemented very crude attempt at security privleges. This is not really intended to be security, just a quick and dirty mechanism to avoid prying eyes. More robust security is left to future implementation.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@538 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 18:22:21 +00:00
abijah
b1dfe5bd50 Added ability to customize the action on grid links
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@537 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 17:30:12 +00:00
abijah
3696aa011c Removed the Signed column from the leases grid unless requested
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@536 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 17:04:21 +00:00
abijah
6e03664f28 More tweaking with grids that need to be reloaded immediately after page load. Fixed a bug with the statement_entry balance field, as there were two of them.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@534 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 15:56:40 +00:00
abijah
1b0d752a55 Modified the statement entries grid, as well as the underlying controller, to support a couple new fields, 'applied' and 'balance'.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@532 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 07:27:56 +00:00
abijah
9cac7f481e Finally have a (slightly kludgy) fix for keeping the 'collected' grid from being rendered before ready.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@531 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 07:24:52 +00:00
abijah
9fe55bd782 Fixed bug with display on the bank deposit page, caused by reusing a class name in the edit tender page.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@530 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 07:21:34 +00:00
abijah
f8cb1e2ef7 Added ability to rename columns on a case by case basis
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@529 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 07:15:04 +00:00
abijah
baa737e628 Changed invoice generation to match the receipt changes in r521 (no more debug, transition to the lease after entry, etc).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@525 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 01:08:39 +00:00
abijah
aa307150c8 Whitespace only change
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@524 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 01:07:20 +00:00
abijah
0727d5f091 Fixed problem when effective_date is an empty string, as opposed to completely missing.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@523 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 01:07:01 +00:00
abijah
aa65c18c39 Renamed the menu items for invoice and receipt creation.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@522 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-11 01:06:28 +00:00
abijah
666df07a7c Removed the debug portions of receipt entry, and added a checkbox to allow the user to either keep entering receipts, or have the page automatically transition to the customer page.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@521 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 23:57:20 +00:00
abijah
452deb981f Missed adding this file on the last checkin (r519)
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@520 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 22:24:23 +00:00
abijah
29f6d1a717 Added the ability to edit a tender. I've locked this down to just editing the data1-4 fields at the moment, since there are accounting ramifications if we were to change the tender type.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@519 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 22:23:47 +00:00
abijah
025c3a555a Added automatic grid reload, since the default grid load is inconsistent with the displayed settings. The solution here sucks, but at least it avoids the inconsistency.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@518 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 22:22:56 +00:00
abijah
467387a581 Removed the two dangerous links from the Debug menu, as we're preparing to enter new data and don't want to zap it.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@517 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 19:20:29 +00:00
abijah
acb8fb44be Added the ability to accept payments (or a write-off) on a closed lease, if there is a balance owing. Added the ability to do write off bad debt at the customer level, since some charges may not be on a lease, like the NSF fee.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@516 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 19:01:20 +00:00
abijah
fea21b3ea0 Added the close date to the leases grid, to make it clear which customer leases are still open. Perhaps we should exclude the field by default, and just add it into the customer view page.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@515 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 19:00:02 +00:00
abijah
3a248fc1b4 Added Bad Debt as one of the accounts for the collected report
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@514 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 18:58:41 +00:00
abijah
d49201211b Added confirmation to the NSF functionality, as well as a timestamp field instead of forcing time=now.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@513 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 05:49:43 +00:00
abijah
4358732878 Changed the Deposits menu item to be at the top level, instead of underneath the Accounts menu.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@512 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 05:48:49 +00:00
abijah
fdcdc94bec Modified NSF to use positive amounts for the ledger entries (swapping credit/debit, of course). The statement entries remain using negative amounts, as they are negative disbursements.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@511 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 04:46:54 +00:00
abijah
9a3a24ca0c Preventing moved-out leases in sitelink data from being closed, since the security deposit hasn't been released. Added a temporary function to release the deposit, so I can manually release and close the few leases needed.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@510 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 01:27:12 +00:00
abijah
35f17ed1de Added customer since/until fields. Changed the ledger entry grid on the customers page into a receipts grid, and added it to the lease page as well, even though it is for the customer in general, and may include receipts for leases other than the one being viewed. I may put more effort into this later, but for now it solves the problem of getting the receipt tenders visible when viewing the lease.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@509 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-10 00:56:41 +00:00
abijah
bc95b31521 Added transaction model writeOff function, and a new statement_entry WRITEOFF type. Not a big deal, but it makes presentation a bit more straightforward.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@508 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-09 15:11:51 +00:00
abijah
bd26ec7218 Added mechanism to automatically assess late fees. This uses hardcoded assumptions, since our late fee table has not yet been implemented.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@507 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-08 16:37:12 +00:00
abijah
8df94e9c5f Implemented a single function to assess rent across all leases.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@506 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-08 16:09:49 +00:00
abijah
6f62d84818 Implemented mechanism for automatic assessment of monthly rent
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@505 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-08 15:44:52 +00:00
abijah
b02a504abb Add refund capability to the customer, and in fact only the customer, as we've revoked refund capability from the lease. This is to help work through various issues surrounding use of security deposits and general refund functionality. For example, a customer who has overpayed (customer surplus, with zero balance on lease), and then moves out. Where that security deposit surplus goes has been a bit of a thorn. Hopefully, this resolves the issue, although there may still be some bugs to flush out.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@504 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-08 04:27:02 +00:00
abijah
bba3af8163 Some of the finer detail work, mostly around pre-populating the move-in invoice with useful and correct data, and allowing the lease rent and deposit to be set at movein.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@503 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-07 22:52:01 +00:00
abijah
16f7de421f Minor tweaks, a shame for checkin r500 :-(
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@500 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-07 03:55:01 +00:00
abijah
c3e60a65c1 More work with security deposits, reversals, and balances. I've tried to work many different corner cases, but know that not everything has been tested. I think the next steps for testing will be to put in some real data.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@499 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-07 00:10:28 +00:00
abijah
f900853163 Added in internal error function, since the die() statements were hard to spot, and certainly not user friendly for the end user.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@498 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 20:17:02 +00:00
abijah
0de0c7d5d7 Fixed the button text for new customers
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@497 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 18:08:55 +00:00
abijah
a2aa612a5e Fixed the customer selection update for receipts, and added a mechanism to automatically update the oustanding charges grid after entering the receipt.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@496 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 18:08:21 +00:00
abijah
b900e49f54 Added mechanism to do a full replacement of specified post parameters, instead of just merging.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@495 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 18:06:57 +00:00
abijah
ad4e194bf9 Added ability to edit a unit, except for sort/walk order. To handle those things we'll need to: save unit's old sort/walk position; adjust down (by one) all unit positions greater than the old position; adjust up (by one) all unit positions greater than or equal to the new unit position; update the unit's position. I'm not going to worry about it right now.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@494 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 09:27:09 +00:00
abijah
a5e7366cac Implemented refund, at least for the most part. Minor testing, but looks promising. Because of this change the customer account entries grid appears odd, with a refunds showing up as a 'Charge'. So, I'm toying with the idea of having entries show up as customer 'Debits' and 'Credits'. I don't know if this will cause user confusion, but we'll play with it for a while and see. It actually reminds me a bit (coming full circle) of the earliest implementations, which kept track of a lease on its own account/ledger, in which credit/debit would be the exact correct terms.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@493 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 05:11:58 +00:00
abijah
bd85ce9f51 Added ability to format currency without a dollar sign (i.e. in raw numerical format).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@492 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 05:04:34 +00:00
abijah
c58183c965 More work on refund. I'm going to skip the whole voucher/credit_note bit and simply present a page that lets the user enter a date, an account, and the amount to refund, recording 1 payment transaction.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@491 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-06 02:52:45 +00:00
abijah
adc901b7e7 Several changes in an effort to support charge reversals. I can't imagine this is all working flawlessly, as I'm not quite sure how it even _should_ work.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@490 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-05 07:54:57 +00:00
abijah
a84655bcb7 Modified to use toggle for 'this' debug pr block, instead of independent show/hide
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@489 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-05 02:59:09 +00:00
abijah
6a7a784cb6 Discovered that the use of the term 'Payment' has been a misnomer. A company uses the term payment to indicate the monies it pays to _others_. Changing this leaves us without a good replacement term, but disbursement really seems to fit the bill. As comfortable as the term payment was, it was odd in many respects and disbursement does seem more appropriate. I'm sure there are several lingering bugs from this massive search/replace exercise, but this is a decent baseline for the change.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@488 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-05 01:00:09 +00:00
abijah
7c9637ae35 Added a more descriptive fieldname for statement entry views. Instead of Transaction, it will be Receipt, Invoice, Deposit, etc.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@487 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-05 00:06:46 +00:00
abijah
5d934f756f Brought the bad debt write-off functionality up to date. I'm not entirely convinced that calling it a receipt is such a good idea, but bad debt is certainly a non-normal case, and we can work on this later if need be.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@486 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 23:25:31 +00:00
abijah
6904320e4e Modified lease and customer views to have the statement & ledger entries sorted in descending date order by default. May not prefer this in the end, but we'll give it a go for a while.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@485 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 22:12:32 +00:00
abijah
17297beb65 Undid the change from r401, since it wasn't being used and was interferring with the specification of sort_column. If we really want that functionality, we can add it in when needed and come up with a more general solution for view specific sorting as well.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@484 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 22:09:18 +00:00
abijah
e0ea50d142 Fixed bug when marking NSF of a tender that has only been used as a surplus and never applied to any charges. Modified the applyCredits algorithm to try and distinguish between surplus credits of a lease and general customer surplus. I don't know if it works completely, but I do know it creates an issue since a lease surplus can now never be utilized without additional lease charges, a refund (no yet implemented), or moving the surplus to the customer level (not intended to be implemented).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@483 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 21:19:20 +00:00
abijah
81b5bfed26 Made the link clearer that we'll only be waiving the balance of a charge, not the entire charge.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@482 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 20:26:33 +00:00
abijah
2ddcdf9419 Even with all the effort put into getting the counts right on the customers grid, it still didn't work right. The root of the problem is the join to CurrentLease, which can result in multiple rows for the same customer. I can revisit this in the future to put some clever solution back in, but in the meantime, it was easiest just to add fields to the customers table, and simply update it whenever the customer lease situation changes. I don't like it... but it just made life much simpler.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@481 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 19:25:39 +00:00
abijah
e0d15b7396 More work cleaning up and testing the use of charge waivers, as well as security deposit utilizations. Much of this was just around the concept of determining balances, which wasn't / isn't very straightforward. It's not hard to calculate a balance for a particular sitation, but it is difficult to generalize. I think it's reasonable as it now stands, but the StatementEntry::stats algorithm is certainly subject to change. Also, I didn't double check every case which called the stats() function, so I highly suspect we'll find cases where the code is not expecting the new return values, either symantically or logically.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@480 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-04 03:12:09 +00:00
abijah
aa846de284 Continued waiver progress. At the moment, it works ok, but I don't like the way that security deposit balances work. It's probably a general issue, not just security deposits, but it's not clear whether stats from StatementEntry should be subtracting waiver totals from the overall charge reconciliation total. It should in some cases, and not in others. I'll tweak on it in later checkins.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@479 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 21:50:21 +00:00
abijah
09e650d841 Fixed minor bug that was causing the string 'undefined' to be used as the payment index.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@478 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 21:47:24 +00:00
abijah
61e9dad364 Fixed cut/paste error causing invalid customer security deposit balance.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@477 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 21:44:36 +00:00
abijah
91f5ad7660 Added HR to keep the boundary between two pr blocks clear even when the output is collapsed and the stack trace is not.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@476 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 20:17:14 +00:00
abijah
14d896d1b3 Added ability to log items without showing the output. I doubt the mechanism works perfectly in all cases, but it works enough for now and I can tweak it as needed.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@475 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 20:13:48 +00:00
abijah
d2795e8201 Added the ability to suppress logging at display time. I'd like to add an additional log level, the first which passes or suppresses the print, and the second which defaultly displays or hides the output. Perhaps next checkin.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@474 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 19:41:49 +00:00
abijah
df91125e96 Added stack tracing to each model::pr print
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@473 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 18:38:35 +00:00
abijah
7fda78f820 Added ability to waive a partial charge.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@472 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 04:14:44 +00:00
abijah
a59d93ebee First pass at a charge waiver implementation. It hasn't been tested but for a tiny bit.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@471 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 03:46:58 +00:00
abijah
fe66241ca1 Work around for the fact that our implementation has issues with receipts of more than one tender. See the branch statement_ledger_entry_tie_20090802 for more information.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@470 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 03:45:51 +00:00
abijah
4c8e3b380e Left r466 with syntax error... fixed that
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@469 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-03 02:30:29 +00:00
abijah
6b357fc285 Added a new statement entry type, 'WAIVE', and added the beginnings of a function to add a new waiver transaction. Haven't even started the work yet though...
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@466 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-02 22:44:55 +00:00
abijah
a9ef3a75ae Added a check to ensure we're not adding a close with no accounts. This check should probably be in the model, but that can be done later.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@465 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-01 01:48:07 +00:00
abijah
267357a0f7 Combined the addNsf and addTransaction functions, which was always intended. There is more that could be done, but not all bugs are flushed out yet. Its close enough though, that I want to capture it as is to help work through the subtle issues.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@464 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-08-01 01:47:11 +00:00
abijah
1c2c2b457c More work on handling security deposit utilizations. This is headed in the right direction, but unfortunately, there is a problem if the security deposits have not been paid. I could just ignore it, because it's a low priority problem, but I think the solution might be to just provide a waiver on the unpaid charges. Since I need to implement fee waivers anyway, I'll just move onto that next, and then finish out the security deposit work.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@463 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 21:11:00 +00:00
abijah
b00944a412 Changed the reconciledSet to provide an option for returning partially reconciled results. This keeps the same behavior so as not to break anything (although I can only think of paid rents that might suffer).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@462 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 21:07:00 +00:00
abijah
4ef09080be Left a syntax error on the last checkin :-/
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@461 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 19:23:19 +00:00
abijah
b41ce38526 Fixed a couple small bugs with assignCredits, one of which was causing charges to get paid out of order.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@460 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 19:20:35 +00:00
abijah
f210388f6c Added logging
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@459 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 18:35:39 +00:00
abijah
645a3a4797 Removed the ability to NSF items like Cash
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@458 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 18:19:20 +00:00
abijah
8f79f0401f Added a way to associate the meaningful part of NSF with the corresponding tender.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@456 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 18:10:48 +00:00
abijah
e40778ddbd Doh! So someone actually _was_ using the gridDataFilterConditionsStatement function. Guess it would have been worth grepping for...
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@455 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 17:57:30 +00:00
abijah
6b7ce60ae3 Changed the default log level to 5, since we're using rather high levels anyway.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@454 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 17:43:51 +00:00
abijah
2b09f4054f Added some comments, and changed the nsf action to redirect to viewing the tender after creation.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@453 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 17:15:17 +00:00
abijah
33a677431f Some cleanup from the last checkin
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@452 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 16:48:27 +00:00
abijah
9bd37ba282 Added the ability to perform partial deposits. After this checkin, I'll clean up some of the commented out sections of things that we're attempted to get this all working.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@451 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 16:41:56 +00:00
abijah
53a8d8fc60 Fixed a bug where the condition is of the form array('field' => null). That's a valid condition, but was being treated the same as array('field'). This resolves it, although it will break anyone using the gridDataFilterConditionsStatement virtual function. I don't think anyone is at the moment, although I didn't check.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@450 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 03:46:59 +00:00
abijah
4c7e3d1f58 Removed the comment block, since it's not longer needed and has now been saved.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@448 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 03:29:17 +00:00
abijah
5dc5f087ba Added an automatic $35 nsf charge when marking an item as nsf. This required removing the lease_id requirement when adding a transaction, since the charge is not associated with a lease, just a customer. The requirement was artificial anyway; imagine selling POS items to folks that don't even lease from us...
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@447 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 03:07:34 +00:00
abijah
46744a24d3 Modified to use the logging mechanism
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@446 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 02:09:45 +00:00
abijah
b553dbb9c3 Modified to use the logging mechanism
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@445 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:45:57 +00:00
abijah
3c390d940f Modified how NSF customer surplus items are handled. Instead of creating a negative surplus (which was dumb, but a decent first pass), we just void out the surplus. This drops the surplus off the radar, and in fact may cause confusion since the transaction no longer has enough statement entries to total up correctly. However, it doesn't appear too confusing, especially as tender item names are now decorated with strikethrough when they're NSF (the previous checkin).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@444 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:40:16 +00:00
abijah
777a47a27b Added strikethrough over the tender name in grids when the tender is NSF. This helps really call out the situation with NSF items.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@443 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:38:53 +00:00
abijah
237b0a901f Shrunk the comment column in half, since it's usually empty. All we really need is an easy way to spot that there is a comment, and the user can hover or widen the column as needed.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@442 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:35:45 +00:00
abijah
393d9c9d77 Removed the Tender column from grids where the account does not support payments and so the ledger entries should never have a Tender anyway (except for our special case Closing account, which is fine to ignore).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@441 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:34:45 +00:00
abijah
17cd00c90e Renamed prExit to prReturn, to be consitent with the return statement where it should be used. Also, modified the chargePaymentFields to be more explicit on the balance, in anticipation of the new type VOID
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@440 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:31:45 +00:00
abijah
066acd14f0 Changed prints to use the new logging system
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@439 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 01:23:27 +00:00
abijah
87db010dc1 Discovered that an earlier implementation of Model::pr is now being overridden by the AppModel. This is probably a good thing, but it does mean that we're getting a ton of Model prints that we don't want. I added a class logging mechanism to allow individual classes to be turned up or down, and set the default for Model to be very low
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@438 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-31 00:00:10 +00:00
abijah
9360ebe661 Added the debug file/line output directly into the AppModel::_pr function, as the builtin pr function simply logs the same file line for every call.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@437 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 23:32:10 +00:00
abijah
32c81b7367 Finally added a logging mechanism. Only one file has been converted at the moment. I'll go in pieces, as debugging goes on.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@436 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 23:20:47 +00:00
abijah
0b8d8fe1ac Changed the StatementEntry type enum from CREDIT to SURPLUS. I never liked using the term CREDIT, since its use is confusing along side DEBIT/CREDIT. I couldn't think of another name earlier, which is why I used CREDIT, but I decided anything other than CREDIT is good, so I chose SURPLUS.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@435 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 21:40:38 +00:00
abijah
0cb961925d The conditions were clamped down too tight, as they prevented something like 'Table.field !='
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@433 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 21:34:03 +00:00
abijah
fbfc973dad Included a possible mechanism for including the nsf ledger entries when viewing a tender
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@432 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 21:06:02 +00:00
abijah
24f245c3c8 Tweaked filtering to allow for non-trivial conditions
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@431 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 20:59:33 +00:00
abijah
e7e3dbc559 Modified the filtering behavior; A filter specified as 'Table.field' results in the obvious split (Table, field); 'Table' results in (Table, id); 'field' results in (ModelTable, field). Fixed a bug in the app controller which was allowing 'fields' to leak into the top level of 'link'
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@430 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 19:45:19 +00:00
abijah
de0cf6467a Moved the filtering split to a virtual function.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@429 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 19:07:21 +00:00
abijah
48fb7383de Added effective date to each NSF entry, and fixed the account bug with receipts/payments. There is still a bug/question on how to handle nsf credits. It would be nice to simply delete the credit, since the check bounced, but then the original transaction will not add up. Either we decide that it doesn't need to add up, figure out a different type for the credit, or add a void flag to each statement entry and just void out nsf items. Or instead of a void flag, have an nsf_statement_entry_id which will also tie the payment/credit to the nsf payment/credit.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@428 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 18:23:37 +00:00
abijah
94c0efa881 This is probably what we really want for NSF... two transactions. One for backing out the bank deposit, and the other for adjusting A/R and all the associated statement entries. This is working reasonably well, even seemingly fine for the collected rents report. I do see that there is another bug though, that is causing statement entries to have the opposite account_id than intended (A/R instead of Cash) when entering new receipts. Not sure when I broke that, but it must have been when working on deposits.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@427 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 07:30:50 +00:00
abijah
7450d2a98e Preliminary work on NSF. It's far from working, but we're headed in the right direction.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@426 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 06:29:41 +00:00
abijah
3e715bae9f In the cleanup of r423, some code accidentally got squashed. This checkin undoes that problem
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@425 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 06:09:05 +00:00
abijah
347aa20622 Fixed problem when trying to calculate transaction stats. The current solution is a bit dodgy, but by limiting the queries to LedgerEntry and StatementEntry, it works without too much extra effort.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@424 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 02:47:43 +00:00
abijah
510ca489ec Cleanup of old lingering functions and some comment blocks
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@423 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 02:39:09 +00:00
abijah
e4426cde55 Fixed the problem with zero dollar balance forward entries when closing a ledger.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@422 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 02:17:49 +00:00
abijah
25e3c87213 Got the ledger closings to work again. This seems to work ok, although I notice closing the ledger after deposit results in a balance forward entry of $0.00 . I'll work on it next
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@421 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-30 01:59:10 +00:00
abijah
eb209a455b Disabled submission of tender types that have no items
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@420 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 23:25:26 +00:00
abijah
ccf7a5fccd This is the remainder of the previous, invalid, partial checkin.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@419 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 23:25:03 +00:00
abijah
a8862ea3fd Moved deposit creation to the transactions controller. Added the ability to view prior deposit slips.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@418 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 23:17:00 +00:00
abijah
f6a93af27d Added a minimum age feature to the date formatting. It's not really a perfect solution, but it's working for our needs at the moment.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@417 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 23:16:26 +00:00
abijah
362226183e Changed the deposit to use a grouped double entry instead of individual ones for each tender deposited
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@414 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 10:10:11 +00:00
abijah
438610bdf9 Added customer association to each tender
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@413 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 09:33:29 +00:00
abijah
cdf6ce86e3 Re-implemented the deposit functionality. This is mostly working, although I'd like to get customer added to the tenders table, and probably change to a single deposit ledger entry for each tender type. A single entry would require that all tender types have been recorded to the same account, something that isn't mandated at the present, but is likely to be true most of the time. Perhaps they could just be grouped by account id, which should work in all cases and yet align with tender type 99% of the time. I'll have to think about it.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@412 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 08:55:09 +00:00
abijah
3128880c4d Fixed problem with grid display when part of a form
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@411 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 08:50:44 +00:00
abijah
e23413b4d8 Cleaned up some of the old code concerning the deposit action. It's not done, as I'll next be experimenting with having the tenders controller handle deposits.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@410 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 02:43:30 +00:00
abijah
3a6964c4bf Added more information to the tender grid, and a new query to return depositable types
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@409 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 02:41:50 +00:00
abijah
95f43f07bf Fixed the pluralization of cash
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@408 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 02:39:31 +00:00
abijah
bfa71375e8 Started work on handling NSF, but will deal with deposits first, since we can't get an NSF without a deposit first.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@407 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 01:51:34 +00:00
abijah
48123c98b8 Minor change to the default accounts on the collected screen. Probably will be tweaked again later.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@406 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 01:50:33 +00:00
abijah
9d35ed1aa9 Added the debug class to the debug grid query link, so that it doesn't show on print media.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@405 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 01:43:08 +00:00
abijah
ae35d637ac Increased font size for the page number input at the bottom of each jqGrid
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@404 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 01:42:16 +00:00
abijah
6cd0a1378d Added a print media CSS file, and fixed the jqGrid print problem of truncating the table to a single page. Also, used the opportunity to disable printing of the side menu and the debug toolkit, as well as some more jqGrid cleanup.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@403 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-29 01:41:34 +00:00
abijah
1d6c7436d4 Got the collected reports working again. NSF hasn't been tested yet, because NSF entry is not yet working.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@402 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 16:08:15 +00:00
abijah
aa0a60ea2b Modified the grid helper to allow a view to directly specify custom post data without wiping out any custom post data set by the element. It's not been used or tested.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@401 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 16:07:34 +00:00
abijah
d8fe7f9264 Suppressed all the debug print statements from transaction insertion.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@398 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 02:40:42 +00:00
abijah
1372bb68e4 Half-kludgy fix to a filtering bug when a table is already specified using the simple notation, e.g. array('Table'), instead of the more complete form array('Table' => array(...))
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@397 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 02:39:44 +00:00
abijah
cb5b017c7e Bug fixes to get lease security deposit and balance information working.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@396 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 01:18:02 +00:00
abijah
8e720dbfec Added a rent column to the units list
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@395 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 01:17:09 +00:00
abijah
24ccb56e50 Further tweaked the code to add new transactions, primarily by creating separate verification and addition functions in each underlying model. There are logic changes as well, such as adding in the other half of the missing double entry (as well as the double entry itself). This checkin also introduces the creation of CREDIT statement entries, when the customer has overpayed. It's working fairly well, although undoubtedly will need more tweaking.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@394 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 01:16:42 +00:00
abijah
4adf0dd2fc Added a debug dump function to print out a model without getting stuck on the recursive inclusion of other models.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@393 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-28 01:11:27 +00:00
abijah
24bca2c8e9 Implemented a mechanism to track customer overpayments (credits). Also implemented a reconciling algorithm, matching payments to charges. Preliminary testing seems to show that it works well. More thorough testing required.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@392 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-27 04:18:38 +00:00
abijah
2cc58255c3 Playing with different means for determining stats. Nothing decided yet.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@391 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-26 23:23:19 +00:00
abijah
36f439ff38 Added ability to toggle between one big closing ledger entry, and one per house
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@390 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-25 08:08:47 +00:00
abijah
7a42bee8ef Added transaction stats. Added (possibly unwanted) columns.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@389 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-25 08:06:50 +00:00
abijah
0170dbc310 Fixed yet another bug in the merging of actions & options
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@388 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-25 07:57:56 +00:00
abijah
b820929ddc Made progress adapting the receipt code onto the latest db changes.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@387 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 07:43:42 +00:00
abijah
743ff52e59 Made changes to get field alignment on receipt entry. I can get the text on the left by floating the label, but actually, I think I prefer the boxes on the left.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@386 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 05:01:29 +00:00
abijah
efb4655786 Reverted back to r383.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@385 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 04:16:33 +00:00
abijah
7c34c7faff I tried using a table for the receipt entry, which seems like it should work, but really doesn't. I'll check it in for future reference, but I'll be reverting just afterwards
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@384 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 04:15:39 +00:00
abijah
e448519a1e I accidentally tried to cehck in tender.php instead of the intended tender_type.php
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@383 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 03:58:57 +00:00
abijah
a4dff8a6ed Begin the move from having receipts generated based on 'collectable' account, to being based on what the database considers to be a form of tender. This has the excellent benefit of removing all those hardcoded data1/2 fields for each different tender type. That's all database driven now. There is more to do, but this is a good step forward.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@382 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 03:58:12 +00:00
abijah
2db0fc12d3 Forgot this file on the last checkin, since it was using the trackable field
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@381 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 03:55:54 +00:00
abijah
991d4d717e Modified the application to reflect the changed field names in the accounts table. For example, payable is now payments.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@380 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 03:22:56 +00:00
abijah
446ba0ce28 Added a much more user friendly way to present the data1-4 labels for each type, now that we have a tender_types table. Still not a perfect solution, but definitely good enough for now. Modified the application to recognize the new tender_types table. There may be other modifications necessary, but this gets the most obvious spot.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@378 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-24 02:07:43 +00:00
abijah
4cab2c09c1 Fixed some minor display issues and tweaks to compensate for the new stats() algorithm. More are necessary, I'm sure.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@375 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 23:54:40 +00:00
abijah
127f7f3eb9 Finally resolved the StatementEntries::stats() query issue. It has been lingering in an incomplete state because it wasn't clear what it was supposed to really do. I think it is now performing the task expected of it.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@374 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 23:52:59 +00:00
abijah
ce23bf5abd Fixed a bug with idlist caused when we moved all the post data into the 'post' field
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@373 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 23:45:04 +00:00
abijah
5cf40147d9 First pass implementation at generating an invoice, which seems to be working. Largely untested, but worth checking in. Next is to get receipts using the same algorithm, and after that will be to work on a reconciling mechanism, creating payments, and matching them to charges. This checkin includes an additional customer_id fields as part of the transactions table. I think it was an oversight not to be there, as we need some way to keep track of monies which have been paid by a customer, yet not applied. If there is a different way to do it (short of actually having each ledger entry hold customer_id), it escapes me at the moment.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@372 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 21:24:49 +00:00
abijah
4a94fc3798 More tweaking to all of the grid displays. This was mostly visual, but includes some bug fixes as well (such as a manual filter override in the ledger_entries controller).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@371 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 16:53:33 +00:00
abijah
3c22378498 Modified Filtering to have a separate virtual function for Tables and Conditions. This was prompted because a derived controller could override the function to return null to prevent adding a filtering table (such as if the table was already added as a non top-level table), and although the app controller would correctly prevent adding the filter table to the set, it would also prevent adding the filter condition as well. So, making two virtual functions, one for table name when defining filter tables, and one for table name when defining filter conditions, allows the derived controller to stop a table from being added to the set, yet still let the condition through (without having to override the entire filter condition logic).
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@370 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 16:51:32 +00:00
abijah
43b4ef8890 Yet another tweak to Linkable for merging fields and conditions of the options and associations variables.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@369 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 16:35:53 +00:00
abijah
25bc1607cc Many, many changes, and yet still much to do. Many things are working, but certainly nothing beyond simply data retrieval (no editing or adding of any data). Also, some work is still required to ensure the grids have the right columns; we can strip out certain columns for some views (such as removing customer from the leases grid of the customer view... completely redundant). And of course, there are still many bugs and lots to clean up.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@368 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 01:49:46 +00:00
abijah
85edb1c658 Changed the default table configuration to be link instead of contain. contain was never really desired, it just got thrown in there since it wasn't clear how link would behave, and we needed the essential effect of recursive=-1.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@367 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-23 01:11:30 +00:00
abijah
368b730744 Added automagic filtering capability to the app controller. Also, lumped all items needing serialization into a single 'post' item, instead of continuing to piecemeal each added post item. There will be some changes required to some controllers, since custom post data has changed location, but it should be minor. To make use of the new filtering methods, each view will have to set the 'filter' parameter at the time of grid setup.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@365 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-22 23:31:41 +00:00
abijah
8db607ffc5 Of course, the last fix didn't work... I don't know why it looked like it was working. This fix should resolve the issue.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@364 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-22 23:12:04 +00:00
abijah
030617713d Attempted fix for a missing conditions/fields bug. The fix would probably have just been to use empty instead of isset, since apparently the defaults for these items are an empty string instead of null (yuck). However, I already had done a bit of minor logic change and prefer the new form, so that's why more than just the isset call was modified.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@363 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-22 22:54:02 +00:00
abijah
1e359f816c Fixed the Linkable behavior from polluting the query fields when performing a count query.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@362 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-22 22:27:55 +00:00
abijah
b641dc98ec Renamed jqGridData functions to be just gridData. Restructured the virtual function calls with an anticipated need for gridDataCount to be overridden as a whole, instead of just overriding the individual pieces.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@361 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-22 17:06:44 +00:00
abijah
f52fba96c1 Making progress. Much still to do, but there are hints of functionality finally returning so I'm snapshotting.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@360 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-21 10:23:52 +00:00
abijah
5a16e497ad Customers is working somewhat. I want to see if the grid query logic can be simplified, so I'm checking in first
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@359 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-21 09:40:51 +00:00
abijah
d5bcd9a496 Another snapshot
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@356 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-20 23:35:11 +00:00
abijah
bfbf119aca Modified Linkable to be recursive, which is needed to support certain queries that have models joined based on prior joined models. The old way went breadth first, which prevented such a join from working
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@355 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-20 20:22:06 +00:00
abijah
7181bc7f9b Minor logic and formatting changes. Considering making Linkable operate recursively.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@354 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-20 19:00:54 +00:00
abijah
7304a0e889 Another snapshot. I think I'll be taking the CREDIT/DEBIT reconcile functionality out.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@353 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-20 02:17:54 +00:00
abijah
3d262fd4db This rework is nowhere near complete, but there are certain things that are falling in place, and worth capturing. I started a branch for just this purpose of being able to check in intermediate work.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@352 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-19 23:35:25 +00:00
abijah
af0c10f6a6 D:\bin\svnbranch.pl: Branch from /branches/invoice_receipt_20090629 to /branches/yafr_20090716
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@351 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-07-16 18:52:28 +00:00
Abijah
d4122b207b Created directory structure
git-svn-id: file:///svn-source/pmgr/trunk/site@1 97e9348a-65ac-dc4b-aefc-98561f571b83
2009-05-27 04:16:20 +00:00
521 changed files with 13832 additions and 30689 deletions

View File

@@ -3,3 +3,23 @@
RewriteRule ^$ webroot/ [L]
RewriteRule (.*) webroot/$1 [L]
</IfModule>
# Lets deny everyone -- its a clean slate!
order deny,allow
deny from all
# Now allow local access
# Localhost
# allow from 127.0.0
# Local subnet
# allow from 192.168.7
# Provide a mechanism for user authentication
AuthType Digest
AuthName "Property Manager"
AuthUserFile "D:/Website/auth/pmgr.htpasswd"
Require valid-user
# Instead of satisfy all (too restrictive)
# This allows EITHER local domain OR authenticated user
satisfy any

File diff suppressed because it is too large Load Diff

View File

@@ -37,5 +37,14 @@ App::import('Core', 'Helper');
* @subpackage cake.cake
*/
class AppHelper extends Helper {
function url($url = null, $full = false) {
foreach(array('sand_route', 'dev_route') AS $mod) {
if (isset($this->params[$mod]) && is_array($url) && !isset($url[$mod]))
$url[$mod] = $this->params[$mod];
}
return parent::url($url, $full);
}
}
?>

View File

@@ -42,6 +42,275 @@ class AppModel extends Model {
var $useNullForEmpty = true;
var $formatDateFields = true;
// Loaded related models with no association
var $knows = array();
var $app_knows = array('Option');
// Default Log Level, if not specified at the function level
var $default_log_level = 5;
// Class specific log levels
var $class_log_level = array('Model' => 5);
// Function specific log levels
var $function_log_level = array();
// Force the module to log at LEAST at this level
var $min_log_level;
// Force logging of nothing higher than this level
var $max_log_level;
/**************************************************************************
**************************************************************************
**************************************************************************
* function: __construct
*/
function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->knows = array_merge($this->app_knows, $this->knows);
//$this->pr(1, array('knows' => $this->knows));
foreach ($this->knows as $alias => $modelName) {
if (is_numeric($alias)) {
$alias = $modelName;
}
// Don't overwrite any existing alias
if (!empty($this->{$alias}) || get_class($this) == $alias)
continue;
$model = array('class' => $modelName, 'alias' => $alias);
if (PHP5) {
$this->{$alias} = ClassRegistry::init($model);
} else {
$this->{$alias} =& ClassRegistry::init($model);
}
}
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: pr
* - Prints out debug information, if the log level allows
*/
function prClassLevel($level, $class = null) {
$trace = debug_backtrace(false);
$caller = array_shift($trace);
$caller = array_shift($trace);
if (empty($class))
$class = get_class($this);
$this->pr(50, compact('class', 'level'));
$this->class_log_level[$class] = $level;
}
function prFunctionLevel($level, $function = null, $class = null) {
$trace = debug_backtrace(false);
$caller = array_shift($trace);
$caller = array_shift($trace);
if (empty($class))
$class = get_class($this);
if (empty($function))
$function = $caller['function'];
$this->pr(50, compact('class', 'function', 'level'));
$this->function_log_level["{$class}-{$function}"] = $level;
}
function _pr($level, $mixed, $checkpoint = null) {
if (Configure::read() <= 0)
return;
$log_level = $this->default_log_level;
$trace = debug_backtrace(false);
// Get rid of pr/prEnter/prReturn
$caller = array_shift($trace);
// The next entry shows where pr was called from, but it
// shows _what_ was called, which is pr/prEntry/prReturn.
$caller = array_shift($trace);
$file = $caller['file'];
$line = $caller['line'];
// So, this caller holds the calling function name
$caller = $trace[0];
$function = $caller['function'];
$class = $caller['class'];
//$class = $this->name;
// Use class or function specific log level if available
if (isset($this->class_log_level[$class]))
$log_level = $this->class_log_level[$class];
if (isset($this->function_log_level["{$class}-{$function}"]))
$log_level = $this->function_log_level["{$class}-{$function}"];
// Establish log level minimums
$min_log_level = $this->min_log_level;
if (is_array($this->min_log_level)) {
$min_show_level = $min_log_level['show'];
$min_log_level = $min_log_level['log'];
}
// Establish log level maximums
$max_log_level = $this->max_log_level;
if (is_array($this->max_log_level)) {
$max_show_level = $max_log_level['show'];
$max_log_level = $max_log_level['log'];
}
// Determine the applicable log and show levels
if (is_array($log_level)) {
$show_level = $log_level['show'];
$log_level = $log_level['log'];
}
// Adjust log level up/down to min/max
if (isset($min_log_level))
$log_level = max($log_level, $min_log_level);
if (isset($max_log_level))
$log_level = min($log_level, $max_log_level);
// Adjust show level up/down to min/max
if (isset($min_show_level))
$show_level = max($show_level, $min_show_level);
if (isset($max_show_level))
$show_level = min($show_level, $max_show_level);
// If the level is insufficient, bail out
if ($level > $log_level)
return;
if (!empty($checkpoint)) {
$chk = array("checkpoint" => $checkpoint);
if (is_array($mixed))
$mixed = $chk + $mixed;
else
$mixed = $chk + array($mixed);
}
static $pr_unique_number = 0;
$pr_id = 'pr-section-class-' . $class . '-print-' . (++$pr_unique_number);
$pr_trace_id = $pr_id . '-trace';
$pr_output_id = $pr_id . '-output';
$pr_entire_base_class = "pr-section";
$pr_entire_class_class = $pr_entire_base_class . '-class-' . $class;
$pr_entire_function_class = $pr_entire_class_class . '-function-' . $function;
$pr_entire_class = "$pr_entire_base_class $pr_entire_class_class $pr_entire_function_class";
$pr_header_class = "pr-caller";
$pr_trace_class = "pr-trace";
$pr_output_base_class = 'pr-output';
$pr_output_class_class = $pr_output_base_class . '-class-' . $class;
$pr_output_function_class = $pr_output_class_class . '-function-' . $function;
$pr_output_class = "$pr_output_base_class $pr_output_class_class $pr_output_function_class";
echo '<DIV class="'.$pr_entire_class.'" id="'.$pr_id.'">'."\n";
echo '<DIV class="'.$pr_header_class.'">'."\n";
echo '<DIV class="'.$pr_trace_class.'" id="'.$pr_trace_id.'" style="display:none;">'."\n";
echo '<HR />' . "\n";
// Flip trace around so the sequence flows from top to bottom
// Then print out the entire stack trace (in hidden div)
$trace = array_reverse($trace);
for ($i = 0; $i < count($trace); ++$i) {
$bline = $trace[$i]['line'];
$bfile = $trace[$i]['file'];
$bfile = str_replace(ROOT.DS, '', $bfile);
$bfile = str_replace(CAKE_CORE_INCLUDE_PATH.DS, '', $bfile);
if ($i > 0) {
$bfunc = $trace[$i-1]['function'];
$bclas = $trace[$i-1]['class'];
} else {
$bfunc = null;
$bclas = null;
}
echo("$bfile:$bline (" . ($bclas ? "$bclas::$bfunc" : "entry point") . ")<BR>\n");
//echo(($bclas ? "$bclas::$bfunc" : "entry point") . "; $bfile : $bline<BR>\n");
}
echo '</DIV>' . "\n"; // End pr_trace_class
$file = str_replace(ROOT.DS, '', $file);
$file = str_replace(CAKE_CORE_INCLUDE_PATH.DS, '', $file);
echo "<strong>$file:$line ($class::$function)</strong>" . ";\n";
/* $log_show_level = isset($show_level) ? $show_level : '?'; */
/* echo ' L' . $level . "({$log_level}/{$log_show_level})" . ";\n"; */
echo ' L' . $level . ";\n";
echo ' <A HREF="#" onclick="$' . "('#{$pr_trace_id}').slideToggle(); return false;" . '">stack</A>'.";\n";
echo " this ";
echo '<A HREF="#" onclick="$' . "('#{$pr_output_id}').slideToggle(); return false;" . '">t</A>'."/";
echo '<A HREF="#" onclick="$' . "('#{$pr_id}').hide(); return false;" . '">n</A>'.";\n";
echo " $class ";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_class_class}').slideDown(); return false;" . '">s</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_class_class}').slideUp(); return false;" . '">h</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_entire_class_class}').hide(); return false;" . '">n</A>'.";\n";
echo " $function ";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_function_class}').slideDown(); return false;" . '">s</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_function_class}').slideUp(); return false;" . '">h</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_entire_function_class}').hide(); return false;" . '">n</A>'.";\n";
echo " all ";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_base_class}').show(); return false;" . '">s</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_output_base_class}').hide(); return false;" . '">h</A>'."/";
echo '<A HREF="#" onclick="$' . "('.{$pr_entire_base_class}').hide(); return false;" . '">n</A>'."\n";
echo '</DIV>' . "\n"; // End pr_header_class
if (isset($show_level) && $level > $show_level)
$display = 'none';
else
$display = 'block';
echo '<DIV class="'.$pr_output_class.'" id="'.$pr_output_id.'" style="display:'.$display.';">'."\n";
pr($mixed, false, false);
echo '</DIV>' . "\n"; // End pr_output_class
echo '</DIV>' . "\n"; // End pr_entire_class
}
function pr($level, $mixed, $checkpoint = null) {
$this->_pr($level, $mixed, $checkpoint);
}
function prEnter($args, $level = 15) {
$this->_pr($level, $args, 'Function Entry');
}
function prReturn($retval, $level = 16) {
$this->_pr($level, $retval, 'Function Return');
return $retval;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: queryInit
* - Initializes the query fields
*/
function prDump($all = false) {
$vars = get_object_vars($this);
foreach (array_keys($vars) AS $name) {
if (preg_match("/^[A-Z]/", $name))
unset($vars[$name]);
if (preg_match("/^_/", $name) && !$all)
unset($vars[$name]);
}
//$vars['class'] = get_class_vars(get_class($this));
$this->pr(1, $vars);
}
/**
* Get Enum Values
* Snippet v0.1.3
@@ -81,6 +350,30 @@ class AppModel extends Model {
} //end getEnumValues
/**************************************************************************
**************************************************************************
**************************************************************************
* function: queryInit
* - Initializes the query fields
*/
function queryInit(&$query, $link = true) {
if (!isset($query))
$query = array();
if (!isset($query['conditions']))
$query['conditions'] = array();
if (!isset($query['group']))
$query['group'] = null;
if (!isset($query['fields']))
$query['fields'] = null;
if ($link && !isset($query['link']))
$query['link'] = array();
if (!$link && !isset($query['contain']))
$query['contain'] = array();
// In case caller expects query to come back
return $query;
}
/**************************************************************************
**************************************************************************
@@ -136,6 +429,10 @@ class AppModel extends Model {
}
function filter_null($array) {
return array_diff_key($array, array_filter($array, 'is_null'));
}
function recursive_array_replace($find, $replace, &$data) {
if (!isset($data))
return;
@@ -210,4 +507,12 @@ class AppModel extends Model {
return date('Y-m-d', strtotime($dateString));
}
function INTERNAL_ERROR($msg, $depth = 0, $force_stop = false) {
INTERNAL_ERROR($msg, $force_stop, $depth+1);
echo $this->requestAction(array('controller' => 'util',
'action' => 'render_empty'),
array('return', 'bare' => false)
);
$this->_stop();
}
}

5
build_devbox.cmd Normal file
View File

@@ -0,0 +1,5 @@
@echo off
mysqldump --user=pmgr --password=pmgruser --opt property_manager > H:\pmgr_dev.sql
mysql --user=pmgr --password=pmgruser --database=pmgr_dev < H:\pmgr_dev.sql
del H:\pmgr_dev.sql
echo Build Complete!

5
build_sandbox.cmd Normal file
View File

@@ -0,0 +1,5 @@
@echo off
mysqldump --user=pmgr --password=pmgruser --opt property_manager > H:\pmgr_sand.sql
mysql --user=pmgr --password=pmgruser --database=pmgr_sand < H:\pmgr_sand.sql
del H:\pmgr_sand.sql
echo Build Complete!

View File

@@ -31,6 +31,99 @@
* You can also use this to include or require any files in your application.
*
*/
function _box($type) {
static $box = array('type' => null, 'test' => array());
if (!isset($box['type']) && !isset($box['test'][$type])) {
$r = Router::requestRoute();
/* if (!preg_match("/gridData/", $_SERVER['REQUEST_URI'])) { */
/* print("<PRE>Route:\n");print_r($r);print("\n</PRE>\n"); */
/* } */
$box['test'][$type] = !empty($r[3]["${type}_route"]);
if ($box['test'][$type])
$box['type'] = $type;
}
return $box['type'] == $type;
}
function sandbox() { return _box('sand'); }
function devbox() { return _box('dev'); }
function server_request_var($var) {
if (preg_match("/^HTTP_ACCEPT|REMOTE_PORT/", $var))
return false;
return (preg_match("/^HTTP|REQUEST|REMOTE/", $var));
}
function INTERNAL_ERROR($message, $exit = true, $drop = 0) {
$O = new Object();
for ($i=0; $i<3; ++$i) {
$O->log(str_repeat("\\", 80));
$O->log(str_repeat("/", 80));
}
$O->log("INTERNAL ERROR: $message");
echo '<DIV class="internal-error" style="color:#000; background:#c22; padding:0.5em 1.5em 0.5em 1.5em;">' . "\n";
echo '<H1 style="color:#000; margin-bottom:0.2em; font-size:2em;">INTERNAL ERROR:</H1>' . "\n";
echo '<H2 style="color:#000; margin-top:0; margin-left:1.5em; font-size:1.5em">' . $message . '</H2>' . "\n";
echo '<H4 style="color:#000;">This error was not caused by anything that you did wrong.' . "\n";
echo '<BR>It is a problem within the application itself and should be reported to the administrator.</H4>' . "\n";
// Print out the entire stack trace
$O->log(str_repeat("-", 30));
$O->log("Stack:");
echo '<HR style="margin-top:1.0em; margin-bottom:0.5em;">' . "\nStack Trace:\n";
echo '<OL style="margin-top:0.5em; margin-left:0.0em";>' . "\n";
$trace = array_slice(debug_backtrace(false), $drop);
for ($i = 0; $i < count($trace); ++$i) {
$bline = $trace[$i]['line'];
$bfile = $trace[$i]['file'];
$bfile = str_replace(ROOT.DS, '', $bfile);
$bfile = str_replace(CAKE_CORE_INCLUDE_PATH.DS, '', $bfile);
if ($i < count($trace)-1) {
$bfunc = $trace[$i+1]['function'];
$bclas = $trace[$i+1]['class'];
} else {
$bfunc = null;
$bclas = null;
}
$O->log(" $bfile:$bline (" . ($bclas ? "$bclas::$bfunc" : "entry point") . ")");
echo("<LI>$bfile:$bline (" . ($bclas ? "$bclas::$bfunc" : "entry point") . ")</LI>\n");
}
echo "</OL>\n";
$O->log(str_repeat("-", 30));
$O->log("HTTP Request:");
echo '<HR style="margin-top:1.0em; margin-bottom:0.5em;">' . "\nHTTP Request:\n";
echo '<UL style="margin-top:0.5em; margin-left:0.0em";>' . "\n";
foreach($_REQUEST AS $k => $v) {
$O->log(sprintf(" %-20s => %s", $k, $v));
echo("<LI>$k =&gt; $v</LI>\n");
}
echo "</UL>\n";
$O->log(str_repeat("-", 30));
$O->log("Server:");
$SRV = array_intersect_key($_SERVER, array_flip(array_filter(array_keys($_SERVER), 'server_request_var')));
echo '<HR style="margin-top:1.0em; margin-bottom:0.5em;">' . "\nServer:\n";
echo '<UL style="margin-top:0.5em; margin-left:0.0em";>' . "\n";
foreach($SRV AS $k => $v) {
if ($k == 'REQUEST_TIME')
$v = date('c', $v);
$O->log(sprintf(" %-20s => %s", $k, $v));
echo("<LI>$k =&gt; $v</LI>\n");
}
echo "</UL>\n";
echo '<HR style="margin-top:1.0em; margin-bottom:0.5em;">' . "\n";
echo date('c') . "<BR>\n";
echo '</DIV>';
if ($exit)
die();
}
/**
* The settings below can be used to set additional paths to models, views and controllers.
* This is related to Ticket #470 (https://trac.cakephp.org/ticket/470)

View File

@@ -117,7 +117,7 @@
/**
* The name of CakePHP's session cookie.
*/
Configure::write('Session.cookie', 'CAKEPHP');
Configure::write('Session.cookie', 'PMGR');
/**
* Session time out time (in seconds).
* Actual value depends on 'Security.level' setting.

View File

@@ -10,5 +10,12 @@ class DATABASE_CONFIG {
'database' => 'property_manager',
'prefix' => 'pmgr_',
);
function __construct() {
if (devbox())
$this->default['database'] = 'pmgr_dev';
if (sandbox())
$this->default['database'] = 'pmgr_sand';
}
}
?>

View File

@@ -38,7 +38,7 @@
*
* $uninflectedPlural = array('.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox');
*/
$uninflectedPlural = array();
$uninflectedPlural = array('.*cash');
/**
* This is a key => value array of plural irregular words.
* If key matches then the value is returned.

View File

@@ -27,11 +27,49 @@
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
$default_path = array('controller' => 'maps', 'action' => 'view', '1');
/**
* Here, we are connecting '/' (base path) to our site map.
* It's hardcoded to map #1, but at some point we'll implement
* a login mechanism and the default path will be to log on instead.
*/
Router::connect('/', array('controller' => 'maps', 'action' => 'view', '1'));
Router::connect('/', $default_path);
/*
* Route for sandbox functionality
*/
Router::connect('/sand',
array('sand_route' => true) + $default_path);
Router::connect('/sand/:controller/:action/*',
array('sand_route' => true, 'action' => null));
/* Unfortunately, for some reason we need an extra route to solve
* a bug with form generation. When $this->data is set by the
* controller, and a URL is generated by the FormHelper, this
* route is required to ensure the form action is correct. An
* example of a broken page is for /customers/edit/XX. It appears
* the page location uses the route above, it's only URL generation
* that seems to be broken.
*/
Router::connect('/sand/:controller/:action/:id/*',
array('sand_route' => true,'action' => null, 'id'=>null));
/*
* Route for developement functionality
*/
Router::connect('/dev',
array('dev_route' => true) + $default_path);
Router::connect('/dev/:controller/:action/*',
array('dev_route' => true, 'action' => null));
/* Unfortunately, for some reason we need an extra route to solve
* a bug with form generation. When $this->data is set by the
* controller, and a URL is generated by the FormHelper, this
* route is required to ensure the form action is correct. An
* example of a broken page is for /customers/edit/XX. It appears
* the page location uses the route above, it's only URL generation
* that seems to be broken.
*/
Router::connect('/dev/:controller/:action/:id/*',
array('dev_route' => true,'action' => null, 'id'=>null));
?>

View File

@@ -2,28 +2,35 @@
class AccountsController extends AppController {
var $uses = array('Account', 'LedgerEntry');
var $sidemenu_links =
array(array('name' => 'Accounts', 'header' => true),
array('name' => 'All', 'url' => array('controller' => 'accounts', 'action' => 'all')),
array('name' => 'Asset', 'url' => array('controller' => 'accounts', 'action' => 'asset')),
array('name' => 'Liability', 'url' => array('controller' => 'accounts', 'action' => 'liability')),
array('name' => 'Equity', 'url' => array('controller' => 'accounts', 'action' => 'equity')),
array('name' => 'Income', 'url' => array('controller' => 'accounts', 'action' => 'income')),
array('name' => 'Expense', 'url' => array('controller' => 'accounts', 'action' => 'expense')),
array('name' => 'Bank Deposit', 'url' => array('controller' => 'accounts', 'action' => 'deposit')),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Asset',
array('controller' => 'accounts', 'action' => 'asset'), null,
'CONTROLLER', $this->admin_area);
$this->addSideMenuLink('Liability',
array('controller' => 'accounts', 'action' => 'liability'), null,
'CONTROLLER', $this->admin_area);
$this->addSideMenuLink('Equity',
array('controller' => 'accounts', 'action' => 'equity'), null,
'CONTROLLER', $this->admin_area);
$this->addSideMenuLink('Income',
array('controller' => 'accounts', 'action' => 'income'), null,
'CONTROLLER', $this->admin_area);
$this->addSideMenuLink('Expense',
array('controller' => 'accounts', 'action' => 'expense'), null,
'CONTROLLER', $this->admin_area);
$this->addSideMenuLink('All',
array('controller' => 'accounts', 'action' => 'all'), null,
'CONTROLLER', $this->admin_area);
}
@@ -35,88 +42,71 @@ class AccountsController extends AppController {
*/
function index() { $this->all(); }
function asset() { $this->jqGridView('Asset Accounts'); }
function liability() { $this->jqGridView('Liability Accounts'); }
function equity() { $this->jqGridView('Equity Accounts'); }
function income() { $this->jqGridView('Income Accounts'); }
function expense() { $this->jqGridView('Expense Accounts'); }
function all() { $this->jqGridView('All Accounts', 'all'); }
function asset() { $this->gridView('Asset Accounts'); }
function liability() { $this->gridView('Liability Accounts'); }
function equity() { $this->gridView('Equity Accounts'); }
function income() { $this->gridView('Income Accounts'); }
function expense() { $this->gridView('Expense Accounts'); }
function all() { $this->gridView('All Accounts', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
function gridDataSetup(&$params) {
parent::gridDataSetup($params);
if (!isset($params['action']))
$params['action'] = 'all';
}
function jqGridDataCountTables(&$params, &$model) {
return parent::jqGridDataTables($params, $model);
function gridDataCountTables(&$params, &$model) {
// Our count should NOT include anything extra,
// so we need the virtual function to prevent
// the base class from just calling our
// gridDataTables function
return parent::gridDataTables($params, $model);
}
function jqGridDataTables(&$params, &$model) {
function gridDataTables(&$params, &$model) {
return array
('link' =>
array(// Models
'CurrentLedger' => array
(// Models
'LedgerEntry'
/* REVISIT <AP> 20090615:
* I'll remove this 'conditions' section on a future checkin,
* after I've proven out the %{MODEL_ALIAS} feature will be
* sticking around.
=> array
('conditions' =>
array('OR' =>
array('LedgerEntry.debit_ledger_id = CurrentLedger.id',
'LedgerEntry.credit_ledger_id = CurrentLedger.id'),
),
),
* END REVISIT
*/
),
),
);
}
function jqGridDataFields(&$params, &$model) {
return array
('Account.*',
'SUM(IF(LedgerEntry.debit_ledger_id = CurrentLedger.id,
LedgerEntry.amount, NULL)) AS debits',
'SUM(IF(LedgerEntry.credit_ledger_id = CurrentLedger.id,
LedgerEntry.amount, NULL)) AS credits',
"SUM(IF(Account.type IN ('ASSET', 'EXPENSE'),
IF(LedgerEntry.debit_ledger_id = CurrentLedger.id, 1, -1),
IF(LedgerEntry.credit_ledger_id = CurrentLedger.id, 1, -1)
) * IF(LedgerEntry.amount, LedgerEntry.amount, 0)
) AS balance",
'COUNT(LedgerEntry.id) AS entries');
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
return array_merge($fields,
$this->Account->Ledger->LedgerEntry->debitCreditFields(true));
}
function jqGridDataConditions(&$params, &$model) {
$conditions = parent::jqGridDataConditions($params, $model);
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
if (in_array($params['action'], array('asset', 'liability', 'equity', 'income', 'expense'))) {
$conditions[] = array('Account.type' => strtoupper($params['action']));
}
$conditions[] = array('Account.level >=' =>
$this->Permission->level('controller.accounts'));
return $conditions;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Account'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -129,7 +119,11 @@ class AccountsController extends AppController {
*/
function newledger($id = null) {
if (!$this->Account->closeCurrentLedger($id)) {
$result = $this->Account->closeCurrentLedgers($id);
if ($result['error']) {
pr(compact('result'));
die("Unable to create new ledger.");
$this->Session->setFlash(__('Unable to create new Ledger.', true));
}
if ($id)
@@ -152,94 +146,20 @@ class AccountsController extends AppController {
$this->redirect(array('action'=>'index'));
}
$payment_accounts = $this->Account->collectableAccounts();
//$payment_accounts[$this->Account->nameToID('Bank')] = 'Bank';
$default_accounts = array_diff_key($payment_accounts,
array($this->Account->concessionAccountID() => 1));
$this->set(compact('payment_accounts', 'default_accounts'));
$this->Account->recursive = -1;
$account = $this->Account->read(null, $id);
$account = $account['Account'];
$accounts = $this->Account->collectableAccounts();
$payment_accounts = $accounts['all'];
$default_accounts = $accounts['default'];
$this->set(compact('payment_accounts', 'default_accounts'));
$title = ($account['name'] . ': Collected Report');
$this->set(compact('account', 'title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: deposit
* - Prepares the books for a bank deposit
*/
function deposit() {
if ($this->data) {
// Action the close based on provided data
//pr($this->data);
// Get data about each closed ledger.
$deposit = array('total' => 0, 'ledgers' => array());
foreach ($this->data['Tillable']['Ledger'] AS $ledger_id => $ledger) {
if (!$ledger['checked'])
continue;
$ledger_entries =
$this->Account->Ledger->find
('all',
array('link' => array
('Account' =>
array('fields' => array('name')),
'LedgerEntry' =>
array('fields' => array('id', 'amount'),
'MonetarySource' =>
array('fields' => array('name')),
'Customer' =>
array('fields' => array('name')),
//'Transaction' =>
//array('fields' => array('stamp')),
),
),
'fields' => false,
'conditions' => array(array('Ledger.id' => $ledger_id),
array('LedgerEntry.id IS NOT NULL'),
),
));
$deposit['total'] += $ledger['amount'];
$deposit['ledgers'][] = array('id' => $ledger_id,
'name' => $ledger['account_name'],
'total' => $ledger['amount'],
'entries' => $ledger_entries);
}
// Perform the accounting work necessary to close the
// monetary ledgers and deposit into the bank account.
$this->Account->closeAndDeposit($deposit['ledgers'], $this->data['Deposit']['Account']['id']);
$title = 'Account: Deposit Slip';
$this->set(compact('title', 'deposit'));
$this->render('deposit_slip');
return;
}
// Prepare a close page...
$tillable_account = $this->Account->relatedAccounts('tillable');
$depositable_account = $this->Account->relatedAccounts('depositable');
foreach ($tillable_account AS &$acct) {
$acct['Account']['stats'] = $this->Account->stats($acct['Account']['id']);
}
$title = 'Account: Prepare Deposit';
$this->set(compact('title', 'tillable_account', 'depositable_account'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -248,46 +168,39 @@ class AccountsController extends AppController {
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
// Get details about the account and its ledgers (no ledger entries yet)
$account = $this->Account->find
('first',
array('contain' =>
array(// Models
'CurrentLedger' =>
array('fields' => array('id', 'sequence')),
array('fields' => array('id', 'sequence', 'name')),
'Ledger' =>
array('Close' => array
('order' => array('Close.stamp' => 'DESC'))),
array('CloseTransaction' => array
('order' => array('CloseTransaction.stamp' => 'DESC'))),
),
'conditions' => array(array('Account.id' => $id)),
'conditions' => array(array('Account.id' => $id),
array('Account.level >=' =>
$this->Permission->level('controller.accounts')),
),
)
);
// Get all ledger entries of the CURRENT ledger
$entries = $this->Account->findLedgerEntries($id);
$account['CurrentLedger']['LedgerEntry'] = $entries['Entries'];
// Summarize each ledger
foreach($account['Ledger'] AS &$ledger)
$ledger = array_merge($ledger,
$this->Account->Ledger->stats($ledger['id']));
if (empty($account)) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
// Obtain stats across ALL ledgers for the summary infobox
$stats = $this->Account->stats($id, true);
$stats = $stats['Ledger'];
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'New Ledger', 'url' => array('action' => 'newledger', $id));
$this->sidemenu_links[] =
array('name' => 'Collected', 'url' => array('action' => 'collected', $id));
$this->addSideMenuLink('New Ledger',
array('action' => 'newledger', $id), null,
'ACTION', $this->admin_area);
$this->addSideMenuLink('Collected',
array('action' => 'collected', $id), null,
'ACTION', $this->admin_area);
// Prepare to render
$title = 'Account: ' . $account['Account']['name'];

View File

@@ -2,18 +2,6 @@
class ContactsController extends AppController {
var $sidemenu_links = array();
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
}
/**************************************************************************
**************************************************************************
@@ -23,32 +11,47 @@ class ContactsController extends AppController {
*/
function index() { $this->all(); }
function all() { $this->jqGridView('All Contacts', 'all'); }
function all() { $this->gridView('All Contacts', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataOrder(&$params, &$model, $index, $direction) {
$order = parent::jqGridDataOrder($params, $model, $index, $direction);
if ($index === 'Contact.last_name') {
$order[] = 'Contact.first_name ' . $direction;
}
if ($index === 'Contact.first_name') {
$order[] = 'Contact.last_name ' . $direction;
}
function gridDataFilterTablesConfig(&$params, &$model, $table) {
$config = parent::gridDataFilterTablesConfig($params, $model, $table);
// Special case for Customer; We need the Contact/Customer relationship
if ($table == 'Customer')
$config = array('fields' => array('ContactsCustomer.type',
'ContactsCustomer.active'),
'conditions' => array('ContactsCustomer.active' => true),
);
return $config;
}
function gridDataOrder(&$params, &$model, $index, $direction) {
$order = parent::gridDataOrder($params, $model, $index, $direction);
// After sorting by whatever the user wants, add these
// defaults into the sort mechanism. If we're already
// sorting by one of them, it will only be redundant,
// and should cause no harm (possible a longer query?)
$order[] = 'Contact.last_name ' . $direction;
$order[] = 'Contact.first_name ' . $direction;
return $order;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
$links['Contact'] = array('id');
return parent::jqGridRecordLinks($params, $model, $records, $links);
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Contact'] = array('display_name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -78,13 +81,9 @@ class ContactsController extends AppController {
));
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'Edit',
'url' => array('action' => 'edit',
$id));
$this->addSideMenuLink('Edit',
array('action' => 'edit', $id), null,
'ACTION');
// Prepare to render.
$title = 'Contact: ' . $contact['Contact']['display_name'];
@@ -126,8 +125,6 @@ class ContactsController extends AppController {
// Now that the work is done, let the user view the updated contact
$this->redirect(array('action'=>'view', $this->data['Contact']['id']));
//$this->render('/empty');
return;
}
if ($id) {

View File

@@ -1,25 +1,39 @@
<?php
class CustomersController extends AppController {
var $sidemenu_links =
array(array('name' => 'Customers', 'header' => true),
array('name' => 'Current', 'url' => array('controller' => 'customers', 'action' => 'current')),
array('name' => 'Past', 'url' => array('controller' => 'customers', 'action' => 'past')),
array('name' => 'All', 'url' => array('controller' => 'customers', 'action' => 'all')),
array('name' => 'Add Customer', 'url' => array('controller' => 'customers', 'action' => 'add')),
);
//var $components = array('RequestHandler');
var $components = array('RequestHandler');
// DEBUG FUNCTION ONLY!
// Call without id to update ALL customers
function force_update($id = null) {
$this->Customer->update($id);
$this->redirect(array('action'=>'index'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Current',
array('controller' => 'customers', 'action' => 'current'), null,
'CONTROLLER');
$this->addSideMenuLink('Past',
array('controller' => 'customers', 'action' => 'past'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'customers', 'action' => 'all'), null,
'CONTROLLER');
/* $this->addSideMenuLink('New Customer', */
/* array('controller' => 'customers', 'action' => 'add'), null, */
/* 'CONTROLLER', $this->new_area); */
}
@@ -31,145 +45,86 @@ class CustomersController extends AppController {
*/
function index() { $this->current(); }
function current() { $this->jqGridView('Current Tenants', 'current'); }
function past() { $this->jqGridView('Past Tenants'); }
function all() { $this->jqGridView('All Customers'); }
function current() { $this->gridView('Current Tenants', 'current'); }
function past() { $this->gridView('Past Tenants'); }
function all() { $this->gridView('All Customers'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
if (!isset($params['action']))
$params['action'] = 'all';
}
function jqGridDataCountTables(&$params, &$model) {
function gridDataCountTables(&$params, &$model) {
return array
('link' =>
array(// Models
'PrimaryContact',
'CurrentLease' => array('fields' => array()),
),
);
}
function jqGridDataTables(&$params, &$model) {
$link = $this->jqGridDataCountTables($params, $model);
$link['link']['LedgerEntry'] = array('fields' => array());
$link['link']['LedgerEntry']['Ledger'] = array('fields' => array());
$link['link']['LedgerEntry']['Ledger']['Account'] = array('fields' => array());
// INNER JOIN would be great, as it would ensure we're only looking
// at the ledger entries that we truly want. However, this also
// removes from the query any units that do not yet have a ledger
// entry in A/R. A solution would be to INNER JOIN these tables,
// and LEFT JOIN it to the rest. Grouping of JOINs, however, is
// implemented with the 'joins' tag, and is not available through
// the Linkable behavior interface.
//$link['link']['LedgerEntry']['Ledger']['Account']['type'] = 'INNER';
$link['link']['LedgerEntry']['Ledger']['Account']['conditions']
= array('Account.id' =>
$this->Customer->LedgerEntry->Ledger->Account->accountReceivableAccountID());
function gridDataTables(&$params, &$model) {
$link = $this->gridDataCountTables($params, $model);
// StatementEntry is needed to determine customer balance
$link['link']['StatementEntry'] = array('fields' => array());
return $link;
}
function jqGridDataFields(&$params, &$model) {
$db = &$model->getDataSource();
$fields = $db->fields($model, $model->alias);
$fields[] = ('COUNT(DISTINCT CurrentLease.id) AS lease_count');
$fields[] = ("SUM(IF(Account.id IS NULL, 0," .
" IF(LedgerEntry.debit_ledger_id = Account.id," .
" 1, -1))" .
" * IF(LedgerEntry.amount IS NULL, 0, LedgerEntry.amount))" .
" AS 'balance'");
return $fields;
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
return array_merge($fields,
$this->Customer->StatementEntry->chargeDisbursementFields(true));
}
function jqGridDataConditions(&$params, &$model) {
$conditions = parent::jqGridDataConditions($params, $model);
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
if ($params['action'] === 'current') {
$conditions[] = 'CurrentLease.id IS NOT NULL';
$conditions[] = array('Customer.current_lease_count >' => 0);
}
elseif ($params['action'] === 'past') {
$conditions[] = 'CurrentLease.id IS NULL';
$conditions[] = array('Customer.current_lease_count' => 0);
$conditions[] = array('Customer.past_lease_count >' => 0);
}
return $conditions;
}
function jqGridDataOrder(&$params, &$model, $index, $direction) {
$order = array();
$order[] = parent::jqGridDataOrder($params, $model, $index, $direction);
function gridDataFilterTablesConfig(&$params, &$model, $table) {
$config = parent::gridDataFilterTablesConfig($params, $model, $table);
if ($index !== 'PrimaryContact.last_name')
$order[] = parent::jqGridDataOrder($params, $model,
'PrimaryContact.last_name', $direction);
if ($index !== 'PrimaryContact.first_name')
$order[] = parent::jqGridDataOrder($params, $model,
'PrimaryContact.first_name', $direction);
if ($index !== 'Customer.id')
$order[] = parent::jqGridDataOrder($params, $model,
'Customer.id', $direction);
// Special case for Contact; We need the Contact/Customer relationship
if ($table == 'Contact')
$config = array('fields' => array('ContactsCustomer.type',
'ContactsCustomer.active'),
'conditions' => array('ContactsCustomer.active' => true),
);
return $config;
}
function gridDataOrder(&$params, &$model, $index, $direction) {
$order = parent::gridDataOrder($params, $model, $index, $direction);
// After sorting by whatever the user wants, add these
// defaults into the sort mechanism. If we're already
// sorting by one of them, it will only be redundant,
// and should cause no harm (possible a longer query?)
$order[] = 'PrimaryContact.last_name ' . $direction;
$order[] = 'PrimaryContact.first_name ' . $direction;
$order[] = 'Customer.id ' . $direction;
return $order;
}
function jqGridDataRecordCount(&$params, &$model, $query) {
// We don't have a good way to use the query to obtain
// our count. The problem is that we're relying on the
// group by for the query, which will destroy the count,
// whether we omit the group by or leave it in.
// So, build a fresh query for counting.
$query['conditions'] = parent::jqGridDataConditions($params, $model);
$count = $model->find('count',
array_merge(array('link' => array_diff_key($query['link'],
array('CurrentLease'=>1))),
array_diff_key($query, array('link'=>1))));
if ($params['action'] === 'all')
return $count;
$query['conditions'][] = 'CurrentLease.id IS NULL';
$count_past = $model->find('count', $query);
// Since we can't easily count 'current' directly, we
// can quickly derive it since 'current' customers
// are mutually exclusive to 'past' customers.
if ($params['action'] == 'current')
$count = $count - $count_past;
elseif ($params['action'] == 'past') {
$count = $count_past;
}
return $count;
}
function jqGridDataRecords(&$params, &$model, $query) {
$customers = parent::jqGridDataRecords($params, $model, $query);
// Get the balance on each customer.
foreach ($customers AS &$customer) {
$stats = $this->Customer->stats($customer['Customer']['id']);
$customer['Customer']['balance'] = $stats['balance'];
}
return $customers;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Customer'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -180,14 +135,18 @@ class CustomersController extends AppController {
* - Sets up the move-in page for the given customer.
*/
function move_in($id = null) {
function move_in($id = null, $unit_id = null) {
$customer = array();
$unit = array();
if (isset($id)) {
if (!empty($id)) {
$this->Customer->recursive = -1;
$customer = current($this->Customer->read(null, $id));
}
if (!empty($unit_id)) {
$this->Customer->Lease->Unit->recursive = -1;
$unit = current($this->Customer->Lease->Unit->read(null, $unit_id));
}
$this->set(compact('customer', 'unit'));
$title = 'Customer Move-In';
@@ -247,49 +206,118 @@ class CustomersController extends AppController {
$this->redirect(array('action'=>'index'));
}
$customer = $this->Customer->details($id);
// Get details on this customer, its contacts and leases
$customer = $this->Customer->find
('first', array
('contain' => array
(// Models
'Contact' =>
array('order' => array('Contact.display_name'),
// Models
'ContactPhone',
'ContactEmail',
'ContactAddress',
),
'Lease' =>
array('Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
),
),
$outstanding_balance = $customer['stats']['balance'];
$outstanding_deposit = $customer['deposits']['summary']['balance'];
'conditions' => array('Customer.id' => $id),
));
//pr($customer);
// Determine how long this customer has been with us.
$leaseinfo = $this->Customer->find
('first', array
('link' => array('Lease' => array('fields' => array())),
'fields' => array('MIN(Lease.movein_date) AS since',
'IF(Customer.current_lease_count = 0, MAX(Lease.moveout_date), NULL) AS until'),
'conditions' => array('Customer.id' => $id),
'group' => 'Customer.id',
));
$this->set($leaseinfo[0]);
// Figure out the outstanding balances for this customer
//$this->set('stats', $this->Customer->stats($id));
$outstanding_balance = $this->Customer->balance($id);
$outstanding_deposit = $this->Customer->securityDepositBalance($id);
// Figure out if this customer has any non-closed leases
$show_moveout = false;
$show_payment = false;
$show_moveout = false; $moveout_lease_id = null;
$show_payment = false; $payment_lease_id = null;
foreach ($customer['Lease'] AS $lease) {
if (!isset($lease['close_date']))
if (!isset($lease['close_date'])) {
if ($show_payment)
$payment_lease_id = null;
else
$payment_lease_id = $lease['id'];
$show_payment = true;
if (!isset($lease['moveout_date']))
}
if (!isset($lease['moveout_date'])) {
if ($show_moveout)
$moveout_lease_id = null;
else
$moveout_lease_id = $lease['id'];
$show_moveout = true;
}
}
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'Edit',
'url' => array('action' => 'edit',
$id));
$this->sidemenu_links[] =
array('name' => 'Move-In',
'url' => array('action' => 'move_in',
$id));
/* if ($show_moveout) { */
/* $this->sidemenu_links[] = */
/* array('name' => 'Move-Out', */
/* 'url' => array('action' => 'move_out', */
/* $id)); */
/* } */
if ($show_payment || $outstanding_balance > 0)
$this->addSideMenuLink('New Receipt',
array('action' => 'receipt', $id), null,
'ACTION');
if ($show_payment) {
$this->sidemenu_links[] =
array('name' => 'Payment',
'url' => array('action' => 'receipt',
$id));
/* $ids = $this->Customer->leaseIds($id, true); */
/* if (count($ids) == 1) */
/* $lease_id = $ids[0]; */
/* else */
/* $lease_id = null; */
$this->addSideMenuLink('New Invoice',
array('controller' => 'leases',
'action' => 'invoice',
$payment_lease_id), null,
'ACTION');
}
$this->addSideMenuLink('Move-In',
array('action' => 'move_in', $id), null,
'ACTION');
if ($show_moveout) {
$this->addSideMenuLink('Move-Out',
array('controller' => 'leases',
'action' => 'move_out',
$moveout_lease_id), null,
'ACTION');
}
if (!$show_moveout && $outstanding_balance > 0)
$this->addSideMenuLink('Write-Off',
array('action' => 'bad_debt', $id), null,
'ACTION');
if ($outstanding_balance < 0)
$this->addSideMenuLink('Issue Refund',
array('action' => 'refund', $id), null,
'ACTION');
$this->addSideMenuLink('Edit',
array('action' => 'edit', $id), null,
'ACTION');
if ($this->admin())
$this->addSideMenuLink('Merge',
array('action' => 'merge', $id), null,
'ACTION');
// Prepare to render.
$title = 'Customer: ' . $customer['Customer']['name'];
$this->set(compact('customer', 'title',
@@ -311,23 +339,20 @@ class CustomersController extends AppController {
if (isset($this->params['form']['cancel'])) {
if (isset($this->data['Customer']['id']))
$this->redirect(array('action'=>'view', $this->data['Customer']['id']));
else
$this->redirect(array('action'=>'index'));
return;
$this->redirect(array('action'=>'index'));
}
// Make sure we have at least one contact
if (!isset($this->data['Contact']) || count($this->data['Contact']) == 0) {
$this->Session->setFlash("MUST SPECIFY AT LEAST ONE CONTACT", true);
$this->redirect(array('action'=>'view', $this->data['Customer']['id']));
return;
}
// Make sure there is a primary contact
if (!isset($this->data['Customer']['primary_contact_entry'])) {
$this->Session->setFlash("MUST SPECIFY A PRIMARY CONTACT", true);
$this->redirect(array('action'=>'view', $this->data['Customer']['id']));
return;
}
// Go through each customer and strip the bogus ID if new
@@ -344,21 +369,56 @@ class CustomersController extends AppController {
pr("CUSTOMER SAVE FAILED");
}
// If existing customer, then view it. Otherwise, since
// this is a new customer, go to the move in screen.
// If existing customer, then view it.
if ($this->data['Customer']['id'])
$this->redirect(array('action'=>'view', $this->Customer->id));
else
$this->redirect(array('action'=>'move_in', $this->Customer->id));
// For debugging, only if the redirects above have been
// commented out, otherwise this section isn't reached.
$this->render('/empty');
return;
// Since this is a new customer, go to the move in screen.
// First set the move-in unit id, if there is one, ...
if (empty($this->data['movein']['Unit']['id']))
$unit_id = null;
else
$unit_id = $this->data['movein']['Unit']['id'];
// ... then redirect
$this->redirect(array('action'=>'move_in',
$this->Customer->id,
$unit_id,
));
}
if ($id) {
$this->data = $this->Customer->details($id);
// REVISIT <AP>: 20090816
// This should never need to be done by a controller.
// However, until things stabilize, this gives the
// user a way to update any cached items on the
// customer, by just clicking Edit then Cancel.
$this->Customer->update($id);
// Get details on this customer, its contacts and leases
$customer = $this->Customer->find
('first', array
('contain' => array
(// Models
'Contact' =>
array('order' => array('Contact.display_name'),
// Models
'ContactPhone',
'ContactEmail',
'ContactAddress',
),
'Lease' =>
array('Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
),
),
'conditions' => array('Customer.id' => $id),
));
$this->data = $customer;
$title = 'Customer: ' . $this->data['Customer']['name'] . " : Edit";
}
else {
@@ -387,10 +447,46 @@ class CustomersController extends AppController {
* - Add a new customer
*/
function add() {
function add($unit_id = null) {
$this->set('movein', array('Unit' => array('id' => $unit_id)));
$this->edit();
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: merge
* - Merges two customers
*/
function merge($id = null) {
if ($id) {
$this->Customer->recursive = -1;
$customer = $this->Customer->read(null, $id);
$customer = $customer['Customer'];
if (empty($customer))
$this->INTERNAL_ERROR("Customer $id does not exist");
$this->set('dst_customer', $customer);
$this->set('dst_name', $customer['name']);
$this->set('dst_id', $id);
}
else {
$this->INTERNAL_ERROR("Merge called with invalid customer");
}
}
function mergeFinal() {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
}
$post = $this->params['form'];
$this->Customer->merge($post['dst-id'], $post['src-id'],
unserialize($post['contact-ids']));
$this->redirect(array('action'=>'view', $post['dst-id']));
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -403,21 +499,18 @@ class CustomersController extends AppController {
$this->Customer->recursive = -1;
$customer = $this->Customer->read(null, $id);
$customer = $customer['Customer'];
$unreconciled = $this->Customer->findUnreconciledLedgerEntries($id);
$charges = $unreconciled['debit'];
}
else {
$customer = null;
$charges = array('balance' => 0, 'entry' => array());
}
$A = new Account();
$payment_accounts = $A->paymentAccounts();
$default_account = $A->cashAccountID();
$this->set(compact('payment_accounts', 'default_account'));
$TT = new TenderType();
$payment_types = $TT->paymentTypes();
$default_type = $TT->defaultPaymentType();
$this->set(compact('payment_types', 'default_type'));
$title = ($customer['name'] . ': Payment Entry');
$this->set(compact('customer', 'charges', 'title'));
$title = ($customer['name'] . ': Receipt Entry');
$this->set(compact('customer', 'title'));
}
@@ -428,58 +521,64 @@ class CustomersController extends AppController {
* - Refunds customer charges
*/
function refund() {
$entries = $this->Customer->LedgerEntry->find
('all', array
function refund($id) {
$customer = $this->Customer->find
('first', array
('contain' => false,
'conditions' => array('LedgerEntry.id' =>
//array(199,200,201)
61
'conditions' => array(array('Customer.id' => $id),
),
));
pr(compact('entries'));
if (empty($customer)) {
$this->redirect(array('action'=>'view', $id));
}
$this->Customer->LedgerEntry->reverse($entries);
// Determine the customer balance, bailing if the customer owes money
$balance = $this->Customer->balance($id);
if ($balance >= 0) {
$this->redirect(array('action'=>'view', $id));
}
// The refund will be for a positive amount
$balance *= -1;
// Get the accounts capable of paying the refund
$refundAccounts = $this->Customer->StatementEntry->Account->refundAccounts();
$defaultAccount = current($refundAccounts);
$this->set(compact('refundAccounts', 'defaultAccount'));
// Prepare to render
$title = ($customer['Customer']['name'] . ': Refund');
$this->set(compact('title', 'customer', 'balance'));
$this->render('/transactions/refund');
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: unreconciledEntries
* - returns the list of unreconciled entries
* action: bad_debt
* - Sets up the write-off entry page, so that the
* user can write off remaining charges of a customer.
*/
function unreconciled($id) {
function bad_debt($id) {
$this->Customer->id = $id;
$customer = $this->Customer->find
('first', array
('contain' => false,
));
//$this->layout = 'ajax';
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
Configure::write('debug', '0');
header("Content-type: text/xml;charset=utf-8");
// Make sure we have a valid customer to write off
if (empty($customer))
$this->redirect(array('action' => 'index'));
App::import('Helper', 'Xml');
$xml = new XmlHelper();
// Get the customer balance
$balance = $this->Customer->balance($id);
// Find the unreconciled entries, then manipulate the structure
// slightly to accomodate the format necessary for XML Helper.
$unreconciled = $this->Customer->findUnreconciledLedgerEntries($id);
$unreconciled = array('entries' =>
array_intersect_key($unreconciled['debit'],
array('entry'=>1, 'balance'=>1)));
// XML Helper will dump an empty tag if the array is empty
if (!count($unreconciled['entries']['entry']))
unset($unreconciled['entries']['entry']);
pr($unreconciled);
//$reconciled = $cust->reconcileNewLedgerEntry($cust_id, 'credit', $amount);
$opts = array();
//$opts['format'] = 'tags';
echo $xml->header();
echo $xml->serialize($unreconciled, $opts);
// Prepare to render
$title = ($customer['Customer']['name'] . ': Write Off Bad Debt');
$this->set(compact('title', 'customer', 'balance'));
$this->render('/transactions/bad_debt');
}
}

View File

@@ -0,0 +1,59 @@
<?php
class DoubleEntriesController extends AppController {
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific entry
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
// Get the Entry and related fields
$entry = $this->DoubleEntry->find
('first',
array('contain' => array('DebitEntry', 'CreditEntry'),
'conditions' => array('DoubleEntry.id' => $id),
));
$entry += $this->DoubleEntry->DebitEntry->Transaction->find
('first',
array('contain' => false,
'conditions' => array('id' => $entry['DebitEntry']['transaction_id']),
));
$entry += $this->DoubleEntry->DebitEntry->find
('first',
array('contain' => array('Ledger' => array('Account')),
'conditions' => array('DebitEntry.id' => $entry['DebitEntry']['id']),
));
$entry['Ledger']['link'] =
$entry['Ledger']['Account']['level'] >=
$this->Permission->level('controller.accounts');
$entry['DebitLedger'] = $entry['Ledger'];
unset($entry['Ledger']);
$entry += $this->DoubleEntry->CreditEntry->find
('first',
array('contain' => array('Ledger' => array('Account')),
'conditions' => array('CreditEntry.id' => $entry['CreditEntry']['id']),
));
$entry['Ledger']['link'] =
$entry['Ledger']['Account']['level'] >=
$this->Permission->level('controller.accounts');
$entry['CreditLedger'] = $entry['Ledger'];
unset($entry['Ledger']);
// Prepare to render.
$title = "Double Ledger Entry #{$entry['DoubleEntry']['id']}";
$this->set(compact('entry', 'title'));
}
}

View File

@@ -2,22 +2,29 @@
class LeasesController extends AppController {
var $sidemenu_links =
array(array('name' => 'Leases', 'header' => true),
array('name' => 'Active', 'url' => array('controller' => 'leases', 'action' => 'active')),
array('name' => 'Closed', 'url' => array('controller' => 'leases', 'action' => 'closed')),
array('name' => 'All', 'url' => array('controller' => 'leases', 'action' => 'all')),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Active',
array('controller' => 'leases', 'action' => 'active'), null,
'CONTROLLER');
$this->addSideMenuLink('Closed',
array('controller' => 'leases', 'action' => 'closed'), null,
'CONTROLLER');
$this->addSideMenuLink('Delinquent',
array('controller' => 'leases', 'action' => 'delinquent'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'leases', 'action' => 'all'), null,
'CONTROLLER');
}
@@ -28,77 +35,68 @@ class LeasesController extends AppController {
* - Generate a listing of leases
*/
function index() { $this->all(); }
function active() { $this->jqGridView('Active Leases'); }
function closed() { $this->jqGridView('Closed Leases'); }
function all() { $this->jqGridView('All Leases', 'all'); }
function index() { $this->active(); }
function active() { $this->gridView('Active Leases', 'active'); }
function delinquent() { $this->gridView('Delinquent Leases'); }
function closed() { $this->gridView('Closed Leases'); }
function all() { $this->gridView('All Leases'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
function gridDataSetup(&$params) {
parent::gridDataSetup($params);
if (!isset($params['action']))
$params['action'] = 'all';
}
function jqGridDataCountTables(&$params, &$model) {
function gridDataCountTables(&$params, &$model) {
return array
('link' => array('Unit' => array('fields' => array('Unit.id', 'Unit.name')),
'Customer' => array('fields' => array('Customer.id', 'Customer.name'))));
('link' => array('Unit' => array('fields' => array('id', 'name')),
'Customer' => array('fields' => array('id', 'name'))));
}
function jqGridDataTables(&$params, &$model) {
$link = $this->jqGridDataCountTables($params, $model);
$link['link']['LedgerEntry'] = array('fields' => array());
$link['link']['LedgerEntry']['Ledger'] = array('fields' => array());
$link['link']['LedgerEntry']['Ledger']['Account'] = array('fields' => array());
// INNER JOIN would be great, as it would ensure we're only looking
// at the ledger entries that we truly want. However, this also
// removes from the query any leases that do not yet have a ledger
// entry in A/R. A solution would be to INNER JOIN these tables,
// and LEFT JOIN it to the rest. Grouping of JOINs, however, is
// implemented with the 'joins' tag, and is not available through
// the Linkable behavior interface.
//$link['link']['LedgerEntry']['Ledger']['Account']['type'] = 'INNER';
$link['link']['LedgerEntry']['Ledger']['Account']['conditions']
= array('Account.id' =>
$this->Lease->LedgerEntry->Ledger->Account->accountReceivableAccountID());
function gridDataTables(&$params, &$model) {
$link = $this->gridDataCountTables($params, $model);
$link['link']['StatementEntry'] = array('fields' => array());
return $link;
}
function jqGridDataFields(&$params, &$model) {
$db = &$model->getDataSource();
$fields = $db->fields($model, $model->alias);
$fields[] = ("SUM(IF(Account.id IS NULL, 0," .
" IF(LedgerEntry.debit_ledger_id = Account.id," .
" 1, -1))" .
" * IF(LedgerEntry.amount IS NULL, 0, LedgerEntry.amount))" .
" AS 'balance'");
return $fields;
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
$fields[] = ("IF(" . $this->Lease->conditionDelinquent() . "," .
" 'DELINQUENT', 'CURRENT') AS 'status'");
return array_merge($fields,
$this->Lease->StatementEntry->chargeDisbursementFields(true));
}
function jqGridDataConditions(&$params, &$model) {
$conditions = parent::jqGridDataConditions($params, $model);
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
if ($params['action'] === 'active') {
$conditions[] = 'Lease.close_date IS NULL';
}
elseif ($params['action'] === 'delinquent') {
$conditions[] = $this->Lease->conditionDelinquent();
}
elseif ($params['action'] === 'closed') {
$conditions[] = 'Lease.close_date IS NOT NULL';
}
if (isset($customer_id))
$conditions[] = array('Lease.customer_id' => $customer_id);
return $conditions;
}
function jqGridDataOrder(&$params, &$model, $index, $direction) {
function gridDataOrder(&$params, &$model, $index, $direction) {
// Do not sort by number, which is type varchar and
// sorts on an ascii basis. Sort by ID instead.
if ($index === 'Lease.number')
@@ -109,31 +107,31 @@ class LeasesController extends AppController {
$index = 'Unit.sort_order';
$order = array();
$order[] = parent::jqGridDataOrder($params, $model, $index, $direction);
$order[] = parent::gridDataOrder($params, $model, $index, $direction);
// If sorting by anything other than id/number
// add sorting by id as a secondary condition.
if ($index !== 'Lease.id' && $index !== 'Lease.number')
$order[] = parent::jqGridDataOrder($params, $model,
$order[] = parent::gridDataOrder($params, $model,
'Lease.id', $direction);
return $order;
}
/* function jqGridRecordsPostProcess(&$params, &$model, &$records) { */
/* function gridDataPostProcess(&$params, &$model, &$records) { */
/* foreach ($records AS &$record) { */
/* $record['Lease']['through_date'] */
/* = $this->Lease->rentChargeThrough($record['Lease']['id']); */
/* } */
/* parent::jqGridRecordsPostProcess($params, $model, $records); */
/* parent::gridDataPostProcess($params, $model, $records); */
/* } */
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Lease'] = array('number');
$links['Unit'] = array('name');
$links['Customer'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -150,21 +148,22 @@ class LeasesController extends AppController {
// Handle the move in based on the data given
//pr(array('Move-in data', $this->data));
foreach (array('deposit', 'rent') AS $currency) {
$this->data['Lease'][$currency]
= str_replace('$', '', $this->data['Lease'][$currency]);
}
$lid = $this->Lease->moveIn($this->data['Lease']['customer_id'],
$this->data['Lease']['unit_id'],
null, null,
$this->data['Lease']['deposit'],
$this->data['Lease']['rent'],
$this->data['Lease']['movein_date'],
$this->data['Lease']['comment']
);
// Since this is a new lease, go to the invoice
// screen so we can start assessing charges.
$this->redirect(array('action'=>'invoice', $lid));
// For debugging, only if the redirect above have been
// commented out, otherwise this section isn't reached.
$this->render('/empty');
$this->redirect(array('action'=>'invoice', $lid, 'move-in'));
}
/**************************************************************************
@@ -177,109 +176,119 @@ class LeasesController extends AppController {
function move_out($id = null) {
if ($this->data) {
// Handle the move out based on the data given
//pr($this->data);
$this->Lease->moveOut($this->data['Lease']['id'],
'VACANT',
$this->data['Lease']['moveout_date'],
//true // Close this lease, if able
false
$this->data['Lease']['moveout_date']
);
$this->redirect($this->data['redirect']);
$this->autoRender = false;
return;
$lease = $this->Lease->find
('first', array
('contain' => array('Customer.id'),
'conditions' => array(array('Lease.id' => $this->data['Lease']['id'])),
));
$this->redirect(array('controller' => 'customers',
'action' => 'view',
$lease['Customer']['id']));
}
if (!isset($id))
die("Oh Nooooo!!");
if (isset($id)) {
$lease = $this->Lease->find
('first', array
('contain' => array
(// Models
'Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
$lease = $this->Lease->find
('first', array
('contain' => array
(// Models
'Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
),
'conditions' => array(array('Lease.id' => $id),
array('Lease.close_date' => null),
),
));
$this->set('customer', $lease['Customer']);
$this->set('unit', $lease['Unit']);
$this->set('lease', $lease['Lease']);
'conditions' => array(array('Lease.id' => $id),
array('Lease.close_date' => null),
),
));
$this->set('customer', $lease['Customer']);
$this->set('unit', $lease['Unit']);
$this->set('lease', $lease['Lease']);
$title = ('Lease #' . $lease['Lease']['number'] . ': ' .
$lease['Unit']['name'] . ': ' .
$lease['Customer']['name'] . ': Move-Out');
}
else {
$title = 'Move-Out';
}
$redirect = array('controller' => 'leases',
'action' => 'view',
$id);
$title = ('Lease #' . $lease['Lease']['number'] . ': ' .
$lease['Unit']['name'] . ': ' .
$lease['Customer']['name'] . ': Prepare Move-Out');
$this->set(compact('title', 'redirect'));
$this->set(compact('title'));
$this->render('/leases/move');
}
/* /\************************************************************************** */
/* ************************************************************************** */
/* ************************************************************************** */
/* * action: promote_credit */
/* * - Moves any lease credit up to the customer level, so that */
/* * it may be used for charges other than those on this lease. */
/* *\/ */
/* function promote_surplus($id) { */
/* $this->Lease->promoteSurplus($id); */
/* $this->redirect(array('controller' => 'leases', */
/* 'action' => 'view', */
/* $id)); */
/* } */
/**************************************************************************
**************************************************************************
**************************************************************************
* action: apply_deposit
* - Applies the security deposit to charges. This is much
* like a receipt, but it's separated to keep it simple and
* to prevent feature overload on the receipt page.
* action: refund
* - Provides lease customer with a refund
*/
function apply_deposit($id) {
$A = new Account();
function refund($id) {
$lease = $this->Lease->find
('first', array
('contain' => array
(// Models
'Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
'Unit' => array('fields' => array('id', 'name')),
'Customer' => array('fields' => array('id', 'name')),
),
'conditions' => array(array('Lease.id' => $id),
// Make sure lease is not closed...
array('Lease.close_date' => null),
),
));
if (empty($lease)) {
$this->redirect(array('action'=>'view', $id));
}
// Determine the lease balance, bailing if the customer owes money
$balance = $this->Lease->balance($id);
if ($balance >= 0) {
$this->redirect(array('action'=>'view', $id));
}
// Get the lease balance, part of lease stats
$this->Lease->statsMerge($lease['Lease'],
array('stats' => $this->Lease->stats($id)));
// The refund will be for a positive amount
$balance *= -1;
// Determine the lease security deposit
$deposit = $this->Lease->findSecurityDeposits($lease['Lease']['id']);
$this->set(compact('deposit'));
$this->set('customer', $lease['Customer']);
$this->set('unit', $lease['Unit']);
$this->set('lease', $lease['Lease']);
$this->set('account', array('id' => $A->securityDepositAccountID()));
/* $redirect = array('controller' => 'leases', */
/* 'action' => 'view', */
/* $id); */
// Get the accounts capable of paying the refund
$refundAccounts = $this->Lease->StatementEntry->Account->refundAccounts();
$defaultAccount = current($refundAccounts);
$this->set(compact('refundAccounts', 'defaultAccount'));
// Prepare to render
$title = ('Lease #' . $lease['Lease']['number'] . ': ' .
$lease['Unit']['name'] . ': ' .
$lease['Customer']['name'] . ': Utilize Security Deposit');
$this->set(compact('title', 'redirect'));
$lease['Customer']['name'] . ': Refund');
$this->set(compact('title', 'lease', 'balance'));
$this->render('/transactions/refund');
}
@@ -287,70 +296,34 @@ class LeasesController extends AppController {
**************************************************************************
**************************************************************************
* action: bad_debt
* - Writes off remaining charges on a lease.
* REVISIT <AP>: 20090710
* Should this be a customer function? What customer
* would have only one lease that results in bad debt.
* - Sets up the write-off entry page, so that the
* user can write off remaining charges on a lease.
*/
function bad_debt($id) {
$A = new Account();
$this->Lease->id = $id;
$lease = $this->Lease->find
('first', array
('contain' => array
(// Models
'Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
'Unit' => array('fields' => array('id', 'name')),
'Customer' => array('fields' => array('id', 'name')),
),
'conditions' => array(array('Lease.id' => $id),
array('Lease.close_date' => null),
),
));
// Make sure we have a valid lease to write off
if (empty($lease))
$this->redirect(array('action' => 'view', $id));
// Get the lease balance, part of lease stats
$this->Lease->statsMerge($lease['Lease'],
array('stats' => $this->Lease->stats($id)));
// Determine the lease security deposit
$deposit = $this->Lease->findSecurityDeposits($lease['Lease']['id']);
if ($deposit['summary']['balance'] > 0)
die("Still have un-utilized security deposit");
$this->set('customer', $lease['Customer']);
$this->set('unit', $lease['Unit']);
$this->set('lease', $lease['Lease']);
$this->set('account', array('id' => $A->badDebtAccountID()));
/* $redirect = array('controller' => 'leases', */
/* 'action' => 'view', */
/* $id); */
// Get the lease balance
$balance = $this->Lease->balance($id);
// Prepare to render
$title = ('Lease #' . $lease['Lease']['number'] . ': ' .
$lease['Unit']['name'] . ': ' .
$lease['Customer']['name'] . ': Write Off Bad Debt');
$this->set(compact('title', 'redirect'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: refund
* - Provides user with a refund
* REVISIT <AP>: 20090710
* Should this be a customer function?
*/
function refund($id) {
$this->set(compact('title', 'lease', 'balance'));
$this->render('/transactions/bad_debt');
}
@@ -361,9 +334,23 @@ class LeasesController extends AppController {
* - Closes a lease to any further action
*/
// REVISIT <AP>: 20090809
// While cleaning up the sitelink data, then delete reldep()
function reldep($id) {
$this->Lease->id = $id;
$stamp = $this->Lease->field('moveout_date');
$this->Lease->releaseSecurityDeposits($id, $stamp);
$this->redirect(array('action'=>'view', $id));
}
function close($id) {
// REVISIT <AP>: 20090708
// We should probably seek confirmation first...
if (!$this->Lease->closeable($id)) {
$this->INTERNAL_ERROR("This lease is not ready to close");
$this->redirect(array('action'=>'view', $id));
}
$this->Lease->close($id);
$this->redirect(array('action'=>'view', $id));
}
@@ -398,15 +385,36 @@ class LeasesController extends AppController {
));
$A = new Account();
$charge_accounts = $A->chargeAccounts();
$charge_accounts = $A->invoiceAccounts();
$default_account = $A->rentAccountID();
$this->set(compact('charge_accounts', 'default_account'));
$rent_account = $A->rentAccountID();
$security_deposit_account = $A->securityDepositAccountID();
$this->set(compact('charge_accounts', 'default_account',
'rent_account', 'security_deposit_account'));
// REVISIT <AP> 20090705:
// Of course, the late charge should come from the late_schedule
$default_rent = $lease['Lease']['rent'];
$default_late = 10;
$this->set(compact('default_rent', 'default_late'));
$this->set(compact('default_late'));
if ($type === 'move-in') {
// Make sure we have a valid lease that we're moving in
if (empty($lease))
$this->redirect(array('action' => 'index'));
$movein = array();
$movein['time'] = strtotime($lease['Lease']['movein_date']);
$movein['effective_time'] = strtotime($lease['Lease']['movein_date']);
$movein_date = getdate($movein['effective_time']);
$movein['through_time'] = mktime(0, 0, 0, $movein_date['mon'] + 1, 0, $movein_date['year']);
$days_in_month = idate('d', $movein['through_time']);
$movein['prorated_days'] = $days_in_month - $movein_date['mday'] + 1;
$movein['prorated_rent'] = $lease['Lease']['rent'] * $movein['prorated_days'] / $days_in_month;
$movein['prorated'] = $movein['prorated_days'] != $days_in_month;
$movein['deposit'] = $lease['Lease']['deposit'];
$this->set(compact('movein'));
}
$title = ('Lease #' . $lease['Lease']['number'] . ': ' .
$lease['Unit']['name'] . ': ' .
@@ -415,6 +423,85 @@ class LeasesController extends AppController {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: assess_rent/late
* - Assesses the new monthly rent/late charge, if need be
*/
function assess_rent($date = null) {
$this->Lease->assessMonthlyRentAll($date);
$this->redirect(array('action'=>'index'));
}
function assess_late($date = null) {
$this->Lease->assessMonthlyLateAll($date);
$this->redirect(array('action'=>'index'));
}
function assess_all($date = null) {
$this->Lease->assessMonthlyRentAll($date);
$this->Lease->assessMonthlyLateAll($date);
$this->redirect(array('action'=>'index'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: overview
* - Displays lease up information
*/
function overview($months = 12) {
$overview = array('months' => array());
for ($month = 0; $month < $months; ++$month) {
//for ($month = 12; $month >= 0; --$month) {
$this_month = "(DATE(NOW() - INTERVAL $month MONTH - INTERVAL DAY(NOW())-1 DAY))";
$next_month = "($this_month + INTERVAL 1 MONTH)";
$row = $this->Lease->find
('first', array('link' => array(),
'fields' => array("MONTHNAME($this_month) AS month",
"YEAR($this_month) AS year"),
));
$mname = $row[0]['month'] .', '. $row[0]['year'];
$overview['months'][$mname] = array('name' => $mname);
foreach(array('start' => array('before' => $this_month, 'after' => $this_month),
'finish' => array('before' => $next_month, 'after' => $next_month),
'peak' => array('before' => $next_month, 'after' => $this_month))
AS $type => $parm) {
$count = $this->Lease->find
('count',
array('link' => array(),
'conditions' => array("movein_date < {$parm['before']}",
"(moveout_date IS NULL OR moveout_date >= {$parm['after']})",
),
));
$overview['months'][$mname][$type] = $count;
}
foreach(array('movein', 'moveout') AS $mvinout) {
$count = $this->Lease->find
('count',
array('link' => array(),
'conditions' => array("{$mvinout}_date < $next_month",
"{$mvinout}_date >= $this_month")
));
$overview['months'][$mname][$mvinout] = $count;
}
}
// Enable the Reports menu section
$this->sideMenuAreaActivate('REPORT');
// Prepare to render.
$this->set('title', 'Lease Up Report');
$this->set(compact('overview'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -433,69 +520,65 @@ class LeasesController extends AppController {
('first',
array('contain' =>
array(// Models
'LeaseType',
'Unit',
'Customer',
'LeaseType(id,name)',
'Unit(id,name)',
'Customer(id,name)',
),
'fields' => array('Lease.*', $this->Lease->delinquentField()),
'conditions' => array(array('Lease.id' => $id)),
)
);
$lease['Lease'] += $lease[0];
unset($lease[0]);
$lease['Lease']['paid_through'] = $this->Lease->rentPaidThrough($id);
$this->set('charge_gaps', $this->Lease->rentChargeGaps($id));
$this->set('charge_through', $this->Lease->rentChargeThrough($id));
// Obtain the overall lease balance
$this->Lease->statsMerge($lease['Lease'],
array('stats' => $this->Lease->stats($id)));
$outstanding_balance = $lease['Lease']['stats']['balance'];
// Determine the lease security deposit
$deposits = $this->Lease->findSecurityDeposits($lease['Lease']['id']);
$outstanding_deposit = $deposits['summary']['balance'];
// Set up dynamic menu items
if (!isset($lease['Lease']['close_date'])) {
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
// Figure out the outstanding balances for this lease
$outstanding_balance = $this->Lease->balance($id);
$outstanding_deposit = $this->Lease->securityDepositBalance($id);
// Set up dynamic menu items. Normally, these will only be present
// on an open lease, but it's possible for a lease to be closed, and
// yet still have an outstanding balance. This can happen if someone
// were to reverse charges, or if a payment should come back NSF.
if (!isset($lease['Lease']['close_date']) || $outstanding_balance > 0) {
if (!isset($lease['Lease']['moveout_date']))
$this->sidemenu_links[] =
array('name' => 'Move-Out', 'url' => array('action' => 'move_out',
$id));
$this->addSideMenuLink('Move-Out',
array('action' => 'move_out', $id), null,
'ACTION');
$this->sidemenu_links[] =
array('name' => 'Charges', 'url' => array('action' => 'invoice',
$id));
if (!isset($lease['Lease']['close_date']))
$this->addSideMenuLink('New Invoice',
array('action' => 'invoice', $id), null,
'ACTION');
$this->sidemenu_links[] =
array('name' => 'Payments', 'url' => array('controller' => 'customers',
'action' => 'receipt',
$lease['Customer']['id']));
$this->addSideMenuLink('New Receipt',
array('controller' => 'customers',
'action' => 'receipt',
$lease['Customer']['id']), null,
'ACTION');
if (isset($lease['Lease']['moveout_date']) && $outstanding_deposit > 0 && $outstanding_balance > 0)
$this->sidemenu_links[] =
array('name' => 'Apply Deposit', 'url' => array('action' => 'apply_deposit',
$id));
// REVISIT <AP>:
// Not allowing refund to be issued from the lease, as
// in fact, we should never have a positive lease balance.
// I'll flag this at the moment, since we might get one
// when a charge is reimbursed; a bug that we'll either
// need to fix, or we'll have to revisit this assumption.
if ($outstanding_balance < 0)
$this->INTERNAL_ERROR("Should not have a customer lease credit.");
if (isset($lease['Lease']['moveout_date']) &&
$outstanding_balance <= 0 &&
($outstanding_deposit - $outstanding_balance) > 0)
$this->sidemenu_links[] =
array('name' => 'Issue Refund', 'url' => array('action' => 'refund',
$id));
/* if ($outstanding_balance < 0) */
/* $this->addSideMenuLink('Issue Refund', */
/* array('action' => 'refund', $id), null, */
/* 'ACTION'); */
if (isset($lease['Lease']['moveout_date']) && $outstanding_deposit == 0 && $outstanding_balance > 0)
$this->sidemenu_links[] =
array('name' => 'Write-Off', 'url' => array('action' => 'bad_debt',
$id));
if (isset($lease['Lease']['moveout_date']) && $outstanding_balance > 0)
$this->addSideMenuLink('Write-Off',
array('action' => 'bad_debt', $id), null,
'ACTION');
if ($this->Lease->closeable($id))
$this->sidemenu_links[] =
array('name' => 'Close', 'url' => array('action' => 'close',
$id));
$this->addSideMenuLink('Close',
array('action' => 'close', $id), null,
'ACTION');
}
// Prepare to render

View File

@@ -2,317 +2,95 @@
class LedgerEntriesController extends AppController {
var $sidemenu_links = array();
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* action: index / current / past / all
* - Creates a list of ledger entries
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
}
function index() { $this->gridView('All Ledger Entries'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
if (isset($params['custom']['ar_account'])) {
$params['custom']['account_id'] =
$this->LedgerEntry->DebitLedger->Account->accountReceivableAccountID();
}
}
function jqGridDataTables(&$params, &$model) {
function gridDataTables(&$params, &$model) {
$link =
array(// Models
'Transaction' =>
array('fields' => array('id', 'stamp'),
),
'Transaction' =>
array('fields' => array('id', 'stamp'),
),
'MonetarySource' =>
array('fields' => array('id', 'name'),
),
'Ledger' =>
array('fields' => array('id', 'sequence'),
'Account' =>
array('fields' => array('id', 'name', 'type'),
),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
'Tender' =>
array('fields' => array('id', 'name', 'nsf_transaction_id'),
),
'Lease' =>
array('fields' => array('id', 'number'),
'Unit' =>
array('fields' => array('id', 'name'),
),
),
/* 'DebitEntry', */
/* 'CreditEntry', */
);
if (isset($params['custom']['account_ftype'])) {
$ftype = $params['custom']['account_ftype'];
$ftype = ucfirst($ftype);
//$ftype = $this->LedgerEntry->DebitLedger->Account->fundamentalOpposite($ftype);
$link[$ftype . 'Ledger'] =
array('fields' => array('id', 'sequence'),
'Account' => array('class' => 'Account',
'fields' => array('id', 'name'),
),
);
}
elseif (isset($params['custom']['ledger_id'])) {
$ledger_id = $params['custom']['ledger_id'];
$link['Ledger'] =
array('fields' => array('id', 'sequence'),
'conditions' => ("Ledger.id = IF(LedgerEntry.debit_ledger_id = $ledger_id," .
" LedgerEntry.credit_ledger_id," .
" LedgerEntry.debit_ledger_id)"),
'Account' => array(
'fields' => array('id', 'name'),
),
);
}
elseif ($params['action'] === 'collected') {
// Income / Receipt / Money
// debit: A/R credit: Income <-- this entry
// debit: Receipt credit: A/R <-- ReceiptLedgerEntry, below
// debit: Money credit: Receipt <-- MoneyLedgerEntry, below
$link['CreditLedger'] =
array('fields' => 'sequence',
'Account' =>
array('fields' => array('id', 'name'),
),
);
// We're searching for the Receipt<->A/R entries,
// which are debits on the A/R account. Find the
// reconciling entries to that A/R debit.
$link['DebitReconciliationLedgerEntry'] =
array('alias' => 'ReceiptLedgerEntry',
'Transaction' =>
array('alias' => 'ReceiptTransaction'),
// Credit Ledger should be A/R;
// Debit Ledger should be Receipt
'DebitLedger' =>
array('alias' => 'ReceiptLedger',
'Account' => array('alias' => 'ReceiptAccount'),
),
// Finally, the Money (Cash/Check/etc) Entry is the one
// which reconciles our ReceiptLedgerEntry debit
'DebitReconciliationLedgerEntry' =>
array('alias' => 'MoneyLedgerEntry',
'linkalias' => 'MoneyLedgerEntryR',
// Credit Ledger should be Receipt;
// Debit Ledger should be our Money Account
'DebitLedger' =>
array('alias' => 'MoneyLedger',
'Account' =>
array('alias' => 'MoneyAccount'),
),
),
);
}
else {
$link['DebitLedger'] =
array('fields' => array('id', 'sequence'),
'DebitAccount' => array('class' => 'Account',
'fields' => array('id', 'name'),
),
);
$link['CreditLedger'] =
array('fields' => array('id', 'sequence'),
'CreditAccount' => array('class' => 'Account',
'fields' => array('id', 'name'),
),
);
}
if (isset($params['custom']['account_id'])) {
$account_id = $params['custom']['account_id'];
$link['Ledger'] =
array('fields' => array('id', 'sequence'),
'conditions' => ("Ledger.id = IF(DebitLedger.account_id = $account_id," .
" LedgerEntry.credit_ledger_id," .
" LedgerEntry.debit_ledger_id)"),
'Account' => array(
'fields' => array('id', 'name'),
),
);
}
if (isset($params['custom']['reconcile_id'])) {
$ftype = $params['custom']['account_ftype'];
$ftype = $this->LedgerEntry->DebitLedger->Account->fundamentalOpposite($ftype);
$ftype = ucfirst($ftype);
$link[$ftype.'ReconciliationLedgerEntry'] =
array('fields' => array('Reconciliation.amount'));
}
return array('link' => $link);
}
function jqGridDataFields(&$params, &$model) {
$ledger_id = (isset($params['custom']['ledger_id'])
? $params['custom']['ledger_id']
: null);
$account_id = (isset($params['custom']['account_id'])
? $params['custom']['account_id']
: null);
$account_type = (isset($params['custom']['account_type'])
? $params['custom']['account_type']
: null);
$fields = $model->ledgerContextFields2($ledger_id, $account_id, $account_type);
if (count(array_intersect($params['fields'], array('applied'))) == 1)
$fields[] = 'SUM(Reconciliation.amount) AS applied';
if ($params['action'] === 'collected')
$fields[] = 'MAX(ReceiptTransaction.stamp) AS last_paid';
return $fields;
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
return array_merge($fields,
$this->LedgerEntry->debitCreditFields());
}
function jqGridDataConditions(&$params, &$model) {
$ledger_id = (isset($params['custom']['ledger_id'])
? $params['custom']['ledger_id']
: null);
$account_type = (isset($params['custom']['account_type'])
? $params['custom']['account_type']
: null);
function gridDataFilterTablesTable(&$params, &$model, $table) {
$table = $this->gridDataFilterTableName($params, $model, $table);
// Account is already part of our standard table set.
// Ensure we don't add it in again as part of filtering.
if ($table == 'Account')
return null;
$conditions = parent::jqGridDataConditions($params, $model);
// Customer needs to be added beneath Transaction
if ($table == 'Customer')
return 'Transaction';
if ($params['action'] === 'collected') {
extract($params['custom']);
if (isset($collected_account_id))
$conditions[] = array('Account.id' => $params['custom']['collected_account_id']);
else
die("INTERNAL ERROR: COLLECTED ACCOUNT ID NOT SET");
if (!empty($collected_from_date))
$conditions[]
= array('ReceiptTransaction.stamp >=' =>
$this->LedgerEntry->Transaction->dateFormatBeforeSave($collected_from_date));
if (!empty($collected_through_date))
$conditions[]
= array('ReceiptTransaction.stamp <=' =>
$this->LedgerEntry->Transaction->dateFormatBeforeSave($collected_through_date . ' 23:59:59'));
if (isset($collected_payment_accounts))
$conditions[] = array('MoneyAccount.id' => $collected_payment_accounts);
else
$conditions[] = array('NOT' => array(array('MoneyAccount.id' => null)));
}
if ($params['action'] === 'ledger') {
$conditions[] = $model->ledgerContextConditions($ledger_id, $account_type);
}
if (isset($params['custom']['reconcile_id'])) {
$ftype = $params['custom']['account_ftype'];
//$ftype = $this->LedgerEntry->DebitLedger->Account->fundamentalOpposite($ftype);
$conditions[] = array('Reconciliation.'.$ftype.'_ledger_entry_id' => $params['custom']['reconcile_id']);
}
if (isset($params['custom']['account_id'])) {
$conditions[] =
array('OR' =>
array(array('CreditAccount.id' => $params['custom']['account_id']),
array('DebitAccount.id' => $params['custom']['account_id'])));
}
if (isset($params['custom']['customer_id'])) {
$conditions[] =
array('Customer.id' => $params['custom']['customer_id']);
/* $Account = new Account(); */
/* if (isset($params['custom']['account_ftype']) || */
/* isset($params['custom']['ledger_id'])) { */
/* $conditions[] = */
/* array('OR' => array('Account.id' => $Account->invoiceAccountID(), */
/* 'Account.id' => $Account->receiptAccountID())); */
/* } else { */
/* $conditions[] = */
/* array('OR' => array('DebitAccount.id' => $Account->invoiceAccountID(), */
/* //'CreditAccount.id' => $Account->invoiceAccountID(), */
/* //'DebitAccount.id' => $Account->receiptAccountID(), */
/* 'CreditAccount.id' => $Account->receiptAccountID(), */
/* )); */
/* } */
}
if (isset($params['custom']['lease_id'])) {
$conditions[] =
array('Lease.id' => $params['custom']['lease_id']);
/* $Account = new Account(); */
/* if (isset($params['custom']['account_ftype']) || */
/* isset($params['custom']['ledger_id'])) { */
/* $conditions[] = */
/* array('OR' => array('Account.id' => $Account->invoiceAccountID(), */
/* 'Account.id' => $Account->receiptAccountID())); */
/* } else { */
/* $conditions[] = */
/* array('OR' => array('DebitAccount.id' => $Account->invoiceAccountID(), */
/* //'CreditAccount.id' => $Account->invoiceAccountID(), */
/* //'DebitAccount.id' => $Account->receiptAccountID(), */
/* 'CreditAccount.id' => $Account->receiptAccountID(), */
/* )); */
/* } */
}
if (isset($params['custom']['transaction_id'])) {
$conditions[] =
array('Transaction.id' => $params['custom']['transaction_id']);
}
if (isset($params['custom']['monetary_source_id'])) {
$conditions[] =
array('MonetarySource.id' => $params['custom']['monetary_source_id']);
}
return $conditions;
return $table;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
$links['Transaction'] = array('id');
$links['LedgerEntry'] = array('id');
$links['Account'] = array('controller' => 'accounts', 'name');
$links['DebitAccount'] = array('controller' => 'accounts', 'name');
$links['CreditAccount'] = array('controller' => 'accounts', 'name');
$links['MonetarySource'] = array('name');
$links['Customer'] = array('name');
$links['Lease'] = array('number');
$links['Unit'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
function gridDataFilterTablesConfig(&$params, &$model, $table) {
$config = parent::gridDataFilterTablesConfig($params, $model, $table);
// Customer is special in that its linked in by Transaction
// Therefore, the actual table used for the join is 'Transaction',
// not 'Customer', and so we need to specify Customer here.
if ($table == 'Customer')
$config = array('Customer' => $config);
return $config;
}
function jqGridDataGroup(&$params, &$model) {
if (isset($params['custom']['group_by_tx']) && $params['custom']['group_by_tx'])
return $model->alias.'.transaction_id';
return parent::jqGridDataGroup($params, $model);
function gridDataFilterConditionsStatement(&$params, &$model, $table, $key, $value) {
//pr(compact('table', 'key', 'value'));
if ($table == 'Account' && $value['value_present'] && $value['value'] === '-AR-')
$value = $this->LedgerEntry->Ledger->Account->accountReceivableAccountID();
return parent::gridDataFilterConditionsStatement($params, $model, $table, $key, $value);
}
function jqGridDataOrder(&$params, &$model, $index, $direction) {
function gridDataOrder(&$params, &$model, $index, $direction) {
/* if ($index === 'balance') */
/* return ($index .' '. $direction); */
$order = parent::jqGridDataOrder($params, $model, $index, $direction);
$order = parent::gridDataOrder($params, $model, $index, $direction);
if ($index === 'Transaction.stamp') {
$order[] = 'LedgerEntry.id ' . $direction;
@@ -321,28 +99,32 @@ class LedgerEntriesController extends AppController {
return $order;
}
function jqGridDataRecords(&$params, &$model, $query) {
if ($params['action'] === 'collected') {
$tquery = array_diff_key($query, array('fields'=>1,'group'=>1,'limit'=>1,'order'=>1));
$tquery['fields'] = array('SUM(Reconciliation.amount) AS applied');
$total = $model->find('first', $tquery);
$params['userdata']['total'] = $total[0]['applied'];
function gridDataPostProcessCalculatedFields(&$params, &$model, &$records) {
parent::gridDataPostProcessCalculatedFields($params, $model, $records);
foreach ($records AS &$record) {
// REVISIT <AP>: 20090730
// We really need the grid to handle this. We probably need to
// either create a hidden column with the nsf id, or pass back
// a list of nsf items as user data. We can then add an onload
// function to sweep through the nsf items and format them.
// For now... this works.
if (!empty($record['Tender']['nsf_transaction_id']))
$record['Tender']['name'] =
'<SPAN class="nsf-tender">' . $record['Tender']['name'] . '</SPAN>';
}
return parent::jqGridDataRecords($params, $model, $query);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: reverse the ledger entry
*/
function reverse($id) {
$this->LedgerEntry->reverse($id);
$this->redirect(array('action'=>'view', $id));
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['LedgerEntry'] = array('id');
$links['Transaction'] = array('id');
// REVISIT <AP>: 20090827
// Need to take 'level' into account
if ($this->Permission->allow('controller.accounts')) {
$links['Ledger'] = array('id');
$links['Account'] = array('name');
}
$links['Tender'] = array('name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -354,114 +136,69 @@ class LedgerEntriesController extends AppController {
*/
function view($id = null) {
if (!$id) {
$entry = $this->LedgerEntry->find
('first',
array('contain' => array
(
'Transaction' =>
array('fields' => array('id', 'stamp'),
),
'Ledger' =>
array('fields' => array('id', 'sequence', 'name'),
'Account' =>
array('fields' => array('id', 'name', 'type'),
),
),
'Tender' =>
array('fields' => array('id', 'name'),
),
'DebitDoubleEntry' => array('id'),
'CreditDoubleEntry' => array('id'),
'DebitEntry' => array('fields' => array('id', 'crdr')),
'CreditEntry' => array('fields' => array('id', 'crdr')),
),
'conditions' => array('LedgerEntry.id' => $id),
));
if (empty($entry) || empty($entry['Ledger']['Account'])) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
// Get the LedgerEntry and related fields
$entry = $this->LedgerEntry->find
('first',
array('contain' => array('MonetarySource.id',
'MonetarySource.name',
'Transaction.id',
'Transaction.stamp',
'DebitLedger.id',
'DebitLedger.sequence',
'DebitLedger.account_id',
'CreditLedger.id',
'CreditLedger.sequence',
'CreditLedger.account_id',
'Customer.id',
'Customer.name',
'Lease.id',
),
if (!empty($entry['DebitEntry']) && !empty($entry['CreditEntry']))
die("LedgerEntry has both a matching DebitEntry and CreditEntry");
if (empty($entry['DebitEntry']) && empty($entry['CreditEntry']))
die("LedgerEntry has neither a matching DebitEntry nor a CreditEntry");
if (empty($entry['DebitEntry']) && count($entry['CreditEntry']) != 1)
die("LedgerEntry has more than one CreditEntry");
if (empty($entry['CreditEntry']) && count($entry['DebitEntry']) != 1)
die("LedgerEntry has more than one DebitEntry");
'fields' => array('LedgerEntry.*'),
if (empty($entry['DebitEntry']))
$entry['MatchingEntry'] = $entry['CreditEntry'][0];
else
$entry['MatchingEntry'] = $entry['DebitEntry'][0];
'conditions' => array('LedgerEntry.id' => $id),
));
//pr($entry);
if (empty($entry['DebitDoubleEntry']['id']))
$entry['DoubleEntry'] = $entry['CreditDoubleEntry'];
else
$entry['DoubleEntry'] = $entry['DebitDoubleEntry'];
// Because 'DebitLedger' and 'CreditLedger' both relate to 'Account',
// CakePHP will not include them in the LedgerEntry->find (or so it
// seems). We'll have to break out each Account separately.
// Get the Account from DebitLedger
$entry['DebitLedger'] += $this->LedgerEntry->DebitLedger->Account->find
('first',
array('contain' => true,
'fields' => array('Account.id', 'Account.name', 'Account.type', 'Account.trackable'),
'conditions' => array('Account.id' => $entry['DebitLedger']['account_id']),
));
$entry['DebitLedger']['Account']['ftype'] =
$this->LedgerEntry->DebitLedger->Account
->fundamentalType($entry['DebitLedger']['Account']['type']);
// Get the Account from CreditLedger
$entry['CreditLedger'] += $this->LedgerEntry->CreditLedger->Account->find
('first',
array('contain' => true,
'fields' => array('Account.id', 'Account.name', 'Account.type', 'Account.trackable'),
'conditions' => array('Account.id' => $entry['CreditLedger']['account_id']),
));
$entry['CreditLedger']['Account']['ftype'] =
$this->LedgerEntry->CreditLedger->Account
->fundamentalType($entry['CreditLedger']['Account']['type']);
// Get the reconciliation balances for this ledger entry
$stats = $this->LedgerEntry->stats($id);
$stats['debit']['amount_reconciled'] = $stats['debit_amount_reconciled'];
$stats['credit']['amount_reconciled'] = $stats['credit_amount_reconciled'];
if ($entry['DebitLedger']['Account']['trackable'])
$stats['debit']['amount_remaining'] = $entry['LedgerEntry']['amount'] - $stats['debit']['amount_reconciled'];
if ($entry['CreditLedger']['Account']['trackable'])
$stats['credit']['amount_remaining'] = $entry['LedgerEntry']['amount'] - $stats['credit']['amount_reconciled'];
//pr($stats);
$reconciled = $this->LedgerEntry->findReconciledLedgerEntries($id);
//pr($reconciled);
// REVISIT <AP>: 20090711
// It's not clear whether we should be able to reverse charges that have
// already been paid/cleared/reconciled. Certainly, that will be the
// case when someone has pre-paid and then moves out early. However, this
// will work well for items accidentally charged but not yet paid for.
if ((!$entry['DebitLedger']['Account']['trackable'] ||
$stats['debit']['amount_reconciled'] == 0) &&
(!$entry['CreditLedger']['Account']['trackable'] ||
$stats['credit']['amount_reconciled'] == 0)
&& 0
)
{
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'Undo',
'url' => array('action' => 'reverse',
$id));
}
if ($this->LedgerEntry->Ledger->Account->type
($entry['CreditLedger']['Account']['id']) == 'INCOME')
{
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'Reverse',
'url' => array('action' => 'reverse',
$id));
}
// REVISIT <AP>: 20090816
// This page doesn't seem very useful, let's just keep it
// all to the double entry view.
$this->redirect(array('controller' => 'double_entries',
'action' => 'view',
$entry['DoubleEntry']['id']));
// Prepare to render.
$title = "Double Ledger Entry #{$entry['LedgerEntry']['id']}";
$this->set(compact('entry', 'title', 'reconciled', 'stats'));
$title = "Ledger Entry #{$entry['LedgerEntry']['id']}";
$this->set(compact('entry', 'title'));
}
}

View File

@@ -2,22 +2,26 @@
class LedgersController extends AppController {
var $sidemenu_links =
array(array('name' => 'Ledgers', 'header' => true),
array('name' => 'Current', 'url' => array('controller' => 'ledgers', 'action' => 'current')),
array('name' => 'Closed', 'url' => array('controller' => 'ledgers', 'action' => 'closed')),
array('name' => 'All', 'url' => array('controller' => 'ledgers', 'action' => 'all')),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Current',
array('controller' => 'ledgers', 'action' => 'current'), null,
'CONTROLLER');
$this->addSideMenuLink('Closed',
array('controller' => 'ledgers', 'action' => 'closed'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'ledgers', 'action' => 'all'), null,
'CONTROLLER');
}
@@ -29,90 +33,86 @@ class LedgersController extends AppController {
*/
function index() { $this->all(); }
function current() { $this->jqGridView('Current Ledgers'); }
function closed() { $this->jqGridView('Closed Ledgers'); }
function all() { $this->jqGridView('All Ledgers', 'all'); }
function current() { $this->gridView('Current Ledgers'); }
function closed() { $this->gridView('Closed Ledgers'); }
function all() { $this->gridView('All Ledgers', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
function gridDataSetup(&$params) {
parent::gridDataSetup($params);
if (!isset($params['action']))
$params['action'] = 'all';
}
function jqGridDataCountTables(&$params, &$model) {
return array('contain' => false);
}
function jqGridDataTables(&$params, &$model) {
function gridDataCountTables(&$params, &$model) {
return array
('link' =>
array(// Models
'Account',
'LedgerEntry',
'Close',
),
);
}
function jqGridDataFields(&$params, &$model) {
return array
('Ledger.*',
'CONCAT(Account.id, "-", Ledger.sequence) AS id_sequence',
'SUM(IF(LedgerEntry.debit_ledger_id = Ledger.id,
LedgerEntry.amount, NULL)) AS debits',
'SUM(IF(LedgerEntry.credit_ledger_id = Ledger.id,
LedgerEntry.amount, NULL)) AS credits',
"SUM(IF(Account.type IN ('ASSET', 'EXPENSE'),
IF(LedgerEntry.debit_ledger_id = Ledger.id, 1, -1),
IF(LedgerEntry.credit_ledger_id = Ledger.id, 1, -1)
) * IF(LedgerEntry.amount, LedgerEntry.amount, 0)
) AS balance",
'COUNT(LedgerEntry.id) AS entries');
function gridDataTables(&$params, &$model) {
$tables = $this->gridDataCountTables($params, $model);
$tables['link'][] = 'LedgerEntry';
$tables['link'][] = 'CloseTransaction';
return $tables;
}
function jqGridDataConditions(&$params, &$model) {
$conditions = parent::jqGridDataConditions($params, $model);
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
$fields[] = 'CONCAT(Account.id, "-", Ledger.sequence) AS id_sequence';
return array_merge($fields,
$this->Ledger->LedgerEntry->debitCreditFields(true));
}
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
if ($params['action'] === 'current') {
$conditions[] = array('NOT' => array('Ledger.closed'));
$conditions[] = array('Ledger.close_transaction_id' => null);
}
elseif ($params['action'] === 'closed') {
$conditions[] = 'Ledger.closed';
$conditions[] = array('Ledger.close_transaction_id !=' => null);
}
$conditions[] = array('Account.level >=' =>
$this->Permission->level('controller.accounts'));
return $conditions;
}
function jqGridDataOrder(&$params, &$model, $index, $direction) {
$id_sequence = false;
if ($index === 'id_sequence') {
$id_sequence = true;
$index = 'Ledger.account_id';
}
function gridDataOrder(&$params, &$model, $index, $direction) {
$order = parent::gridDataOrder($params, $model, $index, $direction);
$order = parent::jqGridDataOrder($params, $model, $index, $direction);
if ($id_sequence) {
$order[] = 'Ledger.sequence ' . $direction;
}
// After sorting by whatever the user wants, add these
// defaults into the sort mechanism. If we're already
// sorting by one of them, it will only be redundant,
// and should cause no harm (possible a longer query?)
$order[] = 'Account.name ' . $direction;
$order[] = 'Ledger.sequence ' . $direction;
return $order;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
$links['Ledger'] = array('id_sequence');
$links['Account'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
// REVISIT <AP>: 20090827
// Need to take 'level' into account
if ($this->Permission->allow('controller.accounts')) {
$links['Ledger'] = array('sequence');
$links['Account'] = array('name');
}
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -124,22 +124,24 @@ class LedgersController extends AppController {
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
// Get details about the ledger itself (no entries yet)
$ledger = $this->Ledger->find
('first',
array('contain' =>
array(// Models
'Account',
),
'conditions' => array(array('Ledger.id' => $id)),
'conditions' => array(array('Ledger.id' => $id),
array('Account.level >=' =>
$this->Permission->level('controller.accounts')),
),
)
);
if (empty($ledger)) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
// Get ledger stats for our summary box
$stats = $this->Ledger->stats($id);

View File

@@ -15,28 +15,28 @@ class MapsController extends AppController {
*/
function index() { $this->all(); }
function all() { $this->jqGridView('All Maps', 'all'); }
function all() { $this->gridView('All Maps', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataTables(&$params, &$model) {
function gridDataTables(&$params, &$model) {
return array
('link' => array('SiteArea' => array('fields' => array('SiteArea.id', 'SiteArea.name')),
),
);
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Map'] = array('id');
return parent::jqGridRecordLinks($params, $model, $records, $links);
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -52,6 +52,7 @@ class MapsController extends AppController {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
$this->sideMenuEnable('SITE', $this->op_area);
$this->set('info', $this->mapInfo($id, $requested_width));
$this->set('title', "Site Map");
}
@@ -85,10 +86,34 @@ class MapsController extends AppController {
'units' => array());
// Find all of the map/unit information from this SiteArea
$this->Map->recursive = 2;
$this->Map->SiteArea->unbindModel(array('hasOne' => array('Map')));
$map = $this->Map->read(null, $id);
//pr($map);
$map = $this->Map->find('first', array('contain' => false,
'conditions' => array('id' => $id)));
$units = $this->Map->Unit->find
('all',
array('link' =>
array('Map' =>
array('fields' => array()),
'CurrentLease' =>
array('fields' => array('id', 'paid_through_date',
$this->Map->Unit->CurrentLease->
delinquentField('CurrentLease')),
'Customer'),
'UnitSize' =>
array('fields' => array('id', 'depth', 'width',
'MapsUnit.pt_top',
'MapsUnit.pt_left',
'MapsUnit.transpose')),
),
'fields' => array('id', 'name', 'status'),
'conditions' => array('Map.id' => $id),
));
/* pr(compact('map', 'units')); */
/* $this->render('/empty'); */
/* return; */
/*****
* The preference would be to leave all things "screen" related
@@ -113,7 +138,7 @@ class MapsController extends AppController {
$info['depth'] = $bottom * $screen_adjustment_factor;
// Go through each unit in the map, calculating the map location
foreach ($map['Unit'] AS $unit) {
foreach ($units AS $unit) {
$lft = $unit['MapsUnit']['pt_left'] + $boundary_adjustment;
$top = $unit['MapsUnit']['pt_top'] + $boundary_adjustment;
@@ -132,10 +157,9 @@ class MapsController extends AppController {
$width *= $screen_adjustment_factor;
$depth *= $screen_adjustment_factor;
//$info['units'][$unit['id']] =
$info['units'][] =
array( 'id' => $unit['id'],
'name' => $unit['name'],
array( 'id' => $unit['Unit']['id'],
'name' => $unit['Unit']['name'],
'left' => $lft,
'right' => $lft + $width,
'top' => $top,
@@ -143,11 +167,15 @@ class MapsController extends AppController {
'width' => $width,
'depth' => $depth,
'n-s' => $unit['MapsUnit']['transpose'] ? 0 : 1,
'status' => $unit['status']
'status' => (($unit['Unit']['status'] === 'OCCUPIED' &&
!empty($unit[0]['delinquent']))
? 'LATE' : $unit['Unit']['status']),
'data' => $unit,
);
}
//pr($info);
/* pr($info); */
/* $this->render('/empty'); exit(); */
return $info;
}
@@ -160,8 +188,10 @@ class MapsController extends AppController {
*/
function legend($id = null, $requested_width = 400) {
$status = $this->Map->Unit->activeStatusEnums();
//pr($status);
$status = array_keys($this->Map->Unit->activeStatusEnums());
$occupied_key = array_search('OCCUPIED', $status);
array_splice($status, $occupied_key+1, 0, array('LATE'));
$rows = 2;
$cols = (int)((count($status) + $rows - 1) / $rows);
@@ -191,7 +221,7 @@ class MapsController extends AppController {
$item_width *= $screen_adjustment_factor;
$item_depth *= $screen_adjustment_factor;
foreach ($status AS $code => $value) {
foreach ($status AS $code) {
$info['units'][] = array('name' => $code,
'status' => $code,
'width' => $item_width,
@@ -241,9 +271,9 @@ class MapsController extends AppController {
$info['palate']['unit']['DIRTY']['bg'] = array('red' => 128, 'green' => 192, 'blue' => 192);
$info['palate']['unit']['VACANT']['bg'] = array('red' => 0, 'green' => 255, 'blue' => 128);
$info['palate']['unit']['OCCUPIED']['bg'] = array('red' => 0, 'green' => 128, 'blue' => 255);
$info['palate']['unit']['LATE']['bg'] = array('red' => 255, 'green' => 64, 'blue' => 64);
$info['palate']['unit']['LOCKED']['bg'] = array('red' => 255, 'green' => 128, 'blue' => 128);
$info['palate']['unit']['LIENED']['bg'] = array('red' => 255, 'green' => 192, 'blue' => 192);
$info['palate']['unit']['LATE']['bg'] = array('red' => 255, 'green' => 192, 'blue' => 192);
$info['palate']['unit']['LOCKED']['bg'] = array('red' => 255, 'green' => 64, 'blue' => 64);
$info['palate']['unit']['LIENED']['bg'] = array('red' => 255, 'green' => 0, 'blue' => 128);
// Determine text color to go with each background
foreach ($info['palate']['unit'] AS &$code) {

View File

@@ -1,110 +0,0 @@
<?php
class MonetarySourcesController extends AppController {
var $sidemenu_links = array();
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: index / all
* - Generate a listing of MonetarySources
*/
function index() { $this->all(); }
function all() { $this->jqGridView('All MonetarySources', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataTables(&$params, &$model) {
return array
('contain' => false,
);
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
$links['MonetarySource'] = array('id');
return parent::jqGridRecordLinks($params, $model, $records, $links);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: nsf
* - Marks a monetary source as having insufficient funds.
*/
function nsf($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
// REVISIT <AP>: 20090713
// For testing purposes, must be deleted
$stamp = '2009-07-09';
$this->MonetarySource->nsf($id, $stamp);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific entry
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
// Get the MonetarySource and related fields
$monetary_source = $this->MonetarySource->find
('first', array
('contain' => false,
));
// REVISIT <AP>: 20090713
// Consider allowing the NSF operation only if the source is used on
// a ledger entry that is debited on a "payable" account (perhaps
// even restricted to "payable" ASSET accounts), credited on Receipt
// (or A/R), and reconciles the credit to an entry that debits on a
// "depositable" account.
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'NSF',
'url' => array('action' => 'nsf',
$id));
// Prepare to render.
$title = "Monetary Source #{$monetary_source['MonetarySource']['id']}";
$this->set(compact('monetary_source', 'title'));
}
}

View File

@@ -0,0 +1,364 @@
<?php
class StatementEntriesController extends AppController {
/**************************************************************************
**************************************************************************
**************************************************************************
* action: index / current / past / all
* - Creates a list of statement entries
*/
function index() { $this->gridView('All Statement Entries'); }
function unpaid() { $this->gridView('Unpaid Charges', 'unreconciled'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function gridDataCountTables(&$params, &$model) {
$link =
array(// Models
'Transaction' =>
array('fields' => array('id', 'stamp'),
),
'Customer' =>
array('fields' => array('id', 'name'),
),
'Lease' =>
array('fields' => array('id', 'number'),
'Unit' =>
array('fields' => array('id', 'name'),
),
),
'Account' =>
array('fields' => array('id', 'name', 'type'),
),
);
if (!empty($params['post']['custom']['statement_entry_id'])) {
$link['ChargeEntry'] = array();
// This query actually represents a union...
// Unpaid Charge/Surplus: ChargeID - NULL; DisbursementID - NULL
// Paid Charge/Refund: ChargeID - NULL; DisbursementID - !NULL
// Disbursement/Reversal: ChargeID - !NULL; DisbursementID - NULL
// <EMPTY SET>: ChargeID - !NULL; DisbursementID - !NULL
//
// The query is really slow unless we add the `id` condition to the join.
// A cleaner query would be nice, but we must work within the Cake framework.
$link['DisbursementEntry'] = array('conditions' =>
'`DisbursementEntry`.`id` = '
. $params['post']['custom']['statement_entry_id']);
}
return array('link' => $link);
}
function gridDataTables(&$params, &$model) {
$tables = $this->gridDataCountTables($params, $model);
if (in_array('applied', $params['post']['fields'])) {
$tables['link'] +=
array('ChargeEntry' => array(),
'DisbursementEntry' => array());
}
return $tables;
}
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
if (in_array('applied', $params['post']['fields'])) {
$fields[] = ("IF(StatementEntry.type = 'CHARGE'," .
" SUM(COALESCE(DisbursementEntry.amount,0))," .
" SUM(COALESCE(ChargeEntry.amount,0)))" .
" AS 'applied'");
}
if (in_array('unapplied', $params['post']['fields'])) {
$fields[] = ("StatementEntry.amount - (" .
"IF(StatementEntry.type = 'CHARGE'," .
" SUM(COALESCE(DisbursementEntry.amount,0))," .
" SUM(COALESCE(ChargeEntry.amount,0)))" .
") AS 'unapplied'");
}
$fields = array_merge($fields,
$this->StatementEntry->chargeDisbursementFields());
return $fields;
}
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
extract($params['post']['custom']);
if (!empty($from_date))
$conditions[]
= array('Transaction.stamp >=' =>
$this->StatementEntry->Transaction->dateFormatBeforeSave($from_date));
if (!empty($through_date))
$conditions[]
= array('Transaction.stamp <=' =>
$this->StatementEntry->Transaction->dateFormatBeforeSave($through_date . ' 23:59:59'));
if (isset($account_id))
$conditions[] = array('StatementEntry.account_id' => $account_id);
if (isset($customer_id))
$conditions[] = array('StatementEntry.customer_id' => $customer_id);
if (isset($statement_entry_id))
$conditions[] = array('OR' =>
array(array('ChargeEntry.id' => $statement_entry_id),
array('DisbursementEntry.id' => $statement_entry_id)));
return $conditions;
}
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['StatementEntry'] = array('id');
$links['Transaction'] = array('id');
// REVISIT <AP>: 20090827
// Need to take 'level' into account
if ($this->Permission->allow('controller.accounts'))
$links['Account'] = array('name');
$links['Customer'] = array('name');
$links['Lease'] = array('number');
$links['Unit'] = array('name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
function gridDataOrder(&$params, &$model, $index, $direction) {
$order = parent::gridDataOrder($params, $model, $index, $direction);
// After sorting by whatever the user wants, add these
// defaults into the sort mechanism. If we're already
// sorting by one of them, it will only be redundant,
// and should cause no harm (possible a longer query?)
if ($index != 'Transaction.stamp' &&
$index != 'StatementEntry.effective_date') {
$order[] = 'Transaction.stamp ' . $direction;
$order[] = 'StatementEntry.effective_date ' . $direction;
}
$order[] = 'StatementEntry.id ' . $direction;
return $order;
}
function gridDataCountExecute(&$params, &$model, $query) {
if ($params['action'] === 'unreconciled') {
// REVISIT <AP> 20100413:
// This is a lame solution, as it runs the same queries twice
// (and causes code duplication). However, I'm not in the mood
// to flush out an actual "count" solution at the moment, and I
// also don't want to cache the results in $params (although
// that is probably the most sensible solution). So, I'll just
// calculate the reconciled set both times and live with the
// performance and maintenance penalty
$lquery = array('conditions' => $query['conditions']);
$set = $this->StatementEntry->reconciledSet('CHARGE', $lquery, true);
return count($set['entries']);
}
return parent::gridDataCountExecute($params, $model, $query);
}
function gridDataRecordsExecute(&$params, &$model, $query) {
if ($params['action'] === 'unreconciled') {
$lquery = array('conditions' => $query['conditions']);
$set = $this->StatementEntry->reconciledSet('CHARGE', $lquery, true);
$entries = array();
foreach ($set['entries'] AS $entry)
$entries[] = $entry['StatementEntry']['id'];
$query['conditions'] = array('StatementEntry.id' => $entries);
$params['userdata']['balance'] = $set['summary']['balance'];
}
if ($params['action'] === 'collected') {
$tquery = array_diff_key($query, array('fields'=>1,'group'=>1,'limit'=>1,'order'=>1));
$tquery['fields'] = array("SUM(COALESCE(StatementEntry.amount,0)) AS 'total'");
$total = $model->find('first', $tquery);
$params['userdata']['total'] = $total[0]['total'];
}
return parent::gridDataRecordsExecute($params, $model, $query);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: reverse the ledger entry
*/
function reverse($id = null) {
if ($this->data) {
//pr($this->data); die();
$this->StatementEntry->reverse
($this->data['StatementEntry']['id'],
$this->data['Transaction']['stamp'],
$this->data['Transaction']['comment']);
$this->redirect(array('action'=>'view',
$this->data['StatementEntry']['id']));
$this->INTERNAL_ERROR('SHOULD HAVE REDIRECTED');
}
$this->StatementEntry->id = $id;
$entry = $this->StatementEntry->find
('first', array
('contain' => array('Customer', 'Transaction', 'Account'),
));
if (empty($entry)) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'customers',
'action'=>'index'));
}
if (!$this->StatementEntry->reversable($id)) {
$this->Session->setFlash(__('Item not reversable.', true));
$this->redirect(array('action'=>'view', $id));
}
// Prepare to render.
$title = ("Charge #{$entry['StatementEntry']['id']}" .
" : {$entry['StatementEntry']['amount']}" .
" : Reverse");
$this->set(compact('entry', 'title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: waive the ledger entry
*/
function waive($id) {
$this->StatementEntry->waive($id);
$this->redirect(array('action'=>'view', $id));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: chargesbymonth
* - Displays charges by month
*/
function chargesbymonth($months = 12) {
$result = $this->StatementEntry->find
('all',
array('link' => array('Account' => array('fields' => 'name')),
'fields' => array_merge(array('MONTHNAME(effective_date) AS month',
'YEAR(effective_date) AS year'),
$this->StatementEntry->chargeDisbursementFields(true)),
'conditions' => array('Account.type' => 'INCOME',
'effective_date >= DATE(NOW() - INTERVAL '.($months-1).' MONTH - INTERVAL DAY(NOW())-1 DAY)',
'effective_date <= NOW()',
),
'group' => array('YEAR(effective_date)', 'MONTH(effective_date)', 'Account.id'),
'order' => array('YEAR(effective_date) DESC', 'MONTH(effective_date) DESC', 'Account.name'),
));
$overview = array('months' => array(), 'charges' => 0);
foreach ($result AS $row) {
$mname = $row[0]['month'] .', '. $row[0]['year'];
if (empty($overview['months'][$mname]))
$overview['months'][$mname] = array('name' => $mname,
'subs' => array(),
'charges' => 0);
$month = &$overview['months'][$mname];
$month['subs'][] = array('name' => $row['Account']['name'],
'charges' => $row[0]['balance']);
$month['charges'] += $row[0]['balance'];
$overview['charges'] += $row[0]['balance'];
}
// Enable the Reports menu section
$this->sideMenuAreaActivate('REPORT');
// Prepare to render.
$this->set('months', $months);
$this->set('title', 'Monthly Charges');
$this->set(compact('overview'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific entry
*/
function view($id = null) {
$entry = $this->StatementEntry->find
('first',
array('contain' => array
('Transaction' => array('fields' => array('id', 'type', 'stamp')),
'Account' => array('id', 'name', 'type', 'level'),
'Customer' => array('fields' => array('id', 'name')),
'Lease' => array('fields' => array('id', 'number')),
),
'conditions' => array(array('StatementEntry.id' => $id),
),
));
if (empty($entry)) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
$entry['Account']['link'] =
$entry['Account']['level'] >=
$this->Permission->level('controller.accounts');
$stats = $this->StatementEntry->stats($id);
if (in_array(strtoupper($entry['StatementEntry']['type']), $this->StatementEntry->debitTypes()))
$stats = $stats['Charge'];
else
$stats = $stats['Disbursement'];
if (strtoupper($entry['StatementEntry']['type']) === 'CHARGE') {
// Set up dynamic menu items
if ($this->StatementEntry->reversable($id))
$this->addSideMenuLink('Reverse',
array('action' => 'reverse', $id), null,
'ACTION');
if ($stats['balance'] > 0)
$this->addSideMenuLink('Waive Balance',
array('action' => 'waive', $id), null,
'ACTION');
}
// Prepare to render.
$title = "Statement Entry #{$entry['StatementEntry']['id']}";
$this->set(compact('entry', 'title', 'stats'));
}
}

View File

@@ -0,0 +1,240 @@
<?php
class TendersController extends AppController {
/**************************************************************************
**************************************************************************
**************************************************************************
* action: index / all
* - Generate a listing of Tenders
*/
function index() { $this->all(); }
function all() { $this->gridView('All Legal Tender', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function gridDataTables(&$params, &$model) {
return array
('link' =>
array('TenderType',
'Customer',
'LedgerEntry' =>
array('Transaction',
),
),
);
}
function gridDataRecordsExecute(&$params, &$model, $query) {
$tquery = array_diff_key($query, array('fields'=>1,'group'=>1,'limit'=>1,'order'=>1));
$tquery['fields'] = array("SUM(COALESCE(LedgerEntry.amount,0)) AS 'total'");
$total = $model->find('first', $tquery);
$params['userdata']['total'] = $total[0]['total'];
return parent::gridDataRecordsExecute($params, $model, $query);
}
function gridDataPostProcessCalculatedFields(&$params, &$model, &$records) {
parent::gridDataPostProcessCalculatedFields($params, $model, $records);
foreach ($records AS &$record) {
// REVISIT <AP>: 20090730
// We really need the grid to handle this. We probably need to
// either create a hidden column with the nsf id, or pass back
// a list of nsf items as user data. We can then add an onload
// function to sweep through the nsf items and format them.
// For now... this works.
if (!empty($record['Tender']['nsf_transaction_id']))
$record['Tender']['name'] =
'<SPAN class="nsf-tender">' . $record['Tender']['name'] . '</SPAN>';
}
}
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Tender'] = array('name', 'id');
$links['Customer'] = array('name');
//$links['TenderType'] = array('name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: deposit
* - Prepares the books for a bank deposit
*/
function deposit() {
// Prepare a close page...
$deposit_types = $this->Tender->TenderType->depositTypes(
// Testing... limit to only one type
//array('limit' => 1)
);
$deposit_accounts = $this->Tender->TenderType->Account->depositAccounts();
foreach ($deposit_types AS $type_id => &$type)
$type = array('id' => $type_id,
'name' => $type,
'stats' => $this->Tender->TenderType->stats($type_id));
//pr(compact('deposit_types', 'deposit_accounts'));
$title = 'Prepare Deposit';
$this->set(compact('title', 'deposit_types', 'deposit_accounts'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: nsf
* - Marks a tender as having insufficient funds.
*/
function nsf($id = null) {
if ($this->data) {
$result = $this->Tender->nsf
($this->data['Tender']['id'],
$this->data['Transaction']['stamp'],
$this->data['Transaction']['comment']);
$this->redirect(array('controller' => 'tenders',
'action' => 'view',
$this->data['Tender']['id']));
}
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
$this->Tender->id = $id;
$tender = $this->Tender->find
('first', array
('contain' => array('Customer', 'LedgerEntry' => array('Transaction')),
));
// Prepare to render.
$title = "Tender #{$tender['Tender']['id']} : {$tender['Tender']['name']} : NSF";
$this->set(compact('tender', 'title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific entry
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
// Get the Tender and related fields
$this->Tender->id = $id;
$tender = $this->Tender->find
('first', array
('contain' => array('TenderType', 'Customer', 'LedgerEntry' => array('Transaction')),
));
if (!empty($tender['Tender']['deposit_transaction_id'])
&& empty($tender['Tender']['nsf_transaction_id'])
// Hard to tell what types of items can come back as NSF.
// For now, assume iff it is a named item, it can be NSF.
// (or if we're in development mode)
&& (!empty($tender['TenderType']['data1_name']) || !empty($this->params['dev']))
) {
$this->addSideMenuLink('NSF',
array('action' => 'nsf', $id), null,
'ACTION');
}
// Watch out for the special "Closing" entries, which have
// tender_type_id set to NULL. Otherwise, allow editing.
if (!empty($tender['TenderType']['id']))
$this->addSideMenuLink('Edit',
array('action' => 'edit', $id), null,
'ACTION');
// Prepare to render.
$title = "Tender #{$tender['Tender']['id']} : {$tender['Tender']['name']}";
$this->set(compact('tender', 'title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: edit
* - Edit tender information
*/
function edit($id = null) {
if (isset($this->data)) {
// Check to see if the operation was cancelled.
if (isset($this->params['form']['cancel'])) {
if (empty($this->data['Tender']['id']))
$this->redirect(array('action'=>'index'));
$this->redirect(array('action'=>'view', $this->data['Tender']['id']));
}
// Make sure we have tender data
if (empty($this->data['Tender']) || empty($this->data['Tender']['id']))
$this->redirect(array('action'=>'index'));
// Figure out which tender type was chosen
// REVISIT <AP>: 20090810; Not ready to change tender type
// $tender_type_id = $this->data['Tender']['tender_type_id'];
$tender_type_id = $this->Tender->field('tender_type_id');
if (empty($tender_type_id))
$this->redirect(array('action'=>'view', $this->data['Tender']['id']));
// Get data fields from the selected tender type
$this->data['Tender'] += $this->data['type'][$tender_type_id];
unset($this->data['type']);
// Save the tender and all associated data
$this->Tender->create();
$this->Tender->id = $this->data['Tender']['id'];
if (!$this->Tender->save($this->data, false)) {
$this->Session->setFlash("TENDER SAVE FAILED", true);
pr("TENDER SAVE FAILED");
}
$this->redirect(array('action'=>'view', $this->Tender->id));
}
if ($id) {
$this->data = $this->Tender->findById($id);
} else {
$this->redirect(array('action'=>'index'));
}
$tender_types = $this->Tender->TenderType->find
('list', array('order' => array('name')));
$this->set(compact('tender_types'));
$types = $this->Tender->TenderType->find('all', array('contain' => false));
$this->set(compact('types'));
// Prepare to render.
$title = ('Tender #' . $this->data['Tender']['id'] .
' : ' . $this->data['Tender']['name'] .
" : Edit");
$this->set(compact('title'));
}
}

View File

@@ -4,20 +4,35 @@ class TransactionsController extends AppController {
var $components = array('RequestHandler');
var $sidemenu_links =
array(array('name' => 'Transactions', 'header' => true),
array('name' => 'All', 'url' => array('controller' => 'transactions', 'action' => 'all')),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Invoices',
array('controller' => 'transactions', 'action' => 'invoice'), null,
'CONTROLLER');
$this->addSideMenuLink('Receipts',
array('controller' => 'transactions', 'action' => 'receipt'), null,
'CONTROLLER');
$this->addSideMenuLink('Deposits',
array('controller' => 'transactions', 'action' => 'deposit'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'transactions', 'action' => 'all'), null,
'CONTROLLER');
// REVISIT <AP>: 20090824
// Right now, we wish to keep things simple. Don't make these
// links available to non-admin users.
if (empty($this->params['admin']))
$this->sideMenuEnable('CONTROLLER', $this->std_area, false);
}
@@ -29,70 +44,72 @@ class TransactionsController extends AppController {
*/
function index() { $this->all(); }
function all() { $this->jqGridView('All Transactions', 'all'); }
function all() { $this->gridView('All Transactions', 'all'); }
function invoice() { $this->gridView('Invoices'); }
function receipt() { $this->gridView('Receipts'); }
function deposit() {
/* $this->addSideMenuLink('New Deposit', */
/* array('controller' => 'tenders', 'action' => 'deposit'), null, */
/* 'CONTROLLER', $this->new_area); */
$this->gridView('Deposits');
}
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
$links['Transaction'] = array('id');
return parent::jqGridRecordLinks($params, $model, $records, $links);
function gridView($title, $action = null, $element = null) {
if ($title != 'Deposits')
$this->set('include', array('Customer'));
parent::gridView($title, $action, $element);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific transaction
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
function gridDataCountTables(&$params, &$model) {
return array
('link' =>
array(// Models
'Account' => array('fields' => array()),
),
);
}
$transaction = $this->Transaction->find
('first',
array('contain' =>
array(// Models
'LedgerEntry' => array('fields' => array('LedgerEntry.id',
'LedgerEntry.amount',
'LedgerEntry.comment'),
//Models
function gridDataTables(&$params, &$model) {
$link = $this->gridDataCountTables($params, $model);
$link['link']['StatementEntry'] = array('fields' => array());
$link['link']['DepositTender'] = array('fields' => array());
$link['link']['Customer'] = array('fields' => array('id', 'name'));
return $link;
}
'DebitLedger' => array
('fields' => array('DebitLedger.id', 'DebitLedger.sequence'),
'Account' => array
('fields' => array('Account.id', 'Account.name')),
),
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
//$fields[] = 'COUNT(StatementEntry.id) AS entries';
$fields[] = ("IF(Transaction.type = 'DEPOSIT'," .
" COUNT(DepositTender.id)," .
" COUNT(StatementEntry.id)) AS entries");
return $fields;
}
'CreditLedger' => array
('fields' => array('CreditLedger.id', 'CreditLedger.sequence'),
'Account' => array
('fields' => array('Account.id', 'Account.name')),
),
),
),
'conditions' => array('Transaction.id' => $id),
));
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
// Figure out the transaction total
$total = 0;
foreach($transaction['LedgerEntry'] AS $entry)
$total += $entry['amount'];
if (in_array($params['action'], array('invoice', 'receipt', 'deposit')))
$conditions[] = array('Transaction.type' => strtoupper($params['action']));
// OK, prepare to render.
$title = 'Transaction #' . $transaction['Transaction']['id'];
$this->set(compact('transaction', 'title', 'total'));
return $conditions;
}
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Transaction'] = array('id', 'action' => ($params['action'] == 'deposit'
? 'deposit_slip' : 'view'));
$links['Customer'] = array('name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -103,7 +120,7 @@ class TransactionsController extends AppController {
* - handles the creation of a charge invoice
*/
function postInvoice() {
function postInvoice($redirect = true) {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
@@ -118,6 +135,17 @@ class TransactionsController extends AppController {
die("<H1>INVOICE FAILED</H1>");
}
if ($redirect) {
if (!empty($this->data['Customer']['id']))
$this->redirect(array('controller' => 'customers',
'action' => 'receipt',
$this->data['Customer']['id']));
else
$this->redirect(array('controller' => 'leases',
'action' => 'view',
$this->data['Lease']['id']));
}
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
@@ -128,23 +156,21 @@ class TransactionsController extends AppController {
**************************************************************************
**************************************************************************
* action: postReceipt
* - handles the creation of a payment receipt
* - handles the creation of a receipt
*/
function postReceipt() {
function postReceipt($redirect = true) {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
}
foreach($this->data['LedgerEntry'] AS &$entry) {
if (!isset($entry['acct'][$entry['account_id']]))
continue;
$entry['MonetarySource'] = $entry['acct'][$entry['account_id']];
foreach($this->data['Entry'] AS &$entry) {
$entry['Tender'] = $entry['type'][$entry['tender_type_id']];
unset($entry['type']);
unset($entry['tender_type_id']);
}
pr($this->data);
if (!$this->Transaction->addReceipt($this->data,
$this->data['Customer']['id'],
(isset($this->data['Lease']['id'])
@@ -157,10 +183,337 @@ class TransactionsController extends AppController {
die("<H1>RECEIPT FAILED</H1>");
}
if ($redirect)
$this->redirect(array('controller' => 'customers',
'action' => 'view',
$this->data['Customer']['id']));
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
}
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: postDeposit
* - handles the creation of a deposit transaction
*/
function postDeposit() {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
}
//pr($this->data);
// Go through each type of tender presented to the user
// Determine which are to be deposited, and which are to
// have their corresponding account ledgers closed.
$deposit_tender_ids = array();
$deposit_type_ids = array();
$close_type_ids = array();
foreach ($this->data['TenderType'] AS $type_id => $type) {
$type['items'] = unserialize($type['items']);
if (empty($type['selection']) ||
$type['selection'] === 'none' ||
($type['selection'] === 'subset' && count($type['items']) == 0))
continue;
// The deposit includes either the whole type, or just certain tenders
if ($type['selection'] === 'all')
$deposit_type_ids[] = $type_id;
else
$deposit_tender_ids = array_merge($deposit_tender_ids, $type['items']);
// Should we close the ledger for this tender type?
// First, the user would have to request that we do so,
// but additionally, we shouldn't close a ledger unless
// all the tenders are included in this deposit. That
// doesn't guarantee that the ledger has a zero balance,
// but it does carry the balance forward, and a total
// deposit would imply a fresh start, so go for it.
if (!empty($type['close']) && $type['selection'] === 'all')
$close_type_ids[] = $type_id;
}
// Make sure we actually have something to deposit
if (empty($deposit_type_ids) && empty($deposit_tender_ids)) {
$this->Session->setFlash(__('Nothing to Deposit', true));
$this->redirect(array('controller' => 'tenders', 'action'=>'deposit'));
}
// Build up a set of conditions based on user selection
$deposit_conditions = array();
if (!empty($deposit_type_ids))
$deposit_conditions[] = array('TenderType.id' => $deposit_type_ids);
if (!empty($deposit_tender_ids))
$deposit_conditions[] = array('DepositTender.id' => $deposit_tender_ids);
// Add in confirmation that items have not already been deposited
$deposit_conditions =
array(array('DepositTender.deposit_transaction_id' => null),
array('OR' => $deposit_conditions));
// Lookup the items to be deposited
$tenders = $this->Transaction->DepositTender->find
('all',
array('contain' => array('TenderType', 'LedgerEntry'),
'conditions' => $deposit_conditions,
));
// Build the deposit transaction
$deposit = array('Transaction' => array(), 'Entry' => array());
foreach ($tenders AS $tender) {
$deposit['Entry'][] =
array('tender_id' => $tender['DepositTender']['id'],
'account_id' => $tender['LedgerEntry']['account_id'],
'amount' => $tender['LedgerEntry']['amount'],
);
}
//pr(compact('deposit_type_ids', 'deposit_tender_ids', 'close_type_ids', 'deposit_conditions', 'deposit'));
// OK, perform the deposit and associated accounting
$result = $this->Transaction->addDeposit
($deposit, $this->data['Deposit']['Account']['id']);
//pr(compact('deposit', 'result'));
// Close any ledgers necessary
if (!empty($close_type_ids)) {
// Find the accounts associated with the types to close ...
$accounts = $this->Transaction->DepositTender->find
('all',
array('contain' => array('TenderType.account_id'),
'conditions' => array(array('TenderType.id' => $close_type_ids)),
));
// ... and close them
$this->Transaction->Account->closeCurrentLedgers
(array_map(create_function('$item', 'return $item["TenderType"]["account_id"];'), $accounts));
}
// Look out for errors
if ($result['error']) {
$this->Session->setFlash(__('Unable to Create Deposit', true));
$this->redirect(array('controller' => 'tenders', 'action'=>'deposit'));
}
// Present the deposit slip to the user
$this->redirect(array('controller' => 'transactions',
'action' => 'deposit_slip',
$result['transaction_id']));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: postWriteOff
* - handles the write off of bad debt
*/
function postWriteOff() {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
}
$data = $this->data;
if (empty($data['Customer']['id']))
$data['Customer']['id'] = null;
if (empty($data['Lease']['id']))
$data['Lease']['id'] = null;
pr(compact('data'));
if (!$this->Transaction->addWriteOff($data,
$data['Customer']['id'],
$data['Lease']['id'])) {
$this->Session->setFlash("WRITE OFF FAILED", true);
// REVISIT <AP> 20090706:
// Until we can work out the session problems,
// just die.
die("<H1>WRITE-OFF FAILED</H1>");
}
// Return to viewing the lease/customer
if (empty($data['Lease']['id']))
$this->redirect(array('controller' => 'customers',
'action' => 'view',
$data['Customer']['id']));
else
$this->redirect(array('controller' => 'leases',
'action' => 'view',
$data['Lease']['id']));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: postRefund
* - handles issuing a customer refund
*/
function postRefund() {
if (!$this->RequestHandler->isPost()) {
echo('<H2>THIS IS NOT A POST FOR SOME REASON</H2>');
return;
}
$data = $this->data;
if (empty($data['Customer']['id']))
$data['Customer']['id'] = null;
if (empty($data['Lease']['id']))
$data['Lease']['id'] = null;
if (!$this->Transaction->addRefund($data,
$data['Customer']['id'],
$data['Lease']['id'])) {
$this->Session->setFlash("REFUND FAILED", true);
// REVISIT <AP> 20090706:
// Until we can work out the session problems,
// just die.
die("<H1>REFUND FAILED</H1>");
}
// Return to viewing the lease/customer
if (empty($data['Lease']['id']))
$this->redirect(array('controller' => 'customers',
'action' => 'view',
$data['Customer']['id']));
else
$this->redirect(array('controller' => 'leases',
'action' => 'view',
$data['Lease']['id']));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: destroy
* - Deletes a transaction and associated entries
* - !!WARNING!! This should be used with EXTREME caution, as it
* irreversibly destroys the data. It is not for normal use.
*/
function destroy($id) {
$this->Transaction->id = $id;
$customer_id = $this->Transaction->field('customer_id');
$this->Transaction->destroy($id);
$this->redirect(array('controller' => 'customers', 'action' => 'view', $customer_id));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific transaction
*/
function view($id = null) {
$transaction = $this->Transaction->find
('first',
array('contain' =>
array(// Models
'Account(id,name,level)',
'Ledger(id,sequence)',
'NsfTender(id,name)',
),
'conditions' => array(array('Transaction.id' => $id),
),
));
if (empty($transaction)) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('action'=>'index'));
}
$transaction['Account']['link'] =
$transaction['Account']['level'] >=
$this->Permission->level('controller.accounts');
if ($transaction['Transaction']['type'] === 'DEPOSIT')
$this->addSideMenuLink('View Slip',
array('action' => 'deposit_slip', $id), null,
'ACTION');
$this->addSideMenuLink('Destroy',
array('action' => 'destroy', $id),
array('confirmMessage' =>
"This may leave the database in an unstable state." .
" Do NOT do this unless you know what you're doing." .
" Proceed anyway?"),
'ACTION', $this->admin_area);
// OK, prepare to render.
$title = 'Transaction #' . $transaction['Transaction']['id'];
$this->set(compact('transaction', 'title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: deposit_slip
* - Special presentation
* Processes the user input and updates the database
*/
function deposit_slip($id) {
// Find the deposit transaction
$this->Transaction->id = $id;
$deposit = $this->Transaction->find('first', array('contain' => false));
// Get a summary of all forms of tender in the deposit
$tenders = $this->Transaction->find
('all',
array('link' => array('DepositTender' =>
array('fields' => array(),
'TenderType',
'LedgerEntry' =>
array('fields' => array()))),
'fields' => array(//'TenderType.id', 'TenderType.name',
"COUNT(DepositTender.id) AS 'count'",
"SUM(LedgerEntry.amount) AS 'total'"),
//'conditions' => array(array('DepositTender.deposit_transaction_id' => $id)),
'conditions' => array(array('Transaction.id' => $id)),
'group' => 'TenderType.id',
));
// Verify the deposit exists, and that something was actually deposited
if (empty($deposit) || empty($tenders)) {
$this->Session->setFlash(__('Invalid Deposit.', true));
$this->redirect(array('action'=>'deposit'));
}
// Add the summary to our deposit slip data container
$deposit['types'] = array();
foreach ($tenders AS $tender) {
$deposit['types'][$tender['TenderType']['id']] =
$tender['TenderType'] + $tender[0];
}
$deposit_total = 0;
foreach ($deposit['types'] AS $type)
$deposit_total += $type['total'];
if ($deposit['Transaction']['amount'] != $deposit_total)
$this->INTERNAL_ERROR("Deposit items do not add up to deposit slip total");
$this->addSideMenuLink('View',
array('action' => 'view', $id), null,
'ACTION');
$title = 'Deposit Slip';
$this->set(compact('title', 'deposit'));
$this->render('deposit_slip');
return;
}
}

View File

@@ -0,0 +1,255 @@
<?php
class UnitSizesController extends AppController {
/**************************************************************************
**************************************************************************
**************************************************************************
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('1 Bedroom',
array('controller' => 'unit_sizes', 'action' => 'bd1'), null,
'CONTROLLER');
$this->addSideMenuLink('2 Bedroom',
array('controller' => 'unit_sizes', 'action' => 'bd2'), null,
'CONTROLLER');
$this->addSideMenuLink('3 Bedroom',
array('controller' => 'unit_sizes', 'action' => 'bd3'), null,
'CONTROLLER');
$this->addSideMenuLink('4+ Bedroom',
array('controller' => 'unit_sizes', 'action' => 'bd4'), null,
'CONTROLLER');
$this->addSideMenuLink('Auto',
array('controller' => 'unit_sizes', 'action' => 'auto'), null,
'CONTROLLER');
$this->addSideMenuLink('Boat',
array('controller' => 'unit_sizes', 'action' => 'boat'), null,
'CONTROLLER');
$this->addSideMenuLink('RV',
array('controller' => 'unit_sizes', 'action' => 'rv'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'unit_sizes', 'action' => 'all'), null,
'CONTROLLER');
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: index / unavailable / vacant / occupied / all
* - Generate a listing of units
*/
function index() { $this->all(); }
function bd1() { $this->gridView('Sizes for 1 Bedroom'); }
function bd2() { $this->gridView('Sizes for 2 Bedrooms'); }
function bd3() { $this->gridView('Sizes for 3 Bedroom'); }
function bd4() { $this->gridView('Sizes for 4+ Bedroom'); }
function auto() { $this->gridView('Sizes for an Automobile'); }
function boat() { $this->gridView('Sizes for a Boat'); }
function rv() { $this->gridView('Sizes for an RV'); }
function all() { $this->gridView('All Unit Sizes', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function gridDataCountTables(&$params, &$model) {
return array('link' => array('UnitType'));
}
function gridDataTables(&$params, &$model) {
$tables = $this->gridDataCountTables($params, $model);
$tables['link']['Unit'] = array();
return $tables;
}
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
$fields[] = 'ROUND(UnitSize.width/12, 1) AS width';
$fields[] = 'ROUND(UnitSize.depth/12, 1) AS depth';
$fields[] = 'ROUND(UnitSize.height/12, 1) AS height';
$fields[] = 'ROUND(UnitSize.width/12 * UnitSize.depth/12, 0) AS sqft';
$fields[] = 'ROUND(UnitSize.width/12 * UnitSize.depth/12 * UnitSize.height/12, 0) AS cuft';
$fields[] = 'ROUND(UnitSize.rent / (UnitSize.width/12 * UnitSize.depth/12), 2) AS sqcost';
$fields[] = 'ROUND(UnitSize.rent / (UnitSize.width/12 * UnitSize.depth/12 * UnitSize.height/12), 2) AS cucost';
$fields[] = 'COUNT(Unit.id) AS units';
$fields[] = 'SUM(IF(' . $this->UnitSize->Unit->conditionUnavailable() . ', 1, 0)) AS unavailable';
$fields[] = 'SUM(IF(' . $this->UnitSize->Unit->conditionAvailable() . ', 1, 0)) AS available';
$fields[] = 'SUM(IF(' . $this->UnitSize->Unit->conditionOccupied() . ', 1, 0)) AS occupied';
$fields[] = 'SUM(IF(' . $this->UnitSize->Unit->conditionOccupied() . ', 0, 1)) / COUNT(unit.id) AS vacancy';
$fields[] = 'SUM(IF(' . $this->UnitSize->Unit->conditionOccupied() . ', 1, 0)) / COUNT(unit.id) AS occupancy';
return $fields;
}
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
// REVISIT <AP>: 20090825
// Sizes should come from the database.
// For now, I took an assumed average need, then bracketed
// with +/- 50 sqft. This gives a 100sqft range for each.
if ($params['action'] === 'bd1') { // 75 sqft
$conditions[] = array('UnitType.id' => array_keys($this->UnitSize->UnitType->enclosedTypes()));
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) <= 125';
}
elseif ($params['action'] === 'bd2') { // 125 sqft
$conditions[] = array('UnitType.id' => array_keys($this->UnitSize->UnitType->enclosedTypes()));
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) >= 75';
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) <= 175';
}
elseif ($params['action'] === 'bd3') { // 175 sqft
$conditions[] = array('UnitType.id' => array_keys($this->UnitSize->UnitType->enclosedTypes()));
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) >= 125';
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) <= 225';
}
elseif ($params['action'] === 'bd4') { // 225 sqft
$conditions[] = array('UnitType.id' => array_keys($this->UnitSize->UnitType->enclosedTypes()));
$conditions[] = '(UnitSize.width/12 * UnitSize.depth/12) >= 175';
}
elseif (in_array($params['action'], array('auto', 'boat', 'rv'))) {
$conditions[] = array('UnitType.id' =>
array_merge(array_keys($this->UnitSize->UnitType->enclosedTypes()),
array_keys($this->UnitSize->UnitType->outdoorTypes())));
list($width, $depth, $height) = array(8, 15, null);
if ($params['action'] === 'auto')
$depth = 15;
elseif ($params['action'] === 'boat')
$depth = 15;
elseif ($params['action'] === 'rv')
list($width, $depth, $height) = array(10, 25, 12);
$conditions[] = "(UnitSize.width/12) >= $width";
$conditions[] = "(UnitSize.depth/12) >= $depth";
if (isset($height))
$conditions[] = array('OR' =>
array("(UnitSize.height/12) >= $height",
//"UnitSize.height IS NULL",
array('UnitType.id' =>
array_keys($this->UnitSize->UnitType->outdoorTypes())),
));
}
return $conditions;
}
function gridDataOrder(&$params, &$model, $index, $direction) {
if ($index == 'UnitType.name')
$index = 'UnitType.code';
$order = parent::gridDataOrder($params, $model, $index, $direction);
// After sorting by whatever the user wants, add these
// defaults into the sort mechanism. If we're already
// sorting by one of them, it will only be redundant,
// and should cause no harm (possible a longer query?)
$order[] = 'UnitType.code ' . $direction;
$order[] = 'sqft ' . $direction;
$order[] = 'UnitSize.rent ' . $direction;
return $order;
}
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['UnitSize'] = array('name');
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: view
* - Displays information about a specific entry
*/
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid Item.', true));
$this->redirect(array('controller' => 'accounts', 'action'=>'index'));
}
// Get the UnitSize and related fields
$this->UnitSize->id = $id;
$size = $this->UnitSize->find
('first', array
('contain' => array('UnitType'),
'fields' => array('UnitSize.*', 'UnitType.*',
'ROUND(UnitSize.width/12, 1) AS width',
'ROUND(UnitSize.depth/12, 1) AS depth',
'ROUND(UnitSize.height/12, 1) AS height',
'ROUND(UnitSize.width/12 * UnitSize.depth/12, 0) AS sqft',
'ROUND(UnitSize.width/12 * UnitSize.depth/12 * UnitSize.height/12, 0) AS cuft'),
));
$size['UnitSize'] = $size[0] + $size['UnitSize'];
unset($size[0]);
$this->set(compact('size'));
$this->set('stats', $this->UnitSize->stats($id));
// Prepare to render.
$title = "Unit Size : {$size['UnitSize']['name']}";
$this->set(compact('title'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: edit
* - Edit unit_size information
*/
function edit($id = null) {
$this->INTERNAL_ERROR('NOT READY');
if (isset($this->data)) {
// Check to see if the operation was cancelled.
if (isset($this->params['form']['cancel'])) {
if (empty($this->data['UnitSize']['id']))
$this->redirect(array('action'=>'index'));
$this->redirect(array('action'=>'view', $this->data['UnitSize']['id']));
}
// Make sure we have unit_size data
if (empty($this->data['UnitSize']) || empty($this->data['UnitSize']['id']))
$this->redirect(array('action'=>'index'));
// Save the unit_size and all associated data
$this->UnitSize->create();
$this->UnitSize->id = $this->data['UnitSize']['id'];
if (!$this->UnitSize->save($this->data, false)) {
$this->Session->setFlash("UNIT_SIZE SAVE FAILED", true);
pr("UNIT_SIZE SAVE FAILED");
}
$this->redirect(array('action'=>'view', $this->UnitSize->id));
}
if ($id) {
$this->data = $this->UnitSize->findById($id);
} else {
$this->redirect(array('action'=>'index'));
}
// Prepare to render.
$title = ('UnitSize ' . $this->data['UnitSize']['name'] .
" : Edit");
$this->set(compact('title'));
}
}

View File

@@ -2,23 +2,35 @@
class UnitsController extends AppController {
var $sidemenu_links =
array(array('name' => 'Units', 'header' => true),
array('name' => 'Occupied', 'url' => array('controller' => 'units', 'action' => 'occupied')),
array('name' => 'Vacant', 'url' => array('controller' => 'units', 'action' => 'vacant')),
array('name' => 'Unavailable', 'url' => array('controller' => 'units', 'action' => 'unavailable')),
array('name' => 'All', 'url' => array('controller' => 'units', 'action' => 'all')),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* override: sideMenuLinks
* - Generates controller specific links for the side menu
* override: addGridViewSideMenuLinks
* - Adds grid view navigation side menu links
*/
function sideMenuLinks() {
return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
function addGridViewSideMenuLinks() {
parent::addGridViewSideMenuLinks();
$this->addSideMenuLink('Unavailable',
array('controller' => 'units', 'action' => 'unavailable'), null,
'CONTROLLER');
$this->addSideMenuLink('Vacant',
array('controller' => 'units', 'action' => 'vacant'), null,
'CONTROLLER');
$this->addSideMenuLink('Occupied',
array('controller' => 'units', 'action' => 'occupied'), null,
'CONTROLLER');
$this->addSideMenuLink('Overlocked',
array('controller' => 'units', 'action' => 'locked'), null,
'CONTROLLER');
$this->addSideMenuLink('Liened',
array('controller' => 'units', 'action' => 'liened'), null,
'CONTROLLER');
$this->addSideMenuLink('All',
array('controller' => 'units', 'action' => 'all'), null,
'CONTROLLER');
}
@@ -30,77 +42,66 @@ class UnitsController extends AppController {
*/
function index() { $this->all(); }
function unavailable() { $this->jqGridView('Unavailable Units'); }
function vacant() { $this->jqGridView('Vacant Units'); }
function occupied() { $this->jqGridView('Occupied Units'); }
function all() { $this->jqGridView('All Units', 'all'); }
function unavailable() { $this->gridView('Unavailable Units'); }
function vacant() { $this->gridView('Vacant Units'); }
function occupied() { $this->gridView('Occupied Units'); }
function locked() { $this->gridView('Overlocked Units'); }
function liened() { $this->gridView('Liened Units'); }
function all() { $this->gridView('All Units', 'all'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* virtuals: jqGridData
* - With the application controller handling the jqGridData action,
* virtuals: gridData
* - With the application controller handling the gridData action,
* these virtual functions ensure that the correct data is passed
* to jqGrid.
*/
function jqGridDataSetup(&$params) {
parent::jqGridDataSetup($params);
function gridDataSetup(&$params) {
parent::gridDataSetup($params);
if (!isset($params['action']))
$params['action'] = 'all';
}
function jqGridDataCountTables(&$params, &$model) {
$link = array
('link' =>
array(// Models
'UnitSize' => array('fields' => array('id', 'name')),
),
);
function gridDataCountTables(&$params, &$model) {
return array
('link' => array('UnitSize' => array('fields' => array('id', 'name')),
'CurrentLease' => array('fields' => array('id'))));
if ($params['action'] === 'occupied')
$link['Lease'] = array('fields' => array(),
// Models
'Contact' => array('fields' => array('display_name'),
//'type' => 'LEFT',
),
);
/* if ($params['action'] === 'occupied') */
/* $link['Lease'] = array('fields' => array(), */
/* // Models */
/* 'Contact' => array('fields' => array('display_name'), */
/* //'type' => 'LEFT', */
/* ), */
/* ); */
}
function gridDataTables(&$params, &$model) {
$link = $this->gridDataCountTables($params, $model);
$link['link']['CurrentLease']['StatementEntry'] = array('fields' => array());
return $link;
}
function jqGridDataTables(&$params, &$model) {
$link = $this->jqGridDataCountTables($params, $model);
$link['link']['CurrentLease']['LedgerEntry'] = array('fields' => array());
$link['link']['CurrentLease']['LedgerEntry']['Ledger'] = array('fields' => array());
$link['link']['CurrentLease']['LedgerEntry']['Ledger']['Account'] = array('fields' => array());
// INNER JOIN would be great, as it would ensure we're only looking
// at the ledger entries that we truly want. However, this also
// removes from the query any leases that do not yet have a ledger
// entry in A/R. A solution would be to INNER JOIN these tables,
// and LEFT JOIN it to the rest. Grouping of JOINs, however, is
// implemented with the 'joins' tag, and is not available through
// the Linkable behavior interface.
//$link['link']['CurrentLease']['LedgerEntry']['Ledger']['Account']['type'] = 'INNER';
$link['link']['CurrentLease']['LedgerEntry']['Ledger']['Account']['conditions']
= array('Account.id' =>
$this->Unit->CurrentLease->LedgerEntry->Ledger->Account->accountReceivableAccountID());
return $link;
/* function gridDataTables(&$params, &$model) { */
/* return array */
/* ('link' => array('Unit' => array('fields' => array('Unit.id', 'Unit.name')), */
/* 'Customer' => array('fields' => array('Customer.id', 'Customer.name')))); */
/* } */
function gridDataFields(&$params, &$model) {
$fields = parent::gridDataFields($params, $model);
$fields[] = 'ROUND(UnitSize.width/12 * UnitSize.depth/12, 0) AS sqft';
return array_merge($fields,
$this->Unit->Lease->StatementEntry->chargeDisbursementFields(true));
}
function jqGridDataFields(&$params, &$model) {
$db = &$model->getDataSource();
$fields = $db->fields($model, $model->alias);
$fields[] = ("SUM(IF(Account.id IS NULL, 0," .
" IF(LedgerEntry.debit_ledger_id = Account.id," .
" 1, -1))" .
" * LedgerEntry.amount) AS 'balance'");
return $fields;
}
function jqGridDataConditions(&$params, &$model) {
$conditions = parent::jqGridDataConditions($params, $model);
function gridDataConditions(&$params, &$model) {
$conditions = parent::gridDataConditions($params, $model);
if ($params['action'] === 'unavailable') {
$conditions[] = $this->Unit->conditionUnavailable();
@@ -114,31 +115,37 @@ class UnitsController extends AppController {
elseif ($params['action'] === 'unoccupied') {
$conditions[] = array('NOT' => array($this->Unit->conditionOccupied()));
}
elseif ($params['action'] === 'locked') {
$conditions[] = $this->Unit->conditionLocked();
}
elseif ($params['action'] === 'liened') {
$conditions[] = $this->Unit->conditionLiened();
}
return $conditions;
}
function jqGridDataOrder(&$params, &$model, $index, $direction) {
function gridDataOrder(&$params, &$model, $index, $direction) {
// Instead of sorting by name, sort by defined order
if ($index === 'Unit.name')
$index = 'Unit.sort_order';
$order = array();
$order[] = parent::jqGridDataOrder($params, $model, $index, $direction);
$order[] = parent::gridDataOrder($params, $model, $index, $direction);
// If sorting by anything other than name (defined order)
// add the sort-order as a secondary condition
if ($index !== 'Unit.name')
$order[] = parent::jqGridDataOrder($params, $model,
$order[] = parent::gridDataOrder($params, $model,
'Unit.sort_order', $direction);
return $order;
}
function jqGridRecordLinks(&$params, &$model, &$records, $links) {
function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
$links['Unit'] = array('name');
$links['UnitSize'] = array('name');
return parent::jqGridRecordLinks($params, $model, $records, $links);
return parent::gridDataPostProcessLinks($params, $model, $records, $links);
}
@@ -153,7 +160,7 @@ class UnitsController extends AppController {
$customer = array();
$unit = array();
if (isset($id)) {
if (!empty($id)) {
$this->Unit->recursive = -1;
$unit = current($this->Unit->read(null, $id));
}
@@ -203,6 +210,84 @@ class UnitsController extends AppController {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: lock/unlock
* - Transitions the unit into / out of the LOCKED state
*/
function status($id, $status) {
$this->Unit->updateStatus($id, $status, true);
$this->redirect(array('action' => 'view', $id));
}
function lock($id) { $this->status($id, 'LOCKED'); }
function unlock($id) { $this->status($id, 'OCCUPIED'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* action: overview
* - Displays overview information for all units
*/
function overview() {
$result = $this->Unit->find
('all',
array('link' => array('UnitSize' => array('fields' => array(), 'UnitType' => array('fields' => array('name')))),
'fields' => array('status', 'COUNT(Unit.id) AS cnt', 'SUM(Unit.rent) AS rents'),
//'conditions' => array('
'group' => array('UnitType.id', 'Unit.status'),
'order' => array('UnitType.name', 'Unit.status')
));
$overview = array('types' => array(), 'count' => 0, 'rents' => 0);
foreach ($result AS $row) {
$utname = $row['UnitType']['name'];
if (empty($overview['types'][$utname]))
$overview['types'][$utname] = array('name' => $utname,
'subs' => array(),
'count' => 0,
'rents' => 0,
'phys_pct' => 0,
'econ_pct' => 0);
$type = &$overview['types'][$utname];
$type['subs'][] = array('name' => $row['Unit']['status'],
'count' => $row[0]['cnt'],
'rents' => $row[0]['rents'],
'phys_subpct' => 0,
'phys_totpct' => 0,
'econ_subpct' => 0,
'econ_totpct' => 0);
$type['count'] += $row[0]['cnt'];
$type['rents'] += $row[0]['rents'];
$overview['count'] += $row[0]['cnt'];
$overview['rents'] += $row[0]['rents'];
}
foreach ($overview['types'] AS &$type) {
foreach ($type['subs'] AS &$sub) {
$sub['phys_subpct'] = $sub['count'] / $type['count'];
$sub['econ_subpct'] = $sub['rents'] / $type['rents'];
$sub['phys_totpct'] = $sub['count'] / $overview['count'];
$sub['econ_totpct'] = $sub['rents'] / $overview['rents'];
}
$type['phys_pct'] = $type['count'] / $overview['count'];
$type['econ_pct'] = $type['rents'] / $overview['rents'];
}
// Enable the Reports menu section
$this->sideMenuAreaActivate('REPORT');
// Prepare to render.
$this->set('title', 'Unit Overview');
$this->set(compact('overview'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -242,37 +327,135 @@ class UnitsController extends AppController {
$stats['CurrentLease']['balance'];
// Figure out the total security deposit for the current lease.
$deposits = $this->Unit->Lease->findSecurityDeposits($unit['CurrentLease']['id']);
$outstanding_deposit = $deposits['summary']['balance'];
$deposits = $this->Unit->Lease->securityDeposits($unit['CurrentLease']['id']);
$outstanding_deposit = $this->Unit->Lease->securityDepositBalance($unit['CurrentLease']['id']);
}
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
// If the unit is occupied, but not locked, provide a
// mechanism to do so. This doesn't have to be restricted
// to past due customers. There are times we need to
// overlock customers in good standing, such as if their
// lock breaks, is cut, or missing for any reason.
if ($this->Unit->occupied($unit['Unit']['status']) &&
!$this->Unit->locked($unit['Unit']['status']))
$this->addSideMenuLink('Lock',
array('action' => 'Lock', $id), null,
'ACTION');
if (isset($unit['CurrentLease']['id']) &&
!isset($unit['CurrentLease']['moveout_date'])) {
$this->sidemenu_links[] =
array('name' => 'Move-Out', 'url' => array('action' => 'move_out',
$id));
// If the unit is locked, provide an option to unlock it,
// unless it's locked due to lien, which is not so simple.
if ($this->Unit->locked($unit['Unit']['status']) &&
!$this->Unit->liened($unit['Unit']['status']))
$this->addSideMenuLink('Unlock',
array('action' => 'unlock', $id), null,
'ACTION');
// If there is a current lease on this unit, then provide
// a link to move the tenant out. Current lease for a unit
// has a bit different definition than a current lease for
// a customer, since a lease stays with a customer until it
// is finally closed. A lease, however, only stays with a
// unit while occupied (since a unit is not responsible for
// any lingering financial obligations, like a customer is).
// Of course, if there is no current lease, provide a link
// to move a new tenant in (if the unit is available).
if (isset($unit['CurrentLease']['id'])) {
$this->addSideMenuLink('Move-Out',
array('action' => 'move_out', $id), null,
'ACTION');
} elseif ($this->Unit->available($unit['Unit']['status'])) {
$this->addSideMenuLink('Move-In',
array('action' => 'move_in', $id), null,
'ACTION');
} else {
$this->sidemenu_links[] =
array('name' => 'Move-In', 'url' => array('action' => 'move_in',
$id));
// Unit is unavailable (dirty, damaged, reserved, business-use, etc)
}
if (isset($unit['CurrentLease']['id']) &&
!isset($unit['CurrentLease']['close_date'])) {
$this->sidemenu_links[] =
array('name' => 'Payment', 'url' => array('controller' => 'customers',
'action' => 'receipt',
$unit['CurrentLease']['customer_id']));
// If there is a current lease, allow new charges to
// be added, and payments to be made.
if (isset($unit['CurrentLease']['id'])) {
$this->addSideMenuLink('New Invoice',
array('controller' => 'leases',
'action' => 'invoice',
$unit['CurrentLease']['id']), null,
'ACTION');
$this->addSideMenuLink('New Receipt',
array('controller' => 'customers',
'action' => 'receipt',
$unit['CurrentLease']['customer_id']), null,
'ACTION');
}
// Always allow the unit to be edited.
$this->addSideMenuLink('Edit',
array('action' => 'edit', $id), null,
'ACTION');
// Prepare to render.
$title = 'Unit ' . $unit['Unit']['name'];
$this->set(compact('unit', 'title',
'outstanding_balance',
'outstanding_deposit'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* action: edit
* - Edit unit information
*/
function edit($id = null) {
if (isset($this->data)) {
// Check to see if the operation was cancelled.
if (isset($this->params['form']['cancel'])) {
if (empty($this->data['Unit']['id']))
$this->redirect(array('action'=>'index'));
$this->redirect(array('action'=>'view', $this->data['Unit']['id']));
}
// Make sure we have unit data
if (empty($this->data['Unit']))
$this->redirect(array('action'=>'index'));
// Make sure we have a rental rate
if (empty($this->data['Unit']['rent']))
$this->redirect(array('action'=>'view', $this->data['Unit']['id']));
// Save the unit and all associated data
$this->Unit->create();
$this->Unit->id = $this->data['Unit']['id'];
if (!$this->Unit->save($this->data, false)) {
$this->Session->setFlash("UNIT SAVE FAILED", true);
pr("UNIT SAVE FAILED");
}
$this->redirect(array('action'=>'view', $this->Unit->id));
}
if ($id) {
$this->data = $this->Unit->findById($id);
$title = 'Unit ' . $this->data['Unit']['name'] . " : Edit";
}
else {
$title = "Enter New Unit";
$this->data = array();
}
$statusEnums = $this->Unit->allowedStatusSet($id);
$statusEnums = array_combine(array_keys($statusEnums),
array_keys($statusEnums));
$this->set(compact('statusEnums'));
$unit_sizes = $this->Unit->UnitSize->find
('list', array('order' => array('unit_type_id', 'width', 'depth', 'id')));
$this->set(compact('unit_sizes'));
// Prepare to render.
$this->set(compact('title'));
}
}

View File

@@ -0,0 +1,76 @@
<?php
class UtilController extends AppController {
var $uses = array();
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reset_data
* - Development function. TO BE DELETED
*/
function reset_data() {
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
Configure::write('debug', '0');
$script = $_SERVER['DOCUMENT_ROOT'] . '/pmgr/build.cmd';
echo "<P>" . date('r') . "\n";
//echo "<P>Script: $script" . "\n";
$handle = popen($script . ' 2>&1', 'r');
//echo "<P>Handle: $handle; " . gettype($handle) . "\n";
echo "<P><PRE>\n";
while (($read = fread($handle, 2096))) {
echo $read;
}
echo "</PRE>\n";
pclose($handle);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: rebuild_box
*/
function rebuild_box($type) {
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
Configure::write('debug', '0');
$script = preg_replace('%/webroot/index.php$%',
'/build_'.$type.'box.cmd',
$_SERVER['SCRIPT_FILENAME']);
// REVISIT <AP>: 20090828
// Just use system call
$handle = popen($script . ' 2>&1', 'r');
while (($read = fread($handle, 2096))) {
// Do nothing
}
pclose($handle);
$url = $_SERVER['HTTP_REFERER'];
if (empty($url))
$url = "/";
$this->redirect($url);
}
function rebuild_sandbox() { $this->rebuild_box('sand'); }
function rebuild_devbox() { $this->rebuild_box('dev'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* function: logmsg
* - action to allow posting log message data
*/
function logmsg() {
}
}

View File

@@ -1,13 +1,6 @@
<?php
class Account extends AppModel {
var $name = 'Account';
var $validate = array(
'id' => array('numeric'),
'name' => array('notempty'),
'external_name' => array('notempty')
);
var $hasOne = array(
'CurrentLedger' => array(
'className' => 'Ledger',
@@ -16,13 +9,14 @@ class Account extends AppModel {
// engine specific code. However, it doesn't
// work with the Linkable behavior. I need to
// look into that, just not right now.
//'conditions' => array('CurrentLedger.close_id' => null),
'conditions' => array('CurrentLedger.close_id IS NULL'),
//'conditions' => array(array('CurrentLedger.close_transaction_id' => null)),
'conditions' => array('CurrentLedger.close_transaction_id IS NULL'),
),
);
var $hasMany = array(
'Ledger',
'LedgerEntry',
);
@@ -78,7 +72,7 @@ class Account extends AppModel {
else
$fund = $this->fundamentalType($id_or_type);
if ($fund == 'debit')
if (strtolower($fund) == 'debit')
return 'credit';
return 'debit';
@@ -104,6 +98,22 @@ class Account extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: debitCreditFields
* - Returns the fields necessary to determine whether the queried
* entries are a debit, or a credit, and also the effect each have
* on the overall balance of the account.
*/
function debitCreditFields($sum = false, $balance = true,
$entry_name = 'LedgerEntry', $account_name = 'Account') {
return $this->LedgerEntry->debitCreditFields
($sum, $balance, $entry_name, $account_name);
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -111,22 +121,42 @@ class Account extends AppModel {
* - Returns the ID of the desired account
*/
function securityDepositAccountID() { return $this->nameToID('Security Deposit'); }
function rentAccountID() { return $this->nameToID('Rent'); }
function lateChargeAccountID() { return $this->nameToID('Late Charge'); }
function nsfAccountID() { return $this->nameToID('NSF'); }
function nsfChargeAccountID() { return $this->nameToID('NSF Charge'); }
function taxAccountID() { return $this->nameToID('Tax'); }
function accountReceivableAccountID() { return $this->nameToID('A/R'); }
function cashAccountID() { return $this->nameToID('Cash'); }
function checkAccountID() { return $this->nameToID('Check'); }
function moneyOrderAccountID() { return $this->nameToID('Money Order'); }
function concessionAccountID() { return $this->nameToID('Concession'); }
function pettyCashAccountID() { return $this->nameToID('Petty Cash'); }
function invoiceAccountID() { return $this->nameToID('Invoice'); }
function receiptAccountID() { return $this->nameToID('Receipt'); }
function badDebtAccountID() { return $this->nameToID('Bad Debt'); }
function lookup($name, $check = true) {
$id = $this->nameToID($name);
if (empty($id) && $check)
$this->INTERNAL_ERROR("Missing Account '$name'");
return $id;
}
function securityDepositAccountID() { return $this->lookup('Security Deposit'); }
function rentAccountID() { return $this->lookup('Rent'); }
function lateChargeAccountID() { return $this->lookup('Late Charge'); }
function nsfAccountID() { return $this->lookup('NSF'); }
function nsfChargeAccountID() { return $this->lookup('NSF Charge'); }
function taxAccountID() { return $this->lookup('Tax'); }
function accountReceivableAccountID() { return $this->lookup('A/R'); }
function accountPayableAccountID() { return $this->lookup('A/P'); }
function cashAccountID() { return $this->lookup('Cash'); }
function checkAccountID() { return $this->lookup('Check'); }
function moneyOrderAccountID() { return $this->lookup('Money Order'); }
function achAccountID() { return $this->lookup('ACH'); }
function concessionAccountID() { return $this->lookup('Concession'); }
function waiverAccountID() { return $this->lookup('Waiver'); }
function pettyCashAccountID() { return $this->lookup('Petty Cash'); }
function invoiceAccountID() { return $this->lookup('Invoice'); }
function receiptAccountID() { return $this->lookup('Receipt'); }
function badDebtAccountID() { return $this->lookup('Bad Debt'); }
function customerCreditAccountID() { return $this->lookup(
// REVISIT <AP>: 20090816
// Use of A/R works, and saves an excess of accounts.
// However, a dedicated account is nice, since it can
// quickly be spotted how much is _really_ due, vs
// how much has been pre-paid. Customer credits in
// A/R is not as clear, although a report is an
// obvious solution.
//'A/R'
'Credit'
); }
/**************************************************************************
**************************************************************************
@@ -157,59 +187,36 @@ class Account extends AppModel {
function relatedAccounts($attribute, $extra = null) {
$this->cacheQueries = true;
$account = $this->find('all', array
('contain' => array('CurrentLedger'),
'fields' => array('Account.id', 'Account.type', 'Account.name', 'CurrentLedger.id'),
'conditions' => array('Account.'.$attribute => true),
'order' => array('Account.name'),
) + (isset($extra) ? $extra : array())
);
$accounts = $this->find('all', array
('fields' => array('Account.id', 'Account.name'),
'conditions' => array('Account.'.$attribute => true),
'order' => array('Account.name'),
) + (isset($extra) ? $extra : array())
);
$this->cacheQueries = false;
return $account;
// Rearrange to be of the form (id => name)
$rel_accounts = array();
foreach ($accounts AS $acct) {
$rel_accounts[$acct['Account']['id']] = $acct['Account']['name'];
}
return $rel_accounts;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: chargeAccounts
* - Returns an array of accounts suitable for charges
* function: xxxAccounts
* - Returns an array of accounts suitable for activity xxx
*/
function chargeAccounts() {
// Get all accounts that support charges
$accounts = $this->relatedAccounts('chargeable', array('order' => 'name'));
function invoiceAccounts() { return $this->relatedAccounts('invoices'); }
function receiptAccounts() { return $this->relatedAccounts('receipts'); }
function depositAccounts() { return $this->relatedAccounts('deposits'); }
function refundAccounts() { return $this->relatedAccounts('refunds'); }
// Rearrange to be of the form (id => name)
$charge_accounts = array();
foreach ($accounts AS $acct) {
$charge_accounts[$acct['Account']['id']] = $acct['Account']['name'];
}
return $charge_accounts;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: paymentAccounts
* - Returns an array of accounts suitable for payments
*/
function paymentAccounts() {
// Get all accounts that support payments
$accounts = $this->relatedAccounts('payable', array('order' => 'name'));
// Rearrange to be of the form (id => name)
$payment_accounts = array();
foreach ($accounts AS $acct) {
$payment_accounts[$acct['Account']['id']] = $acct['Account']['name'];
}
return $payment_accounts;
}
/**************************************************************************
**************************************************************************
@@ -219,14 +226,30 @@ class Account extends AppModel {
*/
function collectableAccounts() {
$accounts = $this->paymentAccounts();
$accounts = $this->receiptAccounts();
foreach(array($this->nsfAccountID(),
$this->securityDepositAccountID())
foreach(array($this->customerCreditAccountID(),
$this->securityDepositAccountID(),
$this->nsfAccountID(),
$this->waiverAccountID(),
$this->badDebtAccountID(),
//$this->lookup('Closing'),
//$this->lookup('Equity'),
)
AS $account_id) {
$accounts[$account_id] = $this->name($account_id);
}
$accounts['all'] = $accounts['default'] = $accounts;
foreach(array($this->concessionAccountID(),
$this->waiverAccountID(),
$this->badDebtAccountID(),
)
AS $account_id) {
unset($accounts['default'][$account_id]);
}
return $accounts;
}
@@ -294,513 +317,37 @@ class Account extends AppModel {
* - Closes the current account ledger, and opens a new one
* with the old balance carried forward.
*/
function closeCurrentLedger($id = null, $close_id = null) {
$contain = array('CurrentLedger' => array('fields' => array('CurrentLedger.id')));
if (!$close_id) {
$close = new Close();
$close->create();
if (!$close->save(array('stamp' => null), false)) {
return false;
}
$close_id = $close->id;
}
function closeCurrentLedgers($ids = null) {
$this->cacheQueries = true;
$account = $this->find('all', array
('contain' => $contain,
('contain' => array('CurrentLedger.id'),
'fields' => array(),
'conditions' =>
$id ? array(array('Account.id' => $id)) : array()
'conditions' => (empty($ids)
? array()
: array(array('Account.id' => $ids)))
));
$this->cacheQueries = false;
//pr(compact('id', 'account'));
foreach ($account AS $acct) {
if (!$this->Ledger->closeLedger($acct['CurrentLedger']['id'], $close_id))
return false;
}
return true;
$ledger_ids = array();
foreach ($account AS $acct)
$ledger_ids[] = $acct['CurrentLedger']['id'];
return $this->Ledger->closeLedgers($ledger_ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findLedgerEntries
* function: ledgerEntries
* - Returns an array of ledger entries that belong to the given
* account, either just from the current ledger, or from all ledgers.
*/
function findLedgerEntries($id, $all = false, $cond = null, $link = null) {
/* pr(array('function' => 'Account::findLedgerEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* )); */
$entries = array();
foreach ($this->ledgers($id, $all) AS $ledger_id) {
$ledger_entries = $this->Ledger->findLedgerEntries
($ledger_id, $this->type($id), $cond, $link);
$entries = array_merge($entries, $ledger_entries);
}
$stats = $this->stats($id, $all, $cond);
$entries = array('Entries' => $entries,
'summary' => $stats['Ledger']);
/* pr(array('function' => 'Account::findLedgerEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* 'vars' => compact('stats'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findLedgerEntriesRelatedToAccount
* - Returns an array of ledger entries that belong to the given
* account, and are related to a specific account, either just from
* the current ledger, or from all ledgers.
*/
function findLedgerEntriesRelatedToAccount($id, $rel_ids, $all = false, $cond = null, $link = null) {
/* pr(array('function' => 'Account::findLedgerEntriesRelatedToAccount', */
/* 'args' => compact('id', 'rel_ids', 'all', 'cond', 'link'), */
/* )); */
if (!isset($cond))
$cond = array();
if (!is_array($rel_ids))
$rel_ids = array($rel_ids);
$ledger_ids = array();
foreach ($rel_ids AS $rel_id)
$ledger_ids = array_merge($ledger_ids, $this->ledgers($rel_id));
array_push($cond, $this->Ledger->LedgerEntry->conditionEntryAsCreditOrDebit($ledger_ids));
$entries = $this->findLedgerEntries($id, $all, $cond, $link);
/* pr(array('function' => 'Account::findLedgerEntriesRelatedToAccount', */
/* 'args' => compact('id', 'relid', 'all', 'cond', 'link'), */
/* 'vars' => compact('ledger_ids'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findUnreconciledLedgerEntries
* - Returns ledger entries that are not yet reconciled
* (such as charges not paid).
*/
function findUnreconciledLedgerEntries($id = null, $fundamental_type = null, $cond = null) {
if (!isset($cond))
$cond = array();
$cond[] = array('Account.id' => $id);
foreach (($fundamental_type
? array($fundamental_type)
: array('debit', 'credit')) AS $fund) {
$ucfund = ucfirst($fund);
$unreconciled[$fund]['entry'] = $this->find
('all', array
('link' => array
('Ledger' => array
('fields' => array(),
"LedgerEntry" => array
('class' => "{$ucfund}LedgerEntry",
'fields' => array('id', 'customer_id', 'lease_id', 'amount'),
"ReconciliationLedgerEntry" => array
('class' => "{$ucfund}ReconciliationLedgerEntry",
'fields' => array
("COALESCE(SUM(Reconciliation.amount),0) AS 'reconciled'",
"LedgerEntry.amount - COALESCE(SUM(Reconciliation.amount),0) AS 'balance'",
),
),
),
),
),
'group' => ("LedgerEntry.id" .
" HAVING LedgerEntry.amount" .
" <> COALESCE(SUM(Reconciliation.amount),0)"),
'conditions' => $cond,
'fields' => array(),
));
$balance = 0;
foreach ($unreconciled[$fund]['entry'] AS &$entry) {
$entry = array_merge(array_diff_key($entry["LedgerEntry"], array(0=>true)),
$entry[0]);
$balance += $entry['balance'];
}
$unreconciled[$fund]['balance'] = $balance;
}
return $unreconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconcileNewLedgerEntry
* - Returns which ledger entries a new credit/debit would
* reconcile, and how much.
*
* - REVISIT <AP> 20090617
* This should be subject to different algorithms, such
* as apply to oldest charges first, newest first, to fees
* before rent, etc. Until we get there, I'll hardcode
* whatever algorithm is simplest.
*/
function reconcileNewLedgerEntry($id, $fundamental_type, $amount, $cond = null) {
$ofund = $this->fundamentalOpposite($fundamental_type);
$unreconciled = array($ofund => array('entry'=>array(), 'balance'=>0));
$applied = 0;
// if there is no money in the entry, it can reconcile nothing
// don't bother wasting time sifting ledger entries.
if ($amount > 0) {
$unreconciled = $this->findUnreconciledLedgerEntries($id, $ofund, $cond);
foreach ($unreconciled[$ofund]['entry'] AS $i => &$entry) {
// Determine if amount is sufficient to cover the entry
if ($amount > $entry['balance'])
$apply = $entry['balance'];
elseif ($amount > 0)
$apply = $amount;
else {
unset($unreconciled[$ofund]['entry'][$i]);
continue;
}
$entry['applied'] = $apply;
$entry['reconciled'] += $apply;
$entry['balance'] -= $apply;
$applied += $apply;
$amount -= $apply;
}
}
$unreconciled[$ofund]['unapplied'] = $amount;
$unreconciled[$ofund]['applied'] = $applied;
$unreconciled[$ofund]['balance'] -= $applied;
return $unreconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: postLedgerEntry
* -
* transaction_data
* - transaction_id (optional... if set all else is ignored)
* - Transaction
* - stamp (optional... otherwise NOW is used)
* - comment
*
* monetary_source_data
* - monetary_source_id (optional... if set all else is ignored)
* - account_name
* - MonetarySource
* - name
*/
function postLedgerEntry($transaction_data,
$monetary_data,
$entry_data,
$reconcile = null) {
//pr(compact('transaction_data', 'monetary_data', 'entry_data', 'reconcile'));
// Automatically figure out the customer if we have the lease
if (isset($entry_data['lease_id']) && !isset($entry_data['customer_id'])) {
$L = new Lease();
$L->recursive = -1;
$lease = $L->read(null, $entry_data['lease_id']);
$entry_data['customer_id'] = $lease['Lease']['customer_id'];
}
if (!isset($entry_data['lease_id']))
$entry_data['lease_id'] = null;
if (!isset($entry_data['customer_id']))
$entry_data['customer_id'] = null;
// Get the Transaction squared away
if (isset($transaction_data['transaction_id'])) {
$transaction_data
= array_intersect_key($transaction_data,
array('transaction_id'=>1,
'split_transaction_id'=>1));
}
elseif (isset($transaction_data['Transaction'])) {
$transaction_data
= array_intersect_key($transaction_data,
array('Transaction'=>1,
'split_transaction_id'=>1));
}
else {
$transaction_data = array('Transaction'=>array('stamp' => null));
}
// Get the Monetary Source squared away
if (isset($monetary_data)) {
if (!isset($monetary_data['monetary_source_id'])) {
// Convert Account ID to name or vice versa
if (isset($monetary_data['account_id'])) {
$monetary_data['account_name'] = $this->name($monetary_data['account_id']);
} elseif (isset($monetary_data['account_name'])) {
$monetary_data['account_id'] = $this->nameToID($monetary_data['account_name']);
}
if ($monetary_data['account_id'] == $this->cashAccountID()) {
// No distinguishing features of Cash, just
// use the shared monetary source
$monetary_data['monetary_source_id'] =
$this->Ledger->LedgerEntry->MonetarySource->nameToID('Cash');
}
}
if (isset($monetary_data['monetary_source_id'])) {
$monetary_data
= array_intersect_key($monetary_data,
array('monetary_source_id'=>1));
}
else {
// The monetary source needs to be unique
// Create a new one dedicated to this entry
// Give it a fancy name based on the check number
$monetary_data['MonetarySource']['name'] = $monetary_data['account_name'];
if ($monetary_data['account_name'] === $this->name($this->checkAccountID()) ||
$monetary_data['account_name'] === $this->name($this->moneyOrderAccountID())) {
$monetary_data['MonetarySource']['name'] .=
' #' . $monetary_data['MonetarySource']['data1'];
}
$monetary_data
= array_intersect_key($monetary_data,
array('MonetarySource'=>1));
}
}
else {
$monetary_data = array();
}
// Make sure to clean out any unwanted data from the entry
$entry_data
= array_diff_key($entry_data,
array('transaction_id'=>1, 'Transaction'=>1,
'monetary_source_id'=>1, 'MonetarySource'=>1));
// Then add in the transaction and monetary source data
//pr(compact('transaction_data', 'monetary_data', 'entry_data'));
if (isset($transaction_data))
$entry_data += $transaction_data;
if (isset($monetary_data))
$entry_data += $monetary_data;
// Set up the debit ledger id
if (!isset($entry_data['debit_ledger_id'])) {
$entry_data['debit_ledger_id'] =
(isset($entry_data['debit_account_id'])
? $this->currentLedgerID($entry_data['debit_account_id'])
: (isset($entry_data['debit_account_name'])
? $this->currentLedgerID($this->nameToID($entry_data['debit_account_name']))
: null
)
);
}
// Set up the credit ledger id
if (!isset($entry_data['credit_ledger_id'])) {
$entry_data['credit_ledger_id'] =
(isset($entry_data['credit_account_id'])
? $this->currentLedgerID($entry_data['credit_account_id'])
: (isset($entry_data['credit_account_name'])
? $this->currentLedgerID($this->nameToID($entry_data['credit_account_name']))
: null
)
);
}
//pr(array('pre-save', compact('entry_data')));
// Create it!
$new_entry = new LedgerEntry();
$new_entry->create();
if (!$new_entry->saveAll($entry_data, array('validate'=>false))) {
return array('error' => true);
}
// See if the user has entered some sort of non-array
// for the reconcile parameter.
if (isset($reconcile) && is_bool($reconcile) && $reconcile) {
$reconcile = array('debit' => true, 'credit' => true);
}
elseif (isset($reconcile) && $reconcile == 'invoice') {
$reconcile = array('credit' => 'invoice');
}
elseif (isset($reconcile) && $reconcile == 'receipt') {
$reconcile = array('debit' => 'receipt');
}
elseif (!isset($reconcile) || !is_array($reconcile)) {
$reconcile = array();
}
// Reconcile the new entry... assume we'll have success
$err = false;
foreach (array_intersect_key($reconcile, array('credit'=>1,'debit'=>1))
AS $dc_type => $reconcile_set) {
if (!isset($reconcile_set) || (is_bool($reconcile_set) && !$reconcile_set))
continue;
if ($reconcile_set === 'receipt') {
$C = new Customer();
$reconciled = $C->reconcileNewLedgerEntry($entry_data['customer_id'],
$this->fundamentalOpposite($dc_type),
$entry_data['amount']);
/* pr(array("reconcile receipt", */
/* compact('reconciled', 'split_transaction', 'transaction_data'))); */
$split_transaction = array_intersect_key($transaction_data,
array('Transaction'=>1,
'split_transaction_id'=>1));
if (isset($split_transaction['split_transaction_id']))
$split_transaction['transaction_id'] = $split_transaction['split_transaction_id'];
if (is_array($reconciled) && count($reconciled[$dc_type]['entry'])) {
foreach ($reconciled[$dc_type]['entry'] AS $rec) {
//pr(compact('rec', 'split_transaction'));
if (!$rec['applied'])
continue;
// Create an entry to handle the splitting of the funds ("Payment")
// and reconcile against the new cash/check/etc entry created above,
// as well as the A/R account.
// Payment must debit the Receipt ledger, and credit the A/R ledger
// debit: Receipt credit: A/R
$ids = $this->postLedgerEntry
($split_transaction,
null,
array('debit_ledger_id' => $this->currentLedgerID($this->receiptAccountID()),
'credit_ledger_id' => $this->currentLedgerID($this->accountReceivableAccountID()),
'amount' => $rec['applied'],
'lease_id' => $rec['lease_id'],
'customer_id' => $rec['customer_id'],
),
array('debit' => array(array('LedgerEntry' => array('id' => $new_entry->id,
'amount' => $rec['applied']))),
'credit' => array(array('LedgerEntry' => array('id' => $rec['id'],
'amount' => $rec['applied']))))
);
// Keep using the same split transaction for all reconciled entries
$split_transaction = array_intersect_key($ids, array('transaction_id'=>1));
//pr(compact('ids', 'split_transaction'));
}
//pr("end reconciled is array");
}
//pr("end reconcile receipt");
}
if (is_array($reconcile_set)) {
//pr("reconcile_set is array");
foreach ($reconcile_set AS $reconcile_entry) {
if (!isset($reconcile_entry['LedgerEntry']['id']))
continue;
$amount = $reconcile_entry['LedgerEntry']['amount'];
if (!$amount)
continue;
if ($dc_type == 'debit') {
$debit_ledger_entry_id = $new_entry->id;
$credit_ledger_entry_id = $reconcile_entry['LedgerEntry']['id'];
}
else {
$debit_ledger_entry_id = $reconcile_entry['LedgerEntry']['id'];
$credit_ledger_entry_id = $new_entry->id;
}
$R = new Reconciliation();
$R->create();
if (!$R->save(compact('amount',
'debit_ledger_entry_id',
'credit_ledger_entry_id'), false))
$err = true;
}
}
}
$new_entry->recursive = -1;
$new_entry->read();
//pr(array('post-save', $entry->data));
$ret = array
('error' => $err,
'id' => $new_entry->data['LedgerEntry']['id'],
'transaction_id' => $new_entry->data['LedgerEntry']['transaction_id'],
'monetary_source_id' => $new_entry->data['LedgerEntry']['monetary_source_id']);
if (isset($split_transaction['transaction_id']))
$ret['split_transaction_id'] = $split_transaction['transaction_id'];
return $ret;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: closeAndDeposit
* - Closes the current set of ledgers, transferring
* their balances to specified ledger.
*/
function closeAndDeposit($set, $deposit_account_id) {
$close = new Close();
$close->create();
if (!$close->save(array('stamp' => null, 'comment' => 'Deposit'), false)) {
return false;
}
$transaction = array();
foreach ($set AS $ledger) {
// REVISIT <AP>: 20090710
// If the user said to include a ledger in the
// set, should we really be excluding it?
if ($ledger['total'] == 0)
continue;
$ids = $this->postLedgerEntry
($transaction,
null,
array('debit_account_id' => $deposit_account_id,
'credit_ledger_id' => $ledger['id'],
'amount' => $ledger['total']),
// Reconcile the account for cash/check/etc,
// which is the credit side of this entry.
array('credit' => $ledger['entries']));
//pr(compact('ids'));
if ($ids['error'])
die("closeAndDeposit : postLedgerEntry returned error!");
$transaction = array_intersect_key($ids, array('transaction_id'=>1));
$this->Ledger->closeLedger($ledger['id'], $close->id);
}
function ledgerEntries($id, $all = false, $cond = null, $link = null) {
$ledgers = $this->ledgers($id, $all);
return $this->Ledger->ledgerEntries($ledgers, $cond, $link);
}
@@ -811,37 +358,17 @@ class Account extends AppModel {
* - Returns summary data from the requested account.
*/
function stats($id = null, $all = false, $cond = null) {
function stats($id = null, $all = false, $query = null) {
if (!$id)
return null;
// All old, closed ledgers MUST balance to 0.
// However, the user may want the ENTIRE running totals,
// (not just the balance), so we may have to query all
// ledgers, as dictated by the $all parameter.
$account = $this->find('first',
array('contain' =>
($all
? array('Ledger' => array
('fields' => array('id')))
: array('CurrentLedger' => array
('fields' => array('id')))
),
'conditions' => array
(array('Account.id' => $id))
));
$this->queryInit($query);
$query['link'] = array('Account' => $query['link']);
$stats = array();
if ($all) {
foreach ($account['Ledger'] AS $ledger)
$this->statsMerge($stats['Ledger'],
$this->Ledger->stats($ledger['id'], $cond));
}
else {
$stats['Ledger'] =
$this->Ledger->stats($account['CurrentLedger']['id'], $cond);
}
foreach ($this->ledgers($id, $all) AS $ledger)
$this->statsMerge($stats['Ledger'],
$this->Ledger->stats($ledger, $query));
return $stats;
}

View File

@@ -86,7 +86,7 @@ class LinkableBehavior extends ModelBehavior {
protected $_defaults = array('type' => 'LEFT');
function pr($lev, $mixed) {
if ($lev >= 5)
if ($lev >= 3)
return;
pr($mixed);
@@ -130,277 +130,357 @@ class LinkableBehavior extends ModelBehavior {
}
public function beforeFind(&$Model, $query) {
$this->pr(10,
array('function' => 'Linkable::beforeFind',
'args' => array('Model->alias' => '$Model->alias') + compact('query'),
));
if (isset($query[$this->_key])) {
$optionsDefaults = $this->_defaults + array('reference' =>
array('class' => $Model->alias,
'alias' => $Model->alias),
$this->_key => array());
$optionsKeys = $this->_options + array($this->_key => true);
if (!isset($query['fields']) || $query['fields'] === true) {
//$query['fields'] = array_keys($Model->_schema);
$query['fields'] = $Model->getDataSource()->fields($Model);
} elseif (!is_array($query['fields'])) {
$query['fields'] = array($query['fields']);
}
$query = am(array('joins' => array()), $query, array('recursive' => -1));
$iterators[] = $query[$this->_key];
$cont = 0;
do {
$iterator = $iterators[$cont];
$defaults = $optionsDefaults;
if (isset($iterator['defaults'])) {
$defaults = array_merge($defaults, $iterator['defaults']);
unset($iterator['defaults']);
}
$iterations = Set::normalize($iterator);
$this->pr(25,
array('checkpoint' => 'Iterations',
compact('iterations'),
));
foreach ($iterations as $alias => $options) {
if (is_null($options)) {
$options = array();
}
$options = am($defaults, compact('alias'), $options);
if (empty($options['alias'])) {
throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__));
}
if (!isset($query[$this->_key]))
return $query;
if (empty($options['class']))
$options['class'] = $alias;
if (!isset($options['conditions']))
$options['conditions'] = array();
elseif (!is_array($options['conditions']))
$options['conditions'] = array($options['conditions']);
$this->pr(20,
array('checkpoint' => 'Begin Model Work',
compact('alias', 'options'),
));
$modelClass = $options['class'];
$modelAlias = $options['alias'];
$referenceClass = $options['reference']['class'];
$referenceAlias = $options['reference']['alias'];
$_Model =& ClassRegistry::init($modelClass); // the incoming model to be linked in query
$Reference =& ClassRegistry::init($referenceClass); // the already in query model that links to $_Model
$this->pr(12,
array('checkpoint' => 'Aliases Established',
'Model' => ($modelAlias .' : '. $modelClass .
' ('. $_Model->alias .' : '. $_Model->name .')'),
'Reference' => ($referenceAlias .' : '. $referenceClass .
' ('. $Reference->alias .' : '. $Reference->name .')'),
));
$db =& $_Model->getDataSource();
$associatedThroughReference = 0;
$association = null;
// Figure out how these two models are related, creating
// a relationship if one doesn't otherwise already exists.
if (($associations = $Reference->getAssociated()) &&
isset($associations[$_Model->alias])) {
$this->pr(12, array('checkpoint' => "Reference defines association to _Model"));
$associatedThroughReference = 1;
$type = $associations[$_Model->alias];
$association = $Reference->{$type}[$_Model->alias];
}
elseif (($associations = $_Model->getAssociated()) &&
isset($associations[$Reference->alias])) {
$this->pr(12, array('checkpoint' => "_Model defines association to Reference"));
$type = $associations[$Reference->alias];
$association = $_Model->{$type}[$Reference->alias];
}
else {
// No relationship... make our best effort to create one.
$this->pr(12, array('checkpoint' => "No assocation between _Model and Reference"));
$type = 'belongsTo';
$_Model->bind($Reference->alias);
// Grab the association now, since we'll unbind in a moment.
$association = $_Model->{$type}[$Reference->alias];
$_Model->unbindModel(array('belongsTo' => array($Reference->alias)));
}
// Determine which model holds the foreign key
if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference) {
$primaryAlias = $referenceAlias;
$foreignAlias = $modelAlias;
$primaryModel = $Reference;
$foreignModel = $_Model;
} else {
$primaryAlias = $modelAlias;
$foreignAlias = $referenceAlias;
$primaryModel = $_Model;
$foreignModel = $Reference;
}
if ($associatedThroughReference)
$associationAlias = $referenceAlias;
else
$associationAlias = $modelAlias;
$this->recursive_array_replace("%{MODEL_ALIAS}",
$associationAlias,
$association['conditions']);
$this->pr(15,
array('checkpoint' => 'Models Established - Check Associations',
'primaryModel' => $primaryAlias .' : '. $primaryModel->name,
'foreignModel' => $foreignAlias .' : '. $foreignModel->name,
compact('type', 'association'),
));
if ($type === 'hasAndBelongsToMany') {
if (isset($association['with']))
$linkClass = $association['with'];
else
$linkClass = Inflector::classify($association['joinTable']);
$Link =& $_Model->{$linkClass};
if (isset($options['linkalias']))
$linkAlias = $options['linkalias'];
else
$linkAlias = $Link->alias;
$this->pr(17,
array('checkpoint' => 'Linking HABTM',
compact('linkClass', 'linkAlias'),
));
// Get the foreign key fields (for the link table) directly from
// the defined model associations, if they exists. This is the
// users direct specification, and therefore definitive if present.
$modelLink = $Link->escapeField($association['foreignKey'], $linkAlias);
$referenceLink = $Link->escapeField($association['associationForeignKey'], $linkAlias);
// If we haven't figured out the foreign keys, see if there is a
// model for the link table, and if it has the appropriate
// associations with the two tables we're trying to join.
if (empty($modelLink) && isset($Link->belongsTo[$_Model->alias]))
$modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey'], $linkAlias);
if (empty($referenceLink) && isset($Link->belongsTo[$Reference->alias]))
$referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey'], $linkAlias);
// We're running quite thin here. None of the models spell
// out the appropriate linkages. We'll have to SWAG it.
if (empty($modelLink))
$modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id', $linkAlias);
if (empty($referenceLink))
$referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id', $linkAlias);
// Get the primary key from the tables we're joining.
$referenceKey = $Reference->escapeField(null, $referenceAlias);
$modelKey = $_Model->escapeField(null, $modelAlias);
// Join the linkage table to our model. We'll use an inner join,
// as the whole purpose of the linkage table is to make this
// connection. As we are embedding this join, the INNER will not
// cause any problem with the overall query, should the user not
// be concerned with whether or not the join has any results.
// They control that with the 'type' parameter which will be at
// the top level join.
$options['joins'][] = array('type' => 'INNER',
'alias' => $modelAlias,
'conditions' => "{$modelKey} = {$modelLink}",
'table' => $db->fullTableName($_Model, true));
// Now for the top level join. This will be added into the list
// of joins down below, outside of the HABTM specific code.
$options['class'] = $linkClass;
$options['alias'] = $linkAlias;
$options['table'] = $Link->getDataSource()->fullTableName($Link);
$options['conditions'][] = "{$referenceLink} = {$referenceKey}";
}
elseif (isset($association['foreignKey']) && $association['foreignKey']) {
$foreignKey = $primaryModel->escapeField($association['foreignKey'], $primaryAlias);
$primaryKey = $foreignModel->escapeField($foreignModel->primaryKey, $foreignAlias);
$this->pr(17,
array('checkpoint' => 'Linking due to foreignKey',
compact('foreignKey', 'primaryKey'),
));
// Only differentiating to help show the logical flow.
// Either way works and this test can be tossed out
if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference)
$options['conditions'][] = "{$primaryKey} = {$foreignKey}";
else
$options['conditions'][] = "{$foreignKey} = {$primaryKey}";
}
else {
$this->pr(17,
array('checkpoint' => 'Linking with no logic (expecting user defined)',
));
// No Foreign Key... nothing we can do.
}
$this->pr(19,
array('checkpoint' => 'Conditions',
array('options[conditions]' => $options['conditions'],
'association[conditions]' => $association['conditions'],
),
));
// The user may have specified conditions directly in the model
// for this join. Make sure to adhere to those conditions.
if (isset($association['conditions']) && is_array($association['conditions']))
$options['conditions'] = array_merge($options['conditions'], $association['conditions']);
elseif (!empty($association['conditions']))
$options['conditions'][] = $association['conditions'];
$this->pr(19,
array('checkpoint' => 'Conditions2',
array('options[conditions]' => $options['conditions'],
),
));
if (empty($options['table'])) {
$options['table'] = $db->fullTableName($_Model, true);
}
if (!isset($options['fields']) || !is_array($options['fields']))
$options['fields'] = $db->fields($_Model, $modelAlias);
elseif (!empty($options['fields']))
$options['fields'] = $db->fields($_Model, $modelAlias, $options['fields']);
$query['fields'] = array_merge($query['fields'], $options['fields'],
(empty($association['fields'])
? array() : $db->fields($_Model, $modelAlias, $association['fields'])));
$options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys));
$options = array_intersect_key($options, $optionsKeys);
if (!empty($options[$this->_key])) {
$iterators[] = $options[$this->_key] +
array('defaults' =>
array_merge($defaults,
array('reference' =>
array('class' => $modelClass,
'alias' => $modelAlias))));
}
$query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'joins' => true, 'conditions' => true));
$this->pr(19,
array('checkpoint' => 'Model Join Complete',
compact('options', 'modelClass', 'modelAlias', 'query'),
));
}
++$cont;
$notDone = isset($iterators[$cont]);
} while ($notDone);
if (!isset($query['fields']) || $query['fields'] === true) {
$query['fields'] = $Model->getDataSource()->fields($Model);
} elseif (!is_array($query['fields'])) {
$query['fields'] = array($query['fields']);
}
$this->pr(20,
array('function' => 'Linkable::beforeFind',
'return' => compact('query'),
$query = am(array('joins' => array()), $query, array('recursive' => -1));
$reference = array('class' => $Model->alias,
'alias' => $Model->alias,
);
$result = array_diff_key($query, array($this->_key => 1));
$this->buildQuery($Model, $Model->alias, $Model->alias, $Model->findQueryType,
$query[$this->_key], $result);
return $result;
}
function buildQuery(&$Reference, $referenceClass, $referenceAlias, $query_type, $links, &$result) {
if (empty($links))
return;
$this->pr(10,
array('begin' => 'Linkable::buildQuery',
'args' => compact('referenceClass', 'referenceAlias', 'query_type', 'links'),
));
//$defaults = $this->_defaults;// + array($this->_key => array());
//$optionsKeys = $this->_options + array($this->_key => true);
$links = Set::normalize($links);
$this->pr(24,
array('checkpoint' => 'Normalized links',
compact('links'),
));
foreach ($links as $alias => $options) {
if (is_null($options)) {
$options = array();
}
//$options = array_intersect_key($options, $optionsKeys);
//$options = am($this->_defaults, compact('alias'), $options);
$options += compact('alias');
$options += $this->_defaults;
if (empty($options['alias'])) {
throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__));
}
if (empty($options['class']))
$options['class'] = $alias;
if (!isset($options['conditions']))
$options['conditions'] = null;
elseif (!is_array($options['conditions']))
$options['conditions'] = array($options['conditions']);
$this->pr(20,
array('checkpoint' => 'Begin Model Work',
compact('referenceAlias', 'alias', 'options'),
));
$modelClass = $options['class'];
$modelAlias = $options['alias'];
$Model =& ClassRegistry::init($modelClass); // the incoming model to be linked in query
$this->pr(12,
array('checkpoint' => 'Model Established',
'Reference' => ($referenceAlias .' : '. $referenceClass .
' ('. $Reference->alias .' : '. $Reference->name .')'),
'Model' => ($modelAlias .' : '. $modelClass .
' ('. $Model->alias .' : '. $Model->name .')'),
));
$db =& $Model->getDataSource();
$associatedThroughReference = 0;
$association = null;
// Figure out how these two models are related, creating
// a relationship if one doesn't otherwise already exists.
if (($associations = $Reference->getAssociated()) &&
isset($associations[$Model->alias])) {
$this->pr(12, array('checkpoint' => "Reference ($referenceClass) defines association to Model ($modelClass)"));
$associatedThroughReference = 1;
$type = $associations[$Model->alias];
$association = $Reference->{$type}[$Model->alias];
}
elseif (($associations = $Model->getAssociated()) &&
isset($associations[$Reference->alias])) {
$this->pr(12, array('checkpoint' => "Model ($modelClass) defines association to Reference ($referenceClass)"));
$type = $associations[$Reference->alias];
$association = $Model->{$type}[$Reference->alias];
}
else {
// No relationship... make our best effort to create one.
$this->pr(12, array('checkpoint' => "No assocation between Reference ($referenceClass) and Model ($modelClass)"));
$type = 'belongsTo';
$Model->bind($Reference->alias);
// Grab the association now, since we'll unbind in a moment.
$association = $Model->{$type}[$Reference->alias];
$Model->unbindModel(array('belongsTo' => array($Reference->alias)));
}
// Determine which model holds the foreign key
if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference) {
$primaryAlias = $referenceAlias;
$foreignAlias = $modelAlias;
$primaryModel = $Reference;
$foreignModel = $Model;
} else {
$primaryAlias = $modelAlias;
$foreignAlias = $referenceAlias;
$primaryModel = $Model;
$foreignModel = $Reference;
}
if ($associatedThroughReference)
$associationAlias = $referenceAlias;
else
$associationAlias = $modelAlias;
$this->pr(30,
array('checkpoint' => 'Options/Association pre-merge',
compact('association', 'options'),
));
// A couple exceptions before performing a union of
// options and association. Namely, most fields result
// in either/or, but a couple should include BOTH the
// options AND the association settings.
foreach (array('fields', 'conditions') AS $fld) {
$this->pr(31,
array('checkpoint' => 'Options/Associations field original',
compact('fld') +
array("options[$fld]" =>
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
"association[$fld]" =>
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
)));
if (!isset($options[$fld]) ||
(empty($options[$fld]) && !is_array($options[$fld])))
unset($options[$fld]);
elseif (!empty($options[$fld]) && !is_array($options[$fld]))
$options[$fld] = array($options[$fld]);
if (!isset($association[$fld]) ||
(empty($association[$fld]) && !is_array($association[$fld])))
unset($association[$fld]);
elseif (!empty($association[$fld]) && !is_array($association[$fld]))
$association[$fld] = array($association[$fld]);
$this->pr(31,
array('checkpoint' => 'Options/Associations field normalize',
compact('fld') +
array("options[$fld]" =>
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
"association[$fld]" =>
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
)));
if (isset($options[$fld]) && isset($association[$fld]))
$options[$fld] = array_merge($options[$fld],
$association[$fld]);
$this->pr(31,
array('checkpoint' => 'Options/Associations field merge complete',
compact('fld') +
array("options[$fld]" =>
array('value' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? $options[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $options) ? (isset($options[$fld]) ? gettype($options[$fld]) : '-null-') : '-unset-'),
"association[$fld]" =>
array('value' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? $association[$fld] : '-null-') : '-unset-',
'type' => array_key_exists($fld, $association) ? (isset($association[$fld]) ? gettype($association[$fld]) : '-null-') : '-unset-'),
)));
}
$this->pr(30,
array('checkpoint' => 'Options/Association post-merge',
compact('association', 'options'),
));
// For any option that's not already set, use
// whatever is specified by the assocation.
$options += array_intersect_key($association, $this->_options);
// Replace all instances of the MODEL_ALIAS variable
// tag with the correct model alias.
$this->recursive_array_replace("%{MODEL_ALIAS}",
$associationAlias,
$options['conditions']);
$this->pr(15,
array('checkpoint' => 'Models Established - Check Associations',
'primaryModel' => $primaryAlias .' : '. $primaryModel->name,
'foreignModel' => $foreignAlias .' : '. $foreignModel->name,
compact('type', 'association', 'options'),
));
if ($type === 'hasAndBelongsToMany') {
if (isset($association['with']))
$linkClass = $association['with'];
else
$linkClass = Inflector::classify($association['joinTable']);
$Link =& $Model->{$linkClass};
if (isset($options['linkalias']))
$linkAlias = $options['linkalias'];
else
$linkAlias = $Link->alias;
// foreignKey and associationForeignKey can refer to either
// the model or the reference, depending on which class
// actually defines the association. Make sure to we're
// using the foreign keys to point to the right class.
if ($associatedThroughReference) {
$modelAFK = 'associationForeignKey';
$referenceAFK = 'foreignKey';
} else {
$modelAFK = 'foreignKey';
$referenceAFK = 'associationForeignKey';
}
$this->pr(17,
array('checkpoint' => 'Linking HABTM',
compact('linkClass', 'linkAlias',
'modelAFK', 'referenceAFK'),
));
// Get the foreign key fields (for the link table) directly from
// the defined model associations, if they exists. This is the
// users direct specification, and therefore definitive if present.
$modelLink = $Link->escapeField($association[$modelAFK], $linkAlias);
$referenceLink = $Link->escapeField($association[$referenceAFK], $linkAlias);
// If we haven't figured out the foreign keys, see if there is a
// model for the link table, and if it has the appropriate
// associations with the two tables we're trying to join.
if (empty($modelLink) && isset($Link->belongsTo[$Model->alias]))
$modelLink = $Link->escapeField($Link->belongsTo[$Model->alias]['foreignKey'], $linkAlias);
if (empty($referenceLink) && isset($Link->belongsTo[$Reference->alias]))
$referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey'], $linkAlias);
// We're running quite thin here. None of the models spell
// out the appropriate linkages. We'll have to SWAG it.
if (empty($modelLink))
$modelLink = $Link->escapeField(Inflector::underscore($Model->alias) . '_id', $linkAlias);
if (empty($referenceLink))
$referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id', $linkAlias);
// Get the primary key from the tables we're joining.
$referenceKey = $Reference->escapeField(null, $referenceAlias);
$modelKey = $Model->escapeField(null, $modelAlias);
$this->pr(21,
array('checkpoint' => 'HABTM links/keys',
array(compact('modelLink', 'modelKey'),
compact('referenceLink', 'referenceKey')),
));
// Join the linkage table to our model. We'll use an inner join,
// as the whole purpose of the linkage table is to make this
// connection. As we are embedding this join, the INNER will not
// cause any problem with the overall query, should the user not
// be concerned with whether or not the join has any results.
// They control that with the 'type' parameter which will be at
// the top level join.
$options['joins'][] = array('type' => 'INNER',
'alias' => $modelAlias,
'conditions' => "{$modelKey} = {$modelLink}",
'table' => $db->fullTableName($Model, true));
// Now for the top level join. This will be added into the list
// of joins down below, outside of the HABTM specific code.
$options['class'] = $linkClass;
$options['alias'] = $linkAlias;
$options['table'] = $Link->getDataSource()->fullTableName($Link);
$options['conditions'][] = "{$referenceLink} = {$referenceKey}";
}
elseif (isset($association['foreignKey']) && $association['foreignKey']) {
$foreignKey = $primaryModel->escapeField($association['foreignKey'], $primaryAlias);
$primaryKey = $foreignModel->escapeField($foreignModel->primaryKey, $foreignAlias);
$this->pr(17,
array('checkpoint' => 'Linking due to foreignKey',
compact('foreignKey', 'primaryKey'),
));
// Only differentiating to help show the logical flow.
// Either way works and this test can be tossed out
if (($type === 'hasMany' || $type === 'hasOne') ^ $associatedThroughReference)
$options['conditions'][] = "{$primaryKey} = {$foreignKey}";
else
$options['conditions'][] = "{$foreignKey} = {$primaryKey}";
}
else {
$this->pr(17,
array('checkpoint' => 'Linking with no logic (expecting user defined)',
));
// No Foreign Key... nothing we can do.
}
$this->pr(19,
array('checkpoint' => 'Conditions',
array('options[conditions]' => $options['conditions'],
),
));
if (empty($options['table'])) {
$options['table'] = $db->fullTableName($Model, true);
}
if (!isset($options['fields']) || !is_array($options['fields']))
$options['fields'] = $db->fields($Model, $modelAlias);
elseif (!empty($options['fields']))
$options['fields'] = $db->fields($Model, $modelAlias, $options['fields']);
// When performing a count query, fields are useless.
// For everything else, we need to add them into the set.
if ($query_type !== 'count')
$result['fields'] = array_merge($result['fields'], $options['fields']);
$result['joins'][] = array_intersect_key($options,
array('type' => true,
'alias' => true,
'table' => true,
'joins' => true,
'conditions' => true));
$sublinks = array_diff_key($options, $this->_options);
$this->buildQuery($Model, $modelClass, $modelAlias, $query_type, $sublinks, $result);
$this->pr(19,
array('checkpoint' => 'Model Join Complete',
compact('referenceAlias', 'modelAlias', 'options', 'result'),
));
}
$this->pr(20,
array('return' => 'Linkable::buildQuery',
compact('referenceAlias'),
));
return $query;
}
}

View File

@@ -1,12 +0,0 @@
<?php
class Close extends AppModel {
var $belongsTo = array(
);
var $hasMany = array(
'Ledger',
);
}
?>

View File

@@ -3,13 +3,6 @@ class Contact extends AppModel {
var $displayField = 'display_name';
var $validate = array(
'id' => array('numeric'),
'display_name' => array('notempty'),
'id_federal' => array('ssn'),
'id_exp' => array('date')
);
var $hasMany = array(
'ContactsMethod',
'ContactsCustomer',
@@ -48,14 +41,15 @@ class Contact extends AppModel {
function saveContact($id, $data) {
// Establish a display name if not already given
if (!$data['Contact']['display_name'])
if (!$data['Contact']['display_name'] &&
$data['Contact']['first_name'] && $data['Contact']['last_name'])
$data['Contact']['display_name'] =
(($data['Contact']['first_name'] &&
$data['Contact']['last_name'])
? $data['Contact']['last_name'] . ', ' . $data['Contact']['first_name']
: ($data['Contact']['first_name']
? $data['Contact']['first_name']
: $data['Contact']['last_name']));
$data['Contact']['last_name'] . ', ' . $data['Contact']['first_name'];
foreach (array('last_name', 'first_name', 'company_name') AS $fld) {
if (!$data['Contact']['display_name'] && $data['Contact'][$fld])
$data['Contact']['display_name'] = $data['Contact'][$fld];
}
// Save the contact data
$this->create();

View File

@@ -19,19 +19,26 @@ class Customer extends AppModel {
'conditions' => 'CurrentLease.close_date IS NULL',
),
'Lease',
'LedgerEntry',
'ContactsCustomer',
'StatementEntry',
'ContactsCustomer' => array(
// It would be nice to claim a dependency here, which would
// simplify deletion of a customer. However, for this to work
// Cake must have a primaryKey as a single field. This table
// makes use of a complex key, so we're out of luck.
/* 'dependent' => true, */
),
'Transaction',
'Tender',
);
var $hasAndBelongsToMany = array(
'Contact',
'Transaction' => array(
'joinTable' => 'ledger_entries',
'foreignKey' => 'customer_id',
'associationForeignKey' => 'transaction_id',
'Contact' => array(
'unique' => true,
),
);
//var $default_log_level = 20;
/**************************************************************************
**************************************************************************
@@ -57,131 +64,101 @@ class Customer extends AppModel {
* function: leaseIds
* - Returns the lease IDs for the given customer
*/
function leaseIds($id) {
function leaseIds($id, $current = false) {
$Lease = $current ? 'CurrentLease' : 'Lease';
$this->cacheQueries = true;
$customer = $this->find('first',
array('contain' =>
array('Lease' => array('fields' => array('id'))),
array($Lease => array('fields' => array('id'))),
'fields' => array(),
'conditions' => array(array('Customer.id' => $id))));
$this->cacheQueries = false;
$ids = array();
foreach ($customer['Lease'] AS $lease)
foreach ($customer[$Lease] AS $lease)
$ids[] = $lease['id'];
return $ids;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findSecurityDeposits
* function: securityDeposits
* - Returns an array of security deposit entries
*/
function findSecurityDeposits($id, $link = null) {
/* pr(array('function' => 'Customer::findSecurityDeposits', */
/* 'args' => compact('id', 'link'), */
/* )); */
function securityDeposits($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
$A = new Account();
$entries = $A->findLedgerEntries
($A->securityDepositAccountID(),
true, array('LedgerEntry.customer_id' => $id), $link);
$query['conditions'][] = array('StatementEntry.customer_id' => $id);
$query['conditions'][] = array('StatementEntry.account_id' =>
$this->StatementEntry->Account->securityDepositAccountID());
/* pr(array('function' => 'Customer::findSecurityDeposits', */
/* 'args' => compact('id', 'link'), */
/* 'vars' => compact('customer'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
$set = $this->StatementEntry->reconciledSet('CHARGE', $query, false, true);
return $this->prReturn($set);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findUnreconciledLedgerEntries
* - Returns ledger entries that are not yet reconciled
* (such as charges not paid).
* function: securityDepositBalance
* - Returns the balance of the customer security deposit(s)
*/
function findUnreconciledLedgerEntries($id = null, $fundamental_type = null) {
$A = new Account();
$unreconciled = $A->findUnreconciledLedgerEntries
($A->accountReceivableAccountID(),
$fundamental_type,
array('LedgerEntry.customer_id' => $id));
function securityDepositBalance($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
return $unreconciled;
$sd_account_id =
$this->StatementEntry->Account->securityDepositAccountID();
$squery = $query;
$squery['conditions'][] = array('StatementEntry.customer_id' => $id);
$squery['conditions'][] = array('StatementEntry.account_id' => $sd_account_id);
$stats = $this->StatementEntry->stats(null, $squery);
$this->pr(26, compact('squery', 'stats'));
// OK, we know now how much we charged for a security
// deposit, as well as how much we received to pay for it.
// Now we need to know if any has been released.
// Yes... this sucks.
$lquery = $query;
$lquery['link'] = array('Transaction' =>
array('fields' => array(),
'Customer' =>
(empty($query['link'])
? array('fields' => array())
: $query['link'])));
$lquery['conditions'][] = array('Transaction.customer_id' => $id);
$lquery['conditions'][] = array('LedgerEntry.account_id' => $sd_account_id);
$lquery['conditions'][] = array('LedgerEntry.crdr' => 'DEBIT');
$lquery['fields'][] = 'SUM(LedgerEntry.amount) AS total';
$released = $this->StatementEntry->Transaction->LedgerEntry->find
('first', $lquery);
$this->pr(26, compact('lquery', 'released'));
return $this->prReturn($stats['Charge']['disbursement'] - $released[0]['total']);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconcileNewLedgerEntry
* - Returns which ledger entries a new credit/debit would
* reconcile, and how much.
*
* - REVISIT <AP> 20090617
* This should be subject to different algorithms, such
* as apply to oldest charges first, newest first, to fees
* before rent, etc. Until we get there, I'll hardcode
* whatever algorithm is simplest.
* function: unreconciledCharges
* - Returns charges have not yet been fully paid
*/
function reconcileNewLedgerEntry($id, $fundamental_type, $amount) {
$A = new Account();
$reconciled = $A->reconcileNewLedgerEntry
($A->accountReceivableAccountID(),
$fundamental_type,
$amount,
array('LedgerEntry.customer_id' => $id));
function unreconciledCharges($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
return $reconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: details
* - Returns detail information for the customer
*/
function details($id = null) {
// Query the DB for need information.
$customer = $this->find
('first', array
('contain' => array
(// Models
'Contact' =>
array('order' => array('Contact.display_name'),
// Models
'ContactPhone',
'ContactEmail',
'ContactAddress',
),
'Lease' =>
array('Unit' =>
array('order' => array('sort_order'),
'fields' => array('id', 'name'),
),
),
),
'conditions' => array('Customer.id' => $id),
));
// Figure out the outstanding balance for this customer
$customer['stats'] = $this->stats($id);
// Figure out the total security deposit for the current lease.
$customer['deposits'] = $this->findSecurityDeposits($id);
return $customer;
$query['conditions'][] = array('StatementEntry.customer_id' => $id);
$set = $this->StatementEntry->reconciledSet('CHARGE', $query, true);
return $this->prReturn($set);
}
@@ -200,10 +177,8 @@ class Customer extends AppModel {
continue;
$I = new Contact();
$I->create();
if (!$I->save($contact, false)) {
if (!$I->saveContact(null, array('Contact' => $contact)))
return false;
}
$contact['id'] = $I->id;
}
@@ -227,6 +202,13 @@ class Customer extends AppModel {
}
$id = $this->id;
// Appears that $this->save() "helpfully" choses to add in
// any missing data fields, populated with default values.
// So, after saving is complete, the fields 'lease_count',
// 'past_lease_count', and 'current_lease_count' have all
// been reset to zero. Gee, thanks Cake...
$this->update($id);
// Remove all associated Customer Contacts, as it ensures
// any entries deleted by the user actually get deleted
// in the system. We'll recreate the needed ones anyway.
@@ -261,6 +243,200 @@ class Customer extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: update
* - Update any cached or calculated fields
*/
function update($id) {
$this->prEnter(compact('id'));
if (empty($id)) {
$customers = $this->find('all', array('contain' => false, 'fields' => array('id')));
foreach ($customers AS $customer) {
// This SHOULDN'T happen, but check to be sure
// or we'll get infinite recursion.
if (empty($customer['Customer']['id']))
continue;
$this->update($customer['Customer']['id']);
}
return;
}
// updateLeaseCount is typically handled directly when needed.
// However, this function is used to _ensure_ customer info is
// current, so we're obligated to call it anyway.
$this->updateLeaseCount($id);
$current_leases =
$this->find('all',
// REVISIT <AP>: 20090816
// Do we need to update leases other than the current ones?
// It may be necessary. For example, a non-current lease
// can still be hit with an NSF item. In that case, it
// could have stale data if we look only to current leases.
//array('link' => array('CurrentLease' => array('type' => 'INNER')),
array('link' => array('Lease' => array('type' => 'INNER')),
'conditions' => array('Customer.id' => $id)));
foreach ($current_leases AS $lease) {
if (!empty($lease['CurrentLease']['id']))
$this->Lease->update($lease['CurrentLease']['id']);
if (!empty($lease['Lease']['id']))
$this->Lease->update($lease['Lease']['id']);
}
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: merge
* - Merges two customers into one
*/
function merge($dst_id, $src_id, $contacts) {
$this->prEnter(compact('dst_id', 'src_id', 'contacts'));
// Get the entire list of destination customer contacts
$dst_contacts = array();
$result = $this->find('all',
array('link' => array('ContactsCustomer'),
'fields' => array('ContactsCustomer.contact_id', 'ContactsCustomer.type'),
'conditions' => array(array('id' => $dst_id,
'ContactsCustomer.active' => true))));
foreach ($result AS $contact) {
$dst_contacts[$contact['ContactsCustomer']['contact_id']] = $contact['ContactsCustomer'];
}
$this->pr(17, compact('dst_contacts'));
// Get the entire list of source customer contacts
$src_contacts = array();
$result = $this->find('all',
array('link' => array('ContactsCustomer'),
'fields' => array('ContactsCustomer.contact_id', 'ContactsCustomer.type'),
'conditions' => array(array('id' => $src_id,
'ContactsCustomer.active' => true))));
foreach ($result AS $contact) {
$src_contacts[$contact['ContactsCustomer']['contact_id']] = $contact['ContactsCustomer'];
}
$this->pr(17, compact('src_contacts'));
// Verify the contacts list are all valid source customer contacts
foreach ($contacts AS $contact_id) {
if (!array_key_exists($contact_id, $src_contacts))
return $this->prReturn(false);
}
// Remove any contacts which are already destination customer contacts
$new_contacts = array_diff($contacts, array_keys($dst_contacts));
$all_contacts = array_merge($new_contacts, array_keys($dst_contacts));
$this->pr(17, compact('new_contacts', 'all_contacts'));
// For now, we'll assume the operation will succeed.
$ret = true;
// Add each desired source customer contact to the destination customer
foreach ($new_contacts AS $contact_id) {
$CM = new ContactsCustomer();
if (!$CM->save(array('customer_id' => $dst_id)
+ $src_contacts[$contact_id], false)) {
$ret = false;
}
}
$this->Lease->updateAll
(array('Lease.customer_id' => $dst_id),
array('Lease.customer_id' => $src_id)
);
$this->Tender->updateAll
(array('Tender.customer_id' => $dst_id),
array('Tender.customer_id' => $src_id)
);
$this->StatementEntry->updateAll
(array('StatementEntry.customer_id' => $dst_id),
array('StatementEntry.customer_id' => $src_id)
);
$this->Transaction->updateAll
(array('Transaction.customer_id' => $dst_id),
array('Transaction.customer_id' => $src_id)
);
// Make sure our lease counts, etc are correct
$this->update($dst_id);
// Delete the old customer
$this->pr(12, compact('src_id'), "Delete Customer");
$this->delete($src_id);
// Delete all the orphaned customers
foreach (array_diff(array_keys($src_contacts), $all_contacts) AS $contact_id) {
// Delete un-used or duplicate contacts
// REVISIT <AP> 20100702:
// Not sure if we really want to do this.
// On the one hand, they're probably really redundant,
// and only clutter up the list of all contacts. On the
// other hand, it destroys data, not only losing the
// history, but making it difficult to recover if the
// merge is a mistake. Additionally, we need to do
// extra checking to ensure that the contact is not
// in use by some other customer.
// We need some sort of Contact.deleted field...
$this->pr(12, compact('contact_id'), "Delete Contact");
$this->Contact->delete($contact_id);
}
// Finally, delete all customer contact relationships
$this->ContactsCustomer->deleteAll
(array('customer_id' => $src_id), false);
// Return the result
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: updateLeaseCount
* - Updates the internal lease count
*/
function updateLeaseCount($id) {
$this->id = $id;
$lease_count =
$this->find('count',
array('link' => array('Lease' => array('type' => 'INNER')),
'conditions' => array('Customer.id' => $id)));
$current_count =
$this->find('count',
array('link' => array('CurrentLease' => array('type' => 'INNER')),
'conditions' => array('Customer.id' => $id)));
$this->saveField('lease_count', $lease_count);
$this->saveField('current_lease_count', $current_count);
$this->saveField('past_lease_count', $lease_count - $current_count);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: balance
* - Returns the balance of money owed on the lease
*/
function balance($id) {
$stats = $this->stats($id);
return $stats['balance'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -268,17 +444,62 @@ class Customer extends AppModel {
* - Returns summary data from the requested customer.
*/
function stats($id = null) {
function stats($id = null, $query = null) {
//$this->prFunctionLevel(20);
$this->prEnter(compact('id', 'query'));
if (!$id)
return null;
return $this->prExit(null);
$A = new Account();
$stats = $A->stats($A->accountReceivableAccountID(), true,
array('LedgerEntry.customer_id' => $id));
$this->queryInit($query);
// Pull to the top level and return
$stats = $stats['Ledger'];
return $stats;
// REVISIT <AP>: 20090725
// We'll need to go directly to the statement entries if
// transactions are not always associated with the customer.
// This could happen if either we remove the customer_id
// field from Transaction, or we allow multiple customers
// to be part of the same transaction (essentially making
// the Transaction.customer_id meaningless).
/* $stats = $this->StatementEntry->find */
/* ('first', array */
/* ('contain' => false, */
/* 'fields' => $this->StatementEntry->chargeDisbursementFields(true), */
/* 'conditions' => array('StatementEntry.customer_id' => $id), */
/* )); */
$find_stats = $this->StatementEntry->find
('first', array
('contain' => false,
'fields' => $this->StatementEntry->chargeDisbursementFields(true),
'conditions' => array('StatementEntry.customer_id' => $id),
));
$find_stats = $find_stats[0];
$this->pr(17, compact('find_stats'));
$tquery = $query;
$tquery['conditions'][] = array('StatementEntry.customer_id' => $id);
$statement_stats = $this->StatementEntry->stats(null, $tquery);
$statement_stats['balance'] = $statement_stats['Charge']['balance'];
$this->pr(17, compact('statement_stats'));
$tquery = $query;
//$tquery['conditions'][] = array('StatementEntry.customer_id' => $id);
$tquery['conditions'][] = array('Transaction.customer_id' => $id);
$transaction_stats = $this->Transaction->stats(null, $tquery);
$transaction_stats += $transaction_stats['StatementEntry'];
$this->pr(17, compact('transaction_stats'));
$tquery = $query;
//$tquery['conditions'][] = array('StatementEntry.customer_id' => $id);
$tquery['conditions'][] = array('Transaction.customer_id' => $id);
$ar_transaction_stats = $this->Transaction->stats(null, $tquery,
$this->Transaction->Account->accountReceivableAccountID());
$ar_transaction_stats += $ar_transaction_stats['LedgerEntry'];
$this->pr(17, compact('ar_transaction_stats'));
//$stats = $ar_transaction_stats;
$stats = $find_stats;
return $this->prReturn($stats);
}
}

21
models/default_option.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
class DefaultOption extends AppModel {
var $belongsTo =
array('OptionValue',
);
function values($name = null) {
$this->prEnter(compact('name'));
$query = array();
$this->queryInit($query);
$query['link']['DefaultOption'] = array();
$query['link']['DefaultOption']['type'] = 'INNER';
$query['link']['DefaultOption']['fields'] = array();
return $this->prReturn($this->OptionValue->values($name, $query));
}
}

View File

@@ -0,0 +1,21 @@
<?php
class DefaultPermission extends AppModel {
var $belongsTo =
array('PermissionValue',
);
function values($name = null) {
$this->prEnter(compact('name'));
$query = array();
$this->queryInit($query);
$query['link']['DefaultPermission'] = array();
$query['link']['DefaultPermission']['type'] = 'INNER';
$query['link']['DefaultPermission']['fields'] = array();
return $this->prReturn($this->PermissionValue->values($name, $query));
}
}

109
models/double_entry.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
class DoubleEntry extends AppModel {
var $belongsTo = array(
'DebitEntry' => array(
'className' => 'LedgerEntry',
),
'CreditEntry' => array(
'className' => 'LedgerEntry',
),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: verifyDoubleEntry
* - Verifies consistenty of new double entry data
* (not in a pre-existing double entry)
*/
function verifyDoubleEntry($entry1, $entry2, $entry1_tender = null) {
/* pr(array("DoubleEntry::verifyDoubleEntry()" */
/* => compact('entry1', 'entry2', 'entry1_tender'))); */
$LE = new LedgerEntry();
if (!$LE->verifyLedgerEntry($entry1, $entry1_tender)) {
/* pr(array("DoubleEntry::verifyDoubleEntry()" */
/* => "Entry1 verification failed")); */
return false;
}
if (!$LE->verifyLedgerEntry($entry2)) {
/* pr(array("DoubleEntry::verifyDoubleEntry()" */
/* => "Entry2 verification failed")); */
return false;
}
if (!(($entry1['crdr'] === 'DEBIT' && $entry2['crdr'] === 'CREDIT') ||
($entry1['crdr'] === 'CREDIT' && $entry2['crdr'] === 'DEBIT')) ||
($entry1['amount'] != $entry2['amount'])) {
/* pr(array("DoubleEntry::verifyDoubleEntry()" */
/* => "Double Entry verification failed")); */
return false;
}
return true;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addDoubleEntry
* - Inserts new Double Entry into the database
*/
function addDoubleEntry($entry1, $entry2, $entry1_tender = null) {
//$this->prFunctionLevel(16);
$this->prEnter(compact('entry1', 'entry2', 'entry1_tender'));
$ret = array();
if (!$this->verifyDoubleEntry($entry1, $entry2, $entry1_tender))
return $this->prReturn(array('error' => true) + $ret);
// Handle the case where a double entry involves the same
// exact ledger. This would not serve any useful purpose.
// It is not, however, an error. It is semantically correct
// just not really logically correct. To make this easier,
// just ensure ledger_id is set for each entry, even though
// it would be handled later by the LedgerEntry model.
//array($entry1, $entry2) AS &$entry) {
for ($i=1; $i <= 2; ++$i) {
if (empty(${'entry'.$i}['ledger_id']))
${'entry'.$i}['ledger_id'] =
$this->DebitEntry->Account->currentLedgerID(${'entry'.$i}['account_id']);
}
if ($entry1['ledger_id'] == $entry2['ledger_id'])
return $this->prReturn(array('error' => false));
// Since this model only relates to DebitEntry and CreditEntry...
$LE = new LedgerEntry();
// Add the first ledger entry to the database
$result = $LE->addLedgerEntry($entry1, $entry1_tender);
$ret['Entry1'] = $result;
if ($result['error'])
return $this->prReturn(array('error' => true) + $ret);
// Add the second ledger entry to the database
$result = $LE->addLedgerEntry($entry2);
$ret['Entry2'] = $result;
if ($result['error'])
return $this->prReturn(array('error' => true) + $ret);
// Now link them as a double entry
$double_entry = array();
$double_entry['debit_entry_id'] =
($entry1['crdr'] === 'DEBIT') ? $ret['Entry1']['ledger_entry_id'] : $ret['Entry2']['ledger_entry_id'];
$double_entry['credit_entry_id'] =
($entry1['crdr'] === 'CREDIT') ? $ret['Entry1']['ledger_entry_id'] : $ret['Entry2']['ledger_entry_id'];
$ret['data'] = $double_entry;
$this->create();
if (!$this->save($double_entry))
return $this->prReturn(array('error' => true) + $ret);
$ret['double_entry_id'] = $this->id;
return $this->prReturn($ret + array('error' => false));
}
}

38
models/group.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
class Group extends AppModel {
var $hasMany =
array('GroupOption',
'Membership',
);
var $knows =
array('User',
'Site',
);
static $current_group_ids;
function currentGroupIds() {
if (empty(self::$current_group_ids))
self::$current_group_ids = $this->groupIds();
if (empty(self::$current_group_ids))
// We must force a stop here, since this is typically
// called very early on, and so will cause a recursive
// crash as we try to render the internal error and
// again stumble on this problem.
$this->INTERNAL_ERROR('INVALID MEMBERSHIP', 0, true);
return self::$current_group_ids;
}
function groupIds($user_id = null, $site_id = null) {
if (empty($user_id))
$user_id = $this->User->currentUserId();
if (empty($site_id))
$site_id = $this->Site->currentSiteId();
return $this->Membership->memberGroups($user_id, $site_id);
}
}

25
models/group_option.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
class GroupOption extends AppModel {
var $belongsTo =
array('Group',
'OptionValue',
);
function values($ids, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['GroupOption'] = array();
$query['link']['GroupOption']['fields'] = array();
$query['link']['GroupOption']['Group'] = array();
$query['link']['GroupOption']['Group']['fields'] = array();
$query['conditions'][] = array('Group.id' => $ids);
$query['order'][] = 'Group.rank';
return $this->prReturn($this->OptionValue->values($name, $query));
}
}

View File

@@ -0,0 +1,25 @@
<?php
class GroupPermission extends AppModel {
var $belongsTo =
array('Group',
'PermissionValue',
);
function values($ids, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['GroupPermission'] = array();
$query['link']['GroupPermission']['fields'] = array();
$query['link']['GroupPermission']['Group'] = array();
$query['link']['GroupPermission']['Group']['fields'] = array();
$query['conditions'][] = array('Group.id' => $ids);
$query['order'][] = 'Group.rank';
return $this->prReturn($this->PermissionValue->values($name, $query));
}
}

View File

@@ -1,27 +1,6 @@
<?php
class Lease extends AppModel {
var $name = 'Lease';
var $validate = array(
'id' => array('numeric'),
'number' => array('alphanumeric'),
'lease_type_id' => array('numeric'),
'unit_id' => array('numeric'),
'late_schedule_id' => array('numeric'),
'lease_date' => array('date'),
'movein_planned_date' => array('date'),
'movein_date' => array('date'),
'moveout_date' => array('date'),
'moveout_planned_date' => array('date'),
'notice_given_date' => array('date'),
'notice_received_date' => array('date'),
'close_date' => array('date'),
'deposit' => array('money'),
'rent' => array('money'),
'next_rent' => array('money'),
'next_rent_date' => array('date')
);
var $belongsTo = array(
'LeaseType',
'Unit',
@@ -30,109 +9,118 @@ class Lease extends AppModel {
);
var $hasMany = array(
'LedgerEntry',
'StatementEntry',
);
//var $default_log_level = array('log' => 30, 'show' => 30);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: accountId
* - Returns the accountId of the given lease
*/
function accountId($id) {
$A = new Account();
return $A->invoiceAccountID();
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findAccountEntries
* - Returns an array of ledger entries from the account of the given
* lease.
*/
function findAccountEntries($id, $all = false, $cond = null, $link = null) {
/* pr(array('function' => 'Lease::findAccountEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* )); */
if (!isset($cond))
$cond = array();
$cond[] = array('LedgerEntry.lease_id' => $id);
$A = new Account();
$entries = $A->findLedgerEntries($this->accountId($id),
$all, $cond, $link);
/* pr(array('function' => 'Lease::findAccountEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* 'vars' => compact('lease'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findSecurityDeposits
* function: securityDeposits
* - Returns an array of security deposit entries
*/
function findSecurityDeposits($id, $link = null) {
/* pr(array('function' => 'Lease::findSecurityDeposits', */
/* 'args' => compact('id', 'link'), */
/* )); */
function securityDeposits($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
$A = new Account();
$entries = $A->findLedgerEntries
($A->securityDepositAccountID(),
true, array('LedgerEntry.lease_id' => $id), $link);
$query['conditions'][] = array('StatementEntry.lease_id' => $id);
$query['conditions'][] = array('StatementEntry.account_id' =>
$this->StatementEntry->Account->securityDepositAccountID());
/* pr(array('function' => 'Lease::findSecurityDeposits', */
/* 'args' => compact('id', 'link'), */
/* 'vars' => compact('lease'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
$set = $this->StatementEntry->reconciledSet('CHARGE', $query, false, true);
return $this->prReturn($set);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findUnreconciledLedgerEntries
* - Returns ledger entries that are not yet reconciled
* (such as charges not paid).
* function: securityDepositBalance
* - Returns the balance of the lease security deposit(s)
*/
function findUnreconciledLedgerEntries($id = null, $fundamental_type = null) {
$A = new Account();
return $A->findUnreconciledLedgerEntries
($this->accountId($id), $fundamental_type, array('LedgerEntry.lease_id' => $id));
function securityDepositBalance($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
// REVISIT <AP>: 20090807
// Let's try simplifying the security deposit issue.
// Presume that security deposits are NOT used at all,
// until the customer moves out of the unit. At that
// time, the ENTIRE deposit is converted to customer
// credit. Piece of cake.
// For more information, see file revision history,
// including the revision just before this, r503.
$this->id = $id;
$moveout_date = $this->field('moveout_date');
if (!empty($moveout_date))
return $this->prReturn(0);
$query['conditions'][] = array('StatementEntry.lease_id' => $id);
$query['conditions'][] = array('StatementEntry.account_id' =>
$this->StatementEntry->Account->securityDepositAccountID());
$stats = $this->StatementEntry->stats(null, $query);
return $this->prReturn($stats['Charge']['disbursement']);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconcileNewLedgerEntry
* - Returns which ledger entries a new credit/debit would
* reconcile, and how much.
*
* - REVISIT <AP> 20090617
* This should be subject to different algorithms, such
* as apply to oldest charges first, newest first, to fees
* before rent, etc. Until we get there, I'll hardcode
* whatever algorithm is simplest.
* function: releaseSecurityDeposits
* - Releases all security deposits associated with this lease.
* That simply makes a disbursement out of them, which can be used
* to pay outstanding customer charges, or simply to become
* a customer surplus (customer credit).
*/
function releaseSecurityDeposits($id, $stamp = null, $query = null) {
//$this->prFunctionLevel(30);
$this->prEnter(compact('id', 'stamp', 'query'));
function reconcileNewLedgerEntry($id, $fundamental_type, $amount) {
$A = new Account();
return $A->reconcileNewLedgerEntry
($this->accountId($id), $fundamental_type, $amount, array('LedgerEntry.lease_id' => $id));
$secdeps = $this->securityDeposits($id, $query);
$secdeps = $secdeps['entries'];
$this->pr(20, compact('secdeps'));
// If there are no paid security deposits, then
// we can consider all security deposits released.
if (count($secdeps) == 0)
return $this->prReturn(true);
// Build a transaction
$release = array('Transaction' => array(), 'Entry' => array());
$release['Transaction']['stamp'] = $stamp;
$release['Transaction']['comment'] = "Security Deposit Release";
foreach ($secdeps AS $charge) {
if ($charge['StatementEntry']['type'] !== 'CHARGE')
die("INTERNAL ERROR: SECURITY DEPOSIT IS NOT CHARGE");
// Since security deposits are being released, this also means
// any unpaid (or only partially paid) security deposit should
// have the remaining balance reversed.
if ($charge['StatementEntry']['balance'] > 0)
$this->StatementEntry->reverse($charge['StatementEntry']['id'], true, $stamp);
$release['Entry'][] =
array('amount' => $charge['StatementEntry']['reconciled'],
'account_id' => $this->StatementEntry->Account->securityDepositAccountID(),
'comment' => "Released Security Deposit",
);
}
$customer_id = $secdeps[0]['StatementEntry']['customer_id'];
$lease_id = $secdeps[0]['StatementEntry']['lease_id'];
// Add receipt of the security deposit funds. Do NOT
// flag them as part of the lease, as all received funds
// are only associated with the customer, for future
// (or present) disbursement on any lease.
$result = $this->StatementEntry->Transaction->addReceipt
($release, $customer_id, null);
return $this->prReturn($result);
}
@@ -148,42 +136,68 @@ class Lease extends AppModel {
*/
function rentLastCharges($id) {
$A = new Account();
$this->prEnter(compact('id'));
$rent_account_id = $this->StatementEntry->Account->rentAccountID();
$entries = $this->find
('all',
array('link' =>
array(// Models
'LedgerEntry' => array
('Ledger' => array
('fields' => array(),
'Account' => array
('fields' => array(),
'Ledger' => array
('alias' => 'Lx',
'fields' => array(),
'LedgerEntry' => array
('alias' => 'LEx',
'fields' => array(),
'conditions' => array
('LEx.effective_date = DATE_ADD(LedgerEntry.through_date, INTERVAL 1 day)',
'LEx.lease_id = LedgerEntry.lease_id',
)
),
),
),
),
),
'StatementEntry',
'SEx' =>
array('class' => 'StatementEntry',
'fields' => array(),
'conditions' => array
('SEx.lease_id = StatementEntry.lease_id',
'SEx.type' => 'CHARGE',
'SEx.account_id' => $rent_account_id,
'SEx.reverse_transaction_id IS NULL',
'SEx.effective_date = DATE_ADD(StatementEntry.through_date, INTERVAL 1 day)',
),
),
),
//'fields' => array('id', 'amount', 'effective_date', 'through_date'),
'fields' => array(),
'conditions' => array(array('Lease.id' => $id),
array('Account.id' => $A->rentAccountID()),
array('LEx.id' => null),
array('StatementEntry.type' => 'CHARGE'),
array('StatementEntry.account_id' => $rent_account_id),
array('StatementEntry.reverse_transaction_id IS NULL'),
array('SEx.id' => null),
),
)
);
return $entries;
return $this->prReturn($entries);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: lateCharges
* - Returns a list of late charges from this lease
*/
function lateCharges($id) {
$this->prEnter(compact('id'));
$late_account_id = $this->StatementEntry->Account->lateChargeAccountID();
$entries = $this->StatementEntry->find
('all',
array('link' =>
array(// Models
'Lease',
),
//'fields' => array('id', 'amount', 'effective_date', 'through_date'),
'conditions' => array(array('Lease.id' => $id),
array('StatementEntry.type' => 'CHARGE'),
array('StatementEntry.account_id' => $late_account_id),
),
)
);
return $this->prReturn($entries);
}
@@ -195,10 +209,11 @@ class Lease extends AppModel {
*/
function rentChargeGaps($id) {
$this->prEnter(compact('id'));
$entries = $this->rentLastCharges($id);
if ($entries && count($entries) > 1)
return true;
return false;
return $this->prReturn(true);
return $this->prReturn(false);
}
@@ -214,12 +229,13 @@ class Lease extends AppModel {
*/
function rentChargeThrough($id) {
$this->prEnter(compact('id'));
$entries = $this->rentLastCharges($id);
if (!$entries)
return false;
return $this->prReturn(false);
if (count($entries) != 1)
return null;
return $entries[0]['LedgerEntry']['through_date'];
return $this->prReturn(null);
return $this->prReturn($entries[0]['StatementEntry']['through_date']);
}
@@ -231,64 +247,315 @@ class Lease extends AppModel {
*/
function rentPaidThrough($id) {
$this->prEnter(compact('id'));
$rent_account_id = $this->StatementEntry->Account->rentAccountID();
// Income / Receipt / Money
// debit: A/R credit: Income <-- this entry
// debit: Receipt credit: A/R <-- ReceiptLedgerEntry, below
// debit: Money credit: Receipt <-- MoneyLedgerEntry, below
// First, see if we can find any unpaid entries. Of course,
// the first unpaid entry gives us a very direct indication
// of when the customer is paid up through, which is 1 day
// prior to the effective date of that first unpaid charge.
$rent = $this->StatementEntry->reconciledSet
('CHARGE',
array('fields' =>
array('StatementEntry.*',
'DATE_SUB(StatementEntry.effective_date, INTERVAL 1 DAY) AS paid_through',
),
$query = array
('link' => array
(
'CreditLedger' =>
array('fields' => array(),
'Account' =>
array('fields' => array(),
),
),
'conditions' =>
array(array('StatementEntry.lease_id' => $id),
array('StatementEntry.account_id' => $rent_account_id),
array('StatementEntry.reverse_transaction_id IS NULL'),
),
// We're searching for the Receipt<->A/R entries,
// which are debits on the A/R account. Find the
// reconciling entries to that A/R debit.
'DebitReconciliationLedgerEntry' =>
array('alias' => 'ReceiptLedgerEntry',
'fields' => array(),
'order' => array('StatementEntry.effective_date'),
),
true);
$this->pr(20, $rent, "Unpaid rent");
// Finally, the Money (Cash/Check/etc) Entry is the one
// which reconciles our ReceiptLedgerEntry debit
'DebitReconciliationLedgerEntry' =>
array('alias' => 'MoneyLedgerEntry',
'linkalias' => 'MoneyLedgerEntryR',
'fields' => array('SUM(COALESCE(MoneyLedgerEntryR.amount,0)) AS paid'),
),
),
),
if ($rent['entries'])
return $this->prReturn($rent['entries'][0]['StatementEntry']['paid_through']);
'fields' => array('LedgerEntry.amount',
'DATE_SUB(LedgerEntry.effective_date, INTERVAL 1 DAY) AS paid_through',
),
'group' => 'LedgerEntry.id HAVING paid <> LedgerEntry.amount',
// If we don't have any unpaid charges (great!), then the
// customer is paid up through the last day of the last
// charge. So, search for paid charges, which already
// have the paid through date saved as part of the entry.
$rent = $this->StatementEntry->reconciledSet
('CHARGE',
array('conditions' =>
array(array('StatementEntry.lease_id' => $id),
array('StatementEntry.account_id' => $rent_account_id),
array('StatementEntry.reverse_transaction_id IS NULL'),
),
'conditions' => array(array('LedgerEntry.lease_id' => $id),
array('Account.id' => $this->LedgerEntry->Ledger->Account->rentAccountID()),
),
'order' => array('LedgerEntry.effective_date',
),
);
'order' => array('StatementEntry.through_date DESC'),
),
false);
$this->pr(20, $rent, "Paid rent");
$rent = $this->LedgerEntry->find('first', $query);
if ($rent)
return $rent[0]['paid_through'];
if ($rent['entries'])
return $this->prReturn($rent['entries'][0]['StatementEntry']['through_date']);
$query['fields'] = 'LedgerEntry.through_date';
$query['order'] = 'LedgerEntry.through_date DESC';
$query['group'] = 'LedgerEntry.id';
$rent = $this->LedgerEntry->find('first', $query);
if ($rent)
return $rent['LedgerEntry']['through_date'];
return null;
// After all that, having found that there are no unpaid
// charges, and in fact, no paid charges either, we cannot
// possibly say when the customer is paid through.
return $this->prReturn(null);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: assessMonthlyRent
* - Charges rent for the month, if not already charged.
*/
function assessMonthlyRent($id, $date = null) {
$this->prEnter(compact('id', 'date'));
$this->id = $id;
if (empty($date))
$date = time();
if (is_string($date))
$date = strtotime($date);
// REVISIT <AP>: 20090808
// Anniversary Billing not supported
$anniversary = 0 && $this->field('anniversary_billing');
if (empty($anniversary)) {
$date_parts = getdate($date);
$date = mktime(0, 0, 0, $date_parts['mon'], 1, $date_parts['year']);
}
// Make sure we're not trying to assess rent on a closed lease
$close_date = $this->field('close_date');
$this->pr(17, compact('close_date'));
if (!empty($close_date))
return $this->prReturn(null);
// Don't assess rent after customer has moved out
$moveout_date = $this->field('moveout_date');
$this->pr(17, compact('moveout_date'));
if (!empty($moveout_date) && strtotime($moveout_date) < $date)
return $this->prReturn(null);
// Determine when the customer has already been charged through
// and, of course, don't charge them if they've already been.
$charge_through_date = strtotime($this->rentChargeThrough($id));
$this->pr(17, compact('date', 'charge_through_date')
+ array('date_str' => date('Y-m-d', $date),
'charge_through_date_str' => date('Y-m-d', $charge_through_date)));
if ($charge_through_date >= $date)
return $this->prReturn(null);
// OK, it seems we're going to go ahead and charge the customer
// on this lease. Calculate the new charge through date, which
// is 1 day shy of 1 month from $date. For example, if we're
// charging for 8/1/09, charge through will be 8/31/09, and
// charging for 8/15/09, charge through will be 9/14/09.
$date_parts = getdate($date);
$charge_through_date = mktime(0, 0, 0,
$date_parts['mon']+1,
$date_parts['mday']-1,
$date_parts['year']);
// Build the invoice transaction
$invoice = array('Transaction' => array(), 'Entry' => array());
// REVISIT <AP>: 20090808
// Keeping Transaction.stamp until the existing facility
// is up to date. Then we want the stamp to be now()
// (and so can just delete the next line).
$invoice['Transaction']['stamp'] = date('Y-m-d', $date);
$invoice['Entry'][] =
array('effective_date' => date('Y-m-d', $date),
'through_date' => date('Y-m-d', $charge_through_date),
'amount' => $this->field('rent'),
'account_id' => $this->StatementEntry->Account->rentAccountId(),
);
// Record the invoice and return the result
$this->pr(21, compact('invoice'));
$result = $this->StatementEntry->Transaction->addInvoice
($invoice, null, $id);
return $this->prReturn($result);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: assessMonthlyRentAll
* - Ensures rent has been charged on all open leases
*/
function assessMonthlyRentAll($date = null) {
$this->prEnter(compact('date'));
$leases = $this->find
('all', array('contain' => false,
'conditions' => array('Lease.close_date' => null),
));
$ret = array('Lease' => array());
foreach ($leases AS $lease) {
$result = $this->assessMonthlyRent($lease['Lease']['id'], $date);
$ret['Lease'][$lease['Lease']['id']] = $result;
if ($result['error'])
$ret['error'] = true;
}
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: assessMonthlyLate
* - Assess late charges for the month, if not already charged.
*/
function assessMonthlyLate($id, $date = null) {
$this->prEnter(compact('id', 'date'));
$this->id = $id;
if (empty($date))
$date = time();
if (is_string($date))
$date = strtotime($date);
// REVISIT <AP>: 20090808
// Anniversary Billing not supported
$anniversary = 0 && $this->field('anniversary_billing');
if (empty($anniversary)) {
$date_parts = getdate($date);
$date = mktime(0, 0, 0, $date_parts['mon'], 11, $date_parts['year']);
}
// Don't assess a late charge if the late charge date hasn't
// even come yet. This is questionable whether we really
// should restrict, since the user could know what they're
// doing, and/or the server clock could be off (although that
// would certainly have much larger ramifications). But, the
// fact is that this check likely handles the vast majority
// of the expected behavior, and presents an issue for very
// few users, if any at all.
if ($date > time())
return $this->prReturn(null);
// Make sure we're not trying to assess late charges on a closed lease
$close_date = $this->field('close_date');
$this->pr(17, compact('close_date'));
if (!empty($close_date))
return $this->prReturn(null);
// Don't assess late charges after customer has moved out
$moveout_date = $this->field('moveout_date');
$this->pr(17, compact('moveout_date'));
if (!empty($moveout_date) && strtotime($moveout_date) < $date)
return $this->prReturn(null);
// Determine when the customer has been charged through for rent
// and don't mark them as late if they haven't even been charged rent
$charge_through_date = strtotime($this->rentChargeThrough($id));
$this->pr(17, compact('date', 'charge_through_date')
+ array('date_str' => date('Y-m-d', $date),
'charge_through_date_str' => date('Y-m-d', $charge_through_date)));
if ($charge_through_date <= $date)
return $this->prReturn(null);
// Determine if the customer is actually late. This is based on
// when they've paid through, plus 10 days before they're late.
// REVISIT <AP>: 20090813
// Of course, 10 days is a terrible hardcode. This should be
// driven from the late schedule, saved as part of the lease
// (when finally implemented).
$paid_through_date = strtotime($this->rentPaidThrough($id));
$this->pr(17, compact('date', 'paid_through_date')
+ array('date_str' => date('Y-m-d', $date),
'paid_through_date_str' => date('Y-m-d', $paid_through_date)));
$date_parts = getdate($paid_through_date);
$paid_through_date = mktime(0, 0, 0, $date_parts['mon'], $date_parts['mday']+10, $date_parts['year']);
if ($paid_through_date >= $date)
return $this->prReturn(null);
// Determine if the customer has already been charged a late fee
// and, of course, don't charge them if they've already been.
$late_charges = $this->lateCharges($id);
foreach ($late_charges AS $late) {
if (strtotime($late['StatementEntry']['effective_date']) == $date)
return $this->prReturn(null);
}
// Build the invoice transaction
$invoice = array('Transaction' => array(), 'Entry' => array());
// REVISIT <AP>: 20090808
// Keeping Transaction.stamp until the existing facility
// is up to date. Then we want the stamp to be now()
// (and so can just delete the next line).
$invoice['Transaction']['stamp'] = date('Y-m-d', $date);
$invoice['Entry'][] =
array('effective_date' => date('Y-m-d', $date),
'amount' => 10,
'account_id' => $this->StatementEntry->Account->lateChargeAccountId(),
);
// Record the invoice and return the result
$this->pr(21, compact('invoice'));
$result = $this->StatementEntry->Transaction->addInvoice
($invoice, null, $id);
return $this->prReturn($result);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: assessMonthlyLateAll
* - Ensures rent has been charged on all open leases
*/
function assessMonthlyLateAll($date = null) {
$this->prEnter(compact('date'));
$leases = $this->find
('all', array('contain' => false,
'conditions' => array('Lease.close_date' => null),
));
$ret = array('Lease' => array());
foreach ($leases AS $lease) {
$result = $this->assessMonthlyLate($lease['Lease']['id'], $date);
$ret['Lease'][$lease['Lease']['id']] = $result;
if ($result['error'])
$ret['error'] = true;
}
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* functions: delinquency
* - SQL fragments to determine whether a lease is delinquent
*/
function conditionDelinquent($table_name = 'Lease') {
if (empty($table_name)) $t = ''; else $t = $table_name . '.';
return ("({$t}close_date IS NULL AND" .
" NOW() > DATE_ADD({$t}paid_through_date, INTERVAL 1 DAY))");
}
function delinquentDaysSQL($table_name = 'Lease') {
if (empty($table_name)) $t = ''; else $t = $table_name . '.';
return ("IF(" . $this->conditionDelinquent($table_name) . "," .
" DATEDIFF(NOW(), {$t}paid_through_date)-1," .
" NULL)");
}
function delinquentField($table_name = 'Lease') {
return ($this->delinquentDaysSQL($table_name) . " AS 'delinquent'");
}
@@ -301,7 +568,10 @@ class Lease extends AppModel {
function moveIn($customer_id, $unit_id,
$deposit = null, $rent = null,
$stamp = null, $comment = null) {
$stamp = null, $comment = null)
{
$this->prEnter(compact('customer_id', 'unit_id',
'deposit', 'rent', 'stamp', 'comment'));
$lt = $this->LeaseType->find('first',
array('conditions' =>
@@ -355,13 +625,16 @@ class Lease extends AppModel {
'deposit' => $deposit,
'rent' => $rent,
'comment' => $comment), false)) {
return null;
return $this->prReturn(null);
}
// Set the lease number to be the same as the lease ID
$this->id;
$this->saveField('number', $this->id);
// Update the current lease count for the customer
$this->Customer->updateLeaseCount($customer_id);
// Update the unit status
$this->Unit->updateStatus($unit_id, 'OCCUPIED');
@@ -372,7 +645,7 @@ class Lease extends AppModel {
// was waived, pro-rated, etc.
// Return the new lease ID
return $this->id;
return $this->prReturn($this->id);
}
@@ -384,7 +657,10 @@ class Lease extends AppModel {
*/
function moveOut($id, $status = 'VACANT',
$stamp = null, $close = false) {
$stamp = null, $close = true)
{
$this->prEnter(compact('id', 'status', 'stamp', 'close'));
// Use NOW if not given a moveout date
if (!isset($stamp))
$stamp = date('Y-m-d G:i:s');
@@ -399,10 +675,16 @@ class Lease extends AppModel {
// Save it!
$this->save($this->data, false);
// Release the security deposit(s)
$this->releaseSecurityDeposits($id, $stamp);
// Close the lease, if so requested
if ($close)
$this->close($id, $stamp);
// Update the current lease count for the customer
$this->Customer->updateLeaseCount($this->field('customer_id'));
// Finally, update the unit status
$this->recursive = -1;
$this->read();
@@ -418,8 +700,10 @@ class Lease extends AppModel {
*/
function close($id, $stamp = null) {
$this->prEnter(compact('id', 'stamp'));
if (!$this->closeable($id))
return false;
return $this->prReturn(false);
// Reset the data
$this->create();
@@ -434,7 +718,11 @@ class Lease extends AppModel {
// Save it!
$this->save($this->data, false);
return true;
// Update the current lease count for the customer
$this->Customer->updateLeaseCount($this->field('customer_id'));
return $this->prReturn(true);
}
@@ -446,39 +734,94 @@ class Lease extends AppModel {
*/
function closeable($id) {
$this->prEnter(compact('id'));
$this->recursive = -1;
$this->read(null, $id);
// We can't close a lease that's still in use
if (!isset($this->data['Lease']['moveout_date']))
return false;
return $this->prReturn(false);
// We can't close a lease that's already closed
if (isset($this->data['Lease']['close_date']))
return false;
$deposits = $this->findSecurityDeposits($id);
$stats = $this->stats($id);
return $this->prReturn(false);
// A lease can only be closed if there are no outstanding
// security deposits, and if the account balance is zero.
if ($deposits['summary']['balance'] != 0 || $stats['balance'] != 0)
return false;
// security deposits ...
if ($this->securityDepositBalance($id) != 0)
return $this->prReturn(false);
// ... and if the account balance is zero.
if ($this->balance($id) != 0)
return $this->prReturn(false);
// Apparently this lease meets all the criteria!
return true;
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addCharge
* - Adds an additional charge to the lease
* function: refund
* - Marks any lease balance as payable to the customer.
*/
function addCharge($id, $charge) {
function refund($id, $stamp = null) {
$this->prEnter(compact('id'));
$balance = $this->balance($id);
if ($balance >= 0)
return $this->prReturn(array('error' => true));
$balance *= -1;
// Build a transaction
$refund = array('Transaction' => array(), 'Entry' => array());
$refund['Transaction']['stamp'] = $stamp;
$refund['Transaction']['comment'] = "Lease Refund";
$refund['Entry'][] =
array('amount' => $balance);
$result = $this->StatementEntry->Transaction->addRefund
($refund, null, $id);
return $this->prReturn($result);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: update
* - Update any cached or calculated fields
*/
function update($id) {
$this->prEnter(compact('id'));
$this->id = $id;
$this->saveField('charge_through_date', $this->rentChargeThrough($id));
$this->saveField('paid_through_date', $this->rentPaidThrough($id));
$moveout = $this->field('moveout_date');
if (empty($moveout))
$this->Unit->update($this->field('unit_id'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: balance
* - Returns the balance of money owed on the lease
*/
function balance($id) {
$this->prEnter(compact('id'));
$stats = $this->stats($id);
return $this->prReturn($stats['balance']);
}
@@ -489,17 +832,19 @@ class Lease extends AppModel {
* - Returns summary data from the requested lease.
*/
function stats($id = null) {
function stats($id = null, $query = null) {
$this->prEnter(compact('id', 'query'));
if (!$id)
return null;
return $this->prReturn(null);
$A = new Account();
$stats = $A->stats($A->accountReceivableAccountID(), true,
array('LedgerEntry.lease_id' => $id));
// Pull to the top level and return
$stats = $stats['Ledger'];
return $stats;
$find_stats = $this->StatementEntry->find
('first', array
('contain' => false,
'fields' => $this->StatementEntry->chargeDisbursementFields(true),
'conditions' => array('StatementEntry.lease_id' => $id),
));
$find_stats = $find_stats[0];
return $this->prReturn($find_stats);
}
}

View File

@@ -1,48 +1,15 @@
<?php
class Ledger extends AppModel {
var $name = 'Ledger';
var $validate = array(
'id' => array('numeric'),
'name' => array('notempty'),
);
var $belongsTo = array(
'Account',
'PriorLedger' => array('className' => 'Ledger'),
'Close',
'CloseTransaction' => array('className' => 'Transaction'),
);
var $hasMany = array(
'LedgerEntry' => array(
'foreignKey' => false,
// conditions will be used when JOINing tables
// (such as find with LinkableBehavior)
'conditions' => array('OR' =>
array('LedgerEntry.debit_ledger_id = %{MODEL_ALIAS}.id',
'LedgerEntry.credit_ledger_id = %{MODEL_ALIAS}.id')),
// finderQuery will be used when tables are put
// together across several querys, not with JOIN.
// (such as find with ContainableBehavior)
'finderQuery' => 'SELECT `LedgerEntry`.*
FROM pmgr_ledger_entries AS `LedgerEntry`
WHERE LedgerEntry.debit_ledger_id = ({$__cakeID__$})
OR LedgerEntry.credit_ledger_id = ({$__cakeID__$})',
'counterQuery' => ''
),
'DebitLedgerEntry' => array(
'className' => 'LedgerEntry',
'foreignKey' => 'debit_ledger_id',
'dependent' => false,
),
'CreditLedgerEntry' => array(
'className' => 'LedgerEntry',
'foreignKey' => 'credit_ledger_id',
'dependent' => false,
),
'Transaction',
'LedgerEntry',
);
@@ -55,10 +22,11 @@ class Ledger extends AppModel {
function accountID($id) {
$this->cacheQueries = true;
$item = $this->find('first', array
('contain' => 'Account.id',
('link' => array('Account'),
'conditions' => array('Ledger.id' => $id),
));
$this->cacheQueries = false;
//pr(compact('id', 'item'));
return $item['Account']['id'];
}
@@ -80,120 +48,86 @@ class Ledger extends AppModel {
* function: closeLedger
* - Closes the current ledger, and returns a fresh one
*/
function closeLedger($id, $close_id) {
$this->recursive = -1;
function closeLedgers($ids) {
$ret = array('new_ledger_ids' => array());
$stamp = date('Y-m-d G:i:s');
$this->id = $id;
$this->read();
$this->data['Ledger']['close_id'] = $close_id;
$this->save($this->data, false);
$entries = array();
foreach ($ids AS $id) {
// Query stats to get the balance forward
$stats = $this->stats($id);
$stats = $this->stats($id);
// Populate fields from the current ledger
$this->recursive = -1;
$this->id = $id;
$this->read();
$this->read();
$this->data['Ledger']['id'] = null;
$this->data['Ledger']['close_id'] = null;
$this->data['Ledger']['prior_ledger_id'] = $id;
$this->data['Ledger']['comment'] = null;
++$this->data['Ledger']['sequence'];
$this->id = null;
$this->save($this->data, false);
//pr($this->data);
// Build a new ledger to replace the current one
$this->data['Ledger']['id'] = null;
$this->data['Ledger']['close_transaction_id'] = null;
$this->data['Ledger']['prior_ledger_id'] = $id;
$this->data['Ledger']['comment'] = null;
++$this->data['Ledger']['sequence'];
$this->data['Ledger']['name'] =
($this->data['Ledger']['account_id'] .
'-' .
$this->data['Ledger']['sequence']);
if ($stats['balance'] == 0)
return $this->id;
// Save the new ledger
$this->id = null;
if (!$this->save($this->data, false))
return array('error' => true, 'new_ledger_data' => $this->data) + $ret;
$ret['new_ledger_ids'][] = $this->id;
$this->read();
$ftype = $this->Account->fundamentalType($this->data['Ledger']['account_id']);
$otype = $this->Account->fundamentalOpposite($ftype);
// Create a transaction for balance transfer
$transaction = new Transaction();
$transaction->create();
if (!$transaction->save(array(), false)) {
return null;
$entries[] = array('old_ledger_id' => $id,
'new_ledger_id' => $this->id,
'amount' => $stats['balance']);
}
// Create an entry to carry the balance forward
$carry_entry_data = array
($ftype.'_ledger_id' => $this->id,
$otype.'_ledger_id' => $id,
'transaction_id' => $transaction->id,
'amount' => $stats['balance'],
'comment' => "Ledger Balance Forward",
);
// Perform the close
$result = $this->Transaction->addClose(array('Transaction' => array(),
'Ledger' => $entries));
$ret['Transaction'] = $result;
if ($result['error'])
return array('error' => true) + $ret;
$carry_entry = new LedgerEntry();
$carry_entry->create();
if (!$carry_entry->save($carry_entry_data, false)) {
return null;
}
return $this->id;
return $ret + array('error' => false);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findLedgerEntries
* - Returns an array of ledger entries that belong to a given
* ledger. There is extra work done... see the LedgerEntry model.
* function: debitCreditFields
* - Returns the fields necessary to determine whether the queried
* entries are a debit, or a credit, and also the effect each have
* on the overall balance of the ledger.
*/
function findLedgerEntries($id, $account_type = null, $cond = null, $link = null) {
/* pr(array('function' => 'Ledger::findLedgerEntries', */
/* 'args' => compact('id', 'account_type', 'cond', 'link'), */
/* )); */
function debitCreditFields($sum = false, $balance = true,
$entry_name = 'LedgerEntry', $account_name = 'Account') {
return $this->LedgerEntry->debitCreditFields
($sum, $balance, $entry_name, $account_name);
}
if (!isset($account_type)) {
$ledger = $this->find('first', array
('contain' => array
('Account' => array
('fields' => array('type'),
),
),
'fields' => array(),
'conditions' => array(array('Ledger.id' => $id)),
));
$account_type = $ledger['Account']['type'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: ledgerEntries
* - Returns an array of ledger entries that belong to a given
* ledger. There is extra work done to establish debit/credit
*/
function ledgerEntries($ids, $query = null) {
if (empty($ids))
return null;
// If the requested entries are limited by date, we must calculate
// a balance forward, or the resulting balance will be thrown off.
//
// REVISIT <AP>: This obviously is more general than date.
// As such, it will not work (or, only work if the
// condition only manages to exclude the first parts
// of the ledger, nothing in the middle or at the
// end. For now, I'll just create an 'other' entry,
// not necessarily a balance forward.
$entries = $this->LedgerEntry->find
('all', array
('link' => array('Ledger' => array('Account')),
'fields' => array_merge(array("LedgerEntry.*"),
$this->LedgerEntry->debitCreditFields()),
'conditions' => array('LedgerEntry.ledger_id' => $ids),
));
$bf = array();
if (0 && isset($cond)) {
//$date = '<NOT IMPLEMENTED>';
$stats = $this->stats($id, array('NOT' => array($cond)));
$bf = array(array(array('debit' => $stats['debits'],
'credit' => $stats['credits'],
'balance' => $stats['balance']),
'LedgerEntry' => array('id' => null,
//'comment' => "Balance Forward from $date"),
'comment' => "-- SUMMARY OF EXCLUDED ENTRIES --"),
'Transaction' => array('id' => null,
//'stamp' => $date,
'stamp' => null,
'comment' => null),
));
}
$entries = $this->LedgerEntry->findInLedgerContext($id, $account_type, $cond, $link);
/* pr(array('function' => 'Ledger::findLedgerEntries', */
/* 'args' => compact('id', 'account_type', 'cond', 'link'), */
/* 'vars' => compact('ledger'), */
/* 'return' => compact('entries'), */
/* )); */
//pr(compact('entries'));
return $entries;
}
@@ -204,41 +138,36 @@ class Ledger extends AppModel {
* function: stats
* - Returns summary data from the requested ledger.
*/
function stats($id, $cond = null) {
if (!isset($cond))
$cond = array();
$cond[] = array('Ledger.id' => $id);
function stats($id, $query = null) {
if (!$id)
return null;
$stats = $this->find
('first', array
('link' =>
array(// Models
'Account' => array('fields' => array()),
//'LedgerEntry' => array('fields' => array()),
'LedgerEntry' =>
array('fields' => array(),
'Transaction' => array('fields' => array('stamp')),
),
),
'fields' =>
array("SUM(IF(LedgerEntry.debit_ledger_id = Ledger.id,
LedgerEntry.amount, NULL)) AS debits",
"SUM(IF(LedgerEntry.credit_ledger_id = Ledger.id,
LedgerEntry.amount, NULL)) AS credits",
"SUM(IF(Account.type IN ('ASSET', 'EXPENSE'),
IF(LedgerEntry.debit_ledger_id = Ledger.id, 1, -1),
IF(LedgerEntry.credit_ledger_id = Ledger.id, 1, -1)
) * IF(LedgerEntry.amount, LedgerEntry.amount, 0)
) AS balance",
"COUNT(LedgerEntry.id) AS entries"),
'conditions' => $cond,
'group' => 'Ledger.id',
));
$this->queryInit($query);
if (!isset($query['link']['Account']))
$query['link']['Account'] = array();
if (!isset($query['link']['Account']['fields']))
$query['link']['Account']['fields'] = array();
if (!isset($query['fields']))
$query['fields'] = array();
$query['fields'] = array_merge($query['fields'],
$this->debitCreditFields(true));
$query['conditions'][] = array('LedgerEntry.ledger_id' => $id);
$query['group'][] = 'LedgerEntry.ledger_id';
$stats = $this->LedgerEntry->find('first', $query);
// The fields are all tucked into the [0] index,
// and the rest of the array is useless (empty).
$stats = $stats[0];
// Make sure we have a member for debit/credit
foreach(array('debits', 'credits') AS $crdr)
if (!isset($stats[$crdr]))
$stats[$crdr] = null;
// Make sure we have a non-null balance
if (!isset($stats['balance']))
$stats['balance'] = 0;

View File

@@ -1,525 +1,177 @@
<?php
class LedgerEntry extends AppModel {
var $name = 'LedgerEntry';
var $validate = array(
'id' => array('numeric'),
'transaction_id' => array('numeric'),
'amount' => array('money')
var $belongsTo = array(
'Transaction',
'Account',
'Ledger',
);
var $hasOne = array(
'Tender' => array(
'dependent' => true,
),
'DebitDoubleEntry' => array(
'className' => 'DoubleEntry',
'foreignKey' => 'debit_entry_id',
'dependent' => true,
),
'CreditDoubleEntry' => array(
'className' => 'DoubleEntry',
'foreignKey' => 'credit_entry_id',
'dependent' => true,
),
'DoubleEntry' => array(
'foreignKey' => false,
),
);
var $hasMany = array(
'DebitReconciliation' => array(
'className' => 'Reconciliation',
'foreignKey' => 'debit_ledger_entry_id',
),
'CreditReconciliation' => array(
'className' => 'Reconciliation',
'foreignKey' => 'credit_ledger_entry_id',
),
);
var $belongsTo = array(
'MonetarySource',
'Transaction',
'Customer',
'Lease',
'DebitLedger' => array(
'className' => 'Ledger',
'foreignKey' => 'debit_ledger_id',
),
'CreditLedger' => array(
'className' => 'Ledger',
'foreignKey' => 'credit_ledger_id',
),
'Ledger' => array(
'foreignKey' => false,
// conditions will be used when JOINing tables
// (such as find with LinkableBehavior)
'conditions' => array('OR' =>
array('%{MODEL_ALIAS}.debit_ledger_id = Ledger.id',
'%{MODEL_ALIAS}.credit_ledger_id = Ledger.id')),
// finderQuery will be used when tables are put
// together across several querys, not with JOIN.
// (such as find with ContainableBehavior)
'finderQuery' => 'NOT-IMPLEMENTED',
'counterQuery' => ''
),
);
var $hasAndBelongsToMany = array(
'DebitReconciliationLedgerEntry' => array(
// The Debit half of the double entry matching THIS Credit (if it is one)
'DebitEntry' => array(
'className' => 'LedgerEntry',
'joinTable' => 'reconciliations',
'foreignKey' => 'credit_ledger_entry_id',
'associationForeignKey' => 'debit_ledger_entry_id',
'joinTable' => 'double_entries',
'linkalias' => 'DDE',
'foreignKey' => 'credit_entry_id',
'associationForeignKey' => 'debit_entry_id',
),
// STUPID CakePHP bug screws up when using Containable
// and CLASS contains CLASS. This extra HABTM give the
// option of multiple depths on one CLASS, since there
// isn't an alias specification for Containable.
'DebitReconciliationLedgerEntry2' => array(
// The Credit half of the double entry matching THIS Debit (if it is one)
'CreditEntry' => array(
'className' => 'LedgerEntry',
'joinTable' => 'reconciliations',
'foreignKey' => 'credit_ledger_entry_id',
'associationForeignKey' => 'debit_ledger_entry_id',
),
'CreditReconciliationLedgerEntry' => array(
'className' => 'LedgerEntry',
'joinTable' => 'reconciliations',
'foreignKey' => 'debit_ledger_entry_id',
'associationForeignKey' => 'credit_ledger_entry_id',
'joinTable' => 'double_entries',
'linkalias' => 'CDE',
'foreignKey' => 'debit_entry_id',
'associationForeignKey' => 'credit_entry_id',
),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: conditionEntryAsCreditOrDebit
* - returns the condition necessary to match a set of
* Ledgers to all related LedgerEntries
*/
function conditionEntryAsCreditOrDebit($ledger_ids) {
return array('OR' =>
array(array('debit_ledger_id' => $ledger_ids),
array('credit_ledger_id' => $ledger_ids)));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: ledgerContext query helpers
* - Returns parameters necessary to generate a query which
* puts ledger entries into the context of a ledger. Since
* debit/credit depends on the account type, it is required
* as an argument for each function to avoid having to
* query the ledger/account to find it out.
* function: debitCreditFields
* - Returns the fields necessary to determine whether the queried
* entries are a debit, or a credit, and also the effect each have
* on the overall balance of the account/ledger.
*/
function ledgerContextFields($ledger_id = null, $account_type = null) {
$fields = array('id', 'effective_date', 'through_date',
'lease_id', 'customer_id', 'comment', 'amount');
if (isset($ledger_id)) {
$fields[] = ("IF(LedgerEntry.debit_ledger_id = $ledger_id," .
" LedgerEntry.amount, NULL) AS debit");
$fields[] = ("IF(LedgerEntry.credit_ledger_id = $ledger_id," .
" LedgerEntry.amount, NULL) AS credit");
function debitCreditFields($sum = false, $balance = true,
$entry_name = 'LedgerEntry', $account_name = 'Account') {
$fields = array
(
($sum ? 'SUM(' : '') .
"IF({$entry_name}.crdr = 'DEBIT'," .
" {$entry_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS debit' . ($sum ? 's' : ''),
if (isset($account_type)) {
if (in_array($account_type, array('ASSET', 'EXPENSE')))
$ledger_type = 'debit';
else
$ledger_type = 'credit';
($sum ? 'SUM(' : '') .
"IF({$entry_name}.crdr = 'CREDIT'," .
" {$entry_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS credit' . ($sum ? 's' : ''),
);
$fields[] = ("(IF(LedgerEntry.{$ledger_type}_ledger_id = $ledger_id," .
" 1, -1) * LedgerEntry.amount) AS balance");
}
}
if ($balance)
$fields[] =
($sum ? 'SUM(' : '') .
"IF(${account_name}.type IN ('ASSET', 'EXPENSE')," .
" IF({$entry_name}.crdr = 'DEBIT', 1, -1)," .
" IF({$entry_name}.crdr = 'CREDIT', 1, -1))" .
" * IF({$entry_name}.amount, {$entry_name}.amount, 0)" .
($sum ? ')' : '') . ' AS balance';
return $fields;
}
function ledgerContextFields2($ledger_id = null, $account_id = null, $account_type = null) {
$fields = array('id', 'effective_date', 'through_date', 'comment', 'amount');
if (isset($ledger_id)) {
$fields[] = ("IF(LedgerEntry.debit_ledger_id = $ledger_id," .
" SUM(LedgerEntry.amount), NULL) AS debit");
$fields[] = ("IF(LedgerEntry.credit_ledger_id = $ledger_id," .
" SUM(LedgerEntry.amount), NULL) AS credit");
if (isset($account_id) || isset($account_type)) {
$Account = new Account();
$account_ftype = $Account->fundamentalType($account_id ? $account_id : $account_type);
$fields[] = ("(IF(LedgerEntry.{$account_ftype}_ledger_id = $ledger_id," .
" 1, -1) * SUM(LedgerEntry.amount)) AS balance");
}
}
elseif (isset($account_id)) {
$fields[] = ("IF(DebitLedger.account_id = $account_id," .
" SUM(LedgerEntry.amount), NULL) AS debit");
$fields[] = ("IF(CreditLedger.account_id = $account_id," .
" SUM(LedgerEntry.amount), NULL) AS credit");
$Account = new Account();
$account_ftype = ucfirst($Account->fundamentalType($account_id));
$fields[] = ("(IF({$account_ftype}Ledger.account_id = $account_id," .
" 1, -1) * SUM(LedgerEntry.amount)) AS balance");
}
if ($sum)
$fields[] = "COUNT({$entry_name}.id) AS entries";
return $fields;
}
function ledgerContextConditions($ledger_id, $account_type) {
if (isset($ledger_id)) {
return array
('OR' =>
array(array('LedgerEntry.debit_ledger_id' => $ledger_id),
array('LedgerEntry.credit_ledger_id' => $ledger_id)),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: verifyLedgerEntry
* - Verifies consistenty of new ledger entry data
* (not in a pre-existing ledger entry)
*/
function verifyLedgerEntry($entry, $tender = null) {
/* pr(array("LedgerEntry::verifyLedgerEntry()" */
/* => compact('entry', 'tender'))); */
if (empty($entry['account_id']) ||
empty($entry['crdr']) ||
empty($entry['amount'])
) {
/* pr(array("LedgerEntry::verifyLedgerEntry()" */
/* => "Entry verification failed")); */
return false;
}
return array();
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findInLedgerContext
* - Returns an array of ledger entries that belong to a given ledger.
* There is extra logic to also figure out whether the ledger_entry
* amount is either a credit, or a debit, depending on how it was
* written into the ledger, as well as whether the amount increases or
* decreases the balance depending on the particular account type of
* the ledger.
*/
function findInLedgerContext($ledger_id, $account_type, $cond = null, $link = null) {
if (!isset($link))
$link = array('Transaction');
if (!isset($cond))
$cond = array();
$fields = $this->ledgerContextFields($ledger_id, $account_type);
$cond[] = $this->ledgerContextConditions($ledger_id, $account_type);
$order = array('Transaction.stamp');
$entries = $this->find
('all',
array('link' => $link,
'fields' => $fields,
'conditions' => $cond,
'order' => $order,
));
return $entries;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findReconciledLedgerEntries
* - Returns ledger entries that are reconciled to the given entry.
* (such as payments towards a charge).
*/
function findReconciledLedgerEntries($id = null, $fundamental_type = null) {
foreach (($fundamental_type
? array($fundamental_type)
: array('debit', 'credit')) AS $fund) {
$ucfund = ucfirst($fund);
$reconciled[$fund]['entry'] = $this->find
('all', array
('link' => array
("ReconciliationLedgerEntry" => array
('class' => "{$ucfund}ReconciliationLedgerEntry",
'fields' => array
('id',
"COALESCE(SUM(Reconciliation.amount),0) AS 'reconciled'",
"LedgerEntry.amount - COALESCE(SUM(Reconciliation.amount),0) AS 'balance'",
),
),
),
'group' => ("ReconciliationLedgerEntry.id"),
'conditions' => array('LedgerEntry.id' => $id),
'fields' => array(),
));
//pr($reconciled);
$balance = 0;
foreach ($reconciled[$fund]['entry'] AS &$entry) {
$entry = array_merge($entry["ReconciliationLedgerEntry"], $entry[0]);
$balance += $entry['balance'];
}
$reconciled[$fund]['balance'] = $balance;
}
return $reconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reverse
* - Reverses the ledger entry
*/
function reverse1($id, $amount = null, $transaction_id = null, $rec_id = null) {
/* pr(array('LedgerEntry::reverse', */
/* compact('id', 'amount', 'transaction_id', 'rec_id'))); */
// Get the LedgerEntry and related fields
$entry = $this->find
('first',
array('contain' => array('MonetarySource.id',
'Transaction.id',
'DebitLedger.id',
'DebitLedger.account_id',
'CreditLedger.id',
'CreditLedger.account_id',
'DebitReconciliationLedgerEntry'
/* => */
/* array('DebitLedger.id', */
/* 'DebitLedger.account_id', */
/* 'CreditLedger.id', */
/* 'CreditLedger.account_id', */
/* ) */
,
'CreditReconciliationLedgerEntry'
/* => */
/* array('DebitLedger.id', */
/* 'DebitLedger.account_id', */
/* 'CreditLedger.id', */
/* 'CreditLedger.account_id', */
/* ) */
,
'Customer.id',
'Lease.id',
),
'fields' => array('LedgerEntry.*'),
'conditions' => array(array('LedgerEntry.id' => $id),
/* array('NOT' => */
/* array('OR' => */
/* array(array('DebitReconciliationLedgerEntry.id' => $rec_id), */
/* array('CreditReconciliationLedgerEntry.id' => $rec_id), */
/* ), */
/* ), */
/* ), */
),
));
//pr($entry);
if (!isset($amount))
$amount = $entry['LedgerEntry']['amount'];
$A = new Account();
$ids = $this->Ledger->Account->postLedgerEntry
(array('transaction_id' => $transaction_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($entry['CreditLedger']['account_id']),
'credit_ledger_id' => $A->currentLedgerID($entry['DebitLedger']['account_id']),
'effective_date' => $entry['LedgerEntry']['effective_date'],
//'effective_date' => $entry['LedgerEntry']['effective_date'],
'amount' => $amount,
'lease_id' => $entry['Lease']['id'],
'customer_id' => $entry['Customer']['id'],
'comment' => "Reversal of Ledger Entry #{$id}",
),
array('debit' => array(array('LedgerEntry' => array('id' => $entry['LedgerEntry']['id'],
'amount' => $amount,
))),
'credit' => array(array('LedgerEntry' => array('id' => $entry['LedgerEntry']['id'],
'amount' => $amount,
))),
));
if ($ids['error'])
return null;
$tid = $ids['transaction_id'];
pr(compact('entry'));
foreach (array('Debit', 'Credit') AS $dc_type) {
foreach ($entry[$dc_type . 'ReconciliationLedgerEntry'] AS $RLE) {
pr(array('checkpoint' => "Reverse $dc_type LE",
compact('id', 'rec_id', 'RLE')));
if ($RLE['id'] == $rec_id) {
pr(array('checkpoint' => "Skipping Reverse $dc_type LE, due to rec_id",
compact('id', 'RLE')));
continue;
}
if (!$this->reverse($RLE['id'], $RLE['Reconciliation']['amount'], $tid, $id))
$ids['error'] = true;
/* $rids = $this->Ledger->Account->postLedgerEntry */
/* (array('transaction_id' => $tid), */
/* null, */
/* array('debit_ledger_id' => $A->currentLedgerID($RLE['CreditLedger']['account_id']), */
/* 'credit_ledger_id' => $A->currentLedgerID($RLE['DebitLedger']['account_id']), */
/* 'effective_date' => $RLE['effective_date'], */
/* //'effective_date' => $RLE['effective_date'], */
/* 'amount' => $RLE['Reconciliation']['amount'], */
/* 'lease_id' => $entry['Lease']['id'], */
/* 'customer_id' => $entry['Customer']['id'], */
/* 'comment' => "Reversal of Ledger Entry #{$RLE['id']}", */
/* ), */
/* array('debit' => array(array('LedgerEntry' => array('id' => $RLE['id'], */
/* 'amount' => $RLE['Reconciliation']['amount'], */
/* ))), */
/* 'credit' => array(array('LedgerEntry' => array('id' => $RLE['id'], */
/* 'amount' => $RLE['Reconciliation']['amount'], */
/* ))), */
/* )); */
/* if ($rids['error']) */
/* $ids['error'] = true; */
}
}
if ($ids['error'])
return null;
return $ids['id'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reverse
* - Reverses the charges
*
* SAMPLE MOVE IN w/ PRE PAYMENT
* DEPOSIT RENT A/R RECEIPT CHECK PETTY BANK
* ------- ------- ------- ------- ------- ------- -------
* |25 | 25| | | | |
* | |20 20| | | | |
* | |20 20| | | | |
* | |20 20| | | | |
* | | |25 25| | | |
* | | |20 20| | | |
* | | |20 20| | | |
* | | |20 20| | | |
* | | | |85 85| | |
* | | | | |85 | 85|
* MOVE OUT and REFUND FINAL MONTH
* DEPOSIT RENT C/P RECEIPT CHECK PETTY BANK
* ------- ------- ------- ------- ------- ------- -------
* 25| | |25 | | | | t20 e20a
* | 20| |20 | | | | t20 e20b
* -ONE REFUND CHECK-
* | | 25| |25 | | | t30 e30a
* | | 20| |20 | | | t30 e30b
* | | | 45| | | |45 t40 e40
* -OR MULTIPLE-
* | | 15| |15 | | | t50a e50a
* | | | 15| | |15 | t60a e60a
* | | 30| |30 | | | t50b e50b
* | | | 30| | | |30 t60b e60b
* | | | | | | |
OPTION 1
* |-25 | -25| | | | |
* | |-20 -20| | | | |
* | | |-25 -25| | | |
* | | |-20 -20| | | |
OPTION 2
* |-25 | | -25| | | |
* | |-20 | -20| | | |
* | | | |-15 | -15| |
* | | | |-30 | | -30|
* | | | | | | |
*
*/
function reverse($ledger_entries, $stamp = null) {
pr(array('LedgerEntry::reverse',
compact('ledger_entries', 'stamp')));
// If the user only wants to reverse one ID, we'll allow it
if (!is_array($ledger_entries))
$ledger_entries = $this->find
('all', array
('contain' => false,
'conditions' => array('LedgerEntry.id' => $ledger_entries)));
$A = new Account();
$ar_account_id = $A->accountReceivableAccountID();
$receipt_account_id = $A->receiptAccountID();
$transaction_id = null;
foreach ($ledger_entries AS $entry) {
$entry = $entry['LedgerEntry'];
$amount = -1*$entry['amount'];
if (isset($entry['credit_account_id']))
$refund_account_id = $entry['credit_account_id'];
elseif (isset($entry['CreditLedger']['Account']['id']))
$refund_account_id = $entry['CreditLedger']['Account']['id'];
elseif (isset($entry['credit_ledger_id']))
$refund_account_id = $this->Ledger->accountID($entry['credit_ledger_id']);
else
return null;
// post new refund in the income account
$ids = $A->postLedgerEntry
(array('transaction_id' => $transaction_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($ar_account_id),
'credit_ledger_id' => $A->currentLedgerID($refund_account_id),
'effective_date' => $entry['effective_date'],
'through_date' => $entry['through_date'],
'amount' => $amount,
'lease_id' => $entry['lease_id'],
'customer_id' => $entry['customer_id'],
'comment' => "Refund; Entry #{$entry['id']}",
),
array('debit' => array
(array('LedgerEntry' =>
array('id' => $entry['id'],
'amount' => $amount))),
)
);
if ($ids['error'])
return null;
$transaction_id = $ids['transaction_id'];
pr(array('checkpoint' => 'Posted Refund Ledger Entry',
compact('ids', 'amount', 'refund_account_id', 'ar_account_id')));
if (isset($tender) && !$this->Tender->verifyTender($tender)) {
/* pr(array("LedgerEntry::verifyLedgerEntry()" */
/* => "Tender verification failed")); */
return false;
}
return true;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addLedgerEntry
* - Inserts new Ledger Entry into the database
*/
function addLedgerEntry($entry, $tender = null) {
//$this->prFunctionLevel(16);
$this->prEnter(compact('entry', 'tender'));
$ret = array('data' => $entry);
if (!$this->verifyLedgerEntry($entry, $tender))
return $this->prReturn(array('error' => true) + $ret);
if (empty($entry['ledger_id']))
$entry['ledger_id'] =
$this->Account->currentLedgerID($entry['account_id']);
$this->create();
if (!$this->save($entry))
return $this->prReturn(array('error' => true) + $ret);
$ret['ledger_entry_id'] = $this->id;
if (isset($tender)) {
$tender['account_id'] = $entry['account_id'];
$tender['ledger_entry_id'] = $ret['ledger_entry_id'];
$result = $this->Tender->addTender($tender);
$ret['Tender'] = $result;
if ($result['error'])
return $this->prReturn(array('error' => true) + $ret);
}
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns summary data from the requested ledger entry
*/
function stats($id) {
function stats($id = null, $query = null, $set = null) {
$this->queryInit($query);
$query = array
(
'fields' => array("SUM(Reconciliation.amount) AS 'reconciled'"),
'conditions' => array(isset($cond) ? $cond : array(),
array('LedgerEntry.id' => $id)),
'group' => 'LedgerEntry.id',
);
// Get the applied amounts on the debit side
$query['link'] =
array('DebitReconciliationLedgerEntry' => array('alias' => 'DRLE', 'DRLETransaction' => array('class' => 'Transaction')));
$tmpstats = $this->find('first', $query);
$stats['debit_amount_reconciled'] = $tmpstats[0]['reconciled'];
// Get the applied amounts on the credit side
$query['link'] =
array('CreditReconciliationLedgerEntry' => array('alias' => 'CRLE', 'CRLETransaction' => array('class' => 'Transaction')));
$tmpstats = $this->find('first', $query);
$stats['credit_amount_reconciled'] = $tmpstats[0]['reconciled'];
return $stats;
// REVISIT <AP>: 20090816
// This function appeared to be dramatically broken,
// a throwback to an earlier time. I deleted its
// contents and added this error to ensure it does
// not get used.
$this->INTERNAL_ERROR('This function should not be used');
}
}

View File

@@ -1,15 +0,0 @@
<?php
class MapsUnit extends AppModel {
var $name = 'MapsUnit';
var $validate = array(
'id' => array('numeric'),
'map_id' => array('numeric'),
'unit_id' => array('numeric'),
'pt_top' => array('numeric'),
'pt_left' => array('numeric'),
'transpose' => array('boolean')
);
}
?>

37
models/membership.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
class Membership extends AppModel {
var $belongsTo =
array('User',
'Site',
'Group'
);
function memberGroups($user_id, $site_id) {
$this->prEnter(compact('user_id', 'site_id'));
$this->cacheQueries = true;
$groups = $this->find('all', array
('recursive' => -1,
'fields' => array('group_id'),
'conditions' => array(array('user_id' => $user_id),
array('site_id' => $site_id)),
));
$this->cacheQueries = false;
if (empty($groups))
return $this->prReturn(null);
$group_ids = array();
foreach ($groups AS $group)
$group_ids[] = $group['Membership']['group_id'];
return $this->prReturn($group_ids);
}
function memberOf($user_id, $site_id) {
$groups = $this->memberGroups($user_id, $site_id);
return (!empty($groups));
}
}

View File

@@ -1,268 +0,0 @@
<?php
class MonetarySource extends AppModel {
var $name = 'MonetarySource';
var $validate = array(
'id' => array('numeric'),
'name' => array('notempty'),
'tillable' => array('boolean')
);
var $belongsTo = array(
);
var $hasMany = array(
'LedgerEntry',
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: nsf
* - Flags the ledger entry as having insufficient funds
* - NOTE: nsf only works if given the monetary source id
* to transaction e3, below
* - NOTE: In order to show that the rent formerly considered
* "collected" is now recognized in reverse, we must
* credit A/R with a negative amount in order to
* reconcile it against the Rent<->A/R ledger entry.
*
* FEE RENT A/R RECEIPT CHECK NSF BANK
* ------- ------- ------- ------- ------- ------- -------
* | |30 30| | | | | t1 e1a : R e2/e7a :
* | |20 20| | | | | t1 e1b : R e2/e7b :
* | | | | | | |
* | | |30 30| | | | t2 e2a : R e3 : R e1a
* | | |20 20| | | | t2 e2b : R e3 : R e1b
* | | | |50 50| | | t2 e3 : R e4 : R e2
* | | | | | | |
* | | | | |50 | 50| t3 e4 : : R e3
* | | | | | | |
* | | | | | |-50 -50| t4 e5 : : R e6
* | | | |-50 | -50| | t5 e6 : R e5 : R e7a/e7b
* | | |-30 -30| | | | t6 e7a : R e6 : R e1a
* | | |-20 -20| | | | t6 e7b : R e6 : R e1b
* |35 | 35| | | | | t6 e8
*
*/
function nsf($id, $stamp = null) {
pr(array('MonetarySource::nsf',
compact('id')));
$A = new Account();
// Get the LedgerEntries that use this monetary source
$source = $this->find
('first',
array('contain' =>
array(/* e3 */
'LedgerEntry' =>
array('Transaction.id',
'MonetarySource.id',
'Customer.id',
'Lease.id',
/* e3 debit */
'DebitLedger' => /* e.g. CHECK Ledger */
array('fields' => array(),
'Account' => /* e.g. CHECK Account */
array('fields' => array('id', 'name'),
'conditions' =>
array('Account.payable' => 1,
'Account.type' => 'ASSET'),
),
),
/* e3 credit */
'CreditLedger' => /* i.e. RECEIPT Ledger */
array('fields' => array('id'),
'Account' => /* i.e. RECEIPT Account */
array('fields' => array('id', 'name'),
'conditions' =>
array('Account.id' => $A->receiptAccountID()),
),
),
/* e2 */
'DebitReconciliationLedgerEntry' =>
array(/* e2 credit */
'CreditLedger' => /* i.e. A/R Ledger */
array('fields' => array(),
'Account' => /* i.e. A/R Account */
array('fields' => array(),
'conditions' =>
array('Account.id' => $A->accountReceivableAccountID()),
),
),
/* e1 */
// STUPID CakePHP bug screws up CLASS contains CLASS.
// Use the same class, but with different name.
'DebitReconciliationLedgerEntry2',
),
/* e4 */
'CreditReconciliationLedgerEntry' =>
array(/* e4 debit */
'DebitLedger' => /* e.g. BANK Ledger */
array('fields' => array('id'),
'Account' => /* e.g. BANK Account */
array('fields' => array('id', 'name'),
'conditions' =>
array('Account.depositable' => 1),
),
),
),
),
),
'conditions' => array(array('MonetarySource.id' => $id)),
));
pr($source);
$nsf_account_id = $A->nsfAccountID();
$nsf_fee_account_id = $A->nsfChargeAccountID();
$ar_account_id = $A->accountReceivableAccountID();
$receipt_account_id = $A->receiptAccountID();
$t4_id = null;
$t5_id = null;
foreach ($source['LedgerEntry'] AS $e3) {
// We expect only a single e4 entry
$e4 = $e3['CreditReconciliationLedgerEntry'];
if (count($e4) < 1)
continue;
if (count($e4) > 1)
die('Too many e4 entries');
// Pullup e4 from the single member array
$e4 = $e4[0];
// e3 amount
$amount = -$e3['amount'];
// e4 account
$bank_account_id = $e4['DebitLedger']['account_id'];
// post new e5
$e5_ids = $A->postLedgerEntry
(array('transaction_id' => $t4_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($bank_account_id),
'credit_ledger_id' => $A->currentLedgerID($nsf_account_id),
'effective_date' => $stamp,
'amount' => $amount,
'lease_id' => $e3['lease_id'],
'customer_id' => $e3['customer_id'],
'comment' => "NSF Bank Reversal; Monetary Source #{$id}",
)
);
if ($e5_ids['error'])
return null;
$t4_id = $e5_ids['transaction_id'];
pr(array('checkpoint' => 'Posted Ledger Entry e5',
compact('e5_ids', 'amount')));
// post new e6... this will be our crossover point
// from typical positive entries to negative entries.
// Therefore, no reconciliation on this account.
$e6_ids = $A->postLedgerEntry
(array('transaction_id' => $t5_id),
array('monetary_source_id' => $e3['monetary_source_id']),
array('debit_ledger_id' => $A->currentLedgerID($nsf_account_id),
'credit_ledger_id' => $A->currentLedgerID($receipt_account_id),
'effective_date' => $stamp,
'amount' => $amount,
'lease_id' => $e3['lease_id'],
'customer_id' => $e3['customer_id'],
'comment' => "NSF tracker; Monetary Source #{$id}",
),
array('debit' => array
(array('LedgerEntry' =>
array('id' => $e5_ids['id'],
'amount' => $amount))),
)
);
if ($e6_ids['error'])
return null;
$t5_id = $e6_ids['transaction_id'];
pr(array('checkpoint' => 'Posted Ledger Entry e6',
compact('e6_ids', 'amount')));
$t6_id = null;
foreach ($e3['DebitReconciliationLedgerEntry'] AS $e2) {
foreach ($e2['DebitReconciliationLedgerEntry2'] AS $e1) {
$amount = -1*$e1['Reconciliation']['amount'];
// post new e7
$e7_ids = $A->postLedgerEntry
(array('transaction_id' => $t6_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($receipt_account_id),
'credit_ledger_id' => $A->currentLedgerID($ar_account_id),
'effective_date' => $stamp,
'amount' => $amount,
'lease_id' => $e1['lease_id'],
'customer_id' => $e1['customer_id'],
'comment' => "NSF Receipt; Monetary Source #{$id}",
),
array('debit' => array
(array('LedgerEntry' =>
array('id' => $e6_ids['id'],
'amount' => $amount))),
'credit' => array
(array('LedgerEntry' =>
array('id' => $e1['id'],
'amount' => $amount))),
)
);
if ($e7_ids['error'])
return null;
$t6_id = $e7_ids['transaction_id'];
pr(array('checkpoint' => 'Posted Ledger Entry e7',
compact('e7_ids', 'amount')));
}
}
}
// Cheat for now
$customer_id = $source['LedgerEntry'][0]['customer_id'];
$lease_id = null;
// post new e8
$e8_ids = $A->postLedgerEntry
(array('transaction_id' => $t6_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($ar_account_id),
'credit_ledger_id' => $A->currentLedgerID($nsf_fee_account_id),
'effective_date' => $stamp,
'amount' => 35,
'lease_id' => $lease_id,
'customer_id' => $customer_id,
'comment' => "NSF Fee; Monetary Source #{$id}",
)
);
if ($e8_ids['error'])
return null;
pr(array('checkpoint' => 'Posted Ledger Entry e8',
compact('e8_ids')));
return true;
}
}
?>

76
models/option.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
class Option extends AppModel {
var $hasMany =
array('OptionValue',
);
var $knows =
array('User', 'Site', 'Group');
static $option_set = array();
function getAll($name, $force = false) {
/* $this->prClassLevel(30); */
/* //$this->OptionValue->prClassLevel(30); */
/* $this->Group->Membership->prClassLevel(30); */
/* $this->OptionValue->SiteOption->prClassLevel(30); */
/* $this->OptionValue->UserOption->prClassLevel(30); */
/* $this->OptionValue->GroupOption->prClassLevel(30); */
/* $this->OptionValue->DefaultOption->prClassLevel(30); */
$this->prEnter(compact('name'));
if (!empty(self::$option_set[$name]) && !$force)
return $this->prReturn(self::$option_set[$name]);
self::$option_set[$name] = array();
$site_id = $this->Site->currentSiteId();
$user_id = $this->User->currentUserId();
$group_ids = $this->Group->currentGroupIds();
/* $site_id = 2; */
/* $user_id = 4; */
/* $group_ids = $this->Group->groupIds($user_id, $site_id); */
if (!empty($site_id))
self::$option_set[$name] =
array_merge(self::$option_set[$name],
$this->OptionValue->SiteOption->values($site_id, $name));
if (!empty($user_id))
self::$option_set[$name] =
array_merge(self::$option_set[$name],
$this->OptionValue->UserOption->values($user_id, $name));
if (!empty($group_ids))
self::$option_set[$name] =
array_merge(self::$option_set[$name],
$this->OptionValue->GroupOption->values($group_ids, $name));
self::$option_set[$name] =
array_merge(self::$option_set[$name],
$this->OptionValue->DefaultOption->values($name));
return $this->prReturn(self::$option_set[$name]);
}
function get($name) {
$this->prEnter(compact('name'));
$values = $this->getAll($name);
if (empty($values))
return null;
return $this->prReturn($values[0]);
}
function enabled($name) {
$val = $this->get($name);
return (!empty($val));
}
function disabled($name) {
return (!$this->enabled($name));
}
}

35
models/option_value.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
class OptionValue extends AppModel {
var $belongsTo =
array('Option',
);
var $hasMany =
array('UserOption',
'SiteOption',
'GroupOption',
'DefaultOption',
);
function values($name = null, $query = null) {
$this->prEnter(compact('name', 'query'));
$this->queryInit($query);
$query['link']['Option'] = array();
if (!empty($name)) {
$query['conditions'][] = array('Option.name' => $name);
$query['link']['Option']['fields'] = array();
}
$this->cacheQueries = true;
$values = array();
foreach ($this->find('all', $query) AS $result)
$values[] = $result['OptionValue']['value'];
$this->cacheQueries = false;
return $this->prReturn($values);
}
}

105
models/permission.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
class Permission extends AppModel {
var $hasMany =
array('PermissionValue',
);
var $knows =
array('User', 'Site', 'Group');
static $permission_set = array();
function getAll($name, $force = false) {
/* $this->prClassLevel(30); */
/* $this->PermissionValue->prClassLevel(30); */
/* $this->Group->Membership->prClassLevel(30); */
/* $this->PermissionValue->SitePermission->prClassLevel(30); */
/* $this->PermissionValue->UserPermission->prClassLevel(30); */
/* $this->PermissionValue->GroupPermission->prClassLevel(30); */
/* $this->PermissionValue->DefaultPermission->prClassLevel(30); */
$this->prEnter(compact('name'));
if (!empty(self::$permission_set[$name]) && !$force)
return $this->prReturn(self::$permission_set[$name]);
self::$permission_set[$name] = array();
$site_id = $this->Site->currentSiteId();
$user_id = $this->User->currentUserId();
$group_ids = $this->Group->currentGroupIds();
/* $site_id = 1; */
/* $user_id = 2; */
/* $group_ids = $this->Group->groupIds($user_id, $site_id); */
if (empty($group_ids)) {
self::$permission_set[$name][$name][] = array('access' => 'DENY', 'level' => null);
$site_id = null;
$user_id = null;
}
if (!empty($site_id))
self::$permission_set[$name] =
array_merge(self::$permission_set[$name],
$this->PermissionValue->SitePermission->values($site_id, $name));
if (!empty($user_id))
self::$permission_set[$name] =
array_merge(self::$permission_set[$name],
$this->PermissionValue->UserPermission->values($user_id, $name));
if (!empty($group_ids)) {
self::$permission_set[$name] =
array_merge(self::$permission_set[$name],
$this->PermissionValue->GroupPermission->values($group_ids, $name));
self::$permission_set[$name] =
array_merge(self::$permission_set[$name],
$this->PermissionValue->DefaultPermission->values($name));
self::$permission_set[$name][] = array('access' => 'ALLOW', 'level' => null);
}
return $this->prReturn(self::$permission_set[$name]);
}
function get($name) {
$this->prEnter(compact('name'));
// REVISIT <AP>: 20090827
// This is a pretty crappy algorithm. How do we decide whether DENY really
// means DENY, or whether an ALLOW has priority.
// Oh well, it works for now...
$values = $this->getAll($name);
$result = array_shift($values);
foreach ($values AS $value)
if (empty($result['level']) || (!empty($value['level']) && $value['level'] < $result['level']))
$result['level'] = $value['level'];
if ($result['access'] !== 'ALLOW')
$result['level'] = 9999999;
return $this->prReturn($result);
}
function allow($name) {
$this->prEnter(compact('name'));
$result = $this->get($name);
return $this->prReturn($result['access'] === 'ALLOW');
}
function deny($name) {
$this->prEnter(compact('name'));
return $this->prReturn(!$this->allow($name));
}
function level($name) {
$this->prEnter(compact('name'));
$result = $this->get($name);
return $this->prReturn($result['level']);
}
}

View File

@@ -0,0 +1,36 @@
<?php
class PermissionValue extends AppModel {
var $belongsTo =
array('Permission',
);
var $hasMany =
array('UserPermission',
'SitePermission',
'GroupPermission',
'DefaultPermission',
);
function values($name = null, $query = null) {
$this->prEnter(compact('name', 'query'));
$this->queryInit($query);
$query['link']['Permission'] = array();
if (!empty($name)) {
$query['conditions'][] = array('Permission.name' => $name);
$query['link']['Permission']['fields'] = array();
}
$this->cacheQueries = true;
$values = array();
foreach ($this->find('all', $query) AS $result)
$values[] = array('access' => $result['PermissionValue']['access'],
'level' => $result['PermissionValue']['level']);
$this->cacheQueries = false;
return $this->prReturn($values);
}
}

View File

@@ -1,15 +0,0 @@
<?php
class Reconciliation extends AppModel {
var $belongsTo = array(
'DebitLedgerEntry' => array(
'className' => 'LedgerEntry',
//'foreignKey' => 'credit_ledger_entry_id',
),
'CreditLedgerEntry' => array(
'className' => 'LedgerEntry',
//'foreignKey' => 'credit_ledger_entry_id',
),
);
}

View File

@@ -1,16 +1,37 @@
<?php
class Site extends AppModel {
var $name = 'Site';
var $validate = array(
'id' => array('numeric'),
'name' => array('notempty')
);
var $hasMany =
array('SiteArea',
'SiteOption',
'Membership',
);
var $hasMany = array(
'SiteArea',
'SiteOption',
);
static $current_site_id;
function currentSiteId() {
if (!empty(self::$current_site_id))
return self::$current_site_id;
// REVISIT <AP>: 20090827
// Must get the actual site
$code = 'VSS';
$site = $this->find
('first',
array('recursive' => -1,
'conditions' => compact('code')));
if (!empty($site['Site']['id']))
self::$current_site_id = $site['Site']['id'];
else
// We must force a stop here, since this is typically
// called very early on, and so will cause a recursive
// crash as we try to render the internal error and
// again stumble on this problem.
$this->INTERNAL_ERROR('UNKNOWN SITE', 0, true);
return self::$current_site_id;
}
}
?>

24
models/site_option.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
class SiteOption extends AppModel {
var $belongsTo =
array('Site',
'OptionValue',
);
function values($id, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['SiteOption'] = array();
$query['link']['SiteOption']['fields'] = array();
$query['link']['SiteOption']['Site'] = array();
$query['link']['SiteOption']['Site']['fields'] = array();
$query['conditions'][] = array('Site.id' => $id);
return $this->prReturn($this->OptionValue->values($name, $query));
}
}

View File

@@ -0,0 +1,24 @@
<?php
class SitePermission extends AppModel {
var $belongsTo =
array('Site',
'PermissionValue',
);
function values($id, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['SitePermission'] = array();
$query['link']['SitePermission']['fields'] = array();
$query['link']['SitePermission']['Site'] = array();
$query['link']['SitePermission']['Site']['fields'] = array();
$query['conditions'][] = array('Site.id' => $id);
return $this->prReturn($this->PermissionValue->values($name, $query));
}
}

784
models/statement_entry.php Normal file
View File

@@ -0,0 +1,784 @@
<?php
class StatementEntry extends AppModel {
var $belongsTo = array(
'Transaction',
'Customer',
'Lease',
'Account',
// The charge to which this disbursement applies (if it is one)
'ChargeEntry' => array(
'className' => 'StatementEntry',
),
);
var $hasMany = array(
// The disbursements that apply to this charge (if it is one)
'DisbursementEntry' => array(
'className' => 'StatementEntry',
'foreignKey' => 'charge_entry_id',
'dependent' => true,
),
);
//var $default_log_level = array('log' => 30, 'show' => 15);
var $max_log_level = 19;
/**************************************************************************
**************************************************************************
**************************************************************************
* function: debit/creditTypes
*/
function debitTypes() {
return array('CHARGE', 'PAYMENT', 'REFUND');
}
function creditTypes() {
return array('DISBURSEMENT', 'WAIVER', 'REVERSAL', 'WRITEOFF', 'SURPLUS');
}
function voidTypes() {
return array('VOID');
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: chargeDisbursementFields
*/
function chargeDisbursementFields($sum = false, $entry_name = 'StatementEntry') {
$debits = $this->debitTypes();
$credits = $this->creditTypes();
$voids = $this->voidTypes();
foreach ($debits AS &$enum)
$enum = "'" . $enum . "'";
foreach ($credits AS &$enum)
$enum = "'" . $enum . "'";
foreach ($voids AS &$enum)
$enum = "'" . $enum . "'";
$debit_set = implode(", ", $debits);
$credit_set = implode(", ", $credits);
$void_set = implode(", ", $voids);
$fields = array
(
($sum ? 'SUM(' : '') .
"IF({$entry_name}.type IN ({$debit_set})," .
" {$entry_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
"IF({$entry_name}.type IN({$credit_set})," .
" {$entry_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS disbursement' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
"IF({$entry_name}.type IN ({$debit_set}), 1," .
" IF({$entry_name}.type IN ({$credit_set}), -1, 0))" .
" * IF({$entry_name}.amount, {$entry_name}.amount, 0)" .
($sum ? ')' : '') . ' AS balance',
);
if ($sum)
$fields[] = "COUNT({$entry_name}.id) AS entries";
return $fields;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: verifyStatementEntry
* - Verifies consistenty of new statement entry data
* (not in a pre-existing statement entry)
*/
function verifyStatementEntry($entry) {
$this->prFunctionLevel(10);
$this->prEnter(compact('entry'));
if (empty($entry['type']) ||
//empty($entry['effective_date']) ||
empty($entry['account_id']) ||
empty($entry['amount'])
) {
return $this->prReturn(false);
}
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addStatementEntry
* - Inserts new Statement Entry into the database
*/
function addStatementEntry($entry) {
$this->prEnter(compact('entry'));
$ret = array('data' => $entry);
if (!$this->verifyStatementEntry($entry))
return $this->prReturn(array('error' => true, 'verify_data' => $entry) + $ret);
$this->create();
if (!$this->save($entry))
return $this->prReturn(array('error' => true, 'save_data' => $entry) + $ret);
$ret['statement_entry_id'] = $this->id;
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: waive
* - Waives the charge balance
*
*/
function waive($id, $stamp = null) {
$this->prEnter(compact('id', 'stamp'));
// Get the basic information about the entry to be waived.
$this->recursive = -1;
$charge = $this->read(null, $id);
$charge = $charge['StatementEntry'];
if ($charge['type'] !== 'CHARGE')
$this->INTERNAL_ERROR("Waiver item is not CHARGE.");
// Query the stats to get the remaining balance
$stats = $this->stats($id);
// Build a transaction
$waiver = array('Transaction' => array(), 'Entry' => array());
$waiver['Transaction']['stamp'] = $stamp;
$waiver['Transaction']['comment'] = "Charge Waiver";
// Add the charge waiver
$waiver['Entry'][] =
array('amount' => $stats['Charge']['balance'],
'comment' => null,
);
// Record the waiver transaction
return $this->prReturn($this->Transaction->addWaiver
($waiver, $id, $charge['customer_id'], $charge['lease_id']));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reversable
* - Returns true if the charge can be reversed; false otherwise
*/
function reversable($id) {
$this->prEnter(compact('id'));
if (empty($id))
return $this->prReturn(false);
// Verify the item is an actual charge
$this->id = $id;
$charge_type = $this->field('type');
if ($charge_type !== 'CHARGE')
return $this->prReturn(false);
// Determine anything reconciled against the charge
$reverse_transaction_id = $this->field('reverse_transaction_id');
if (!empty($reverse_transaction_id))
return $this->prReturn(false);
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reverse
* - Reverses the charges
*/
function reverse($id, $stamp = null, $comment) {
$this->prEnter(compact('id', 'stamp'));
// Verify the item can be reversed
if (!$this->reversable($id))
$this->INTERNAL_ERROR("Item is not reversable.");
// Get the basic information about this charge
$charge = $this->find('first', array('contain' => true));
//$charge = $charge['StatementEntry'];
// Query the stats to get the remaining balance
$stats = $this->stats($id);
$charge['paid'] = $stats['Charge']['disbursement'];
// Record the reversal transaction
$result = $this->Transaction->addReversal
($charge, $stamp, $comment ? $comment : 'Charge Reversal');
if (empty($result['error'])) {
// Mark the charge as reversed
$this->id = $id;
$this->saveField('reverse_transaction_id', $result['transaction_id']);
}
return $this->prReturn($result);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconciledSet
* - Returns the set of entries satisfying the given conditions,
* along with any entries that they reconcile
*/
function reconciledSetQuery($set, $query) {
$this->queryInit($query);
if (in_array($set, $this->debitTypes()))
$query['link']['DisbursementEntry'] = array('fields' => array("SUM(DisbursementEntry.amount) AS reconciled"));
elseif (in_array($set, $this->creditTypes()))
$query['link']['ChargeEntry'] = array('fields' => array("SUM(ChargeEntry.amount) AS reconciled"));
else
die("INVALID RECONCILE SET");
$query['conditions'][] = array('StatementEntry.type' => $set);
$query['group'] = 'StatementEntry.id';
return $query;
}
function reconciledSet($set, $query = null, $unrec = false, $if_rec_include_partial = false) {
//$this->prFunctionLevel(array('log' => 16, 'show' => 10));
$this->prEnter(compact('set', 'query', 'unrec', 'if_rec_include_partial'));
$lquery = $this->reconciledSetQuery($set, $query);
$result = $this->find('all', $lquery);
$this->pr(20, compact('lquery', 'result'));
$resultset = array();
foreach ($result AS $i => $entry) {
$this->pr(25, compact('entry'));
if (!empty($entry[0]))
$entry['StatementEntry'] = $entry[0] + $entry['StatementEntry'];
unset($entry[0]);
$entry['StatementEntry']['balance'] =
$entry['StatementEntry']['amount'] - $entry['StatementEntry']['reconciled'];
// Since HAVING isn't a builtin feature of CakePHP,
// we'll have to post-process to get the desired entries
if ($entry['StatementEntry']['balance'] == 0)
$reconciled = true;
elseif ($entry['StatementEntry']['reconciled'] == 0)
$reconciled = false;
else // Partial disbursement; depends on unrec
$reconciled = (!$unrec && $if_rec_include_partial);
// Add to the set, if it's been requested
if ($reconciled == !$unrec)
$resultset[] = $entry;
}
return $this->prReturn(array('entries' => $resultset,
'summary' => $this->stats(null, $query)));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconciledEntries
* - Returns a list of entries that reconcile against the given entry.
* (such as disbursements towards a charge).
*/
function reconciledEntriesQuery($id, $query = null) {
$this->queryInit($query, false);
$this->id = $id;
$this->recursive = -1;
$this->read();
$query['conditions'][] = array('StatementEntry.id' => $id);
if (in_array($this->data['StatementEntry']['type'], $this->debitTypes())) {
$query['link']['DisbursementEntry'] = array();
$query['conditions'][] = array('DisbursementEntry.id !=' => null);
}
if (in_array($this->data['StatementEntry']['type'], $this->creditTypes())) {
$query['link']['ChargeEntry'] = array();
$query['conditions'][] = array('ChargeEntry.id !=' => null);
}
return $query;
}
function reconciledEntries($id, $query = null) {
$this->prEnter(compact('id', 'query'));
$lquery = $this->reconciledEntriesQuery($id, $query);
$result = $this->find('all', $lquery);
foreach (array_keys($result) AS $i)
unset($result[$i]['StatementEntry']);
return $this->prReturn(array('entries' => $result));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: outstandingDebits
* - Determines all debit types that have not yet been resolved.
* The name is a bit dumb, but it means any statement entry type
* that a positive customer balance could be used to offset. In
* other words, entries that are still in need of matching
* disbursements. Most notably, this means charges but could
* also mean things like refunds as well.
*/
function outstandingDebits($query = null, $customer_id = null,
$lease_id = null, $debit_types = null)
{
$this->prEnter(compact('query', 'lease_id',
'customer_id', 'charge_ids',
'debit_types'));
$this->queryInit($query);
if (empty($debit_types))
$debit_types = $this->debitTypes();
if (!empty($customer_id))
$query['conditions'][] = array('StatementEntry.customer_id' => $customer_id);
if (!empty($lease_id))
$query['conditions'][] = array('StatementEntry.lease_id' => $lease_id);
/* if (isset($charge_ids)) { */
/* // REVISIT <AP> 20100330: */
/* // Not using $query here, as this code was extracted from its */
/* // original location in assignCredits, and so I'm keeping the */
/* // logic consistent. It does seem, however, that we shouldn't */
/* // be ignoring $query if passed in. I'm sure this won't be */
/* // looked at until someone _does_ pass $query in (and it break), */
/* // so hopefully at that time, we can understand what needs to */
/* // happen in that case (requirements are not clear at present). */
/* $lquery = array('contain' => false, */
/* 'conditions' => array('StatementEntry.id' => $charge_ids)); */
/* } else { */
/* $lquery = $query; */
/* // If we're working with a specific lease, then limit charges to it */
/* if (!empty($lease_id)) */
/* $lquery['conditions'][] = array('StatementEntry.lease_id' => $lease_id); */
/* } */
if (empty($query['order']))
$query['order'] = 'StatementEntry.effective_date ASC';
$debits = array();
foreach ($debit_types AS $dtype) {
$rset = $this->reconciledSet($dtype, $query, true);
$entries = $rset['entries'];
$debits = array_merge($debits, $entries);
$this->pr(18, compact('dtype', 'entries'), "Outstanding Debit Entries");
}
return $this->prReturn($debits);
}
function outstandingCharges($query = null, $customer_id = null, $lease_id = null) {
return $this->outstandingDebits($query, $customer_id, $lease_id, array('CHARGE'));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: assignCredits
* - Assigns all credits to existing charges
*
* REVISIT <AP>: 20090726
* This algorithm shouldn't be hardcoded. We need to allow
* the user to specify how disbursements should be applied.
*
*/
function assignCredits($query = null, $receipt_id = null,
$charge_ids = null, $disbursement_type = null,
$customer_id = null, $lease_id = null)
{
//$this->prFunctionLevel(25);
$this->prEnter(compact('query', 'receipt_id',
'charge_ids', 'disbursement_type',
'customer_id', 'lease_id'));
$this->queryInit($query);
if (empty($disbursement_type))
$disbursement_type = 'DISBURSEMENT';
$ret = array();
// First, find all known credits, unless this call is to make
// credit adjustments to a specific charge
if (empty($receipt_id)) {
if (!empty($charge_ids))
$this->INTERNAL_ERROR("Charge IDs, yet no corresponding receipt");
$lquery = $query;
if (!empty($customer_id))
$lquery['conditions'][] = array('StatementEntry.customer_id' => $customer_id);
$lquery['conditions'][] = array('StatementEntry.type' => 'SURPLUS');
// REVISIT <AP>: 20090804
// We need to ensure that we're using surplus credits ONLY from either
// the given lease, or those that do not apply to any specific lease.
// However, by doing this, it forces any lease surplus amounts to
// remain frozen with that lease until either there is a lease charge,
// we refund the money, or we "promote" that surplus to the customer
// level and out of the leases direct control.
// That seems like a pain. Perhaps we should allow any customer
// surplus to be used on any customer charge.
$lquery['conditions'][] =
array('OR' =>
array(array('StatementEntry.lease_id' => null),
(!empty($lease_id)
? array('StatementEntry.lease_id' => $lease_id)
: array()),
));
$lquery['order'][] = 'StatementEntry.effective_date ASC';
$credits = $this->find('all', $lquery);
$this->pr(18, compact('credits'),
"Credits Established");
}
else {
// Establish credit from the (newly added) receipt
$lquery =
array('link' =>
array('StatementEntry',
'LedgerEntry' =>
array('conditions' =>
array('LedgerEntry.account_id <> Transaction.account_id')
),
),
'conditions' => array('Transaction.id' => $receipt_id),
'fields' => array('Transaction.id', 'Transaction.stamp', 'Transaction.amount'),
);
$receipt_credit = $this->Transaction->find('first', $lquery);
if (!$receipt_credit)
$this->INTERNAL_ERROR("Unable to locate receipt.");
$stats = $this->Transaction->stats($receipt_id);
$receipt_credit['balance'] = $stats['undisbursed'];
$receipt_credit['receipt'] = true;
$credits = array($receipt_credit);
$this->pr(18, compact('credits'),
"Receipt Credit Added");
}
// Now find all unpaid charges, using either the specific set
// of charges given, or all outstanding charges based on the
// query, customer and/or lease
if (!empty($charge_ids)) {
$this->INTERNAL_ERROR("PERHAPS IMPLEMENTED - THOUGH NEVER TESTED");
$lquery = $query;
$lquery['conditions'][] = array('StatementEntry.id' => $charge_ids);
$charges = $this->reconciledSet('CHARGE', $query, true);
} else {
$charges = $this->outstandingDebits($query, $customer_id, $lease_id);
}
// Work through all unpaid charges, applying disbursements as we go
foreach ($charges AS $charge) {
$this->pr(20, compact('charge'),
'Process Charge');
$charge['balance'] = $charge['StatementEntry']['balance'];
// Use explicit credits before using the new receipt credit
foreach ($credits AS &$credit) {
if (empty($charge['balance']))
break;
if ($charge['balance'] < 0)
$this->INTERNAL_ERROR("Negative Charge Balance");
if (!isset($credit['balance']))
$credit['balance'] = $credit['StatementEntry']['amount'];
if (empty($credit['balance']))
continue;
if ($credit['balance'] < 0)
$this->INTERNAL_ERROR("Negative Credit Balance");
$this->pr(20, compact('charge'),
'Attempt Charge Reconciliation');
if (empty($credit['receipt']))
$disbursement_account_id = $credit['StatementEntry']['account_id'];
else
$disbursement_account_id = $credit['LedgerEntry']['account_id'];
// REVISIT <AP>: 20090811
// Need to come up with a better strategy for handling
// concessions. For now, just restricting concessions
// to apply only towards rent will resolve the most
// predominant (or only) needed usage case.
if ($disbursement_account_id == $this->Account->concessionAccountID() &&
$charge['StatementEntry']['account_id'] != $this->Account->rentAccountID())
continue;
// Set the disbursement amount to the maximum amount
// possible without exceeding the charge or credit balance
$disbursement_amount = round(min($charge['balance'], $credit['balance']), 2);
if (!isset($credit['applied']))
$credit['applied'] = 0;
$credit['applied'] = round($credit['applied'] + $disbursement_amount, 2);
$credit['balance'] = round($credit['balance'] - $disbursement_amount, 2);
$this->pr(20, compact('credit', 'disbursement_amount'),
($credit['balance'] > 0 ? 'Utilized' : 'Exhausted') .
(empty($credit['receipt']) ? ' Credit' : ' Receipt'));
if (strtotime($charge['StatementEntry']['effective_date']) >
strtotime($credit['StatementEntry']['effective_date']))
$disbursement_edate = $charge['StatementEntry']['effective_date'];
else
$disbursement_edate = $credit['StatementEntry']['effective_date'];
if (empty($credit['receipt'])) {
// Explicit Credit
$result = $this->Transaction->addTransactionEntries
(array('include_ledger_entry' => true,
'include_statement_entry' => true),
array('type' => 'INVOICE',
'id' => $credit['StatementEntry']['transaction_id'],
'account_id' => $this->Account->accountReceivableAccountID(),
'crdr' => 'CREDIT',
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => $charge['StatementEntry']['lease_id'],
),
array
(array('type' => $disbursement_type,
'effective_date' => $disbursement_edate,
'account_id' => $credit['StatementEntry']['account_id'],
'amount' => $disbursement_amount,
'charge_entry_id' => $charge['StatementEntry']['id'],
),
));
$ret['Disbursement'][] = $result;
if ($result['error'])
$ret['error'] = true;
}
else {
// Receipt Credit
if (strtotime($charge['StatementEntry']['effective_date']) >
strtotime($credit['Transaction']['stamp']))
$disbursement_edate = $charge['StatementEntry']['effective_date'];
else
$disbursement_edate = $credit['Transaction']['stamp'];
// Add a disbursement that uses the available credit to pay the charge
$disbursement =
array('type' => $disbursement_type,
'effective_date' => $disbursement_edate,
'amount' => $disbursement_amount,
'account_id' => $credit['LedgerEntry']['account_id'],
'transaction_id' => $credit['Transaction']['id'],
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => $charge['StatementEntry']['lease_id'],
'charge_entry_id' => $charge['StatementEntry']['id'],
'comment' => null,
);
$this->pr(20, compact('disbursement'), 'New Disbursement Entry');
$result = $this->addStatementEntry($disbursement);
$ret['Disbursement'][] = $result;
if ($result['error'])
$ret['error'] = true;
}
// Adjust the charge balance to reflect the new disbursement
$charge['balance'] = round($charge['balance'] - $disbursement_amount, 2);
$this->pr(20, compact('charge', 'disbursement_amount'),
($charge['balance'] > 0 ? 'Unfinished' : 'Fully Paid') . ' Charge');
if ($charge['balance'] < 0)
die("HOW DID WE GET A NEGATIVE CHARGE AMOUNT?");
}
// Break the $credit reference to avoid future problems
unset($credit);
}
$this->pr(18, compact('credits'),
'Disbursements complete');
// Clean up any explicit credits that have been used
foreach ($credits AS $credit) {
if (!empty($credit['receipt']))
continue;
if (empty($credit['applied']))
continue;
if ($credit['balance'] > 0) {
$this->pr(20, compact('credit'),
'Update Credit Entry');
$this->id = $credit['StatementEntry']['id'];
$this->saveField('amount', $credit['balance']);
}
else {
$this->pr(20, compact('credit'),
'Delete Exhausted Credit Entry');
$this->delete($credit['StatementEntry']['id'], false);
}
}
// Check for any implicit receipt credits, converting
// into explicit credits if there is a remaining balance.
foreach ($credits AS $credit) {
if (empty($credit['receipt']))
continue;
if (empty($credit['balance']))
continue;
// See if there is an existing explicit credit
// for this transaction.
$explicit_credit = $this->find
('first', array('contain' => false,
'conditions' =>
array(array('transaction_id' => $credit['Transaction']['id']),
array('type' => 'SURPLUS')),
));
if (!empty($explicit_credit)) {
// REVISIT <AP>: 20090815
// Testing whether or not this case occurs
$this->INTERNAL_ERROR('Existing explicit credit unexpected');
// Since there IS an existing explicit credit, we must update
// its balance instead of creating a new one, since it has
// already been incorporated in the overall credit balance.
// If we were to create a new one, we would erroneously create
// an excess of credit available.
$this->pr(18, compact('explicit_credit', 'credit'),
'Update existing explicit credit');
$EC = new StatementEntry();
$EC->id = $explicit_credit['StatementEntry']['id'];
$EC->saveField('amount', $credit['balance']);
continue;
}
if (!empty($ret['receipt_balance']))
$this->INTERNAL_ERROR('Only one receipt expected in assignCredits');
// Give caller the information necessary to create an explicit
// credit from the passed receipt, which we've not exhausted.
$this->pr(18, compact('credit'), 'Convert to explicit credit');
$ret['receipt_balance'] = $credit['balance'];
}
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns summary data from the requested statement entry
*/
function stats($id = null, $query = null) {
//$this->prFunctionLevel(array('log' => 16, 'show' => 10));
$this->prEnter(compact('id', 'query'));
$this->queryInit($query);
unset($query['group']);
$stats = array();
if (isset($id))
$query['conditions'][] = array('StatementEntry.id' => $id);
$types = array('Charge', 'Disbursement');
foreach ($types AS $type_index => $this_name) {
$that_name = $types[($type_index + 1) % 2];
if ($this_name === 'Charge') {
$this_types = $this->debitTypes();
$that_types = $this->creditTypes();
} else {
$this_types = $this->creditTypes();
$that_types = $this->debitTypes();
}
$this_query = $query;
$this_query['fields'] = array();
$this_query['fields'][] = "SUM(StatementEntry.amount) AS total";
$this_query['conditions'][] = array('StatementEntry.type' => $this_types);
$result = $this->find('first', $this_query);
$stats[$this_name] = $result[0];
$this->pr(17, compact('this_query', 'result'), $this_name.'s');
// Tally the different types that result in credits towards the charges
$stats[$this_name]['reconciled'] = 0;
foreach ($that_types AS $that_type) {
$lc_that_type = strtolower($that_type);
$that_query = $this_query;
$that_query['link']["{$that_name}Entry"] = array('fields' => array());
$that_query['fields'] = array();
if ($this_name == 'Charge')
$that_query['fields'][] = "COALESCE(SUM(${that_name}Entry.amount),0) AS $lc_that_type";
else
$that_query['fields'][] = "COALESCE(SUM(StatementEntry.amount), 0) AS $lc_that_type";
$that_query['conditions'][] = array("{$that_name}Entry.type" => $that_type);
$result = $this->find('first', $that_query);
$stats[$this_name] += $result[0];
$this->pr(17, compact('that_query', 'result'), "{$this_name}s: $that_type");
$stats[$this_name]['reconciled'] += $stats[$this_name][$lc_that_type];
}
// Compute balance information for charges
$stats[$this_name]['balance'] =
$stats[$this_name]['total'] - $stats[$this_name]['reconciled'];
if (!isset($stats[$this_name]['balance']))
$stats[$this_name]['balance'] = 0;
}
// 'balance' is simply the difference between
// the balances of charges and disbursements
$stats['balance'] = $stats['Charge']['balance'] - $stats['Disbursement']['balance'];
if (!isset($stats['balance']))
$stats['balance'] = 0;
// 'account_balance' is really only relevant to
// callers that have requested charge and disbursement
// stats with respect to a particular account.
// It represents the difference between inflow
// and outflow from that account.
$stats['account_balance'] = $stats['Charge']['reconciled'] - $stats['Disbursement']['total'];
if (!isset($stats['account_balance']))
$stats['account_balance'] = 0;
return $this->prReturn($stats);
}
}

166
models/tender.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
class Tender extends AppModel {
var $belongsTo = array(
'TenderType',
'Customer',
'LedgerEntry',
'DepositTransaction' => array(
'className' => 'Transaction',
),
'DepositLedgerEntry' => array(
'className' => 'LedgerEntry',
),
'NsfTransaction' => array(
'className' => 'Transaction',
'dependent' => true,
),
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: afterSave
* - Performs any work needed after the save occurs
*/
function afterSave($created) {
// Come up with a (not necessarily unique) name for the tender.
// For checks & money orders, this will be based on the check
// number. For other types of tender, we'll just use the
// generic name of the tender type, and the tender ID
// Determine our tender type, and set the ID of that model
$this->TenderType->id = $this->field('tender_type_id');
// REVISIT <AP>: 20090810
// The only tender expected to have no tender type
// is our special "Closing" tender.
if (empty($this->TenderType->id))
$newname = 'Closing';
else {
$newname = $this->TenderType->field('name');
$naming_field = $this->TenderType->field('naming_field');
if (!empty($naming_field))
$newname .= ' #' . $this->field($naming_field);
}
if ($newname !== $this->field('name'))
$this->saveField('name', $newname);
return parent::afterSave($created);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: beforeDelete
* - Performs any work needed before the delete occurs
*/
function beforeDelete($cascade = true) {
// REVISIT <AP>: 20090814
// Experimental, and incomplete mechanism to protect
// against trying to delete data that shouldn't be deleted.
$deposit_id = $this->field('deposit_transaction_id');
pr(compact('deposit_id'));
// If this tender has already been deposited, it would
// be a rats nest to figure out how to delete this tender.
if (!empty($deposit_id))
return false;
return parent::beforeDelete($cascade);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: verifyTender
* - Verifies consistenty of new tender data
* (not in a pre-existing tender)
*/
function verifyTender($tender) {
$this->prFunctionLevel(10);
$this->prEnter(compact('tender'));
if (empty($tender['tender_type_id'])) {
return $this->prReturn(false);
}
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addTender
* - Inserts new Tender into the database
*/
function addTender($tender) {
$this->prEnter(compact('tender'));
$ret = array('data' => $tender);
if (!$this->verifyTender($tender))
return $this->prReturn(array('error' => true) + $ret);
$this->create();
if (!$this->save($tender))
return $this->prReturn(array('error' => true) + $ret);
$ret['tender_id'] = $this->id;
return $this->prReturn($ret + array('error' => false));
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: nsf
* - Flags the ledger entry as having insufficient funds
*/
function nsf($id, $stamp = null, $comment = null) {
$this->prEnter(compact('id', 'stamp', 'comment'));
// Get information about this NSF item.
$this->id = $id;
$tender = $this->find
('first', array
('contain' =>
array('LedgerEntry',
'DepositTransaction',
'DepositLedgerEntry',
'NsfTransaction'),
));
$this->pr(20, compact('tender'));
if (!empty($tender['NsfTransaction']['id']))
die("Item has already been set as NSF");
if (empty($tender['DepositTransaction']['id']))
die("Item has not been deposited yet");
$tender['Transaction'] = $tender['DepositTransaction'];
unset($tender['DepositTransaction']);
unset($tender['NsfTransaction']);
$T = new Transaction();
$result = $T->addNsf($tender, $stamp, $comment);
if (empty($result['error'])) {
// Flag the tender as NSF, using the items created from addNsf
$this->id = $id;
$this->saveField('nsf_transaction_id', $result['nsf_transaction_id']);
$this->saveField('nsf_ledger_entry_id', $result['nsf_ledger_entry_id']);
}
return $this->prReturn($result);
}
}
?>

115
models/tender_type.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
class TenderType extends AppModel {
var $belongsTo = array(
'Account',
);
var $hasMany = array(
'Tender',
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: accountID
* - Returns the intended account ID for receipt of the given tender
*/
function accountID($id) {
$this->cacheQueries = true;
$item = $this->find('first', array
('contain' => false,
'conditions' => array('TenderType.id' => $id),
));
$this->cacheQueries = false;
return $item['TenderType']['account_id'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: paymentTypes
* - Returns an array of types that can be used for payments
*/
function paymentTypes($query = null) {
$this->queryInit($query);
$query['order'][] = 'name';
$this->cacheQueries = true;
$types = $this->find('all', $query);
$this->cacheQueries = false;
return $types;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: paymentTypes
* - Returns an array of types that can be deposited
*/
function depositTypes($query = null) {
$this->queryInit($query);
$query['order'][] = 'name';
$query['conditions'][] = array('tillable' => true);
$this->cacheQueries = true;
$types = $this->find('all', $query);
$this->cacheQueries = false;
// Rearrange to be of the form (id => name)
$result = array();
foreach ($types AS $type)
$result[$type['TenderType']['id']] = $type['TenderType']['name'];
return $result;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: defaultPaymentType
* - Returns the ID of the default payment type
*/
function defaultPaymentType() {
return $this->nameToID('Check');
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns the stats for the given tender type
*/
function stats($id = null, $query = null) {
if (!$id)
return null;
$this->queryInit($query);
if (!isset($query['link']['Tender']))
$query['link']['Tender'] = array('fields' => array());
if (!isset($query['link']['Tender']['LedgerEntry']))
$query['link']['Tender']['LedgerEntry'] = array('fields' => array());
$query['fields'][] = "SUM(COALESCE(LedgerEntry.amount,0)) AS 'total'";
$query['fields'][] = "SUM(IF(deposit_transaction_id IS NULL, COALESCE(LedgerEntry.amount,0), 0)) AS 'undeposited'";
$query['fields'][] = "SUM(IF(deposit_transaction_id IS NULL, 0, COALESCE(LedgerEntry.amount,0))) AS 'deposited'";
$query['fields'][] = "SUM(IF(nsf_transaction_id IS NULL, 0, COALESCE(LedgerEntry.amount,0))) AS 'nsf'";
$this->id = $id;
$stats = $this->find('first', $query);
return $stats[0];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,8 @@ class Unit extends AppModel {
'Lease',
);
//var $default_log_level = array('log' => 30, 'show' => 15);
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -50,13 +52,70 @@ class Unit extends AppModel {
}
function occupiedEnumValue() {
return statusValue('OCCUPIED');
return $this->statusValue('OCCUPIED');
}
function statusCheck($id_or_enum,
$min = null, $min_strict = false,
$max = null, $max_strict = false)
{
$this->prEnter(compact('id_or_enum', 'min', 'min_strict', 'max', 'max_strict'));
if (is_int($id_or_enum)) {
$this->id = $id_or_enum;
$id_or_enum = $this->field('status');
}
$enum_val = $this->statusValue($id_or_enum);
if (isset($min) && is_string($min))
$min = $this->statusValue($min);
if (isset($max) && is_string($max))
$max = $this->statusValue($max);
$this->pr(17, compact('enum_val', 'min', 'min_strict', 'max', 'max_strict'));
if (isset($min) &&
($enum_val < $min ||
($min_strict && $enum_val == $min)))
return $this->prReturn(false);
if (isset($max) &&
($enum_val > $max ||
($max_strict && $enum_val == $max)))
return $this->prReturn(false);
return $this->prReturn(true);
}
function locked($enum) {
return $this->statusCheck($enum, 'LOCKED', false, null, false);
}
function conditionLocked() {
//return array('Unit.status' => 'LOCKED');
return ('Unit.status >= ' . $this->statusValue('LOCKED'));
}
function liened($enum) {
return $this->statusCheck($enum, 'LIENED', false, null, false);
}
function conditionLiened() {
return ('Unit.status >= ' . $this->statusValue('LIENED'));
}
function occupied($enum) {
return $this->statusCheck($enum, 'OCCUPIED', false, null, false);
}
function conditionOccupied() {
return ('Unit.status >= ' . $this->statusValue('OCCUPIED'));
}
function vacant($enum) {
return $this->statusCheck($enum, 'UNAVAILABLE', true, 'OCCUPIED', true);
}
function conditionVacant() {
return ('Unit.status BETWEEN ' .
($this->statusValue('UNAVAILABLE')+1) .
@@ -64,22 +123,92 @@ class Unit extends AppModel {
($this->statusValue('OCCUPIED')-1));
}
function unavailable($enum) {
return $this->statusCheck($enum, null, false, 'UNAVAILABLE', false);
}
function conditionUnavailable() {
return ('Unit.status <= ' . $this->statusValue('UNAVAILABLE'));
}
function available($enum) { return $this->vacant($enum); }
function conditionAvailable() { return $this->conditionVacant($enum); }
/**************************************************************************
**************************************************************************
**************************************************************************
* function: allowedStatusSet
* - Returns the status set allowed for the given unit
*/
function allowedStatusSet($id) {
$this->prEnter(compact('id'));
$this->id = $id;
$old_status = $this->field('status');
$old_val = $this->statusValue($old_status);
$this->pr(17, compact('old_status', 'old_val'));
$enums = $this->activeStatusEnums();
$this->pr(21, compact('enums'));
foreach ($enums AS $enum => $val) {
if (($old_val < $this->occupiedEnumValue()) !=
($val < $this->occupiedEnumValue())) {
unset($enums[$enum]);
}
}
return $this->prReturn($enums);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: updateStatus
* - Update the given unit to the given status
*/
function updateStatus($id, $status) {
function updateStatus($id, $status, $check = false) {
$this->prEnter(compact('id', 'status', 'check'));
/* if ($check) { */
/* $old_status = $this->field('status'); */
/* $this->pr(17, compact('old_status')); */
/* if ($this->statusValue($old_status) < $this->occupiedEnumValue() && */
/* $this->statusValue($status) >= $this->occupiedEnumValue()) */
/* { */
/* die("Can't transition a unit from vacant to occupied"); */
/* return $this->prReturn(false); */
/* } */
/* if ($this->statusValue($old_status) >= $this->occupiedEnumValue() && */
/* $this->statusValue($status) < $this->occupiedEnumValue()) */
/* { */
/* die("Can't transition a unit from occupied to vacant"); */
/* return $this->prReturn(false); */
/* } */
/* } */
if ($check) {
if (!array_key_exists($status, $this->allowedStatusSet($id)))
return $this->prReturn(false);
}
$this->id = $id;
//pr(compact('id', 'status'));
$this->saveField('status', $status);
return $this->prReturn(true);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: update
* - Update any cached or calculated fields
*/
function update($id) {
}
/**************************************************************************
**************************************************************************
**************************************************************************

View File

@@ -1,25 +1,64 @@
<?php
class UnitSize extends AppModel {
var $name = 'UnitSize';
var $validate = array(
'id' => array('numeric'),
'unit_type_id' => array('numeric'),
'code' => array('notempty'),
'name' => array('notempty'),
'width' => array('numeric'),
'depth' => array('numeric'),
'deposit' => array('money'),
'amount' => array('money')
);
var $belongsTo =
array(
'UnitType',
);
var $belongsTo = array(
'UnitType',
);
var $hasMany =
array(
'Unit',
);
var $hasMany = array(
'Unit',
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns summary data from the requested unit size.
*/
function stats($id = null) {
$this->prEnter(compact('id'));
// Right now, we only work with id not null
if (!$id)
return null;
$stats = array();
// Get the total number of units this size
$stats['all'] =
$this->find('count',
array('link' => array('Unit'),
'conditions' => array(array('UnitSize.id' => $id)),
));
// Get numbers for units in the various states
foreach (array('unavailable', 'vacant', 'occupied', 'locked', 'liened') AS $status) {
$statusfunc = 'condition' . ucfirst($status);
$stats[$status] =
$this->find('count',
array('link' => array('Unit'),
'conditions' => array(array('UnitSize.id' => $id),
$this->Unit->{$statusfunc}()),
));
}
// Count up each unit by physical status
foreach
($this->find('all',
array('link' => array('Unit' => array('fields' => array())),
'fields' => array('Unit.status', 'COUNT(Unit.id) AS total'),
'conditions' => array(array('UnitSize.id' => $id)),
'group' => 'Unit.status',
)) AS $status) {
$stats['status'][$status['Unit']['status']] = $status[0]['total'];
}
// Return the collection
return $this->prReturn($stats);
}
}
?>

View File

@@ -1,16 +1,50 @@
<?php
class UnitType extends AppModel {
var $name = 'UnitType';
var $validate = array(
'id' => array('numeric'),
'code' => array('notempty'),
'name' => array('notempty')
);
var $hasMany =
array(
'UnitSize',
);
var $hasMany = array(
'UnitSize',
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: relatedTypes
* - Returns an array of types related by similar attributes
*/
function relatedTypes($attribute, $extra = null) {
$this->cacheQueries = true;
$types = $this->find('all', array
('fields' => array('UnitType.id', 'UnitType.name'),
'conditions' => array('UnitType.'.$attribute => true),
'order' => array('UnitType.name'),
) + (isset($extra) ? $extra : array())
);
$this->cacheQueries = false;
// Rearrange to be of the form (id => name)
$rel_types = array();
foreach ($types AS $type) {
$rel_types[$type['UnitType']['id']] = $type['UnitType']['name'];
}
return $rel_types;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: xxxTypes
* - Returns an array of types suitable for activity xxx
*/
function residentialTypes() { return $this->relatedTypes('residential'); }
function enclosedTypes() { return $this->relatedTypes('enclosed'); }
function climateTypes() { return $this->relatedTypes('climate'); }
function outdoorTypes() { return $this->relatedTypes('outdoor'); }
function coveredTypes() { return $this->relatedTypes('covered'); }
}
?>

39
models/user.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
class User extends AppModel {
var $hasMany =
array('UserOption',
'Membership',
);
static $current_user_id;
function currentUser() {
if (!empty($_SERVER['REMOTE_USER']))
return $_SERVER['REMOTE_USER'];
return null;
}
function currentUserId() {
if (!empty(self::$current_user_id))
return self::$current_user_id;
$user = $this->find
('first',
array('recursive' => -1,
'conditions' => array('login' => $this->currentUser())));
if (!empty($user['User']['id']))
self::$current_user_id = $user['User']['id'];
else
// We must force a stop here, since this is typically
// called very early on, and so will cause a recursive
// crash as we try to render the internal error and
// again stumble on this problem.
$this->INTERNAL_ERROR('UNKNOWN USER', 0, true);
return self::$current_user_id;
}
}

24
models/user_option.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
class UserOption extends AppModel {
var $belongsTo =
array('User',
'OptionValue',
);
function values($id, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['UserOption'] = array();
$query['link']['UserOption']['fields'] = array();
$query['link']['UserOption']['User'] = array();
$query['link']['UserOption']['User']['fields'] = array();
$query['conditions'][] = array('User.id' => $id);
return $this->prReturn($this->OptionValue->values($name, $query));
}
}

View File

@@ -0,0 +1,24 @@
<?php
class UserPermission extends AppModel {
var $belongsTo =
array('User',
'PermissionValue',
);
function values($id, $name = null) {
$this->prEnter(compact('id', 'name'));
$query = array();
$this->queryInit($query);
$query['link']['UserPermission'] = array();
$query['link']['UserPermission']['fields'] = array();
$query['link']['UserPermission']['User'] = array();
$query['link']['UserPermission']['User']['fields'] = array();
$query['conditions'][] = array('User.id' => $id);
return $this->prReturn($this->PermissionValue->values($name, $query));
}
}

View File

@@ -1,2 +0,0 @@
1243395559
a:36:{i:0;s:12:"pmgr_actions";i:1;s:27:"pmgr_actions_late_schedules";i:2;s:17:"pmgr_charge_types";i:3;s:19:"pmgr_config_options";i:4;s:18:"pmgr_config_system";i:5;s:20:"pmgr_config_versions";i:6;s:22:"pmgr_contact_addresses";i:7;s:19:"pmgr_contact_emails";i:8;s:20:"pmgr_contact_methods";i:9;s:19:"pmgr_contact_phones";i:10;s:13:"pmgr_contacts";i:11;s:18:"pmgr_group_options";i:12;s:22:"pmgr_group_permissions";i:13;s:11:"pmgr_groups";i:14;s:19:"pmgr_late_schedules";i:15;s:19:"pmgr_lease_contacts";i:16;s:16:"pmgr_lease_types";i:17;s:11:"pmgr_leases";i:18;s:14:"pmgr_map_units";i:19;s:9:"pmgr_maps";i:20;s:10:"pmgr_notes";i:21;s:18:"pmgr_payment_types";i:22;s:15:"pmgr_site_areas";i:23;s:21:"pmgr_site_memberships";i:24;s:17:"pmgr_site_options";i:25;s:10:"pmgr_sites";i:26;s:24:"pmgr_transaction_charges";i:27;s:25:"pmgr_transaction_payments";i:28;s:25:"pmgr_transaction_receipts";i:29;s:32:"pmgr_transaction_reconciliations";i:30;s:15:"pmgr_unit_sizes";i:31;s:15:"pmgr_unit_types";i:32;s:10:"pmgr_units";i:33;s:17:"pmgr_user_options";i:34;s:10:"pmgr_users";i:35;s:5:"posts";}

View File

@@ -15,7 +15,8 @@ echo '<div class="account collected">' . "\n";
// Reset the form
function resetForm() {
/* updateEntriesGrid(); */
// Kick off the grid
updateEntriesGrid();
}
function onGridLoadComplete() {
@@ -24,26 +25,30 @@ function onGridLoadComplete() {
}
function updateEntriesGrid() {
var cust = new Array();
var account_ids = new Array();
$("INPUT[type='checkbox']:checked").each(function(i) {
account_ids.push($(this).val());
});
cust['collected_account_id'] = <?php echo $account['id']; ?>;
cust['collected_from_date'] = $('#TxFromDate').val();
cust['collected_through_date'] = $('#TxThroughDate').val();
cust['collected_payment_accounts'] = account_ids;
var cust = new Array();
cust['from_date'] = $('#TxFromDate').val();
cust['through_date'] = $('#TxThroughDate').val();
cust['account_id'] = account_ids;
var dynamic_post = new Array();
dynamic_post['custom'] = cust;
$('#collected-total').html('Calculating...');
$('#collected-entries-jqGrid').clearGridData();
$('#collected-entries-jqGrid').setPostDataItem('custom', serialize(cust));
$('#collected-entries-jqGrid').setPostDataItem('dynamic_post_replace', serialize(dynamic_post));
$('#collected-entries-jqGrid')
.setGridParam({ page: 1 })
.trigger("reloadGrid");
//$('#collected-entries .HeaderButton').click();
//$('#debug').html("<PRE>\n"+htmlEncode(dump($('#collected-entries-jqGrid').getGridParam()))+"\n</PRE>")
var gridstate = $('#collected-entries-jqGrid').getGridParam('gridstate');
if (gridstate == 'hidden')
$('#collected-entries .HeaderButton').click();
}
@@ -153,19 +158,19 @@ echo '<div CLASS="detail supporting">' . "\n";
* Entries
*/
echo $this->element('ledger_entries', array
(// Element configuration
'collected_account_id' => $account['id'],
// Grid configuration
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
(
'grid_div_id' => 'collected-entries',
'grid_div_class' => 'text-below',
'grid_events' => array('loadComplete' => 'onGridLoadComplete()'),
//'grid_setup' => array('hiddengrid' => true),
//'caption' => '<SPAN id="receipt-charges-caption"></SPAN>',
'caption' => 'Collected ' . Inflector::pluralize($account['name'])
'grid_setup' => array('hiddengrid' => true),
'caption' => 'Collected ' . Inflector::pluralize($account['name']),
'action' => 'collected',
'filter' => array('ChargeEntry.account_id' => $account['id']),
'include' => array('Amount'),
'exclude' => array(/*'Type',*/ 'Debit', 'Credit'),
),
));
@@ -173,20 +178,8 @@ echo $this->element('ledger_entries', array
<script type="text/javascript"><!--
$(document).ready(function(){
$("#TxFromDate")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
$("#TxThroughDate")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
datepicker('TxFromDate');
datepicker('TxThroughDate');
resetForm();
});
--></script>
@@ -200,5 +193,3 @@ echo '</div>' . "\n";
echo '</div>' . "\n";
?>
<a href="#" onClick="$('#debug').html(''); return false;">Clear Debug Output</a>

View File

@@ -1,74 +0,0 @@
<?php /* -*- mode:PHP -*- */
echo '<div class="account deposit">' . "\n";
echo '<H2>Perform Bank Deposit</H2>' . "\n";
echo '<P>Make sure to select the checkboxes below for only those types of currency (Cash, Check, etc) which you intend to actually deposit (you can see all the individual items by dropping down the list below the checkbox). Then, select the Deposit Account where you will make the deposit, and click "Perform Deposit" to close the books on the selected currency types and reset them to a zero balance. On the next page, you will be provided with a deposit slip to prepare the actual deposit.' . "\n";
echo '<P><BR>' . "\n";
//pr(compact('tillableAccount', 'depositableAccount'));
echo $form->create(null, array('id' => 'deposit-form',
'url' => array('controller' => 'accounts',
'action' => 'deposit')));
foreach ($tillableAccount AS $acct) {
//$acct = $acct['Account'];
echo "\n";
echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.checked',
array(//'label' => $acct['Account']['name'],
'type' => 'checkbox',
'checked' => true,
'value' => true,
'label' => (" I have exactly " .
FormatHelper::currency($acct['Account']['stats']['Ledger']['balance']) .
" in " . ($acct['Account']['name'] === 'Cash'
? 'Cash'
: Inflector::pluralize($acct['Account']['name'])) .
" and will be depositing it all.")
));
echo "\n";
echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.amount',
array('type' => 'hidden',
'value' => $acct['Account']['stats']['Ledger']['balance'],
));
echo "\n";
echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.account_id',
array('type' => 'hidden',
'value' => $acct['Account']['id'],
));
echo "\n";
echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.account_name',
array('type' => 'hidden',
'value' => $acct['Account']['name'],
));
echo "\n";
$grid_div_id = 'ledger_entries'.$acct['CurrentLedger']['id'].'-list';
echo $this->element('ledger_entries', array
(// Element configuration
'ledger_id' => $acct['CurrentLedger']['id'],
'no_account' => true,
// Grid configuration
'config' => array
(
'grid_div_id' => $grid_div_id,
'caption' => ('<A HREF="#" ONCLICK="$(\'#'.$grid_div_id.' .HeaderButton\').click();'.
' return false;">Items in '.$acct['Account']['name'].' Ledger</A>'),
'grid_setup' => array('hiddengrid' => true),
),
));
}
$options = array();
foreach ($depositableAccount AS $acct) {
$options[$acct['Account']['id']] = $acct['Account']['name'];
}
echo $form->input('Deposit.Account.id', array('label' => 'Deposit Account ',
'options' => $options));
echo $form->end('Perform Deposit');
/* End page div */
echo '</div>' . "\n";

View File

@@ -1,53 +0,0 @@
<?php /* -*- mode:PHP -*- */
echo '<H2>Deposit Slip: ' . date('l, F jS, Y, g:ia') . '</H2>' . "\n";
//pr(compact('deposit'));
// Handle account summaries
$rows = array();
$row_class = array();
foreach ($deposit['ledgers'] AS $ledger) {
$row_class[] = array();
$rows[] = array($ledger['name'].':',
FormatHelper::_n(count($ledger['entries']), 'Item'),
FormatHelper::currency($ledger['total'], true));
}
$row_class[] = 'grand';
$rows[] = array('Deposit Total:',
null,
FormatHelper::currency($deposit['total'], true));
echo $this->element('table',
array('class' => 'deposit-summary',
'rows' => $rows,
'row_class' => $row_class,
'column_class' => array('account', 'quantity', 'total'),
'suppress_alternate_rows' => true,
));
// Print out the items of each ledger
foreach ($deposit['ledgers'] AS $ledger) {
//echo ('Count: ' . count($ledger['entries']) . '<BR>');
//pr($ledger['entries']);
if (count($ledger['entries']) == 0)
continue;
$rows = array();
foreach ($ledger['entries'] AS $entry) {
$rows[] = array($entry['Customer']['name'],
$entry['MonetarySource']['name'],
$entry['LedgerEntry']['amount']);
}
echo $this->element('table',
array('class' => 'item deposit-slip list',
'caption' => $ledger['name'] . ' Items',
'rows' => $rows,
'headers' => array('Customer', 'Item', 'Amount'),
'column_class' => array('customer', 'item', 'amount'),
));
}
/* End page div */
//echo '</div>' . "\n";

View File

@@ -9,12 +9,18 @@ echo '<div class="account view">' . "\n";
* Account Detail Main Section
*/
$rows = array(array('ID', $account['Account']['id']),
array('Name', $account['Account']['name']),
array('Type', $account['Account']['type']),
array('External Name', $account['Account']['external_name']),
array('External Account', $account['Account']['external_account']),
array('Comment', $account['Account']['comment']));
$ledger = $account['Ledger'];
$current_ledger = $account['CurrentLedger'];
if (isset($account['Account']))
$account = $account['Account'];
$rows = array();
$rows[] = array('Name', $account['name']);
$rows[] = array('Type', $account['type']);
$rows[] = array('External Name', $account['external_name']);
$rows[] = array('External Account', $account['external_account']);
$rows[] = array('Comment', $account['comment']);
echo $this->element('table',
array('class' => 'item account detail',
@@ -56,9 +62,10 @@ echo '<div CLASS="detail supporting">' . "\n";
*/
echo $this->element('ledgers', array
('config' => array
('caption' => $account['Account']['name'] . " Ledgers",
'rows' => $account['Ledger'],
(// Grid configuration
'config' => array
('caption' => $account['name'] . " Ledgers",
'filter' => array('Account.id' => $account['id']),
)));
@@ -67,21 +74,37 @@ echo $this->element('ledgers', array
*/
echo $this->element('ledger_entries', array
(// Element configuration
'ledger_id' => $account['CurrentLedger']['id'],
'account_type' => $account['Account']['type'],
'group_by_tx' => true,
// Grid configuration
(// Grid configuration
'config' => array
('caption' =>
"Current Ledger: (" .
"#{$account['Account']['id']}" .
"-" .
"{$account['CurrentLedger']['sequence']}" .
")",
),
));
('grid_div_id' => 'ledger-ledger-entry-list',
'caption' => "Current Ledger: #{$current_ledger['sequence']}",
'filter' => array('Ledger.id' => $current_ledger['id']),
'exclude' => array('Account', 'Amount', 'Cr/Dr', 'Balance',
empty($account['receipts']) ? 'Tender' : null),
'include' => array('Debit', 'Credit', 'Sub-Total'),
'limit' => 50,
)));
/**********************************************************************
* Entire Account
*/
echo $this->element('ledger_entries', array
(// Grid configuration
'config' => array
('grid_div_id' => 'account-ledger-entry-list',
'grid_setup' => array('hiddengrid' => true),
'caption' => "Entire Ledger",
'filter' => array('Account.id' => $account['id']),
'exclude' => array('Account', 'Amount', 'Cr/Dr', 'Balance',
empty($account['receipts']) ? 'Tender' : null),
'include' => array('Debit', 'Credit', 'Sub-Total'),
'limit' => 50,
)));
/* End "detail supporting" div */
echo '</div>' . "\n";

View File

@@ -45,6 +45,7 @@ function contactMethodDiv($obj, $type, $legend, $values = null) {
' CLASS="'.$type.'-method-%{id}-source" ' . "\n" .
' ID="'.$type.'-method-%{id}-source-'.$stype.'"' . "\n" .
' VALUE="'.$stype.'"' . "\n" .
($stype == 'new' ? ' CHECKED' . "\n" : '') .
' />' . "\n" .
' <LABEL FOR="'.$type.'-method-%{id}-source-'.$stype.'">'.$sname.'</LABEL>' . "\n" .
' ';
@@ -76,21 +77,30 @@ function contactMethodDiv($obj, $type, $legend, $values = null) {
'fields' => array
(
'preference' => array
('opts' => array
('label_attributes' => array('class' => 'required'),
'opts' => array
('options' => $obj->varstore['methodPreferences'],
'selected' => (isset($values) ? $values['ContactsMethod']['preference'] : null),
)),
),
'after' => "Intended purpose for this method of communication.",
),
'type' => array
('opts' => array
('label_attributes' => array('class' => 'required'),
'opts' => array
('options' => $obj->varstore['methodTypes'],
'selected' => (isset($values) ? $values['ContactsMethod']['type'] : null),
)),
),
'after' => "How / Where this communication reaches the contact.",
),
'comment' => array
('opts' => array
('label_attributes' => array('class' => 'optional empty'),
'opts' => array
('value' => (isset($values) ? $values['ContactsMethod']['comment'] : null),
)),
),
'after' => "Optional: Comments on how this form of communication relates to the contact.",
),
))) . "\n" .
@@ -113,16 +123,23 @@ function contactMethodTypeDiv($obj, $type, $stype, $values = null) {
if ($type === 'phone') {
if ($stype === 'existing') {
$fields = array
('id' => array('name' => 'Phone/Ext',
('id' => array('label_attributes' => array('class' => 'required empty'),
'name' => 'Phone/Ext',
'opts' => array('options' => $obj->varstore['contactPhones'])),
);
}
elseif ($stype === 'new') {
$fields = array
('type' => array('opts' => array('options' => $obj->varstore['phoneTypes'])),
'phone' => true,
'ext' => array('name' => "Extension"),
'comment' => true,
('type' => array('label_attributes' => array('class' => 'required'),
'opts' => array('options' => $obj->varstore['phoneTypes']),
'after' => "Physical type of the phone."),
'phone' => array('label_attributes' => array('class' => 'required empty'),
'after' => "Required: Phone number."),
'ext' => array('name' => "Extension",
'label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Extension number."),
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Comments about this phone number."),
);
}
elseif ($stype === 'show') {
@@ -149,12 +166,19 @@ function contactMethodTypeDiv($obj, $type, $stype, $values = null) {
}
elseif ($stype === 'new') {
$fields = array
('address' => true,
'city' => true,
'state' => true,
'postcode' => array('name' => 'Zip Code'),
'country' => true,
'comment' => true,
('address' => array('label_attributes' => array('class' => 'required empty'),
'after' => "Required: First line of mailing address."),
'city' => array('label_attributes' => array('class' => 'required empty'),
'after' => "Required."),
'state' => array('label_attributes' => array('class' => 'required empty'),
'after' => "Required."),
'postcode' => array('name' => 'Zip Code',
'label_attributes' => array('class' => 'required empty'),
'after' => "Required."),
'country' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: USA is presumed."),
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Comments about this mailing address."),
);
}
elseif ($stype === 'show') {
@@ -177,13 +201,16 @@ function contactMethodTypeDiv($obj, $type, $stype, $values = null) {
if ($stype === 'existing') {
$fields = array
('id' => array('name' => 'Email',
'label_attributes' => array('class' => 'required'),
'opts' => array('options' => $obj->varstore['contactEmails'])),
);
}
elseif ($stype === 'new') {
$fields = array
('email' => true,
'comment' => true,
('email' => array('label_attributes' => array('class' => 'required empty'),
'after' => "Required: E-mail address."),
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Comments about this email address."),
);
}
elseif ($stype === 'show') {
@@ -204,7 +231,7 @@ function contactMethodTypeDiv($obj, $type, $stype, $values = null) {
'<div ' . "\n" .
' class="'.$type.'-%{id}-div"' . "\n" .
' id="'.$type.'-%{id}-'.$stype.'-div"' . "\n" .
(isset($values) ? '' : ' STYLE="display:none;"' . "\n") .
((isset($values) || $stype == 'new') ? '' : ' STYLE="display:none;"' . "\n") .
'>' . "\n" .
$obj->element
@@ -319,8 +346,27 @@ function contactMethodTypeDiv($obj, $type, $stype, $values = null) {
.slideDown();
}
function setEmpty(input_elem) {
selector = "label[for=" + $(input_elem).attr("id") + "]";
if ($(input_elem).val() == '')
$(selector).addClass('empty');
else
$(selector).removeClass('empty');
}
$(document).ready(function(){
resetForm();
// In case refresh is hit with populated fields
$(":input").each(function(i,elem){ setEmpty(elem); });
// keyup doesn't catch cut from menu
$(":input").live('keyup', function(){
setEmpty(this);
});
$(":input").live('mouseup', function(){
setEmpty(this);
});
});
--></script>
@@ -345,17 +391,30 @@ echo($this->element
array('class' => 'item contact detail',
'caption' => isset($this->data['Contact']) ? 'Edit Contact' : 'New Contact',
'fields' => array
('first_name' => true,
'last_name' => true,
'middle_name' => true,
'display_name' => true,
'company_name' => array('name' => 'Company'),
'id_federal' => array('name' => 'SSN'),
'id_local' => array('name' => 'ID #'),
'id_local_state' => array('name' => 'ID State'),
('last_name' => array('label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended."),
'first_name' => array('label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended."),
'middle_name' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional."),
'company_name' => array('name' => 'Company',
'label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Company name, if corporate contact."),
'display_name' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional with first/last name; Required otherwise."),
'id_federal' => array('name' => 'SSN',
'label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Social Security Number."),
'id_local' => array('name' => 'ID #',
'label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended: Driver's license, for example."),
'id_local_state' => array('name' => 'ID State',
'label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended: State which issued the ID."),
/* 'id_local_exp' => array('name' => 'ID Expiration', */
/* 'opts' => array('empty' => true)), */
'comment' => true,
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Comments about this contact."),
))) . "\n");
echo $form->submit('Update') . "\n";

View File

@@ -9,16 +9,25 @@ echo '<div class="contact view">' . "\n";
* Contact Detail Main Section
*/
$rows = array(array('First Name', $contact['Contact']['first_name']),
array('Middle Name', $contact['Contact']['middle_name']),
array('Last Name', $contact['Contact']['last_name']),
array('Company', $contact['Contact']['company_name']),
array('SSN', $contact['Contact']['id_federal']),
array('ID', ($contact['Contact']['id_local']
. ($contact['Contact']['id_local']
? " - ".$contact['Contact']['id_local_state']
: ""))),
array('Comment', $contact['Contact']['comment']));
$phones = $contact['ContactPhone'];
$addresses = $contact['ContactAddress'];
$emails = $contact['ContactEmail'];
if (isset($contact['Contact']))
$contact = $contact['Contact'];
$rows = array();
$rows[] = array('Display Name', $contact['display_name']);
$rows[] = array('First Name', $contact['first_name']);
$rows[] = array('Middle Name', $contact['middle_name']);
$rows[] = array('Last Name', $contact['last_name']);
$rows[] = array('Company', $contact['company_name']);
$rows[] = array('SSN', $contact['id_federal']);
$rows[] = array('ID', ($contact['id_local']
. ($contact['id_local']
? " - ".$contact['id_local_state']
: "")));
$rows[] = array('Comment', $contact['comment']);
echo $this->element('table',
array('class' => 'item contact detail',
@@ -57,7 +66,7 @@ echo '<div CLASS="detail supporting">' . "\n";
*/
$headers = array('Phone', 'Preference', 'Comment');
$rows = array();
foreach($contact['ContactPhone'] AS $phone) {
foreach($phones AS $phone) {
$rows[] = array(FormatHelper::phone($phone['phone']) .
($phone['ext'] ? " x".$phone['ext'] : ""),
$phone['ContactsMethod']['preference'] . " / " .
@@ -79,7 +88,7 @@ echo $this->element('table',
*/
$headers = array('Address', 'Preference', 'Comment');
$rows = array();
foreach($contact['ContactAddress'] AS $address) {
foreach($addresses AS $address) {
$rows[] = array(preg_replace("/\n/", "<BR>\n", $address['address']) . "<BR>\n" .
$address['city'] . ", " .
$address['state'] . " " .
@@ -103,7 +112,7 @@ echo $this->element('table',
*/
$headers = array('Email', 'Preference', 'Comment');
$rows = array();
foreach($contact['ContactEmail'] AS $email) {
foreach($emails AS $email) {
$rows[] = array($email['email'],
$email['ContactsMethod']['preference'] . " / " .
$email['ContactsMethod']['type'],
@@ -123,9 +132,11 @@ echo $this->element('table',
*/
echo $this->element('customers', array
('config' => array
(// Grid configuration
'config' => array
('caption' => 'Related Customers',
'rows' => $contact['Customer'],
'filter' => array('Contact.id' => $contact['id']),
'include' => array('Relationship'),
)));

View File

@@ -42,11 +42,12 @@ function customerContactDiv($obj, $values = null, $primary = false) {
' CLASS="contact-%{id}-source" ' . "\n" .
' ID="contact-%{id}-source-'.$stype.'"' . "\n" .
' VALUE="'.$stype.'"' . "\n" .
//' CHECKED' . "\n" .
($stype == 'new' ? ' CHECKED' . "\n" : '') .
' />' . "\n" .
' <LABEL FOR="contact-%{id}-source-'.$stype.'">'.$sname.'</LABEL>' . "\n" .
' ';
}
$div .= "<P>(Phone numbers / Addresses can be added later)";
}
$div .= "\n";
@@ -75,23 +76,35 @@ function customerContactDiv($obj, $values = null, $primary = false) {
(
'Customer.primary_contact_entry' => array
('name' => 'Primary Contact',
'label_attributes' => array('class' => null),
'no_prefix' => true,
'opts' => array
('type' => 'radio',
'options' => array('%{id}' => false),
'value' => ($primary ? '%{id}' : 'bogus-value-to-suppress-hidden-input'),
)),
),
'after' => ("Check this button if this contact will be the primary" .
" contact for this customer (there can be only one primary" .
" contact"),
),
'type' => array
('opts' => array
('label_attributes' => array('class' => 'required'),
'opts' => array
('options' => $obj->varstore['contactTypes'],
'selected' => (isset($values) ? $values['ContactsCustomer']['type'] : null),
)),
),
'after' => "An actual tenant, or just an alternate contact?"
),
'comment' => array
('opts' => array
('label_attributes' => array('class' => 'optional empty'),
'opts' => array
('value' => (isset($values) ? $values['ContactsCustomer']['comment'] : null),
)),
),
'after' => "Optional: Comments on the relationship between this customer and this contact."
),
))) . "\n" .
@@ -115,22 +128,37 @@ function customerContactTypeDiv($obj, $stype, $values = null) {
if ($stype === 'existing') {
$fields = array
('id' => array('name' => 'Contact',
'opts' => array('options' => $obj->varstore['contacts'])),
'label_attributes' => array('class' => 'required empty'),
'opts' => array('options' => $obj->varstore['contacts']),
'after' => "Select the existing contact."),
);
}
elseif ($stype === 'new') {
$fields = array
('first_name' => true,
'last_name' => true,
'middle_name' => true,
'display_name' => true,
'company_name' => array('name' => 'Company'),
'id_federal' => array('name' => 'SSN'),
'id_local' => array('name' => 'ID #'),
'id_local_state' => array('name' => 'ID State'),
('last_name' => array('label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended."),
'first_name' => array('label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended."),
'middle_name' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional."),
'company_name' => array('name' => 'Company',
'label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Company name, if corporate contact."),
'display_name' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional with first/last name; Required otherwise."),
'id_federal' => array('name' => 'SSN',
'label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Social Security Number."),
'id_local' => array('name' => 'ID #',
'label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended: Driver's license, for example."),
'id_local_state' => array('name' => 'ID State',
'label_attributes' => array('class' => 'recommended empty'),
'after' => "Recommended: State which issued the ID."),
/* 'id_local_exp' => array('name' => 'ID Expiration', */
/* 'opts' => array('empty' => true)), */
'comment' => true,
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => "Optional: Comments about this contact."),
);
}
elseif ($stype === 'show') {
@@ -151,14 +179,14 @@ function customerContactTypeDiv($obj, $stype, $values = null) {
'<div ' . "\n" .
' class="contact-%{id}-div"' . "\n" .
' id="contact-%{id}-'.$stype.'-div"' . "\n" .
(isset($values) ? '' : ' STYLE="display:none;"' . "\n") .
((isset($values) || $stype == 'new') ? '' : ' STYLE="display:none;"' . "\n") .
'>' . "\n" .
$obj->element
($element,
array('class' => "item contact {$class}",
'field_prefix' => 'Contact.%{id}')
+ compact('rows', 'fields', 'column_class')) .
+ compact('rows', 'fields', 'row_class', 'column_class')) .
($stype === 'show'
? '<input type="hidden" name="data[Contact][%{id}][id]" value="'.$values['id'].'"/>' . "\n"
@@ -221,8 +249,28 @@ function customerContactTypeDiv($obj, $stype, $values = null) {
.slideDown();
}
function setEmpty(input_elem) {
selector = "label[for=" + $(input_elem).attr("id") + "]";
//$("#debug").append($(input_elem).attr("id") + ": " + $(input_elem).val() + "<BR>");
if ($(input_elem).val() == '')
$(selector).addClass('empty');
else
$(selector).removeClass('empty');
}
$(document).ready(function(){
resetForm();
// In case refresh is hit with populated fields
$(":input").each(function(i,elem){ setEmpty(elem); });
// keyup doesn't catch cut from menu
$(":input").live('keyup', function(){
setEmpty(this);
});
$(":input").live('mouseup', function(){
setEmpty(this);
});
});
--></script>
@@ -247,11 +295,15 @@ echo($this->element
array('class' => 'item customer detail',
'caption' => isset($this->data['Customer']) ? 'Edit Customer' : 'New Customer',
'fields' => array
('name' => true,
'comment' => true,
('name' => array('label_attributes' => array('class' => 'optional empty'),
'after' => ("Optional: If this field is left blank, the" .
" customer name will be set to the name of" .
" the primary contact, below.")),
'comment' => array('label_attributes' => array('class' => 'optional empty'),
'after' => 'Optional: Comments about this customer.'),
))) . "\n");
echo $form->submit('Update') . "\n";
echo $form->submit(isset($this->data['Customer']) ? 'Update' : 'Add New Customer') . "\n";
?>
<div CLASS="dynamic-set">
@@ -268,7 +320,12 @@ echo $form->submit('Update') . "\n";
<?php
; // Alignment
echo $form->submit('Update') . "\n";
if (!empty($movein['Unit']['id']))
echo $form->input("movein.Unit.id",
array('type' => 'hidden',
'value' => $movein['Unit']['id'])) . "\n";
echo $form->submit(isset($this->data['Customer']) ? 'Update' : 'Add New Customer') . "\n";
echo $form->submit('Cancel', array('name' => 'cancel')) . "\n";
echo $form->end() . "\n";
echo '</div>' . "\n";

176
views/customers/merge.ctp Normal file
View File

@@ -0,0 +1,176 @@
<?php /* -*- mode:PHP -*- */ ?>
<div class="customer merge">
<?php
; // Editor alignment
/**********************************************************************
**********************************************************************
**********************************************************************
**********************************************************************
* Javascript
*/
// Warnings _really_ screw up javascript
$saved_debug_state = Configure::read('debug');
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
// pre-submit callback
function verifyRequest() {
if (!$("#src-customer-id").val()) {
alert("Must select source customer");
return false;
}
rows = $('#<?php echo "contacts-jqGrid"; ?>').getGridParam('selarrrow');
$('#<?php echo "contact-ids"; ?>').val(serialize(rows));
// return false to prevent the form from being submitted;
// anything other than false will allow submission.
return true;
}
function updateContacts() {
$('#contacts-jqGrid').clearGridData();
var filter = new Array();
filter['ContactsCustomer.customer_id'] = $("#src-customer-id").val();
var dynamic_post = new Array();
dynamic_post['filter'] = filter;
$('#contacts-jqGrid').setPostDataItem('dynamic_post_replace', serialize(dynamic_post));
$('#contacts-jqGrid')
.setGridParam({ page: 1 })
.trigger("reloadGrid");
}
function onRowSelect(grid_id, customer_id) {
//$('#output-debug').append("select: "+grid_id+"; "+customer_id+"<BR>\n");
// Set the item id that will be returned with the form
$("#src-customer-id").val(customer_id);
// Get the item name from the grid
$("#src-customer-name").html($(grid_id).getCell(customer_id, "Customer-name"));
updateContacts();
$("#customers-list .HeaderButton").click();
}
function onGridState(grid_id, state) {
//$('#output-debug').append("state: "+grid_id+"; "+state+"<BR>\n");
if (state == 'visible') {
$(".customer-selection-invalid").hide();
$(".customer-selection-valid").hide();
}
else {
if ($("#src-customer-id").val() > 0) {
$(".customer-selection-invalid").hide();
$(".customer-selection-valid").show();
} else {
$(".customer-selection-invalid").show();
$(".customer-selection-valid").hide();
}
}
}
--></script>
<?php
; // align
// Re-Enable warnings
Configure::write('debug', $saved_debug_state);
echo $form->create(null, array('id' => 'customer-merge-form',
'onsubmit' => 'return verifyRequest();',
'url' => array('controller' => 'customers',
'action' => 'mergeFinal')))."\n";
echo '<input type="hidden" id="src-customer-id" name="src-id" value="0" />'."\n";
echo '<input type="hidden" id="dst-customer-id" name="dst-id" value="'.$dst_id.'" />'."\n";
echo $this->element('customers', array
('config' => array
('grid_div_id' => 'customers-list',
'grid_div_class' => 'text-below',
'caption' => ('<A HREF="#" ONCLICK="$(\'#customers-list .HeaderButton\').click();'.
' return false;">Select Customer</A>'),
'grid_events' => array('onSelectRow' =>
array('ids' =>
'if (ids != null){onRowSelect("#"+$(this).attr("id"), ids);}'),
'onHeaderClick' =>
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), gridstate)'),
),
'filter' => array('Customer.id !=' => $dst_id),
//'nolinks' => true,
'limit' => 10,
)));
echo ('<DIV CLASS="customer-merge grid-selection-text">' .
'<DIV CLASS="customer-selection-valid" style="display:none">' .
'Destination Customer: <SPAN id="src-customer-name"></SPAN>' .
'</DIV>' .
'<DIV CLASS="customer-selection-invalid" style="display:none">' .
'Please select customer to merge into' .
'</DIV>' .
'</DIV>' . "\n");
echo $this->element('contacts', array
(// Grid configuration
'config' => array
(
'grid_div_id' => 'contacts',
'grid_setup' => array('multiselect' => true),
'caption' => 'Source contacts to keep',
'filter' => array('ContactsCustomer.customer_id' => 0),
'include' => array('Relationship', 'License', 'Comment'),
),
));
// Add a hidden item to hold the jqGrid selection,
// which we'll populate prior to form submission.
echo "\n";
echo '<input type="hidden" id="contact-ids" name="contact-ids" value="" />'."\n";
?>
<H3>WARNING!</H3>
The above selected customer is about to be deleted, and all related data (leases, transactions, etc) will be merged into customer #<?php echo $dst_id ?>: <?php echo $dst_name ?>. This process is NOT reversible, so please ensure the selections are correct before proceeding.<BR>
<?php
echo $form->end('Merge Customers') . "\n";
?>
<div id="output-debug" style="display:none"></div>
<?php
// Warnings _really_ screw up javascript
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
$(document).ready(function(){
$("#src-customer-id").val(0);
$("#src-customer-name").html("INTERNAL ERROR");
//onGridState(null, 'visible');
<?php if ($this->params['dev']): ?>
$('#output-debug').show();
<?php endif; ?>
});
--></script>
</div>

View File

@@ -11,6 +11,9 @@
* Javascript
*/
// Warnings _really_ screw up javascript
$saved_debug_state = Configure::read('debug');
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
@@ -23,60 +26,94 @@
success: showResponse, // post-submit callback
// other available options:
//url: url, // override for form's 'action' attribute
//type: 'get', // 'get' or 'post', override for form's 'method' attribute
//dataType: null, // 'xml', 'script', or 'json' (expected server response type)
//clearForm: true, // clear all form fields after successful submit
//resetForm: true, // reset the form after successful submit
// $.ajax options can be used here too, for example:
//timeout: 3000,
url: "<?php echo $html->url(array('controller' => 'transactions',
'action' => 'postReceipt', 0)); ?>"
};
// bind form using 'ajaxForm'
$('#receipt-form').ajaxForm(options);
if ($('#receipt-form').ajaxForm != null)
$('#receipt-form').ajaxForm(options);
else
$('#repeat, label[for=repeat]').remove();
});
// pre-submit callback
function verifyRequest(formData, jqForm, options) {
// formData is an array; here we use $.param to convert it to a string to display it
// but the form plugin does this for you automatically when it submits the data
//var_dump(formData);
//$('#request-debug').html('<PRE>'+dump(formData)+'</PRE>');
$('#request-debug').html('Ommitted');
//$("#debug").html('');
for (var i = 0; i < formData.length; ++i) {
//$("#debug").append(i + ') ' + dump(formData[i]) + '<BR>');
if (formData[i]['name'] == "data[Customer][id]" &&
!(formData[i]['value'] > 0)) {
//$("#debug").append('<P>Missing Customer ID');
alert("Please select a customer first.");
return false;
}
if (formData[i]['name'] == "data[Transaction][stamp]" &&
formData[i]['value'] == '') {
//$("#debug").append('<P>Bad Stamp');
if (formData[i]['value'] != '')
alert(formData[i]['value'] + " is not valid date stamp. Please correct it.");
else
alert("Please enter a valid date stamp first.");
return false;
}
// Terrible way to accomplish this...
for (var j = 0; j < 20; ++j) {
if (formData[i]['name'] == "data[Entry]["+j+"][amount]") {
var val = formData[i]['value'].replace(/\$/,'');
//$("#debug").append('<P>Bad Amount');
if (!(val > 0)) {
if (formData[i]['value'] == '')
alert("Please enter an amount first.");
else
alert('"'+formData[i]['value']+'"' + " is not valid amount. Please correct it.");
return false;
}
}
}
}
//$("#debug").append('OK');
//return false;
$('#response-debug').html('Loading <BLINK>...</BLINK>');
$('#output-debug').html('Loading <BLINK>...</BLINK>');
// here we could return false to prevent the form from being submitted;
// returning anything other than false will allow the form submit to continue
$('#results').html('Working <BLINK>...</BLINK>');
return true;
}
// post-submit callback
function showResponse(responseText, statusText) {
// for normal html responses, the first argument to the success callback
// is the XMLHttpRequest object's responseText property
// if the ajaxForm method was passed an Options Object with the dataType
// property set to 'xml' then the first argument to the success callback
// is the XMLHttpRequest object's responseXML property
// if the ajaxForm method was passed an Options Object with the dataType
// property set to 'json' then the first argument to the success callback
// is the json data object returned by the server
if (statusText == 'success') {
var amount = 0;
$("input.payment.amount").each(function(i) {
amount += $(this).val();
});
$('#results').html('<H3>Receipt Saved<BR>' +
$("#receipt-customer-name").html() +
' : ' + fmtCurrency(amount) +
'</H3>');
if (!$("#repeat").attr("checked")) {
window.location.href =
"<?php echo $html->url(array('controller' => 'customers',
'action' => 'view')); ?>"
+ "/" + $("#customer-id").val();
return;
}
// get a clean slate
//resetForm();
resetForm();
}
else {
alert('not successful??');
$('#results').html('<H2>Failed to save receipt!</H2>');
alert('Failed to save receipt.');
}
$('#response-debug').html('<PRE>'+dump(statusText)+'</PRE>');
}
// Reset the form
@@ -84,33 +121,43 @@ function resetForm() {
$('#payment-entry-id').val(1);
$('#payments').html('');
$("#receipt-customer-id").html("INTERNAL ERROR");
$("#receipt-customer-name").html("INTERNAL ERROR");
$("#receipt-balance").html("INTERNAL ERROR");
$("#receipt-charges-caption").html("Outstanding Charges");
addPaymentSource(false);
datepickerNow('TransactionStamp');
updateCharges($("#customer-id").val());
}
function updateCharges(id) {
$('#charge-entries-jqGrid').clearGridData();
$("#receipt-balance").html("Calculating...");
$("#receipt-charges-caption").html("Please Wait...");
var filter = new Array();
filter['StatementEntry.customer_id'] = id;
var dynamic_post = new Array();
dynamic_post['filter'] = filter;
$('#charge-entries-jqGrid').setPostDataItem('dynamic_post_replace', serialize(dynamic_post));
$('#charge-entries-jqGrid')
.setGridParam({ page: 1 })
.trigger("reloadGrid");
var gridstate = $('#charge-entries-jqGrid').getGridParam('gridstate');
if (gridstate == 'hidden')
$('#charge-entries .HeaderButton').click();
}
function onGridLoadComplete() {
var userdata = $('#charge-entries-jqGrid').getGridParam('userData');
$('#receipt-balance').html(fmtCurrency(userdata['balance']));
$("#receipt-charges-caption").html("Outstanding Charges");
}
function onRowSelect(grid_id, customer_id) {
// Set the customer id that will be returned with the form
$("#customer-id").val(customer_id);
// Get the item names from the grid
//$("#receipt-customer-id").html($(grid_id).getCell(customer_id, 'Customer-id'));
// REVISIT <AP>: 20090708
// This is not intended as a long term solution,
// but I need a way to enter data and then view
// the results. This link will help.
$("#receipt-customer-id").html('<A HREF="/pmgr/site/customers/view/' +
$(grid_id).getCell(customer_id, 'Customer-id').replace(/^#/,'') +
'">' +
$(grid_id).getCell(customer_id, 'Customer-id') +
'</A>');
// Set the customer name, so the user knows who the receipt is for
$("#receipt-customer-name").html($(grid_id).getCell(customer_id, 'Customer-name'));
$("#receipt-balance").html("Calculating...");
$("#receipt-charges-caption").html("Please Wait...");
// Hide the "no customer" message and show the current customer
$(".customer-selection-invalid").hide();
@@ -143,12 +190,18 @@ function addPaymentSource(flash) {
addDiv('payment-entry-id', 'payment', 'payments', flash,
// HTML section
'<FIELDSET CLASS="payment subset">' +
'<LEGEND>Payment #%{id} (%{remove})</LEGEND>' +
<?php /* '<LEGEND>Payment #%{id} (%{remove})</LEGEND>' + */ ?>
'<LEGEND>Payment</LEGEND>' +
'<DIV ID="payment-div-%{id}">' +
<?php
// Rearrange to be of the form (id => name)
$radioOptions = array();
foreach ($paymentTypes AS $type)
$radioOptions[$type['TenderType']['id']] = $type['TenderType']['name'];
echo FormatHelper::phpVarToJavascript
($form->input('LedgerEntry.%{id}.account_id',
($form->input('Entry.%{id}.tender_type_id',
array('type' => 'radio',
'separator' => '<BR>',
'onclick' => ('switchPaymentType(' .
@@ -160,73 +213,48 @@ function addPaymentSource(flash) {
''
),
'legend' => false,
'value' => $defaultAccount,
'options' => $paymentAccounts))) . "+\n";
'value' => $defaultType,
'options' => $radioOptions))) . "+\n";
?>
'</DIV>' +
'<DIV ID="payment-amount-div-%{id}" CLASS="input text required">' +
' <LABEL FOR="payment-amount-%{id}">Amount</LABEL>' +
' <INPUT TYPE="text" SIZE="20"' +
' NAME="data[LedgerEntry][%{id}][amount]"' +
' NAME="data[Entry][%{id}][amount]"' +
' CLASS="payment amount"' +
' ID="payment-amount-%{id}" />' +
' <LABEL CLASS="payment" FOR="payment-amount-%{id}">Amount</LABEL>' +
'</DIV>' +
<?php
foreach ($paymentAccounts AS $account_id => $name) {
foreach ($paymentTypes AS $type) {
$type = $type['TenderType'];
$div = '<DIV';
$div .= ' ID="payment-type-div-%{id}-'.$account_id.'"';
$div .= ' ID="payment-type-div-%{id}-'.$type['id'].'"';
$div .= ' CLASS="payment-type-div-%{id}"';
$div .= ' STYLE="display:none;">';
if ($type['id'] != $defaultType)
$div .= ' STYLE="display:none;"';
$div .= '>';
if ($name == 'Check') {
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-check-number-%{id}">Check Number</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="6" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data1]"';
$div .= ' ID="payment-check-number-%{id}" />';
$div .= '</DIV>';
}
elseif ($name == 'Money Order') {
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-moneyorder-number-%{id}">Money Order Number</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="6" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data1]"';
$div .= ' ID="payment-moneyorder-number-%{id}" />';
$div .= '</DIV>';
}
elseif ($name == 'ACH') {
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-ach-routing-%{id}">Routing Number</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="9" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data1]"';
$div .= ' ID="payment-ach-routing-%{id}" />';
$div .= '</DIV>';
$div .= ' <INPUT TYPE="hidden"';
$div .= ' NAME="data[Entry][%{id}][type]['.$type['id'].'][tender_type_id]"';
$div .= ' VALUE="'.$type['id'].'"';
$div .= '>';
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-ach-account-%{id}">Account Number</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="17" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data2]"';
$div .= ' ID="payment-ach-account-%{id}" />';
$div .= '</DIV>';
}
elseif ($name == 'Credit Card') {
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-creditcard-account-%{id}">Account Number</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="16" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data1]"';
$div .= ' ID="payment-creditcard-account-%{id}" />';
$div .= '</DIV>';
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-creditcard-expiration-%{id}">Expiration Date</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="10" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data2]"';
$div .= ' ID="payment-creditcard-expiration-%{id}" />';
$div .= ' </DIV>';
$div .= '<DIV CLASS="input text required">';
$div .= ' <LABEL FOR="payment-creditcard-cvv2-%{id}">CVV2 Code</LABEL>';
$div .= ' <INPUT TYPE="text" SIZE="10" NAME="data[LedgerEntry][%{id}][acct]['.$account_id.'][data3]"';
$div .= ' ID="payment-creditcard-cvv2-%{id}" />';
$div .= '</DIV>';
}
else {
continue;
for ($i=1; $i<=4; ++$i) {
if (!empty($type["data{$i}_name"])) {
$div .= '<DIV CLASS="input text required">';
$div .= ' <INPUT TYPE="text" SIZE="20"';
$div .= ' NAME="data[Entry][%{id}][type]['.$type['id'].'][data'.$i.']"';
$div .= ' CLASS="payment"';
$div .= ' ID="payment-data'.$i.'-%{id}" />';
$div .= ' <LABEL';
$div .= ' CLASS="payment"';
$div .= ' FOR="payment-data'.$i.'-%{id}">';
$div .= $type["data{$i}_name"];
$div .= ' </LABEL>';
$div .= '</DIV>';
}
}
$div .= '</DIV>';
@@ -239,48 +267,20 @@ function addPaymentSource(flash) {
}
function switchPaymentType(paymentid_base, paymentid, radioid) {
$("."+paymentid_base+"-"+paymentid).slideUp();
var account_id = $("#"+radioid).val();
$("#"+paymentid_base+"-"+paymentid+"-"+account_id).slideDown();
}
function updateChargesGrid(idlist) {
$('#charge-entries-jqGrid').setPostDataItem('idlist', serialize(idlist));
$('#charge-entries-jqGrid')
.setGridParam({ page: 1 })
.trigger("reloadGrid");
var type_id = $("#"+radioid).val();
$("."+paymentid_base+"-"+paymentid+
":not(" +
"#"+paymentid_base+"-"+paymentid+"-"+type_id +
")").slideUp();
$("#"+paymentid_base+"-"+paymentid+"-"+type_id).slideDown();
}
function updateCharges(id) {
var url = '<?php echo ($html->url(array("controller" => $this->params["controller"],
"action" => "unreconciled"))); ?>';
url += '/'+id;
$('#charge-entries-jqGrid').clearGridData();
$.ajax({
type: "GET",
url: url,
dataType: "xml",
success: function(xml) {
var ids = new Array();
$('entry',xml).each(function(i){
ids.push($(this).attr('id'));
});
$('#receipt-balance').html(fmtCurrency($('entries',xml).attr('balance')));
$("#receipt-charges-caption").html("Outstanding Charges");
updateChargesGrid(ids);
}
});
}
--></script>
--></script>
<?php
; // align
//echo '<DIV ID="dialog">' . "\n";
// Re-Enable warnings
Configure::write('debug', $saved_debug_state);
echo $this->element('customers', array
('config' => array
@@ -296,6 +296,7 @@ echo $this->element('customers', array
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), gridstate)'),
),
'action' => 'current',
'nolinks' => true,
'limit' => 10,
)));
@@ -303,8 +304,7 @@ echo $this->element('customers', array
echo ('<DIV CLASS="receipt grid-selection-text">' .
'<DIV CLASS="customer-selection-valid" style="display:none">' .
'Customer <SPAN id="receipt-customer-id"></SPAN>' .
': <SPAN id="receipt-customer-name"></SPAN>' .
'Customer: <SPAN id="receipt-customer-name"></SPAN>' .
/* '<DIV CLASS="supporting">' . */
/* '<TABLE>' . */
@@ -321,18 +321,20 @@ echo ('<DIV CLASS="receipt grid-selection-text">' .
'</DIV>' . "\n");
echo $this->element('ledger_entries', array
(// Element configuration
'account_ftype' => 'credit',
'limit' => 8,
// Grid configuration
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
(
'grid_div_id' => 'charge-entries',
'grid_div_class' => 'text-below',
'grid_events' => array('loadComplete' => 'onGridLoadComplete()'),
'grid_setup' => array('hiddengrid' => true),
'caption' => '<SPAN id="receipt-charges-caption"></SPAN>',
'rows' => $charges['entry'],
'action' => 'unreconciled',
'exclude' => array('Customer', 'Type', 'Debit', 'Credit'),
'include' => array('Applied', 'Balance'),
'remap' => array('Received' => 'Paid'),
'limit' => 8,
),
));
@@ -371,83 +373,65 @@ echo $this->element('form_table',
),
)));
echo "<BR>\n";
echo $form->input('repeat', array('type' => 'checkbox',
'id' => 'repeat',
'label' => 'Enter Multiple Receipts')) . "\n";
echo $form->submit('Generate Receipt') . "\n";
?>
<?php /*
<fieldset CLASS="payment superset">
<legend>Payments</legend>
*/ ?>
<input type="hidden" id="payment-entry-id" value="0">
<div id="payments"></div>
<?php /*
<fieldset> <legend>
<a href="#" onClick="addPaymentSource(true); return false;">Add Another Payment</a>
</legend> </fieldset>
</fieldset>
*/ ?>
<?php echo $form->end('Generate Receipt'); ?>
<?php /* echo '</DIV>' . "\n"; // End of the dialog DIV */ ?>
<div><H4>Request</H4><div id="request-debug"></div></div>
<div><H4>Response</H4><div id="response-debug"></div></div>
<div><H4>Output</H4><div id="output-debug"></div></div>
<div id="results"></div>
<div id="output-debug" style="display:none"></div>
<?php
// Warnings _really_ screw up javascript
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
$(document).ready(function(){
$("#TransactionStamp")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
datepicker('TransactionStamp');
resetForm();
$("#customer-id").val(0);
$("#receipt-customer-name").html("INTERNAL ERROR");
$("#receipt-balance").html("INTERNAL ERROR");
$("#receipt-charges-caption").html("Outstanding Charges");
<?php if (isset($customer['id'])): ?>
$("#customer-id").val(<?php echo $customer['id']; ?>);
//$("#receipt-customer-id").html("<?php echo '#'.$customer['id']; ?>");
$("#receipt-customer-id").html('<A HREF="/pmgr/site/customers/view/' +
"<?php echo $customer['id']; ?>" +
'">#' +
"<?php echo $customer['id']; ?>" +
'</A>');
$("#receipt-customer-name").html("<?php echo $customer['name']; ?>");
$("#receipt-balance").html(fmtCurrency("<?php echo $charges['balance']; ?>"));
$("#receipt-balance").html(fmtCurrency("<?php echo $stats['balance']; ?>"));
onGridState(null, 'hidden');
<?php else: ?>
onGridState(null, 'visible');
<?php endif; ?>
resetForm();
datepickerNow('TransactionStamp');
/* $("#dialog").dialog({ */
/* bgiframe: true, */
/* autoOpen: false, */
/* height: 500, */
/* width: 600, */
/* modal: true, */
/* buttons: { */
/* 'Post a Payment': function() { */
/* var bValid = true; */
/* if (bValid) { */
/* $('#debug').append('<H2>POSTED!</H2>'); */
/* $(this).dialog('close'); */
/* } */
/* }, */
/* Cancel: function() { */
/* $(this).dialog('close'); */
/* } */
/* }, */
/* close: function() { */
/* } */
/* }); */
/* $('#post-payment').click(function() { */
/* $('#dialog').dialog('open'); */
/* }); */
<?php if ($this->params['dev']): ?>
$('#output-debug').html('Post Output');
$('#output-debug').show();
<?php endif; ?>
});
--></script>
</div>
<a href="#" onClick="$('#debug').html(''); return false;">Clear Debug Output</a>

View File

@@ -9,8 +9,12 @@ echo '<div class="customer view">' . "\n";
* Customer Detail Main Section
*/
$rows = array(array('Name', $customer['Customer']['name']),
array('Comment', $customer['Customer']['comment']));
$rows = array();
$rows[] = array('Name', $customer['Customer']['name']);
$rows[] = array('Since', FormatHelper::date($since, true));
if (!empty($until))
$rows[] = array('Until', FormatHelper::date($until, true));
$rows[] = array('Comment', $customer['Customer']['comment']);
echo $this->element('table',
array('class' => 'item customer detail',
@@ -26,7 +30,9 @@ echo $this->element('table',
echo '<div class="infobox">' . "\n";
$rows = array();
$rows[] = array('Security Deposit:', FormatHelper::currency($outstandingDeposit));
$rows[] = array('Balance:', FormatHelper::currency($outstandingBalance));
//$rows[] = array('Charges:', FormatHelper::currency($stats['charges']));
//$rows[] = array('Payments:', FormatHelper::currency($stats['disbursements']));
$rows[] = array('Balance Owed:', FormatHelper::currency($outstandingBalance));
echo $this->element('table',
array('class' => 'summary',
'rows' => $rows,
@@ -47,13 +53,55 @@ echo '<div CLASS="detail supporting">' . "\n";
/**********************************************************************
* Contacts
* Unpaid Charges
*/
echo $this->element('contacts', array
('config' => array
('caption' => 'Customer Contacts',
'rows' => $customer['Contact'],
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
('caption' => 'Outstanding Charges',
'limit' => 10,
'action' => 'unreconciled',
'filter' => array('StatementEntry.customer_id' => $customer['Customer']['id']),
'exclude' => array('Customer'),
)));
/**********************************************************************
* Customer Credits
*/
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
('caption' => 'Oustanding Credits',
'grid_div_id' => 'surplus',
'limit' => 10,
'filter' => array('Customer.id' => $customer['Customer']['id'],
'StatementEntry.type' => 'SURPLUS'),
'exclude' => array('Entry', 'Effective', 'Customer', 'Unit', 'Account', 'Debit', 'Credit'),
'include' => array('Transaction', 'Amount'),
'remap' => array('Transaction' => 'Receipt'),
)));
/**********************************************************************
* Receipt History
*/
echo $this->element('ledger_entries', array
(// Grid configuration
'config' => array
('caption' => 'Receipts',
'limit' => 5,
'filter' => array('Customer.id' => $customer['Customer']['id'],
'Transaction.type' => 'RECEIPT',
'Tender.id !=' => null,
//'Account.id !=' => '-AR-'
),
'include' => array('Transaction'),
'exclude' => array('Entry', 'Account', 'Cr/Dr'),
'remap' => array('Transaction' => 'Receipt'),
)));
@@ -62,9 +110,28 @@ echo $this->element('contacts', array
*/
echo $this->element('leases', array
('config' => array
(// Grid configuration
'config' => array
('caption' => 'Lease History',
'rows' => $customer['Lease'],
'limit' => 5,
'filter' => array('Customer.id' => $customer['Customer']['id']),
'exclude' => array('Customer'),
'sort_column' => 'Move-In',
'sort_order' => 'DESC',
)));
/**********************************************************************
* Contacts
*/
echo $this->element('contacts', array
(// Grid configuration
'config' => array
('caption' => 'Customer Contacts',
'limit' => 5,
'filter' => array('Customer.id' => $customer['Customer']['id']),
'include' => array('Relationship'),
)));
@@ -72,16 +139,15 @@ echo $this->element('leases', array
* Customer Account History
*/
echo $this->element('ledger_entries', array
(// Element configuration
'customer_id' => $customer['Customer']['id'],
'ar_account' => true,
// Grid configuration
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
('caption' => 'Account',
),
));
('caption' => 'Customer Statement',
'filter' => array('Customer.id' => $customer['Customer']['id'],
'type !=' => 'VOID'),
//'include' => array('Sub-Total'),
'exclude' => array('Customer'),
)));
/* End "detail supporting" div */

View File

@@ -0,0 +1,98 @@
<?php /* -*- mode:PHP -*- */
echo '<div class="double-entry view">' . "\n";
// The two entries, debit and credit, are actually individual
// entries in separate accounts (each make up one of the two
// entries required for "double entry").
/**********************************************************************
**********************************************************************
**********************************************************************
**********************************************************************
* DoubleEntry Detail Main Section
*/
$transaction = $entry['Transaction'];
$ledgers = array('debit' => $entry['DebitLedger'],
'credit' => $entry['CreditLedger']);
$entries = array('debit' => $entry['DebitEntry'],
'credit' => $entry['CreditEntry']);
$entry = $entry['DoubleEntry'];
$rows = array();
$rows[] = array('Transaction', $html->link('#'.$transaction['id'],
array('controller' => 'transactions',
'action' => 'view',
$transaction['id'])));
$rows[] = array('Timestamp', FormatHelper::datetime($transaction['stamp']));
$rows[] = array('Comment', $entry['comment']);
echo $this->element('table',
array('class' => 'item double-entry detail',
'caption' => 'Double Ledger Entry',
'rows' => $rows,
'column_class' => array('field', 'value')));
/**********************************************************************
* Debit/Credit Entries
*/
echo ('<DIV CLASS="ledger-double-entry">' . "\n");
foreach ($ledgers AS $type => $ledger) {
$rows = array();
// REVISIT <AP>: 20090816
// Due to low priority, the ledger_entry/double_entry stuff
// is a bit piecemeal at the moment (trying to reuse old
// code as much as possible). So, LedgerEntry view is just
// redirecting here. Of course, presenting a link for the
// LedgerEntry then is, well, quite pointless.
$rows[] = array('ID', '#' . $entries[$type]['id']);
/* $rows[] = array('ID', $html->link('#' . $entries[$type]['id'], */
/* array('controller' => 'entries', */
/* 'action' => 'view', */
/* $entries[$type]['id']))); */
$rows[] = array('Account', ($ledger['link']
? $html->link($ledger['Account']['name'],
array('controller' => 'accounts',
'action' => 'view',
$ledger['Account']['id']))
: $ledger['Account']['name']));
$rows[] = array('Ledger', ($ledger['link']
? $html->link('#' . $ledger['sequence'],
array('controller' => 'ledgers',
'action' => 'view',
$ledger['id']))
: '#' . $ledger['sequence']));
$rows[] = array('Amount', FormatHelper::currency($entries[$type]['amount']));
//$rows[] = array('Effect', $ledger['Account']['ftype'] == $type ? 'INCREASE' : 'DECREASE');
echo $this->element('table',
array('class' => array('item', $type, 'detail'),
'caption' => ucfirst($type) . ' Entry',
'rows' => $rows,
'column_class' => array('field', 'value')));
}
echo ('</DIV>' . "\n");
/**********************************************************************
**********************************************************************
**********************************************************************
**********************************************************************
* Supporting Elements Section
*/
echo '<div CLASS="detail supporting">' . "\n";
/* End "detail supporting" div */
echo '</div>' . "\n";
/* End page div */
echo '</div>' . "\n";

View File

@@ -2,22 +2,19 @@
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'Account.id', 'formatter' => 'id');
$cols['Name'] = array('index' => 'Account.name', 'formatter' => 'longname');
$cols['Type'] = array('index' => 'Account.type', 'formatter' => 'name');
$cols['Entries'] = array('index' => 'entries', 'width' => '60', 'align' => 'right');
$cols['Type'] = array('index' => 'Account.type', 'formatter' => 'enum');
$cols['Entries'] = array('index' => 'entries', 'formatter' => 'number');
$cols['Debits'] = array('index' => 'debits', 'formatter' => 'currency');
$cols['Credits'] = array('index' => 'credits', 'formatter' => 'currency');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Comment'] = array('index' => 'Account.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Name'));
// Render the grid
$grid
->columns($cols)
->sortField('Name')
->defaultFields(array('ID', 'Name'))
->render($this, isset($config) ? $config : null);
->defaultFields(array('Name'))
->searchFields(array('Name'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Comment')));

View File

@@ -2,23 +2,19 @@
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'Contact.id', 'formatter' => 'id');
$cols['Last Name'] = array('index' => 'Contact.last_name', 'formatter' => 'name');
$cols['First Name'] = array('index' => 'Contact.first_name', 'formatter' => 'name');
$cols['Company'] = array('index' => 'Contact.company_name', 'formatter' => 'longname');
if (0) { // REVISIT<AP>: Need to figure out how to put this in play
$cols['Type'] = array('index' => 'ContactsCustomer.type', 'width' => '75');
$cols['Active'] = array('index' => 'ContactsCustomer.active', 'width' => '75');
}
$cols['Comment'] = array('index' => 'Contact.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Last Name', 'First Name'));
$cols['Relationship'] = array('index' => 'ContactsCustomer.type', 'formatter' => 'enum');
$cols['Name'] = array('index' => 'Contact.display_name', 'formatter' => 'longname');
$cols['Last Name'] = array('index' => 'Contact.last_name', 'formatter' => 'name');
$cols['First Name'] = array('index' => 'Contact.first_name', 'formatter' => 'name');
$cols['License'] = array('index' => 'Contact.id_local', 'formatter' => 'name');
$cols['Company'] = array('index' => 'Contact.company_name', 'formatter' => 'longname');
$cols['Comment'] = array('index' => 'Contact.comment', 'formatter' => 'comment');
// Render the grid
$grid
->columns($cols)
->sortField('Last Name')
->defaultFields(array('ID', 'Last Name', 'First Name'))
->render($this, isset($config) ? $config : null);
->defaultFields(array('Last Name', 'First Name'))
->searchFields(array('Last Name', 'First Name', 'Company'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Relationship', 'License', 'Comment')));

View File

@@ -2,23 +2,21 @@
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'Customer.id', 'formatter' => 'id');
if (0) // REVISIT<AP>: Need to figure out how to put this in play
$cols['Relationship'] = array('index' => 'ContactsCustomer.type', 'width' => '75');
$cols['Name'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Last Name'] = array('index' => 'PrimaryContact.last_name', 'formatter' => 'name');
$cols['First Name'] = array('index' => 'PrimaryContact.first_name', 'formatter' => 'name');
$cols['Leases'] = array('index' => 'lease_count', 'width' => '60');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Comment'] = array('index' => 'Customer.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Last Name', 'First Name'));
$cols['Relationship'] = array('index' => 'ContactsCustomer.type', 'formatter' => 'enum');
$cols['Name'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Last Name'] = array('index' => 'PrimaryContact.last_name', 'formatter' => 'name');
$cols['First Name'] = array('index' => 'PrimaryContact.first_name', 'formatter' => 'name');
$cols['Units'] = array('index' => 'current_lease_count', 'formatter' => 'number');
$cols['Past Leases'] = array('index' => 'past_lease_count', 'formatter' => 'number');
$cols['Leases'] = array('index' => 'lease_count', 'formatter' => 'number');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Comment'] = array('index' => 'Customer.comment', 'formatter' => 'comment');
// Render the grid
$grid
->columns($cols)
->sortField('Name')
->defaultFields(array('ID', 'Name'))
->render($this, isset($config) ? $config : null);
->defaultFields(array('Name'))
->searchFields(array('Name', 'Last Name', 'First Name'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Relationship', 'Past Leases', 'Comment')));

View File

@@ -32,8 +32,8 @@ foreach ($fields AS $field => $config) {
$include_after = true;
}
$column_class = array();
if (empty($column_class))
$column_class = array();
if ($include_before)
$column_class[] = 'before';
$column_class[] = 'field';
@@ -79,7 +79,13 @@ foreach ($fields AS $field => $config) {
$cells[] = null;
}
$name = $config['name'];
if (empty($config['opts']['label']))
$name = $form->label($field, $config['name'],
empty($config['label_attributes'])
? null : $config['label_attributes']);
else
$name = $config['name'];
if (isset($config['with_name_before']))
$name = $config['with_name_before'] . $name;
elseif (isset($with_name_before))
@@ -123,7 +129,7 @@ foreach ($fields AS $field => $config) {
}
echo $this->element('table',
compact('class', 'caption', 'headers',
compact('id', 'class', 'caption', 'headers',
'rows', 'row_class', 'suppress_alternate_rows',
'column_class')
);

View File

@@ -19,6 +19,7 @@ if (!isset($limitOptions)) {
}
sort($limitOptions, SORT_NUMERIC);
$limitOptions = array_unique($limitOptions, SORT_NUMERIC);
//$limitOptions[] = 'ALL'; // Would be nice... jqGrid shows 'NaN of NaN'
if (!isset($height))
$height = 'auto';
@@ -46,12 +47,9 @@ if (!isset($grid_setup))
$grid_setup = array();
// Do some prework to bring in the appropriate libraries
$imgpath = '/pmgr/site/css/jqGrid/basic/images';
$html->css('jqGrid/basic/grid', null, null, false);
$html->css('jqGrid/jqModal', null, null, false);
$javascript->link('jqGrid/jquery.jqGrid.js', false);
$javascript->link('jqGrid/js/jqModal', false);
$javascript->link('jqGrid/js/jqDnR', false);
$html->css('ui.jqgrid', null, null, false);
$javascript->link('jqGrid/grid.locale-en', false);
$javascript->link('jqGrid/jquery.jqGrid.min', false);
$javascript->link('pmgr_jqGrid', false);
@@ -62,63 +60,74 @@ $javascript->link('pmgr_jqGrid', false);
// we'll just pass the desired fields to the controller
// as part of the data fetch.
$url = $html->url(array('controller' => $controller,
'action' => 'jqGridData',
'debug' => 0,
'action' => 'gridData',
));
// Create extra parameters that jqGrid will pass to our
// controller whenever data is requested.
// 'fields' will allow the controller to return only the
// requested fields, and in the right order. Since fields
// is a complex structure (an array), we'll need to
// serialize it first for transport over HTTP.
// requested fields, and in the right order.
$postData = array();
$postData['fields'] = serialize(array_map(create_function('$col',
'return $col["index"];'),
array_values($jqGridColumns)));
$postData['fields'] = array_map(create_function('$col',
'return $col["index"];'),
array_values($jqGridColumns));
// Determine if we're to be using a custom list, or if
// the data will simply be action based.
if (isset($custom_ids)) {
if (!isset($action))
$action = 'idlist';
$postData['idlist'] = serialize($custom_ids);
}
elseif (!isset($action)) {
$action = null;
$postData['idlist'] = $custom_ids;
}
if (isset($custom_post_data)) {
$postData['custom'] = serialize($custom_post_data);
}
// 'action' will ensure that the controller provides the
// correct subset of data
$postData['action'] = $action;
if (isset($nolinks)) {
if (isset($nolinks))
$postData['nolinks'] = true;
}
// 'action' will ensure that the controller does the right thing
// 'filter' allows the app controller to automagic filter
// 'custom' is for use solely by derived controllers
$postData['action'] = isset($action) ? $action : null;
$postData['filter'] = isset($filter) ? $filter : null;
$postData['custom'] = isset($custom_post_data) ? $custom_post_data : null;
// Perform column customizations.
// This will largely be based off of the 'formatter' parameter,
// but could be on any pertinent condition.
foreach ($jqGridColumns AS &$col) {
foreach ($jqGridColumns AS $header => &$col) {
$default = array();
// Make sure every column has a name
$default['name'] = preg_replace("/\./", '-', $col['index']);
$default['name'] = preg_replace("/\./", '-', $col['index']);
$default['force'] = isset($col['forcewidth']) ? $col['forcewidth'] : null;
// Perform customization based on formatter
if (isset($col['formatter'])) {
if ($col['formatter'] === 'id') {
// Switch currency over to our own custom formatting
// Use our custom formatting for ids
$col['formatter'] = array('--special' => 'idFormatter');
$default['width'] = 50;
$default['align'] = 'center';
// For IDs, force the width by default,
// unless otherwise instructed NOT to.
if (!isset($default['force']))
$default['force'] = true;
}
elseif ($col['formatter'] === 'number') {
$default['width'] = 60;
$default['align'] = 'right';
// No special formatting for number
unset($col['formatter']);
}
elseif ($col['formatter'] === 'percentage') {
$col['formatter'] = array('--special' => 'percentageFormatter');
$default['width'] = 60;
$default['align'] = 'right';
}
elseif ($col['formatter'] === 'currency') {
// Switch currency over to our own custom formatting
// Use our custom formatting for currency
$col['formatter'] = array('--special' => 'currencyFormatter');
$default['width'] = 85;
$default['align'] = 'right';
@@ -128,21 +137,45 @@ foreach ($jqGridColumns AS &$col) {
$default['width'] = 95;
$default['align'] = 'center';
}
elseif ($col['formatter'] === 'name' || $col['formatter'] === 'longname') {
elseif (preg_match("/^(long|short)?name$/",
$col['formatter'], $matches)) {
$default['width'] = 100;
if ($col['formatter'] === 'longname')
if (!empty($matches[1]) && $matches[1] === 'long')
$default['width'] *= 1.5;
if (!empty($matches[1]) && $matches[1] === 'short')
$default['width'] *= 0.7;
// No special formatting for name
unset($col['formatter']);
}
elseif (preg_match("/^(long|short)?enum$/",
$col['formatter'], $matches)) {
$default['width'] = 60;
if (!empty($matches[1]) && $matches[1] === 'long')
$default['width'] *= 1.5;
if (!empty($matches[1]) && $matches[1] === 'short')
$default['width'] *= 0.7;
//$default['align'] = 'right';
// No special formatting for enum
unset($col['formatter']);
}
elseif ($col['formatter'] === 'comment') {
$default['width'] = 300;
$default['width'] = 150;
$default['sortable'] = false;
// No special formatting for comment
unset($col['formatter']);
}
// else just let the formatter pass through untouched
// Just a rough approximation to ensure columns
// are wide enough to fully display their header.
$min_width = strlen($header) * 7;
$min_width = 0; // REVISIT <AP>: 20090829; if/while jqGrid is fixed width
if ((!isset($default['width']) || $default['width'] < $min_width) && !$default['force'])
$default['width'] = $min_width;
}
$col = array_merge($default, $col);
@@ -164,9 +197,12 @@ if (isset($sort_order)) {
$sortorder = 'ASC';
}
if (1) { // debug
$caption .= ' :: <span id="'.$grid_id.'-query"></span>';
}
$debug = !empty($this->params['dev']);
if ($debug)
$caption .= '<span class="debug grid-query"> :: <span id="'.$grid_id.'-query"></span></span>';
$caption .= ('<span class="grid-error" id="'.$grid_id.'-error"' .
' style="display:none"> :: Error (Please Reload)</span>');
foreach (array_merge(array('loadComplete' => '', 'loadError' => ''),
$grid_events) AS $event => $statement) {
@@ -176,13 +212,21 @@ foreach (array_merge(array('loadComplete' => '', 'loadError' => ''),
$statement = current($statement);
}
if ($event == 'loadComplete') {
if ($event == 'loadComplete' && $debug) {
$grid_events[$event] =
array('--special' => "function($params) {url=jQuery('#{$grid_id}').getGridParam('url');url=url.replace(/\/debug.*$/,'?'); pd=jQuery('#{$grid_id}').getPostData();$.each(pd,function(i){ url+=i+'='+escape(pd[i])+'&'; }); jQuery('#{$grid_id}-query').html('<A HREF=\"'+url+'\">Grid Query</A><BR>'); $statement;}");
array('--special' => "function($params) {url=jQuery('#{$grid_id}').getGridParam('url');url=url+'/debug:1?'; pd=jQuery('#{$grid_id}').getPostData();$.each(pd,function(i){ url+=i+'='+escape(pd[i])+'&'; }); jQuery('#{$grid_id}-query').html('<A HREF=\"'+url+'\">Grid Query</A><BR>'); $statement;}");
}
elseif ($event == 'loadError') {
elseif ($event == 'loadError' && $debug) {
$grid_events[$event] =
array('--special' => "function($params) {url=jQuery('#{$grid_id}').getGridParam('url');url=url.replace(/\/debug.*$/,'?'); pd=jQuery('#{$grid_id}').getPostData();$.each(pd,function(i){ url+=i+'='+escape(pd[i])+'&'; }); jQuery('#{$grid_id}-query').html('<A HREF=\"'+url+'\">Grid Error Query</A><BR>'); $statement;}");
array('--special' => "function($params) {url=jQuery('#{$grid_id}').getGridParam('url');url=url+'/debug:1?'; pd=jQuery('#{$grid_id}').getPostData();$.each(pd,function(i){ url+=i+'='+escape(pd[i])+'&'; }); jQuery('#{$grid_id}-query').html('<A HREF=\"'+url+'\">Grid Error Query</A><BR>'); $statement;}");
}
elseif ($event == 'loadComplete' && !$debug) {
$grid_events[$event] =
array('--special' => "function($params) {jQuery('#{$grid_id}-error').hide(); $statement;}");
}
elseif ($event == 'loadError' && !$debug) {
$grid_events[$event] =
array('--special' => "function($params) {jQuery('#{$grid_id}-error').show(); $statement;}");
}
else {
$grid_events[$event] =
@@ -196,17 +240,20 @@ $jqGrid_setup = array_merge
(array('mtype' => 'GET',
'datatype' => 'xml',
'url' => $url,
'postData' => $postData,
// Since postData is a complex structure (an array), we'll
// need to serialize it first for transport over HTTP.
'postData' => array('post' => serialize($postData)),
'colNames' => array_keys($jqGridColumns),
'colModel' => array('--special' => $jqGridColumns),
'height' => $height,
'width' => 700,
'rowNum' => $limit,
'rowList' => $limitOptions,
'sortname' => $sortname,
'sortorder' => $sortorder,
'caption' => $caption,
'imgpath' => $imgpath,
'viewrecords' => true,
'gridview' => true,
'pager' => $grid_id.'-pager',
),
$grid_events,
@@ -219,67 +266,46 @@ $jqGrid_setup = array_merge
// to kick this thing off.
?>
<?php if ($first_grid): ?>
<script type="text/javascript"><!--
var currencyFormatter = function(cellval, opts, rowObject) {
if (!cellval)
return "";
return fmtCurrency(cellval);
}
var percentageFormatter = function(cellval, opts, rowObject) {
var precision;
if (typeof(opts.colModel) != 'undefined' &&
typeof(opts.colModel.formatoptions) != 'undefined' &&
typeof(opts.colModel.formatoptions.precision) != 'undefined')
precision = opts.colModel.formatoptions.precision;
else
precision = 0;
amount = cellval.toString().replace(/\%/g,'');
amount = (amount*100).toFixed(precision);
return amount+'%';
}
var idFormatter = function(cellval, opts, rowObject) {
if (!cellval)
return cellval;
return '#'+cellval;
}
--></script>
<?php endif; ?>
<DIV ID="<?php echo $grid_div_id; ?>" CLASS="<?php echo $grid_div_class; ?>">
<table id="<?php echo $grid_id; ?>" class="scroll"></table>
<div id="<?php echo $grid_id; ?>-pager" class="scroll" style="text-align:right"></div>
<script type="text/javascript"><!--
jQuery(document).ready(function(){
currencyFormatter = function(el, cellval, opts) {
if (!cellval)
return;
$(el).html(fmtCurrency(cellval));
}
idFormatter = function(el, cellval, opts) {
if (!cellval)
return;
$(el).html('#'+cellval);
}
jQuery('#<?php echo $grid_id; ?>').jqGrid(
<?php echo FormatHelper::phpVarToJavascript($jqGrid_setup); ?>
).navGrid('#<?php echo $grid_id; ?>-pager',
{ view:false,
edit:false,
add:false,
del:false,
search:true,
refresh:true});
<?php
/* jQuery('#t_<?php echo $grid_id; ?>').height(25).hide() */
/* .filterGrid('#<?php echo $grid_id; ?>', { */
/* gridModel:true, */
/* gridToolbar:true, */
/* autosearch:true, */
/* }); */
/* jQuery('#<?php echo $grid_id; ?>').navGrid('#<?php echo $grid_id; ?>-pager', */
/* { view:false, */
/* edit:false, */
/* add:false, */
/* del:false, */
/* search:false, */
/* refresh:false}) */
/* .navButtonAdd('#<?php echo $grid_id; ?>-pager',{ */
/* caption:"Search", */
/* title:"Toggle Search", */
/* buttonimg:'<?php echo $imgpath; ?>' + '/find.gif', */
/* onClickButton:function(){ */
/* if(jQuery('#t_<?php echo $grid_id; ?>').css("display")=="none") { */
/* jQuery('#t_<?php echo $grid_id; ?>').css("display",""); */
/* } else { */
/* jQuery('#t_<?php echo $grid_id; ?>').css("display","none"); */
/* } */
/* } */
/* }); */
?>
<?php echo FormatHelper::phpVarToJavascript($jqGrid_setup) . "\n"; ?>
).navGrid('#<?php echo $grid_id; ?>-pager', { view:false,edit:false,add:false,del:false,search:true,refresh:true});
});
--></script>
<?php
if (count($search_fields) > 0) {
echo('<div>Search By:<BR>' . "\n");

View File

@@ -2,25 +2,36 @@
// Define the table columns
$cols = array();
$cols['LeaseID'] = array('index' => 'Lease.id', 'hidden' => true);
$cols['Lease'] = array('index' => 'Lease.number', 'formatter' => 'id');
$cols['Unit'] = array('index' => 'Unit.name', 'width' => '50', 'align' => 'center');
$cols['Unit'] = array('index' => 'Unit.name', 'formatter' => 'shortname', 'align' => 'center');
$cols['Customer id'] = array('index' => 'Customer.id', 'hidden' => true);
$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Rent'] = array('index' => 'Lease.rent', 'formatter' => 'currency', 'hiddenz' => true);
$cols['Deposit'] = array('index' => 'Lease.deposit', 'formatter' => 'currency', 'hiddenz' => true);
$cols['Rent'] = array('index' => 'Lease.rent', 'formatter' => 'currency');
$cols['Deposit'] = array('index' => 'Lease.deposit', 'formatter' => 'currency');
$cols['Signed'] = array('index' => 'Lease.lease_date', 'formatter' => 'date');
$cols['Move-In'] = array('index' => 'Lease.movein_date', 'formatter' => 'date');
$cols['Move-Out'] = array('index' => 'Lease.moveout_date', 'formatter' => 'date');
$cols['Closed'] = array('index' => 'Lease.close_date', 'formatter' => 'date');
$cols['Charge-Thru'] = array('index' => 'Lease.charge_through_date', 'formatter' => 'date');
$cols['Paid-Thru'] = array('index' => 'Lease.paid_through_date', 'formatter' => 'date');
$cols['Status'] = array('index' => 'status', 'formatter' => 'longenum');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Comment'] = array('index' => 'Lease.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Customer', 'Unit'));
if (!empty($this->params['action'])) {
if ($this->params['action'] === 'closed')
$grid->invalidFields(array('Charge-Thru', 'Paid-Thru', 'Status'));
elseif ($this->params['action'] === 'active')
$grid->invalidFields(array('Closed'));
elseif ($this->params['action'] === 'delinquent')
$grid->invalidFields(array('Closed'));
}
// Render the grid
$grid
->columns($cols)
->sortField('LeaseID')
->defaultFields(array('LeaseID', 'Lease'))
->render($this, isset($config) ? $config : null);
->sortField('Lease')
->defaultFields(array('Lease'))
->searchFields(array('Customer', 'Unit'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Signed', 'Charge-Thru', 'Status', 'Comment')));

View File

@@ -4,136 +4,27 @@
$cols = array();
$cols['Transaction'] = array('index' => 'Transaction.id', 'formatter' => 'id');
$cols['Entry'] = array('index' => 'LedgerEntry.id', 'formatter' => 'id');
$cols['Date'] = array('index' => 'Transaction.stamp', 'formatter' => 'date');
$cols['Effective'] = array('index' => 'LedgerEntry.effective_date', 'formatter' => 'date');
$cols['Through'] = array('index' => 'LedgerEntry.through_date', 'formatter' => 'date');
$cols['Account'] = array('index' => 'Account.name', 'formatter' => 'name');
$cols['Debit Account'] = array('index' => 'DebitAccount.name', 'formatter' => 'name');
$cols['Credit Account'] = array('index' => 'CreditAccount.name', 'formatter' => 'name');
$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Lease'] = array('index' => 'Lease.number', 'formatter' => 'id');
$cols['Unit'] = array('index' => 'Unit.name', 'formatter' => 'name');
$cols['Source'] = array('index' => 'MonetarySource.name', 'formatter' => 'name');
$cols['Cr/Dr'] = array('index' => 'LedgerEntry.crdr', 'formatter' => 'enum');
$cols['Tender'] = array('index' => 'Tender.name', 'formatter' => 'longname');
$cols['Comment'] = array('index' => 'LedgerEntry.comment', 'formatter' => 'comment', 'width'=>150);
$cols['Amount'] = array('index' => 'LedgerEntry.amount', 'formatter' => 'currency');
$cols['Debit'] = array('index' => 'debit', 'formatter' => 'currency');
$cols['Credit'] = array('index' => 'credit', 'formatter' => 'currency');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Sub-Total'] = array('index' => 'subtotal-balance', 'formatter' => 'currency', 'sortable' => false);
$cols['Last Payment'] = array('index' => 'last_paid', 'formatter' => 'date');
$cols['Applied'] = array('index' => "applied", 'formatter' => 'currency');
$cols['Sub-Total'] = array('index' => 'subtotal-LedgerEntry.amount', 'formatter' => 'currency', 'sortable' => false);
// Since group_by_tx is a boolean, let's just get it
// defined, regardless of whether the caller did so.
// group_by_tx will cause all entry fields to be
// invalidated, and will leave only the transaction
// fields. Yes... the caller should just use the
// transactions element instead, in theory. However,
// it hasn't yet been implemented to the level of
// this element, and additionally, the transactions
// element will not allow for customer information
// (rightly so, since it's a ledger_entry field).
// However, at the current implementation, all ledger
// entries of a transaction are for the same customer.
// So... we allow it for now.
if (!isset($group_by_tx))
$group_by_tx = false;
// REVISIT <AP>: 20090715
// If we really want to group by transaction, we need
// a transaction listing, not a ledger_entry listing.
// switch controllers... don't overload this one.
$group_by_tx = false;
if (isset($transaction_id) || isset($reconcile_id))
$grid->invalidFields('Transaction');
if ($group_by_tx)
$grid->invalidFields('Entry');
if ($group_by_tx)
$grid->invalidFields(array('Effective', 'Through'));
if (!isset($collected_account_id))
$grid->invalidFields('Last Payment');
if (isset($account_ftype) || isset($ledger_id) || isset($account_id) || isset($ar_account))
$grid->invalidFields(array('Debit Account', 'Credit Account'));
else
$grid->invalidFields('Account');
if (isset($no_account) || $group_by_tx || isset($collected_account_id))
$grid->invalidFields(array('Account', 'Debit Account', 'Credit Account'));
if (isset($ledger_id) || isset($account_id) || isset($ar_account)) {
$grid->invalidFields('Amount');
$cols['Sub-Total']['index'] = 'subtotal-balance';
} else {
$grid->invalidFields(array('Debit', 'Credit'));
$cols['Sub-Total']['index'] = 'subtotal-LedgerEntry.amount';
}
// group_by_tx SHOULD wipe out Customer, but the reality
// is that it works good at the present, so we'll leave it.
if (isset($lease_id) || isset($customer_id))
$grid->invalidFields(array('Customer'));
if (isset($lease_id) || $group_by_tx)
$grid->invalidFields(array('Lease', 'Unit'));
if (!isset($reconcile_id) && !isset($collected_account_id))
$grid->invalidFields('Applied');
else
$cols['Sub-Total']['index'] = 'subtotal-applied';
if (isset($account_ftype) || isset($collected_account_id))
$grid->invalidFields('Sub-Total');
// Now that columns are defined, establish basic grid parameters
$grid
->columns($cols)
->sortField('Date')
->defaultFields(array('Entry', 'Date', 'Amount', 'Credit', 'Debit'));
if (!isset($config['rows']) && !isset($collected_account_id)) {
$config['action'] = 'ledger';
$grid->limit(50);
}
if (isset($reconcile_id)) {
$grid->customData(compact('reconcile_id'))->limit(20);
}
if (isset($collected_account_id)) {
$config['action'] = 'collected';
$grid->customData(compact('collected_account_id'))->limit(50);
$grid->sortField('Last Payment');
}
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Customer', 'Unit'));
// Include custom data
$grid->customData(compact('ledger_id', 'account_id', 'ar_account',
'account_type', 'account_ftype', 'monetary_source_id',
'customer_id', 'lease_id', 'transaction_id', 'group_by_tx'));
// Render the grid
$grid
->columns($cols)
->sortField('Date', 'DESC')
->defaultFields(array('Entry', 'Date', 'Amount'))
->searchFields(array('Customer', 'Unit'))
->render($this, isset($config) ? $config : null,
array('Transaction', 'Entry', 'Date', 'Effective', 'Last Payment',
'Account', 'Debit Account', 'Credit Account',
'Customer', 'Unit',
'Comment',
'Amount', 'Debit', 'Credit',
'Applied', 'Sub-Total')
);
array_diff(array_keys($cols), array('Transaction', 'Debit', 'Credit',
'Balance', 'Sub-Total', 'Comment')));

View File

@@ -2,23 +2,21 @@
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'id_sequence', 'formatter' => 'id');
$cols['Account'] = array('index' => 'Account.name', 'formatter' => 'longname');
//$cols['Open Date'] = array('index' => 'PriorClose.stamp', 'formatter' => 'date');
$cols['Close Date'] = array('index' => 'Close.stamp', 'formatter' => 'date');
$cols['Sequence'] = array('index' => 'Ledger.sequence', 'formatter' => 'id');
$cols['Open Date'] = array('index' => 'PriorCloseTransaction.stamp', 'formatter' => 'date');
$cols['Close Date'] = array('index' => 'CloseTransaction.stamp', 'formatter' => 'date');
$cols['Comment'] = array('index' => 'Ledger.comment', 'formatter' => 'comment');
$cols['Entries'] = array('index' => 'entries', 'width' => '60', 'align' => 'right');
$cols['Entries'] = array('index' => 'entries', 'formatter' => 'number');
$cols['Debits'] = array('index' => 'debits', 'formatter' => 'currency');
$cols['Credits'] = array('index' => 'credits', 'formatter' => 'currency');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Account', 'Comment'));
// Render the grid
$grid
->columns($cols)
->sortField('ID', 'DESC')
->defaultFields(array('ID', 'Account'))
->render($this, isset($config) ? $config : null);
->sortField('Sequence')
->defaultFields(array('Sequence'))
->searchFields(array('Comment'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Account', 'Open Date', 'Comment')));

View File

@@ -2,20 +2,17 @@
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'Map.id', 'formatter' => 'id');
$cols['Name'] = array('index' => 'Map.name', 'formatter' => 'longname');
$cols['Site Area'] = array('index' => 'SiteArea.name', 'formatter' => 'longname');
$cols['Width'] = array('index' => 'Map.width', 'width' => '50', 'align' => 'right');
$cols['Depth'] = array('index' => 'Map.depth', 'width' => '50', 'align' => 'right');
$cols['Width'] = array('index' => 'Map.width', 'formatter' => 'number');
$cols['Depth'] = array('index' => 'Map.depth', 'formatter' => 'number');
$cols['Comment'] = array('index' => 'Map.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Name'));
// Render the grid
$grid
->columns($cols)
->sortField('Name')
->defaultFields(array('ID', 'Name'))
->render($this, isset($config) ? $config : null);
->defaultFields(array('Name'))
->searchFields(array('Name'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array()));

View File

@@ -1,18 +0,0 @@
<?php /* -*- mode:PHP -*- */
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'MonetarySource.id', 'formatter' => 'id');
$cols['Name'] = array('index' => 'MonetarySource.name', 'formatter' => 'longname');
$cols['Comment'] = array('index' => 'MonetarySource.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('ID', 'Name'));
// Render the grid
$grid
->columns($cols)
->sortField('ID')
->defaultFields(array('ID', 'Name'))
->render($this, isset($config) ? $config : null);

View File

@@ -8,17 +8,78 @@
* @package pmgr
*/
foreach ($menu AS $item) {
if (isset($item['header']))
echo('<DIV CLASS="header">' . $item['name'] . '</DIV>' . "\n");
elseif (isset($item['hr']))
echo('<HR>' . "\n");
elseif (isset($item['url']))
echo('<DIV CLASS="item">'
. $html->link($item['name'], $item['url'],
isset($item['htmlAttributes']) ? $item['htmlAttributes'] : null,
isset($item['confirmMessage']) ? $item['confirmMessage'] : null,
isset($item['escapeTitle']) ? $item['escapeTitle'] : null)
// REVISIT <AP>: 20090823
// Add way to slide the entire menu off the page
. '</DIV>' . "\n");
// The sidemenu-container is necessary to define the
// bounds as the parent of the sidemenu div, which will
// be heavily manipulated by the accordion module. If
// we don't have good control over the parent, the
// accordion will get confused and behave poorly.
echo('<DIV ID="sidemenu-container">' . "\n");
echo('<DIV ID="sidemenu">' . "\n");
$section = 0;
$active_section = null;
foreach ($menu['areas'] AS $area_name => $area) {
if (empty($area['subareas']))
continue;
foreach ($area['subareas'] AS $subarea_name => $subarea) {
if (empty($subarea['priorities']))
continue;
if (!isset($active_section) &&
!empty($menu['active']['area']) && $area_name == $menu['active']['area'] &&
(empty($menu['active']['subarea']) || $subarea_name == $menu['active']['subarea']))
$active_section = $section;
++$section;
echo('<H3' .
//' id="sidemenu-section-'.$area_name.'-'.$subarea_name.'"' .
' class="sidemenu-header">' .
$subarea['name'] .
"</H3>\n");
echo('<DIV class="sidemenu-content">' . "\n");
foreach ($subarea['priorities'] AS $priority) {
foreach ($priority AS $item) {
if (isset($item['url'])) {
echo('<DIV CLASS="sidemenu-item">'
. $html->link($item['name'], $item['url'],
isset($item['htmlAttributes']) ? $item['htmlAttributes'] : null,
isset($item['confirmMessage']) ? $item['confirmMessage'] : null,
isset($item['escapeTitle']) ? $item['escapeTitle'] : null)
. '</DIV>' . "\n");
}
}
}
echo('</DIV>' . "\n");
}
}
echo('</DIV>' . "\n"); // End #sidemenu
echo('</DIV>' . "\n"); // End #sidemenu-container
// Uses both hoverintent, which is a more user friendly mechanism
// than mouseover, as well as click. This provides 1) a workable
// solution for those browsers that don't use pointers, such as
// a touchscreen, and 2) a means to open the menu if the animation
// was running while the user moved the pointer to a new menu area.
$javascript->codeBlock(
<<<JSCB
jQuery(document).ready(function(){
if (jQuery("#sidemenu").accordion != null) {
jQuery("#sidemenu").accordion
({ fillSpace : true,
event : "click hoverintent",
animated : "bounceslide"
JSCB
. (isset($active_section) ? ",\n\t active : $active_section\n" : '') .
<<<JSCB
});
}
});
JSCB
, array('inline' => false));

View File

@@ -0,0 +1,61 @@
<?php /* -*- mode:PHP -*- */
// Define the table columns
$cols = array();
$cols['Transaction'] = array('index' => 'Transaction.id', 'formatter' => 'id');
$cols['Entry'] = array('index' => 'StatementEntry.id', 'formatter' => 'id');
$cols['Date'] = array('index' => 'Transaction.stamp', 'formatter' => 'date');
$cols['Effective'] = array('index' => 'StatementEntry.effective_date', 'formatter' => 'date');
$cols['Through'] = array('index' => 'StatementEntry.through_date', 'formatter' => 'date');
$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Lease'] = array('index' => 'Lease.number', 'formatter' => 'id');
$cols['Unit'] = array('index' => 'Unit.name', 'formatter' => 'shortname');
$cols['Comment'] = array('index' => 'StatementEntry.comment', 'formatter' => 'comment', 'width'=>150);
$cols['Type'] = array('index' => 'StatementEntry.type', 'formatter' => 'longenum');
$cols['Account'] = array('index' => 'Account.name', 'formatter' => 'name');
$cols['Debit'] = array('index' => 'charge', 'formatter' => 'currency');
$cols['Credit'] = array('index' => 'disbursement', 'formatter' => 'currency');
$cols['Amount'] = array('index' => "StatementEntry.amount", 'formatter' => 'currency');
$cols['Received'] = array('index' => "applied", 'formatter' => 'currency');
// 'balance' is already in use as part of charge/disbursement/balance.
// 'unapplied' isn't quite the right term, but it's not customer visible.
$cols['Balance'] = array('index' => "unapplied", 'formatter' => 'currency');
$cols['Sub-Total'] = array('index' => 'subtotal-balance', 'formatter' => 'currency', 'sortable' => false);
if (isset($subtotal_column))
$cols['Sub-Total']['index'] =
'subtotal-' . $cols[$subtotal_column]['index'];
if ((isset($action) && $action == 'unreconciled') ||
(isset($config) && isset($config['action']) && $config['action'] == 'unreconciled')) {
if (isset($config) && !isset($config['grid_div_id']))
$config['grid_div_id'] = 'unpaid';
$include_columns = array('Entry', 'Date',
'Effective', 'Customer', 'Unit',
'Account', 'Amount', 'Received', 'Balance');
}
else {
$include_columns =
array_diff(array_keys($cols),
array('Transaction', 'Through', 'Lease',
'Amount', 'Received', 'Balance', 'Sub-Total',
'Comment'));
}
// Include custom data
$grid->customData(compact('statement_entry_id'));
// Render the grid
$grid
->columns($cols)
->sortField('Date', 'DESC')
->defaultFields(array('Entry', 'Date', 'Charge', 'Payment'))
->searchFields(array('Customer', 'Unit'))
->render($this, isset($config) ? $config : null, $include_columns);

View File

@@ -52,8 +52,8 @@ if (isset($rows) && is_array($rows) && count($rows)) {
foreach ($rows AS $r => &$row) {
foreach ($row AS $c => $col) {
$cell_class = implode(" ", array_merge(isset( $row_class[$r]) ? $row_class[$r] : array(),
isset($column_class[$c]) ? $column_class[$c] : array()));
$cell_class = implode(" ", array_merge(empty( $row_class[$r]) ? array() : $row_class[$r],
empty($column_class[$c]) ? array() : $column_class[$c]));
if ($cell_class)
$row[$c] = array($col, array('class' => $cell_class));
}
@@ -64,9 +64,12 @@ if (isset($rows) && is_array($rows) && count($rows)) {
$class = implode(' ', $class);
// OK, output the table HTML
echo('<TABLE' . (isset($class) ? ' CLASS="'.$class.'"' : '') . '>' . "\n");
echo('<TABLE' .
(empty($id) ? '' : ' ID="'.$id.'"') .
(empty($class) ? '' : ' CLASS="'.$class.'"') .
'>' . "\n");
if (isset($caption))
if (!empty($caption))
echo(' <CAPTION>' . $caption . '</CAPTION>' . "\n");
if (isset($headers) && is_array($headers)) {

View File

@@ -0,0 +1,20 @@
<?php /* -*- mode:PHP -*- */
// Define the table columns
$cols = array();
$cols['Date'] = array('index' => 'Transaction.stamp', 'formatter' => 'date');
$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Item'] = array('index' => 'Tender.name', 'formatter' => 'longname');
$cols['Type'] = array('index' => 'TenderType.name', 'formatter' => 'name');
$cols['Comment'] = array('index' => 'Tender.comment', 'formatter' => 'comment');
$cols['Amount'] = array('index' => 'LedgerEntry.amount', 'formatter' => 'currency');
$cols['Sub-Total'] = array('index' => 'subtotal-LedgerEntry.amount', 'formatter' => 'currency');
// Render the grid
$grid
->columns($cols)
->sortField('Date')
->defaultFields(array('Date', 'Name', 'Amount'))
->searchFields(array('Name', 'Type'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Comment', 'Sub-Total')));

View File

@@ -1,20 +1,25 @@
<?php /* -*- mode:PHP -*- */
if (isset($include))
$include = is_array($include) ? $include : array($include);
else
$include = array();
// Define the table columns
$cols = array();
$cols['ID'] = array('index' => 'Transaction.id', 'formatter' => 'id');
//$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Type'] = array('index' => 'Transaction.type', 'formatter' => 'enum');
$cols['Customer'] = array('index' => 'Customer.name', 'formatter' => 'longname');
$cols['Timestamp'] = array('index' => 'Transaction.stamp', 'formatter' => 'date');
$cols['Due'] = array('index' => 'Transaction.due_date', 'formatter' => 'date');
$cols['Amount'] = array('index' => 'Transaction.amount', 'formatter' => 'currency');
$cols['Entries'] = array('index' => 'entries', 'formatter' => 'number');
$cols['Comment'] = array('index' => 'Transaction.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Due', 'Comment'));
// Render the grid
$grid
->columns($cols)
->sortField('ID')
->sortField('Timestamp', 'DESC')
->defaultFields(array('ID', 'Timestamp'))
->render($this, isset($config) ? $config : null);
->searchFields(array('Type', 'Comment'))
->render($this, isset($config) ? $config : null,
array_merge($include, array_diff(array_keys($cols), array('Customer', 'Comment'))));

View File

@@ -0,0 +1,40 @@
<?php /* -*- mode:PHP -*- */
// Define the table columns
$cols = array();
$cols['Size'] = array('index' => 'UnitSize.name', 'formatter' => 'shortname');
$cols['Type'] = array('index' => 'UnitType.name', 'formatter' => 'shortname');
$cols['Width'] = array('index' => 'UnitSize.width', 'formatter' => 'number');
$cols['Depth'] = array('index' => 'UnitSize.depth', 'formatter' => 'number');
$cols['Height'] = array('index' => 'UnitSize.height', 'formatter' => 'number');
$cols['Area'] = array('index' => 'sqft', 'formatter' => 'number');
$cols['Volume'] = array('index' => 'cuft', 'formatter' => 'number');
$cols['Deposit'] = array('index' => 'UnitSize.deposit', 'formatter' => 'currency');
$cols['Rent'] = array('index' => 'UnitSize.rent', 'formatter' => 'currency');
$cols['Per Foot'] = array('index' => 'sqcost', 'formatter' => 'currency');
$cols['Per Cubic Ft'] = array('index' => 'cucost', 'formatter' => 'currency');
$cols['Unavailable'] = array('index' => 'unavailable', 'formatter' => 'number');
$cols['Occupied'] = array('index' => 'occupied', 'formatter' => 'number');
$cols['Available'] = array('index' => 'available', 'formatter' => 'number');
$cols['Total'] = array('index' => 'units', 'formatter' => 'number');
$cols['Occupancy'] = array('index' => 'occupancy', 'formatter' => 'percentage', 'formatoptions' => array('precision' => 0));
$cols['Vacancy'] = array('index' => 'vacancy', 'formatter' => 'percentage', 'formatoptions' => array('precision' => 0));
$cols['Comment'] = array('index' => 'Unit.comment', 'formatter' => 'comment');
// Render the grid
$grid
->columns($cols)
->sortField('Area')
->defaultFields(array('Size', 'Area'))
->searchFields(array('Size', 'Width', 'Depth', 'Area', 'Deposit', 'Rent'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Width', 'Depth',
'Deposit',
'Height', 'Volume',
//'Per Foot',
'Per Cubic Ft',
'Occupied',
//'Total',
'Occupancy',
//'Vacancy',
'Comment')));

View File

@@ -3,22 +3,24 @@
// Define the table columns
$cols = array();
$cols['Sort'] = array('index' => 'Unit.sort_order', 'hidden' => true);
//$cols['Sort'] = array('index' => 'Unit.sort_order');
//$cols['Walk'] = array('index' => 'Unit.walk_order');
$cols['ID'] = array('index' => 'Unit.id', 'formatter' => 'id');
$cols['Unit'] = array('index' => 'Unit.name', 'width' => '50');
$cols['Size'] = array('index' => 'UnitSize.name', 'width' => '75');
$cols['Status'] = array('index' => 'Unit.status', 'width' => '75');
$cols['Walk'] = array('index' => 'Unit.walk_order', 'formatter' => 'number');
$cols['Unit'] = array('index' => 'Unit.name', 'formatter' => 'shortname');
$cols['Size'] = array('index' => 'UnitSize.name', 'formatter' => 'shortname');
$cols['Area'] = array('index' => 'sqft', 'formatter' => 'number');
$cols['Rent'] = array('index' => 'Unit.rent', 'formatter' => 'currency');
$cols['Deposit'] = array('index' => 'Unit.deposit', 'formatter' => 'currency');
$cols['Status'] = array('index' => 'Unit.status', 'formatter' => 'enum');
$cols['Balance'] = array('index' => 'balance', 'formatter' => 'currency');
$cols['Comment'] = array('index' => 'Unit.comment', 'formatter' => 'comment');
// Set up search fields if requested by caller
if (isset($searchfields))
$grid->searchFields(array('Unit', 'Size', 'Status'));
if (in_array($this->params['action'], array('vacant', 'unavailable')))
$grid->invalidFields('Balance');
// Render the grid
$grid
->columns($cols)
->sortField('Sort')
->defaultFields(array('Sort', 'ID', 'Unit'))
->render($this, isset($config) ? $config : null);
->defaultFields(array('Sort', 'Unit'))
->searchFields(array('Unit', 'Size', 'Status'))
->render($this, isset($config) ? $config : null,
array_diff(array_keys($cols), array('Walk', 'Deposit', 'Comment')));

View File

@@ -0,0 +1,3 @@
<?php /* -*- mode:PHP -*- */
if (!empty($message))
echo $message;

View File

@@ -19,14 +19,26 @@ class FormatHelper extends AppHelper {
true));
}
function currency($amount, $spans = false) {
function currency($amount, $spans = false, $dollar_sign = null) {
if (!isset($amount))
return '-';
//return null;
$currency = self::$number->currency($amount,
'USD',
$spans ? array('before'=>'', 'after'=>'') : array());
// Use of the $number->currency() function results in the clever,
// but problematic, use of cents for amounts less than $1. For
// example, 50 cents is shown as '50c', not '$0.50'. We want to
// keep everything in terms of dollars, especially for the cases
// where this result is placed into a form for input. 50 cents
// will end up as 50 dollars upon submission :-(
$currency = self::$number->format
(abs($amount),
array('places' => 2,
'before' => $spans ? '' : (isset($dollar_sign) ? $dollar_sign : '$'),
'after' => $spans ? '' : null,
));
if ($amount < 0)
$currency = '(' . $currency . ')';
if ($spans)
return ('<SPAN CLASS="dollar-sign">$</SPAN>' .
@@ -35,19 +47,36 @@ class FormatHelper extends AppHelper {
return $currency;
}
function date($date, $age = false) {
if (!$date) return null;
function percent($amount, $precision = 2) {
if (!isset($amount))
return '-';
$date_fmt = 'm/d/Y';
return (self::$time->format($date_fmt, $date) .
($age
? ' (' . self::age($date) . ')'
: ''));
return self::$number->toPercentage($amount*100, $precision);
}
function datetime($datetime) {
if (!$datetime) return null;
return self::$time->nice($datetime);
function date($date, $age = false, $class = null, $time = false) {
if (!$date) return null;
if (empty($class))
$class = '';
if ($time)
$date_html = self::$time->nice($date);
else
$date_html = self::$time->format('m/d/Y', $date);
$date_html = '<span class="fmt-date '.$class.'">'.$date_html.'</span>';
if ($age) {
$date_html .= ' (' . self::age($date, $class, true, $time ? 0 : 60*60*24) . ')';
$date_html = '<span class="fmt-dateage '.$class.'">'.$date_html.'</span>';
}
return $date_html;
}
function datetime($datetime, $age = false, $class = null) {
return self::date($datetime, $age, $class, true);
}
function phone($phone, $ext = null) {
@@ -78,10 +107,13 @@ class FormatHelper extends AppHelper {
return $comment;
}
function age($datetime) {
function age($datetime, $class, $suffix = false, $min_span = 0) {
if (!isset($datetime))
return null;
if (empty($class))
$class = '';
$now = time();
$seconds = self::$time->fromString($datetime);
$backwards = ($seconds > $now);
@@ -90,62 +122,109 @@ class FormatHelper extends AppHelper {
$timeto = $backwards ? $seconds : $now;
$span = $timeto - $timefrom;
// Display seconds if under 45 seconds
//pr(compact('now', 'seconds', 'backwards', 'timefrom', 'timeto', 'span', 'min_span'));
// If now, just use 'now'
if ($span === 0) {
return __('now', true);
$approx = 0;
$unit = 'now';
}
if ($span < 45) {
// Display seconds if under 45 seconds
if ($span < 45 && $span >= $min_span) {
$approx = round($span);
$unit = 'second';
}
// Display minutes if under 45 minutes
elseif (($span /= 60) < 45) {
$approx = round($span);
$unit = 'minute';
}
// Display hours if under 18 hours
elseif (($span /= 60) < 18) {
$approx = round($span);
$unit = 'hour';
}
// Display days if under 6.5 days
elseif (($span /= 24) < 6.5) {
$approx = round($span);
$unit = 'day';
}
// Display weeks if less than 8 weeks
elseif (($span /= 7) < 8) {
$approx = round($span);
$unit = 'week';
}
// Display months if less than 20 months
elseif (($span /= (365.2425 / (7 * 12))) < 20) {
$approx = round($span);
$unit = 'month';
// Months are from 28-31 days. If it's too
// close to being an exact month, just fudge
// by saying the result is 'about' N months
// instead of 'almost' or 'over' N months,
// since we can't be accurate on this without
// taking into account the day of the week.
if ((abs($span - $approx) * (365.2425 / 12)) < 3)
$relative = 'about';
// Display minutes if under 45 minutes
if (!isset($approx)) {
$unit = 'minute';
$span /= 60; $min_span /= 60;
if ($span < 45 && ($span >= $min_span || $min_span <= 1))
$approx = round($span);
}
// Display hours if under 18 hours
if (!isset($approx)) {
$unit = 'hour';
$span /= 60; $min_span /= 60;
if ($span < 18 && ($span >= $min_span || $min_span <= 1))
$approx = round($span);
}
// Display days if under 6.5 days
if (!isset($approx)) {
$unit = 'day';
$span /= 24; $min_span /= 24;
if ($span < 6.5 && ($span >= $min_span || $min_span <= 1))
$approx = round($span);
}
// Display weeks if less than 8 weeks
if (!isset($approx)) {
$unit = 'week';
$span /= 7; $min_span /= 7;
if ($span < 8 && ($span >= $min_span || $min_span <= 1))
$approx = round($span);
}
// Display months if less than 20 months
if (!isset($approx)) {
$unit = 'month';
$span /= 365.2425 / (7*12); $min_span /= 365.2425 / (7*12);
if ($span < 20 && ($span >= $min_span || $min_span <= 1)) {
$approx = round($span);
// Months are from 28-31 days. If it's too
// close to being an exact month, just fudge
// by saying the result is 'about' N months
// instead of 'almost' or 'over' N months,
// since we can't be accurate on this without
// taking into account the day of the week.
if ((abs($span - $approx) * (365.2425 / 12)) < 3)
$relative = 'about';
}
}
// Otherwise, just display years
if (!isset($approx)) {
$unit = 'year';
$span /= 12; $min_span /= 12;
$approx = round($span);
}
//pr(compact('span', 'min_span', 'approx', 'unit'));
if ($approx == 0) {
if ($unit == 'now')
$age = 'now';
elseif ($unit == 'day')
$age = 'today';
else
$age = 'this ' . $unit;
}
else {
$span /= 12;
$approx = round($span);
$unit = 'year';
if (isset($relative))
$age = $relative;
elseif ($approx > $span)
$age = 'almost';
elseif ($approx < $span)
$age = 'over';
else
$age = '';
$age .= ' ' . self::_n($approx, $unit);
if ($suffix) {
if ($backwards)
$age .= ' from now';
else
$age .= ' ago';
}
}
return (__(isset($relative)
? $relative
: ($approx == $span
? ''
: ($approx > $span ? 'almost' : 'over')), true)
. ' '
. self::_n($approx, $unit)
. ($backwards ? '' : __(' ago', true)));
$age = '<span class="fmt-age '.$class.'">'.__($age, true).'</span>';
return $age;
}
/*****************************
@@ -196,14 +275,21 @@ class FormatHelper extends AppHelper {
// Helper function to convert PHP vars to javascript
function phpVarToJavascript($var, $name = '', $depth='', $special = false) {
function phpVarToJavascript($var, $name = '', $depth='', $special = false, $pretty = false) {
// Establish a prefix to use before printing $var
$prefix = $depth;
if ($pretty) {
$prefix = $depth;
$pretty_sp = " ";
$pretty_nl = "\n";
}
else {
$prefix = $pretty_sp = $pretty_nl = '';
}
// If given a name, set it up JS style
if ($name)
$prefix .= $name . ": ";
$prefix .= $name . ":" . $pretty_sp;
if (!isset($var))
return $prefix . 'null';
@@ -256,22 +342,22 @@ class FormatHelper extends AppHelper {
// PHP array indices can be a mix of integer and string based.
// Just guess here, unless flagged as a special case.
if (isset($var[0]) || $special)
return ($prefix . "[\n"
. implode(",\n",
return ($prefix . "[" . $pretty_nl
. implode("," . $pretty_nl,
array_map(array('FormatHelper', 'phpVarToJavascript'),
array_values($var),
array(),
array_fill(0, count($var), $depth.' ')
))
. "\n$depth]");
. ($pretty ? "\n$depth" : '') . "]");
return ($prefix . "{\n"
. implode(",\n",
return ($prefix . "{" . $pretty_nl
. implode("," . $pretty_nl,
array_map(array('FormatHelper', 'phpVarToJavascript'),
array_values($var), array_keys($var),
array_fill(0, count($var), $depth.' ')
))
. "\n$depth}");
. ($pretty ? "\n$depth" : '') . "}");
}
}

View File

@@ -6,6 +6,7 @@ class GridHelper extends AppHelper {
var $included, $invalid;
var $columns;
var $controller;
static $first_grid = true;
function __construct() {
$this->reset();
@@ -91,6 +92,7 @@ class GridHelper extends AppHelper {
= array_map(create_function('$data',
'return $data["id"];'),
$items);
$this->jqGrid_options['action'] = 'idlist';
return $this;
}
@@ -159,14 +161,24 @@ class GridHelper extends AppHelper {
$included = array_merge($included, $config['include']);
if (isset($config['exclude']))
$excluded = array_merge($excluded, $config['exclude']);
unset($config['include'], $config['exclude']);
// Calculate the actual inclusion set
$included = array_diff(array_merge($this->included, $included),
array_merge($this->invalid, $excluded));
// Extract the columns that correspond to the inclusion set
$this->jqGrid_options['jqGridColumns']
= array_intersect_key($this->columns, array_flip($included));
// Defined the columns, based on the inclusion set,
// remapping column names as necessary.
$this->jqGrid_options['jqGridColumns'] = array();
foreach (array_intersect_key($this->columns, array_flip($included)) AS $name => $col) {
if (!empty($config['remap'][$name]))
$name = $config['remap'][$name];
$this->jqGrid_options['jqGridColumns'][$name] = $col;
}
// Make sure search fields are all part of the inclusion set
$this->jqGrid_options['search_fields']
= array_intersect($this->jqGrid_options['search_fields'], $included);
// As an exception to the normal config variables,
// handle 'rows' here. The reason is so that we
@@ -177,8 +189,17 @@ class GridHelper extends AppHelper {
if (isset($config['rows'])) {
// Shrink the limit... user can always override
$this->id_list($config['rows'])->limit(10);
unset($config['rows']);
}
// One more exception, as the search fields get
// defined, but not passed to jqGrid unless
// specifically requested.
if (isset($config['search']))
unset($config['search']);
else
unset($this->jqGrid_options['search_fields']);
// Figure out what controller we're using to
// populate the grid via ajax, and set it.
$controller = $this->controller;
@@ -194,11 +215,22 @@ class GridHelper extends AppHelper {
}
$this->jqGrid_options['controller'] = $controller;
// Add in any custom variables requested
if (isset($config['custom'])) {
$this->customData($config['custom']);
unset($config['custom']);
}
// Incorporate all other user options
if (isset($config))
$this->jqGrid_options = array_merge($this->jqGrid_options, $config);
// Set flag whether or not this is the first grid
$this->jqGrid_options['first_grid'] = self::$first_grid;
//pr(compact('config') + array('jqGrid_options' => $this->jqGrid_options));
echo $view->element('jqGrid', $this->jqGrid_options);
self::$first_grid = false;
// Since we only have one instance of this class
// as a helper, we must assume it could be used

View File

@@ -24,26 +24,80 @@
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<?php
/* print("<!--\n"); */
/* print("SERVER = "); print_r($_SERVER); print("\n"); */
/* print("REQUEST = "); print_r($_REQUEST); print("\n"); */
/* print("COOKIE = "); print_r($_COOKIE); print("\n"); */
/* print("-->\n"); */
?>
<head>
<?php echo $html->charset(); ?>
<title>
<?php if (devbox()) echo "*DEVBOX* "; ?>
<?php if (sandbox()) echo "*SANDBOX* "; ?>
Property Manager: <?php echo $title_for_layout; ?>
<?php if (sandbox()) echo " *SANDBOX*"; ?>
<?php if (devbox()) echo " *DEVBOX*"; ?>
</title>
<?php
// Reset the __scripts variable, which has already been dumped to
// $scripts_for_layout. Elements/Helpers used in the layout may
// also have some scripts to add. They cannot be put into the head
// but we can at least put them into a relatively benign place, so
// scripts don't have to be dumped inline in possibly awkward spots.
// Oh, and yes... I know we're not supposed to be using this variable
// directly, and will possibly get burned someday. Oh well, Cake
// hasn't left us a lot of choice, besides writing our own scripts
// mechanism _additional_ to what Cake has provided :-/
$this->__scripts = array();
if (!empty($_SERVER['HTTPS']))
$protocol = 'https://';
else
$protocol = 'http://';
echo $html->meta('icon') . "\n";
$theme = 'smoothness';
$theme = 'base';
$theme = 'dotluv';
$theme = 'dark-hive';
$theme = 'start';
if (devbox())
$theme = 'dotluv';
if (sandbox())
$theme = 'darkness';
echo $html->css('cake.generic') . "\n";
echo $html->css('themes/'.$theme.'/ui.all') . "\n";
echo $html->css('layout') . "\n";
echo $html->css('print', null, array('media' => 'print')) . "\n";
echo $html->css('sidemenu') . "\n";
//echo $html->css('jquery/base/ui.all') . "\n";
//echo $html->css('jquery/smoothness/ui.all') . "\n";
//echo $html->css('jquery/dotluv/ui.all') . "\n";
echo $html->css('jquery/start/ui.all') . "\n";
echo $javascript->link('jquery/jquery') . "\n";
echo $javascript->link('jquery/jquery-ui') . "\n";
echo $javascript->link('jquery-1.3.2.min') . "\n";
echo $javascript->link('jquery-ui-1.7.2.custom.min') . "\n";
//echo $javascript->link($protocol . 'ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js') . "\n";
//echo $javascript->link($protocol . 'ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.js') . "\n";
//echo $javascript->link($protocol . 'ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js') . "\n";
//echo $javascript->link($protocol . 'ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js') . "\n";
echo $javascript->link('jquery.form') . "\n";
echo $javascript->link('jquery.hoverIntent') . "\n";
echo $javascript->link('pmgr') . "\n";
echo $scripts_for_layout . "\n";
?>
<?php if ($this->params['action'] !== 'INTERNAL_ERROR'): ?>
<script type="text/javascript"><!--
if (typeof(jQuery) == 'undefined') {
window.location.href =
"<?php echo $html->url(array('controller' => 'util',
'action' => 'INTERNAL_ERROR',
'jQuery NOT LOADED!')); ?>";
}
--></script>
<?php endif; ?>
</head>
<body>
@@ -88,5 +142,7 @@
<?php echo $cakeDebug; ?>
<?php /* pr($this); */ ?>
<?php echo implode("\n", $this->__scripts) . "\n"; ?>
</body>
</html>

View File

@@ -1,77 +0,0 @@
<?php /* -*- mode:PHP -*- */
echo '<div class="apply-deposit input">' . "\n";
echo ('<DIV CLASS="apply-deposit grid-selection-text">' .
'Lease #' . $lease['number'] .
' / Customer #' . $customer['id'] .
': ' . $customer['name'] .
' / Unit ' . $unit['name'] .
'<DIV CLASS="supporting">' .
'<TABLE>' .
'<TR><TD CLASS="field">Balance:</TD><TD CLASS="value">'.$lease['stats']['balance'].'</TD></TR>' .
'<TR><TD CLASS="field">Deposit:</TD><TD CLASS="value">'.$deposit['summary']['balance'].'</TD></TR>' .
'</TABLE>' .
'</DIV>' .
'</DIV>' . "\n");
echo $form->create(null, array('id' => 'receipt-form',
'url' => array('controller' => 'transactions',
'action' => 'postReceipt')));
echo $form->input("Customer.id",
array('id' => 'customer-id',
'type' => 'hidden',
'value' => $customer['id']));
echo $form->input("Lease.id",
array('id' => 'lease-id',
'type' => 'hidden',
'value' => $lease['id']));
echo $form->input("LedgerEntry.0.account_id",
array('id' => 'account-id',
'type' => 'hidden',
'value' => $account['id']));
echo $this->element('form_table',
array('class' => "item receipt transaction entry",
//'with_name_after' => ':',
'field_prefix' => 'Transaction',
'fields' => array
("stamp" => array('opts' => array('type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerNow(\'TransactionStamp\'); return false;">Now</A>',
),
"amount" => array('prefix' => 'LedgerEntry.0'),
"comment" => array('opts' => array('size' => 50),
),
)));
echo $form->end('Utilize Deposit');
?>
<script type="text/javascript"><!--
// Reset the form
function resetForm() {
datepickerNow('TransactionStamp');
}
$(document).ready(function(){
$("#TransactionStamp")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
resetForm();
});
--></script>
</div>

View File

@@ -1,80 +0,0 @@
<?php /* -*- mode:PHP -*- */
echo '<div class="bad-debt input">' . "\n";
echo ('<DIV CLASS="bad-debt grid-selection-text">' .
'Lease #' . $lease['number'] .
' / Customer #' . $customer['id'] .
': ' . $customer['name'] .
' / Unit ' . $unit['name'] .
'<DIV CLASS="supporting">' .
'<TABLE>' .
'<TR><TD CLASS="field">Balance:</TD><TD CLASS="value">'.$lease['stats']['balance'].'</TD></TR>' .
'</TABLE>' .
'</DIV>' .
'</DIV>' . "\n");
echo $form->create(null, array('id' => 'receipt-form',
'url' => array('controller' => 'transactions',
'action' => 'postReceipt')));
echo $form->input("Customer.id",
array('id' => 'customer-id',
'type' => 'hidden',
'value' => $customer['id']));
echo $form->input("Lease.id",
array('id' => 'lease-id',
'type' => 'hidden',
'value' => $lease['id']));
echo $form->input("LedgerEntry.0.account_id",
array('id' => 'account-id',
'type' => 'hidden',
'value' => $account['id']));
echo $form->input("LedgerEntry.0.amount",
array('id' => 'amount',
'type' => 'hidden',
'value' => $lease['stats']['balance']));
echo $this->element('form_table',
array('class' => "item receipt transaction entry",
//'with_name_after' => ':',
'field_prefix' => 'Transaction',
'fields' => array
("stamp" => array('opts' => array('type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerNow(\'TransactionStamp\'); return false;">Now</A>',
),
"comment" => array('opts' => array('size' => 50),
),
)));
echo $form->end('Write Off Remaining Balance');
?>
<script type="text/javascript"><!--
// Reset the form
function resetForm() {
datepickerNow('TransactionStamp');
}
$(document).ready(function(){
$("#TransactionStamp")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
resetForm();
});
--></script>
</div>

View File

@@ -4,6 +4,14 @@
<?php
; // Editor alignment
$unit = $lease['Unit'];
$customer = $lease['Customer'];
if (isset($lease['Lease']))
$lease = $lease['Lease'];
//pr(compact('unit', 'customer', 'lease', 'movein'));
/**********************************************************************
**********************************************************************
**********************************************************************
@@ -11,105 +19,134 @@
* Javascript
*/
// Warnings _really_ screw up javascript
$saved_debug_state = Configure::read('debug');
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
var lease_charge_through;
// prepare the form when the DOM is ready
$(document).ready(function() {
var options = {
var options = {
target: '#output-debug', // target element(s) to be updated with server response
beforeSubmit: verifyRequest, // pre-submit callback
success: showResponse, // post-submit callback
// other available options:
//url: url, // override for form's 'action' attribute
//type: 'get', // 'get' or 'post', override for form's 'method' attribute
//dataType: null, // 'xml', 'script', or 'json' (expected server response type)
//clearForm: true, // clear all form fields after successful submit
//resetForm: true, // reset the form after successful submit
// $.ajax options can be used here too, for example:
//timeout: 3000,
url: "<?php echo $html->url(array('controller' => 'transactions',
'action' => 'postInvoice', 0)); ?>"
};
// bind form using 'ajaxForm'
// bind form using 'ajaxForm'
if ($('#invoice-form').ajaxForm != null)
$('#invoice-form').ajaxForm(options);
else
$('#repeat, label[for=repeat]').remove();
});
// pre-submit callback
function verifyRequest(formData, jqForm, options) {
// formData is an array; here we use $.param to convert it to a string to display it
// but the form plugin does this for you automatically when it submits the data
//var_dump(formData);
//$('#request-debug').html('<PRE>'+dump(formData)+'</PRE>');
$('#request-debug').html('Ommitted');
//$("#debug").html('');
for (var i = 0; i < formData.length; ++i) {
//$("#debug").append(i + ') ' + dump(formData[i]) + '<BR>');
if (formData[i]['name'] == "data[Lease][id]" &&
!(formData[i]['value'] > 0)) {
//$("#debug").append('<P>Missing Lease ID');
alert("Must select a lease first");
return false;
}
if (formData[i]['name'] == "data[Transaction][stamp]" &&
formData[i]['value'] == '') {
//$("#debug").append('<P>Bad Stamp');
if (formData[i]['value'] != '')
alert(formData[i]['value'] + " is not valid date stamp. Please correct it.");
else
alert("Please enter a valid date stamp first.");
return false;
}
// Terrible way to accomplish this...
for (var j = 0; j < 20; ++j) {
if (formData[i]['name'] == "data[Entry]["+j+"][amount]") {
var val = formData[i]['value'].replace(/\$/,'');
//$("#debug").append('<P>Bad Amount');
if (!(val > 0)) {
if (formData[i]['value'] == '')
alert("Please enter an amount for Charge #"+j+", or remove the Charge completely.");
else
alert('"'+formData[i]['value']+'"' + " is not a valid amount for Charge #"+j+". Please correct it.");
return false;
}
}
}
}
//$("#debug").append('OK');
//return false;
$('#response-debug').html('Loading <BLINK>...</BLINK>');
$('#output-debug').html('Loading <BLINK>...</BLINK>');
// here we could return false to prevent the form from being submitted;
// returning anything other than false will allow the form submit to continue
$('#results').html('Working <BLINK>...</BLINK>');
return true;
}
// post-submit callback
function showResponse(responseText, statusText) {
// for normal html responses, the first argument to the success callback
// is the XMLHttpRequest object's responseText property
// if the ajaxForm method was passed an Options Object with the dataType
// property set to 'xml' then the first argument to the success callback
// is the XMLHttpRequest object's responseXML property
// if the ajaxForm method was passed an Options Object with the dataType
// property set to 'json' then the first argument to the success callback
// is the json data object returned by the server
if (statusText == 'success') {
var amount = 0;
$("input.invoice.amount").each(function(i) {
amount += (+ $(this).val().replace(/\$/,''));
});
$('#results').html('<H3>Invoice Saved<BR>' +
$("#invoice-customer").html() +
' : ' + fmtCurrency(amount) +
'</H3>');
if (!$("#repeat").attr("checked")) {
window.location.href =
<?php if (empty($movein)): ?>
"<?php echo $html->url(array('controller' => 'customers',
'action' => 'view')); ?>"
+ "/" + $("#customer-id").val();
<?php else: ?>
"<?php echo $html->url(array('controller' => 'customers',
'action' => 'receipt')); ?>"
+ "/" + $("#customer-id").val();
<?php endif; ?>
return;
}
// get a clean slate
//resetForm();
resetForm();
}
else {
alert('not successful??');
$('#results').html('<H2>Failed to save invoice!</H2>');
alert('Failed to save invoice.');
}
$('#response-debug').html('<PRE>'+dump(statusText)+'</PRE>');
}
// Reset the form
function resetForm() {
$("#charge-entry-id").val(1);
function resetForm(nocharge) {
$('#charge-entry-id').val(1);
$('#charges').html('');
$("#invoice-lease").html("INTERNAL ERROR");
$("#invoice-unit").html("INTERNAL ERROR");
$("#invoice-customer").html("INTERNAL ERROR");
$("#invoice-rent").html("INTERNAL ERROR");
$("#invoice-late").html("INTERNAL ERROR");
$("#invoice-deposit").html("INTERNAL ERROR");
addChargeSource(false);
datepickerNow('TransactionStamp');
if (!nocharge)
addChargeSource(false);
}
function onRowSelect(grid_id, lease_id) {
// Set the item id that will be returned with the form
$("#lease-id").val(lease_id);
$("#customer-id").val($(grid_id).getCell(lease_id, 'Customer-id'));
// Get the item names from the grid
//$("#invoice-lease").html($(grid_id).getCell(lease_id, 'Lease-number'));
// REVISIT <AP>: 20090708
// This is not intended as a long term solution,
// but I need a way to enter data and then view
// the results. This link will help.
$("#invoice-lease").html('<A HREF="/pmgr/site/leases/view/' +
$(grid_id).getCell(lease_id, 'Lease-id').replace(/^#/,'') +
'">' +
$(grid_id).getCell(lease_id, 'Lease-number') +
'</A>');
$("#invoice-lease").html($(grid_id).getCell(lease_id, 'Lease-number'));
$("#invoice-unit").html($(grid_id).getCell(lease_id, 'Unit-name'));
$("#invoice-customer").html($(grid_id).getCell(lease_id, 'Customer-name'));
$("#invoice-rent").html($(grid_id).getCell(lease_id, 'Lease-rent'));
@@ -117,6 +154,7 @@ function onRowSelect(grid_id, lease_id) {
$("#invoice-deposit").html($(grid_id).getCell(lease_id, 'Lease-deposit')
? $(grid_id).getCell(lease_id, 'Lease-deposit')
: '-');
lease_charge_through = $(grid_id).getCell(lease_id, 'Lease-charge_through_date')
// Hide the "no lease" message and show the current lease
$(".lease-selection-invalid").hide();
@@ -142,6 +180,78 @@ function onGridState(grid_id, state) {
}
}
function setNextRent(id) {
var chg_thru;
$('.ChargeForm').each( function(i) {
if ($('.ChargeFormThroughDate', this).attr('id') == 'Entry'+id+'ThroughDate')
return;
if ($('.ChargeFormAccount option:selected', this).val() == <?php echo $rentAccount ?>
&& $('.ChargeFormThroughDate', this).val()) {
var dt = new Date($('.ChargeFormThroughDate', this).val());
//$('#debug').append('Rent in ' + i + '; date ' + dt + '<BR>');
if (chg_thru == null || dt > chg_thru)
chg_thru = dt;
}
});
if (!chg_thru)
chg_thru = new Date(lease_charge_through);
if (chg_thru < dateEOM(chg_thru)) {
// Add a charge to finish out the month
datepickerSet('Entry'+id+'EffectiveDate', dateTomorrow(chg_thru));
datepickerSet('Entry'+id+'ThroughDate', dateEOM(chg_thru));
} else {
// Add a whole month's charge for next month
datepickerSet('Entry'+id+'EffectiveDate', dateNextBOM(chg_thru));
datepickerSet('Entry'+id+'ThroughDate', dateNextEOM(chg_thru));
}
// Now add in the amount owed based on the calculated
// effective and through dates.
prorate(id);
}
function prorate(id) {
var edt = datepickerGet('Entry'+id+'EffectiveDate');
var tdt = datepickerGet('Entry'+id+'ThroughDate');
var rent = $('#invoice-rent').html().replace(/\$/,'');
// Reset the comment. It might wipe out a user comment,
// but it's probably low risk/concern
$('#Entry'+id+'Comment').val('');
if (edt == null || tdt == null) {
alert('Can only prorate with both effective and through dates');
rent = 0;
}
else if (edt > tdt) {
alert('Effective date is later than the Through date');
rent = 0;
}
else if (tdt.getMonth() == edt.getMonth() + 1 &&
edt.getDate() == tdt.getDate() + 1) {
// appears to be anniversary billing, one full cycle
}
else if (edt.getTime() == dateBOM(edt).getTime() &&
tdt.getTime() == dateEOM(edt).getTime()) {
// appears to be one full month
}
else {
var one_day=1000*60*60*24;
var days = Math.ceil((tdt.getTime()-edt.getTime()+1)/(one_day));
var dim =
((edt.getMonth() == tdt.getMonth())
? dateEOM(edt).getDate() // prorated within the month.
: 30); // prorated across months.
rent *= days / dim;
$('#Entry'+id+'Comment').val('Rent proration: '+days+'/'+dim+' days');
}
$('#Entry'+id+'Amount').val(fmtCurrency(rent));
}
function addChargeSource(flash) {
var id = $("#charge-entry-id").val();
addDiv('charge-entry-id', 'charge', 'charges', flash,
@@ -152,26 +262,33 @@ function addChargeSource(flash) {
<?php
echo FormatHelper::phpVarToJavascript
($this->element('form_table',
array('class' => "item invoice ledger-entry entry",
array('id' => 'Entry%{id}Form',
'class' => "ChargeForm item invoice ledger-entry entry",
//'with_name_after' => ':',
'field_prefix' => 'LedgerEntry.%{id}',
'field_prefix' => 'Entry.%{id}',
'fields' => array
("account_id" => array('name' => 'Account',
'opts' =>
array('options' => $chargeAccounts,
array('class' => 'ChargeFormAccount',
'options' => $chargeAccounts,
'value' => $defaultAccount,
),
'between' => '<A HREF="#" ONCLICK="setNextRent(\'%{id}\'); return false;">Rent</A>',
),
"effective_date" => array('opts' =>
array('type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerBOM(\'TransactionStamp\',\'LedgerEntry%{id}EffectiveDate\'); return false;">BOM</A>',
array('class' => 'ChargeFormEffectiveDate',
'type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerBOM(\'TransactionStamp\',\'Entry%{id}EffectiveDate\'); return false;">BOM</A>',
),
"through_date" => array('opts' =>
array('type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerEOM(\'LedgerEntry%{id}EffectiveDate\',\'LedgerEntry%{id}ThroughDate\'); return false;">EOM</A>',
array('class' => 'ChargeFormThroughDate',
'type' => 'text'),
'between' => '<A HREF="#" ONCLICK="datepickerEOM(\'Entry%{id}EffectiveDate\',\'Entry%{id}ThroughDate\'); return false;">EOM</A>',
),
"amount" => true,
"comment" => array('opts' => array('size' => 50)),
"amount" => array('opts' => array('class' => 'ChargeFormAmount invoice amount'),
'between' => '<A HREF="#" ONCLICK="prorate(\'%{id}\'); return false;">Prorate</A>',
),
"comment" => array('opts' => array('class' => 'ChargeFormComment', 'size' => 50)),
),
))) . "+\n";
?>
@@ -179,43 +296,40 @@ function addChargeSource(flash) {
'</FIELDSET>'
);
$("#LedgerEntry"+id+"EffectiveDate")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
datepicker("Entry"+id+"EffectiveDate");
datepicker("Entry"+id+"ThroughDate");
$("#LedgerEntry"+id+"ThroughDate")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
return id;
}
--></script>
<?php
; // align
// Re-Enable warnings
Configure::write('debug', $saved_debug_state);
echo $this->element('leases', array
('config' => array
('grid_div_id' => 'leases-list',
'grid_div_class' => 'text-below',
'caption' => ('<A HREF="#" ONCLICK="$(\'#leases-list .HeaderButton\').click();'.
' return false;">Select Lease</A>'),
'grid_setup' => array('hiddengrid' => isset($lease['Lease']['id'])),
'grid_events' => array('onSelectRow' =>
array('ids' =>
'if (ids != null){onRowSelect("#"+$(this).attr("id"), ids);}'),
'onHeaderClick' =>
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), gridstate)'),
),
'nolinks' => true,
'limit' => 10,
)));
if (empty($movein))
echo $this->element('leases', array
('config' => array
('grid_div_id' => 'leases-list',
'grid_div_class' => 'text-below',
'caption' => ('<A HREF="#" ONCLICK="$(\'#leases-list .HeaderButton\').click();'.
' return false;">Select Lease</A>'),
'grid_setup' => array('hiddengrid' => isset($lease['id'])),
'grid_events' => array('onSelectRow' =>
array('ids' =>
'if (ids != null){onRowSelect("#"+$(this).attr("id"), ids);}'),
'onHeaderClick' =>
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), gridstate)'),
),
'include' => array('Charge-Thru'),
'exclude' => array('Closed', 'Paid-Thru'),
'action' => 'active',
'nolinks' => true,
'limit' => 10,
)));
echo ('<DIV CLASS="invoice grid-selection-text">' .
@@ -249,6 +363,11 @@ echo $form->input("Lease.id",
'type' => 'hidden',
'value' => 0));
echo $form->input("Customer.id",
array('id' => 'customer-id',
'type' => 'hidden',
'value' => 0));
/* echo '<fieldset CLASS="invoice">' . "\n"; */
/* echo ' <legend>Invoice</legend>' . "\n"; */
@@ -267,6 +386,13 @@ echo $this->element('form_table',
/* echo '</fieldset>' . "\n"; */
if (empty($movein)) {
echo "<BR>\n";
echo $form->input('repeat', array('type' => 'checkbox',
'id' => 'repeat',
'label' => 'Enter Multiple Invoices')) . "\n";
}
echo $form->submit('Generate Invoice') . "\n";
?>
@@ -281,41 +407,117 @@ echo $form->submit('Generate Invoice') . "\n";
<?php echo $form->end('Generate Invoice'); ?>
<div><H4>Request</H4><div id="request-debug"></div></div>
<div><H4>Response</H4><div id="response-debug"></div></div>
<div><H4>Output</H4><div id="output-debug"></div></div>
<div id="results"></div>
<div id="output-debug" style="display:none"></div>
<?php
// Warnings _really_ screw up javascript
Configure::write('debug', '0');
?>
<script type="text/javascript"><!--
$.fn.removeCol = function(col){
if(!col){ col = 1; }
$('tr td:nth-child('+col+'), tr th:nth-child('+col+')', this).remove();
};
function addHidden(id, fld, name) {
$('#Entry'+id+fld).after
('<input type="hidden"' +
' name="data[Entry]['+id+']['+name+']"' +
' value="' + $('#Entry'+id+fld).val() + '">');
}
$(document).ready(function(){
$("#TransactionStamp")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
datepicker('TransactionStamp');
<?php if (isset($lease['id'])): ?>
$("#lease-id").val(<?php echo $lease['id']; ?>);
$("#customer-id").val(<?php echo $customer['id']; ?>);
$("#invoice-lease").html("<?php echo '#'.$lease['number']; ?>");
$("#invoice-unit").html("<?php echo $unit['name']; ?>");
$("#invoice-customer").html("<?php echo $customer['name']; ?>");
$("#invoice-rent").html("<?php echo FormatHelper::currency($lease['rent']); ?>");
$("#invoice-late").html("<?php echo FormatHelper::currency($defaultLate); ?>");
$("#invoice-deposit").html("<?php echo FormatHelper::currency($lease['deposit']); ?>");
lease_charge_through = <?php
if ($lease['charge_through_date'])
echo 'new Date("'.date('m/d/Y', strtotime($lease['charge_through_date'])).'")';
elseif ($lease['paid_through_date'])
echo 'new Date("'.date('m/d/Y', strtotime($lease['paid_through_date'])).'")';
else
echo 'dateYesterday("'.date('m/d/Y', strtotime($lease['movein_date'])).'")';
?>;
<?php else: ?>
$("#lease-id").val(0);
$("#customer-id").val(<?php echo empty($movein) ? 0 : $customer['id']; ?>);
$("#invoice-lease").html("INTERNAL ERROR");
$("#invoice-unit").html("INTERNAL ERROR");
$("#invoice-customer").html("INTERNAL ERROR");
$("#invoice-rent").html("INTERNAL ERROR");
$("#invoice-late").html("INTERNAL ERROR");
$("#invoice-deposit").html("INTERNAL ERROR");
<?php endif; ?>
<?php if (empty($movein)): ?>
resetForm();
datepickerNow('TransactionStamp');
<?php if (isset($lease['Lease']['id'])): ?>
$("#lease-id").val(<?php echo $lease['Lease']['id']; ?>);
//$("#invoice-lease").html("<?php echo '#'.$lease['Lease']['number']; ?>");
$("#invoice-lease").html('<A HREF="/pmgr/site/leases/view/' +
"<?php echo $lease['Lease']['id']; ?>" +
'">#' +
"<?php echo $lease['Lease']['number']; ?>" +
'</A>');
$("#invoice-unit").html("<?php echo $lease['Unit']['name']; ?>");
$("#invoice-customer").html("<?php echo $lease['Customer']['name']; ?>");
$("#invoice-rent").html("<?php echo FormatHelper::currency($lease['Lease']['rent']); ?>");
$("#invoice-late").html('$10.00');
$("#invoice-deposit").html("<?php echo FormatHelper::currency($lease['Lease']['deposit']); ?>");
<?php else: ?>
var id;
resetForm(true);
$("#TransactionStamp").attr('disabled', true);
$("#TransactionStamp").val("<?php echo date('m/d/Y', $movein['time']); ?>");
$('#TransactionStamp').after
('<input type="hidden"' +
' name="data[Transaction][stamp]"' +
' value="' + $("#TransactionStamp").val() + '">');
$("#TransactionComment").val('Move-In Charges');
<?php if ($movein['deposit'] != 0): ?>
id = addChargeSource(false);
$('#Entry'+id+'Form').removeCol(2);
$('#Entry'+id+'Form input, #Entry'+id+'Form select').attr('disabled', true);
$('#Entry'+id+'EffectiveDate').val("<?php echo date('m/d/Y', $movein['effective_time']); ?>");
addHidden(id, 'EffectiveDate', 'effective_date');
$('#Entry'+id+'AccountId').val(<?php echo $securityDepositAccount; ?>);
addHidden(id, 'AccountId', 'account_id');
$('#Entry'+id+'Amount').val("<?php echo FormatHelper::currency($movein['deposit']); ?>");
addHidden(id, 'Amount', 'amount');
$('#Entry'+id+'Comment').removeAttr('disabled');
<?php endif; ?>
id = addChargeSource(false);
$('#Entry'+id+'Form').removeCol(2);
$('#Entry'+id+'Form input, #Entry'+id+'Form select').attr('disabled', true);
setNextRent(id);
addHidden(id, 'EffectiveDate', 'effective_date');
addHidden(id, 'ThroughDate', 'through_date');
addHidden(id, 'AccountId', 'account_id');
addHidden(id, 'Amount', 'amount');
$('#Entry'+id+'Comment').removeAttr('disabled');
<?php endif; ?>
<?php if (isset($lease['id'])): ?>
onGridState(null, 'hidden');
<?php else: ?>
onGridState(null, 'visible');
<?php endif; ?>
<?php if ($this->params['dev']): ?>
$('#output-debug').html('Post Output');
$('#output-debug').show();
<?php endif; ?>
});
--></script>
</div>
<a href="#" onClick="$('#debug').html(''); return false;">Clear Debug Output</a>

View File

@@ -26,14 +26,53 @@ $move_type = preg_replace("/.*_/", "", $this->action);
// Reset the form
function resetForm() {
$("#customer-id").val(0);
$("#move-customer").html("INTERNAL ERROR");
$("#unit-id").val(0);
$("#lease-id").val(0);
$("#move-customer").html("INTERNAL ERROR");
$("#move-unit").html("INTERNAL ERROR");
$("#move-lease").html("INTERNAL ERROR");
datepickerNow('LeaseMoveDate', false);
}
// pre-submit callback
function verifyRequest() {
//$("#debug").html('');
<?php if ($move_type === 'out'): ?>
if (!($("#lease-id").val() > 0)) {
//$("#debug").append('<P>Missing Lease ID');
alert("Please select the lease");
return false;
}
<?php else: ?>
if (!($("#customer-id").val() > 0)) {
//$("#debug").append('<P>Missing Customer ID');
alert("Please select the customer");
return false;
}
if (!($("#unit-id").val() > 0)) {
//$("#debug").append('<P>Missing Unit ID');
alert("Please select the unit");
return false;
}
<?php endif; ?>
//$("#debug").append('OK');
//return false;
return true;
}
function onRowSelect(grid_id, item_type, item_id) {
cell_name = item_type.charAt(0).toUpperCase() + item_type.substr(1) + "-name";
cell_name = item_type.charAt(0).toUpperCase() + item_type.substr(1);
if (item_type == 'lease')
cell_name += "-number";
else
cell_name += "-name";
// Set the item id that will be returned with the form
$("#"+item_type+"-id").val(item_id);
@@ -41,6 +80,28 @@ function onRowSelect(grid_id, item_type, item_id) {
// Get the item name from the grid
$("#move-"+item_type).html($(grid_id).getCell(item_id, cell_name));
// If a unit was selected, update the rent and deposit
if (item_type == 'unit') {
$("#LeaseRent").val($(grid_id).getCell(item_id, 'Unit-rent'));
$("#LeaseDeposit").val($(grid_id).getCell(item_id, 'Unit-deposit'));
}
// If a unit was selected, update the "Create new customer" link
if (item_type == 'unit') {
$("#customer-selection-new-url").attr
('href',
"<?php echo
$html->url(array('controller' => 'customers',
'action' => 'add')); ?>"
+ '/' + $("#unit-id").val());
}
// If a lease was selected, update the customer and unit
if (item_type == 'lease') {
$("#move-unit").html($(grid_id).getCell(item_id, 'Unit-name'));
$("#move-customer").html($(grid_id).getCell(item_id, 'Customer-name'));
}
// Hide the "no customer" message and show the current customer
$("."+item_type+"-selection-invalid").hide();
$("."+item_type+"-selection-valid").show();
@@ -50,12 +111,12 @@ function onRowSelect(grid_id, item_type, item_id) {
function onGridState(grid_id, item_type, state) {
if (state == 'visible') {
$("."+item_type+"-selection-new").show();
$("."+item_type+"-selection-invalid").hide();
$("."+item_type+"-selection-valid").hide();
}
else {
//if ($(grid_id).getGridParam("selrow"))
//alert("id:" + $("#"+item_type+"-id").val());
$("."+item_type+"-selection-new").hide();
if ($("#"+item_type+"-id").val() > 0) {
$("."+item_type+"-selection-invalid").hide();
$("."+item_type+"-selection-valid").show();
@@ -72,7 +133,42 @@ function onGridState(grid_id, item_type, state) {
<?php
; // align
if ($move_type !== 'out') {
if ($move_type === 'out') {
echo $this->element('leases', array
('config' => array
('grid_div_id' => 'leases-list',
'grid_div_class' => 'text-below',
'caption' => ('<A HREF="#" ONCLICK="$(\'#leases-list .HeaderButton\').click();'.
' return false;">Select Lease</A>'),
'grid_setup' => array('hiddengrid' => isset($lease['id'])),
'grid_events' => array('onSelectRow' =>
array('ids' =>
'if (ids != null){onRowSelect("#"+$(this).attr("id"), "lease", ids);}'),
'onHeaderClick' =>
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), "lease", gridstate)'),
),
'exclude' => array('Closed'),
'action' => 'active',
'nolinks' => true,
'limit' => 10,
)));
echo ('<DIV CLASS="move-inout grid-selection-text">' .
'<DIV CLASS="lease-selection-valid" style="display:none">' .
'Lease <SPAN id="move-lease"></SPAN>' . ' / ' .
'Unit: <SPAN id="move-unit"></SPAN>' . ' / ' .
'Customer: <SPAN id="move-customer"></SPAN>' .
'</DIV>' .
'<DIV CLASS="lease-selection-invalid" style="display:none">' .
'Please select lease' .
'</DIV>' .
'</DIV>' . "\n");
}
else {
echo $this->element('customers', array
('config' => array
('grid_div_id' => 'customers-list',
@@ -90,22 +186,27 @@ if ($move_type !== 'out') {
'nolinks' => true,
'limit' => 10,
)));
}
echo ('<DIV CLASS="move-inout grid-selection-text">' .
echo ('<DIV CLASS="move-inout grid-selection-text">' .
'<DIV CLASS="customer-selection-valid" style="display:none">' .
'Customer: <SPAN id="move-customer"></SPAN>' .
'</DIV>' .
'<DIV CLASS="customer-selection-new" style="display:none">' .
$html->link('Create a new Customer',
array('controller' => 'customers',
'action' => 'add',
(empty($unit['id']) ? null : $unit['id'])),
array('id' => 'customer-selection-new-url')) .
'</DIV>' .
'<DIV CLASS="customer-selection-invalid" style="display:none">' .
'Please select customer' .
'</DIV>' .
'<DIV CLASS="customer-selection-valid" style="display:none">' .
'Customer: <SPAN id="move-customer"></SPAN>' .
'</DIV>' .
'</DIV>' . "\n");
'<DIV CLASS="customer-selection-invalid" style="display:none">' .
'Please select customer' .
'</DIV>' .
'</DIV>' . "\n");
if ($move_type !== 'out') {
echo $this->element('units', array
('config' => array
('grid_div_id' => 'units-list',
@@ -120,43 +221,47 @@ if ($move_type !== 'out') {
array('gridstate' =>
'onGridState("#"+$(this).attr("id"), "unit", gridstate)'),
),
'action' => 'unoccupied',
'include' => array('Deposit'),
'exclude' => array('Balance'),
'action' => 'vacant',
'nolinks' => true,
'limit' => 10,
)));
echo ('<DIV CLASS="move-inout grid-selection-text">' .
'<DIV CLASS="unit-selection-valid" style="display:none">' .
'Unit: <SPAN id="move-unit"></SPAN>' .
'</DIV>' .
'<DIV CLASS="unit-selection-invalid" style="display:none">' .
'Please select unit' .
'</DIV>' .
'</DIV>' . "\n");
}
echo ('<DIV CLASS="move-inout grid-selection-text">' .
'<DIV CLASS="unit-selection-valid" style="display:none">' .
'Unit: <SPAN id="move-unit"></SPAN>' .
'</DIV>' .
'<DIV CLASS="unit-selection-invalid" style="display:none">' .
'Please select unit' .
'</DIV>' .
'</DIV>' . "\n");
echo $form->create(null, array('id' => 'move-inout-form',
'onsubmit' => 'return verifyRequest();',
'url' => array('controller' => 'leases',
'action' => $move_action)));
echo $form->input("Lease.customer_id",
array('id' => 'customer-id',
'type' => 'hidden',
'value' => 0));
echo $form->input("Lease.unit_id",
array('id' => 'unit-id',
'type' => 'hidden',
'value' => 0));
if ($move_type === 'out') {
echo $form->input('Lease.id',
array('type' => 'hidden',
'value' => $lease['id'],
));
array('id' => 'lease-id',
'type' => 'hidden',
'value' => 0));
}
else {
echo $form->input("Lease.customer_id",
array('id' => 'customer-id',
'type' => 'hidden',
'value' => 0));
echo $form->input("Lease.unit_id",
array('id' => 'unit-id',
'type' => 'hidden',
'value' => 0));
}
echo $this->element('form_table',
@@ -169,7 +274,20 @@ echo $this->element('form_table',
'id' => "LeaseMoveDate"),
'between' => '<A HREF="#" ONCLICK="datepickerNow(\'LeaseMoveDate\', false); return false;">Now</A>',
),
"comment" =>
) +
($move_type === 'in' ? array
("deposit" =>
array('opts' => array
('value' => (!empty($unit)
? FormatHelper::currency($unit['deposit'])
: null))),
"rent" =>
array('opts' => array
('value' => (!empty($unit)
? FormatHelper::currency($unit['rent'])
: null))),
) : array()) + array
("comment" =>
($move_type !== 'out'
? array('opts' => array('size' => 50))
: null),
@@ -198,15 +316,23 @@ echo $form->end('Perform Move ' . ucfirst($move_type));
<script type="text/javascript"><!--
$(document).ready(function(){
$("#LeaseMoveDate")
.attr('autocomplete', 'off')
.datepicker({ constrainInput: true,
numberOfMonths: [1, 1],
showCurrentAtPos: 0,
dateFormat: 'mm/dd/yy' });
datepicker('LeaseMoveDate');
resetForm();
<?php if ($move_type === 'out') { ?>
<?php if (isset($lease['id'])): ?>
$("#lease-id").val(<?php echo $lease['id']; ?>);
$("#move-lease").html("#<?php echo $lease['number']; ?>");
$("#move-customer").html("<?php echo $customer['name']; ?>");
$("#move-unit").html("<?php echo $unit['name']; ?>");
onGridState(null, 'lease', 'hidden');
<?php else: ?>
onGridState(null, 'lease', 'visible');
<?php endif; ?>
<?php } else { /* end (move_type === 'out') */ ?>
<?php if (isset($customer['id'])): ?>
$("#customer-id").val(<?php echo $customer['id']; ?>);
$("#move-customer").html("<?php echo $customer['name']; ?>");
@@ -222,6 +348,10 @@ echo $form->end('Perform Move ' . ucfirst($move_type));
<?php else: ?>
onGridState(null, 'unit', 'visible');
<?php endif; ?>
<?php } /* end (move_type === 'out') */ ?>
});
--></script>

74
views/leases/overview.ctp Normal file
View File

@@ -0,0 +1,74 @@
<?php /* -*- mode:PHP -*- */
echo '<div class="lease overview">' . "\n";
/**********************************************************************
**********************************************************************
**********************************************************************
**********************************************************************
* Overview Main Section
*/
/*
$rows = array();
$rows[] = array('', FormatHelper::currency($overview['']));
echo $this->element('table',
array('class' => 'item lease detail',
'caption' => '',
'rows' => $rows,
'column_class' => array('field', 'value')));
*/
/**********************************************************************
**********************************************************************
**********************************************************************
**********************************************************************
* Supporting Elements Section
*/
echo '<div CLASS="detail supporting">' . "\n";
$headers = array('', 'Count');
$row_class = array();
$rows = array();
foreach ($overview['months'] AS $month) {
$row_class[] = 'subheader';
$rows[] = array($month['name']);
$odd = 1;
$row_class[] = array('subitem', $odd ? 'oddrow' : 'evenrow');
$rows[] = array('Beginning of Month', $month['start']); $odd = !$odd;
$row_class[] = array('subitem', $odd ? 'oddrow' : 'evenrow');
$rows[] = array('+ Move-Ins', $month['movein']); $odd = !$odd;
$row_class[] = array('subitem', $odd ? 'oddrow' : 'evenrow');
$rows[] = array('- Move-Outs', $month['moveout']); $odd = !$odd;
$row_class[] = 'grand';
$rows[] = array('End of ' . $month['name'], $month['finish']);
}
echo $this->element('table',
array('class' => 'item lease-overview overview list',
'caption' => 'Leased Units By Month',
'headers' => $headers,
'rows' => $rows,
'row_class' => $row_class,
'suppress_alternate_rows' => true
));
?>
<script type="text/javascript"><!--
jQuery(document).ready(function(){
jQuery('table.overview td.subheader').attr('colSpan', 2);
});
--></script>
<?php
/* End "detail supporting" div */
echo '</div>' . "\n";
/* End page div */
echo '</div>' . "\n";

View File

@@ -16,29 +16,29 @@ $unit = $lease['Unit'];
if (isset($lease['Lease']))
$lease = $lease['Lease'];
$rows = array(array('ID', $lease['id']),
array('Number', $lease['number']),
array('Lease Type', $lease_type['name']),
array('Unit', $html->link($unit['name'],
$rows = array();
$rows[] = array('Number', $lease['number']);
$rows[] = array('Lease Type', $lease_type['name']);
$rows[] = array('Unit', $html->link($unit['name'],
array('controller' => 'units',
'action' => 'view',
$unit['id']))),
array('Customer', $html->link($customer['name'],
$unit['id'])));
$rows[] = array('Customer', $html->link($customer['name'],
array('controller' => 'customers',
'action' => 'view',
$customer['id']))),
array('Lease_Date', FormatHelper::date($lease['lease_date'], true)),
array('Move-in Planned', FormatHelper::date($lease['movein_planned_date'], true)),
array('Move-in', FormatHelper::date($lease['movein_date'], true)),
array('Move-out', FormatHelper::date($lease['moveout_date'], true)),
array('Move-out Planned', FormatHelper::date($lease['moveout_planned_date'], true)),
array('Notice Given', FormatHelper::date($lease['notice_given_date'], true)),
array('Notice Received', FormatHelper::date($lease['notice_received_date'], true)),
array('Closed', FormatHelper::date($lease['close_date'], true)),
array('Deposit', FormatHelper::currency($lease['deposit'])),
array('Rent', FormatHelper::currency($lease['rent'])),
array('Paid Through', FormatHelper::date($lease['paid_through'], true)),
array('Comment', $lease['comment']));
$customer['id'])));
$rows[] = array('Lease_Date', FormatHelper::date($lease['lease_date'], true));
$rows[] = array('Move-in Planned', FormatHelper::date($lease['movein_planned_date'], true));
$rows[] = array('Move-in', FormatHelper::date($lease['movein_date'], true));
$rows[] = array('Move-out', FormatHelper::date($lease['moveout_date'], true));
$rows[] = array('Move-out Planned', FormatHelper::date($lease['moveout_planned_date'], true));
$rows[] = array('Notice Given', FormatHelper::date($lease['notice_given_date'], true));
$rows[] = array('Notice Received', FormatHelper::date($lease['notice_received_date'], true));
$rows[] = array('Closed', FormatHelper::date($lease['close_date'], true));
$rows[] = array('Deposit', FormatHelper::currency($lease['deposit']));
$rows[] = array('Rent', FormatHelper::currency($lease['rent']));
$rows[] = array('Comment', $lease['comment']);
echo $this->element('table',
@@ -55,7 +55,10 @@ echo $this->element('table',
echo '<div class="infobox">' . "\n";
$rows = array();
$rows[] = array('Security Deposit:', FormatHelper::currency($outstandingDeposit));
$rows[] = array('Balance:', FormatHelper::currency($outstandingBalance));
$rows[] = array('Balance Owed:', FormatHelper::currency($outstandingBalance));
$rows[] = array('Paid Through:', FormatHelper::date($lease['paid_through_date'], false));
if ($lease['delinquent'])
$rows[] = array('Delinquent:', FormatHelper::age($lease['paid_through_date'], 'delinquent'));
echo $this->element('table',
array('class' => 'summary',
'rows' => $rows,
@@ -75,20 +78,33 @@ echo '</div>' . "\n";
echo '<div CLASS="detail supporting">' . "\n";
/**********************************************************************
* Unpaid Charges
*/
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
('caption' => 'Outstanding Charges',
'limit' => 10,
'action' => 'unreconciled',
'filter' => array('StatementEntry.lease_id' => $lease['id']),
'exclude' => array('Customer', 'Unit'),
)));
/**********************************************************************
* Lease Account History
*/
echo $this->element('ledger_entries', array
(// Element configuration
'lease_id' => $lease['id'],
'ar_account' => true,
// Grid configuration
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array
('caption' => 'Account',
),
));
('caption' => 'Lease Statement',
'filter' => array('Lease.id' => $lease['id']),
'include' => array('Through'),
'exclude' => array('Customer', 'Lease', 'Unit'),
)));
/* End "detail supporting" div */

Some files were not shown because too many files have changed in this diff Show More