diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..0ed8662
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,5 @@
+
+ RewriteEngine on
+ RewriteRule ^$ webroot/ [L]
+ RewriteRule (.*) webroot/$1 [L]
+
\ No newline at end of file
diff --git a/app_controller.php b/app_controller.php
new file mode 100644
index 0000000..5ed9976
--- /dev/null
+++ b/app_controller.php
@@ -0,0 +1,881 @@
+params['dev'] = false;
+ $this->params['admin'] = false;
+ parent::__construct();
+ }
+
+ function sideMenuLinks() {
+ // Stupid Cake... our constructor sets admin/dev,
+ // but cake stomps it somewhere along the way
+ // after constructing the CakeError controller.
+ if ($this->name === 'CakeError') {
+ $this->params['dev'] = false;
+ $this->params['admin'] = false;
+ }
+
+ $menu = array();
+ $menu[] = array('name' => 'Common', 'header' => true);
+ $menu[] = array('name' => 'Site Map', 'url' => array('controller' => 'maps', 'action' => 'view', 1));
+ $menu[] = array('name' => 'Units', 'url' => array('controller' => 'units', 'action' => 'index'));
+ $menu[] = array('name' => 'Leases', 'url' => array('controller' => 'leases', 'action' => 'index'));
+ $menu[] = array('name' => 'Customers', 'url' => array('controller' => 'customers', 'action' => 'index'));
+ $menu[] = array('name' => 'Deposits', 'url' => array('controller' => 'transactions', 'action' => 'deposit'));
+
+ if ($this->params['admin']) {
+ $menu[] = array('name' => 'Admin', 'header' => true);
+ $menu[] = array('name' => 'Accounts', 'url' => array('controller' => 'accounts', 'action' => 'index'));
+ $menu[] = array('name' => 'Contacts', 'url' => array('controller' => 'contacts', 'action' => 'index'));
+ $menu[] = array('name' => 'Ledgers', 'url' => array('controller' => 'ledgers', 'action' => 'index'));
+ $menu[] = array('name' => 'Tenders', 'url' => array('controller' => 'tenders', 'action' => 'index'));
+ $menu[] = array('name' => 'Transactions', 'url' => array('controller' => 'transactions', 'action' => 'index'));
+ $menu[] = array('name' => 'Ldgr Entries', 'url' => array('controller' => 'ledger_entries', 'action' => 'index'));
+ $menu[] = array('name' => 'Stmt Entries', 'url' => array('controller' => 'statement_entries', 'action' => 'index'));
+ $menu[] = array('name' => 'New Ledgers', 'url' => array('controller' => 'accounts', 'action' => 'newledger'));
+ $menu[] = array('name' => 'Assess Charges', 'url' => array('controller' => 'leases', 'action' => 'assess_all'));
+ }
+
+ if ($this->params['dev']) {
+ $menu[] = array('name' => 'Development', 'header' => true);
+ $menu[] = array('name' => 'Un-Nuke', 'url' => '#', 'htmlAttributes' =>
+ array('onclick' => '$(".pr-section").show(); return false;'));
+ $menu[] = array('name' => 'New Ledgers', 'url' => array('controller' => 'accounts', 'action' => 'newledger'));
+ //array('name' => 'RESET DATA', 'url' => array('controller' => 'accounts', 'action' => 'reset_data'));
+ }
+
+ return $menu;
+ }
+
+ function beforeFilter() {
+ $this->params['dev'] =
+ (!empty($this->params['dev_route']));
+ $this->params['admin'] =
+ (!empty($this->params['admin_route']) || !empty($this->params['dev_route']));
+
+ if (!$this->params['dev'])
+ Configure::write('debug', '0');
+ }
+
+ function beforeRender() {
+ $this->set('sidemenu', $this->sideMenuLinks());
+ }
+
+ function redirect($url, $status = null, $exit = true) {
+ // OK, since the controller will not be able to
+ // utilize our overriden url function in AppHelper,
+ // we'll have to do it manually here.
+ App::import('Helper', 'Html');
+ $url = HtmlHelper::url($url, true);
+
+ if (headers_sent()) {
+ // If we've already sent the headers, it's because
+ // we're debugging, and our debug output has gotten
+ // out before the redirect. That's probably a good
+ // thing, as we don't typically want pages to be
+ // jerked out from under us while trying to read
+ // the debug output. So, since we can't redirect
+ // anyway, we may as well go with the flow and just
+ // render this page instead, using an empty template
+ $this->set('message',
+ ("Intended redirect:
" .
+ ''.$url.''));
+
+ echo $this->render('/empty');
+ if ($exit)
+ $this->_stop();
+ }
+
+ return parent::redirect($url, $status, $exit);
+ }
+
+ function reset_data() {
+ $this->layout = null;
+ $this->autoLayout = false;
+ $this->autoRender = false;
+ Configure::write('debug', '0');
+ $script = $_SERVER['DOCUMENT_ROOT'] . '/pmgr/build.cmd';
+ echo "
" . date('r') . "\n";
+ //echo "
Script: $script" . "\n";
+ $db = & $this->Account->getDataSource();
+ $script .= ' "' . $db->config['database'] . '"';
+ $script .= ' "' . $db->config['login'] . '"';
+ $script .= ' "' . $db->config['password'] . '"';
+ $handle = popen($script . ' 2>&1', 'r');
+ //echo "
Handle: $handle; " . gettype($handle) . "\n";
+ echo "
\n";
+ while (($read = fread($handle, 2096))) {
+ echo $read;
+ }
+ echo "\n";
+ pclose($handle);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * helper: gridView
+ * - called by derived controllers to create an index listing
+ */
+
+ function gridView($title, $action = null, $element = null) {
+ $this->set('title', $title);
+ // The resulting page will contain a grid, which will
+ // use ajax to obtain the actual data for this action
+ $this->set('action', $action ? $action : $this->params['action']);
+ $this->render('/elements/' . ($element ? $element : $this->params['controller']));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: gridData
+ * - Fetches the actual data requested by grid as XML
+ */
+
+ function gridData() {
+ // Grab a copy of the parameters that control this request
+ $params = array();
+ if (isset($this->params['url']) && is_array($this->params['url']))
+ $params = $this->params['url'];
+
+ // Do any preliminary setup necessary
+ $this->gridDataSetup($params);
+
+ // Get the top level model for this grid
+ $model = $this->gridDataModel($params);
+
+ // Get the number of records prior to pagination
+ $count = $this->gridDataCount($params, $model);
+
+ // Determine pagination configuration (and save to $params)
+ $pagination = $this->gridDataPagination($params, $model, $count);
+
+ // Retreive the appropriate subset of data
+ $records = $this->gridDataRecords($params, $model, $pagination);
+
+ // Post process the records
+ $this->gridDataPostProcess($params, $model, $records);
+
+ // Output the resulting record set
+ $this->gridDataOutput($params, $model, $records, $pagination);
+
+ // Call out to finalize everything
+ $this->gridDataFinalize($params);
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * virtual: gridData* functions
+ * - These set up the context for the grid data, and will
+ * need to be overridden in the derived class for anything
+ * other than the most basic of grids.
+ */
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * gridData SETUP / CLEANUP
+ */
+
+ function gridDataModel(&$params) {
+ return $this->{$this->modelClass};
+ }
+
+ function gridDataSetup(&$params) {
+ // Debug only if requested
+ $params['debug'] = !empty($this->passedArgs['debug']);
+
+ if ($params['debug']) {
+ ob_start();
+ }
+ else {
+ $this->layout = null;
+ $this->autoLayout = false;
+ $this->autoRender = false;
+ Configure::write('debug', '0');
+ }
+
+ // Establish some defaults (except for serialized items)
+ $params = array_merge(array('page' => 1,
+ 'rows' => 20),
+ $params);
+
+ // Unserialize our complex structure of post data.
+ // This SHOULD always be set, except when debugging
+ if (isset($params['post']))
+ $params['post'] = unserialize($params['post']);
+ else
+ $params['post'] = array();
+
+ // Unserialize our complex structure of dynamic post data
+ if (isset($params['dynamic_post']))
+ $params['dynamic_post'] = unserialize($params['dynamic_post']);
+ else
+ $params['dynamic_post'] = null;
+
+ // Unserialize our complex structure of dynamic post data
+ if (isset($params['dynamic_post_replace']))
+ $params['dynamic_post_replace'] = unserialize($params['dynamic_post_replace']);
+ else
+ $params['dynamic_post_replace'] = null;
+
+ // Merge the static and dynamic post data
+ if (!empty($params['dynamic_post']))
+ $params['post'] = array_merge_recursive($params['post'], $params['dynamic_post']);
+ if (!empty($params['dynamic_post_replace']))
+ $params['post'] = array_merge($params['post'], $params['dynamic_post_replace']);
+
+ // This SHOULD always be set, except when debugging
+ if (!isset($params['post']['fields']))
+ $params['post']['fields'] = array($this->{$this->modelClass}->alias
+ .'.'.
+ $this->{$this->modelClass}->primaryKey);
+
+ // Make sure the action parameter at least exists, and
+ // promote it to the top level (since it drives the operation).
+ if (isset($params['post']['action']))
+ $params['action'] = $params['post']['action'];
+ else
+ $params['action'] = null;
+ }
+
+ function gridDataFinalize(&$params) {
+ if ($params['debug']) {
+ $xml = ob_get_contents();
+ ob_end_clean();
+
+ $xml = preg_replace("/&/", "&", $xml);
+ $xml = preg_replace("/", "<", $xml);
+ $xml = preg_replace("/>/", ">", $xml);
+ echo ("\n\n$xml\n
\n");
+ }
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * gridData COUNTING
+ */
+
+ function gridDataCount(&$params, &$model) {
+ // Establish the tables and conditions for counting
+ $query = array_intersect_key($this->gridDataCountTableSet($params, $model),
+ array('link'=>1, 'contain'=>1));
+
+ // Conditions for the count
+ $query['conditions'] = $this->gridDataCountConditionSet($params, $model);
+
+ // Grouping (which would not be typical)
+ $query['group'] = $this->gridDataCountGroup($params, $model);
+
+ // DEBUG PURPOSES ONLY!
+ $params['count_query'] = $query;
+
+ // Get the number of records prior to pagination
+ return $this->gridDataCountExecute($params, $model, $query);
+ }
+
+ function gridDataCountExecute(&$params, &$model, $query) {
+ return $model->find('count', $query);
+ }
+
+ function gridDataCountTables(&$params, &$model) {
+ // Same tables for counting as for retreiving
+ return $this->gridDataTables($params, $model);
+ }
+
+ function gridDataCountTableSet(&$params, &$model) {
+ // Preliminary set of tables
+ $query = array_intersect_key($this->gridDataCountTables($params, $model),
+ array('link'=>1, 'contain'=>1));
+
+ // Perform filtering based on user request: $params['post']['filter']
+ return array_intersect_key($this->gridDataFilterTables($params, $model, $query),
+ array('link'=>1, 'contain'=>1));
+ }
+
+ function gridDataCountConditions(&$params, &$model) {
+ // Same conditions for counting as for retreiving
+ return $this->gridDataConditions($params, $model);
+ }
+
+ function gridDataCountConditionSet(&$params, &$model) {
+ // Conditions for the count
+ $conditions = $this->gridDataCountConditions($params, $model);
+
+ // Perform filtering based on user request: $params['post']['filter']
+ return $this->gridDataFilterConditions($params, $model, $conditions);
+ }
+
+ function gridDataCountGroup(&$params, &$model) {
+ // Grouping will screw up the count, since it
+ // causes the results to be split into chunks
+ // based on the GROUP BY clause. We can't have
+ // more than one row of data in the count query,
+ // just a _single_ row with the actual count.
+ return null;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * gridData FILTERING
+ */
+
+ function gridDataFilterTables(&$params, &$model, $query) {
+ if (isset($query['contain']))
+ $link = 'contain';
+ else
+ $link = 'link';
+
+ if (empty($params['post']['filter']))
+ return $query;
+
+ foreach ($params['post']['filter'] AS $filter => $filter_value) {
+ $split = $this->gridDataFilterSplit($params, $model, $filter, $filter_value);
+
+/* pr(array('AppController::gridDataFilterTable' => */
+/* array('checkpoint' => "Filter split") */
+/* + compact('split'))); */
+
+ $table = $this->gridDataFilterTablesTable($params, $model, $split['table']);
+ if (!$table)
+ continue;
+
+ $config = $this->gridDataFilterTablesConfig($params, $model, $split['table']);
+
+/* pr(array('AppController::gridDataFilterTable' => */
+/* array('checkpoint' => "Add filter config to query") */
+/* + array('query[link]' => $query[$link]) */
+/* + compact('config'))); */
+
+ // If the table is already part of the query, append to it
+ if ($table == $model->alias) {
+ $query[$link] =
+ array_merge_recursive(array_diff_key($config, array('fields'=>1)),
+ $query[$link]);
+ }
+ elseif (isset($query[$link][$table])) {
+ $query[$link][$table] =
+ array_merge_recursive($config, $query[$link][$table]);
+ }
+ elseif (in_array($table, $query[$link])) {
+ // REVISIT : 20090727
+ // This presents a dilema. $config may specify fields
+ // as array(), whereas the user has already indicated
+ // they desire ALL fields (by the fact that table is
+ // a member of the query['link'] array without anything
+ // specified). However, $config _could_ specify a
+ // non-standard field in the array, such as using SUM()
+ // or other equation. In that case, we'd have to add
+ // in all the fields of table to the array. That
+ // wouldn't be very hard, but I'll ignore it at the
+ // moment and wait until it's needed.
+ $key = array_search($table, $query[$link]);
+ $query[$link][$table] = array('fields' => null) + $config;
+ unset($query[$link][$key]);
+ }
+ else {
+ // Table is NOT already part of the query... set it now
+ $query[$link][$table] = $config;
+ }
+
+/* pr(array('post-filter-table-config' => */
+/* array('query[link]' => $query[$link]))); */
+
+ }
+
+ return $query;
+ }
+
+ function gridDataFilterTablesTable(&$params, &$model, $table) {
+ // By default, don't add anything if the filter
+ // condition occurs on the actual model table
+ if ($table == $model->alias)
+ return null;
+ return $this->gridDataFilterTableName($params, $model, $table);
+ }
+
+ function gridDataFilterTablesConfig(&$params, &$model, $table) {
+ return array('fields' => array());
+ }
+
+ function gridDataFilterConditions(&$params, &$model, $conditions) {
+ if (empty($params['post']['filter']))
+ return $conditions;
+
+ foreach ($params['post']['filter'] AS $filter => $filter_value) {
+ $split = $this->gridDataFilterSplit($params, $model, $filter, $filter_value);
+
+ $table = $this->gridDataFilterConditionsTable($params, $model, $split['table']);
+ $key = $this->gridDataFilterConditionsKey($params, $model, $split['table'], $split['field']);
+ if (!$key)
+ continue;
+
+ $conditions[]
+ = $this->gridDataFilterConditionsStatement($params, $model, $table, $key,
+ array_intersect_key
+ ($split,
+ array('value'=>1,
+ 'value_present'=>1)));
+ }
+
+ return $conditions;
+ }
+
+ function gridDataFilterConditionsTable(&$params, &$model, $table) {
+ return $this->gridDataFilterTableName($params, $model, $table);
+ }
+
+ function gridDataFilterConditionsKey(&$params, &$model, $table, $id) {
+ // REVISIT : 20090722
+ // When $id is null, we could instantiate the table,
+ // and use the _actual_ primary key. However, I don't
+ // expect that functionality to be used, and will just
+ // stick with 'id' for now.
+ return $id ? $id : 'id';
+ }
+
+ function gridDataFilterConditionsStatement(&$params, &$model, $table, $key, $value) {
+ $key = (empty($table)?"":"{$table}.") . $key;
+ if (isset($value['value_present']) && $value['value_present'])
+ return array($key => $value['value']);
+ else
+ return array($key);
+ }
+
+ function gridDataFilterSplit(&$params, &$model, $filter, $value) {
+ $value_present = true;
+ if (!is_string($filter)) {
+ // only a filter condition was set, not filter=>value
+ $filter = $value;
+ $value = null;
+ $value_present = false;
+ }
+
+ $table = $field = null;
+ if (preg_match("/^\w+\.\w+(\s*[!<>=]+)?$/", $filter)) {
+ list($table, $field) = explode(".", $filter);
+ }
+ elseif (preg_match('/^[A-Z][A-Za-z]*$/', $filter)) {
+ $table = $filter;
+ $field = null;
+ }
+ elseif (preg_match('/^\w+(\s*[!<>=]+)?$/', $filter)) {
+ $table = $model->alias;
+ $field = $filter;
+ }
+ else {
+ // $filter must be a function or other complicated condition
+ $table = null;
+ $field = $filter;
+ }
+
+ return compact('table', 'field', 'value', 'value_present');
+ }
+
+ function gridDataFilterTableName(&$params, &$model, $table) {
+ return Inflector::camelize($table);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ * gridData PAGINATION
+ */
+
+ function gridDataPagination(&$params, &$model, $record_count) {
+ // Retrieve, or calculate, all parameters necesssary for
+ // pagination. Verify the passed in parameters are valid.
+
+ $limit = $params['rows'] > 0 ? $params['rows'] : 10;
+ $total = ($record_count < 0) ? 0 : ceil($record_count/$limit);
+ $page = ($params['page'] <= 1) ? 1 : (($params['page'] > $total) ? $total : $params['page']);
+ $start = $limit * ($page - 1);
+
+ return compact('record_count', 'limit', 'page', 'start', 'total');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ * gridData RETREIVAL
+ */
+
+ function gridDataRecords(&$params, &$model, $pagination) {
+ // Establish the tables for this query
+ $query = array_intersect_key($this->gridDataTableSet($params, $model),
+ array('link'=>1, 'contain'=>1));
+
+ // Specify the fields for the query
+ $query['fields'] = $this->gridDataFields($params, $model);
+
+ // Conditions of the query
+ $query['conditions'] = $this->gridDataConditionSet($params, $model);
+
+ // Data record grouping
+ $query['group'] = $this->gridDataGroup($params, $model);
+
+ // The subset of data based on pagination
+ $query['limit'] = $this->gridDataLimit($params, $model, $pagination['start'], $pagination['limit']);
+
+ // Ordering based on user request
+ $query['order'] = $this->gridDataOrder($params, $model,
+ isset($params['sidx']) ? $params['sidx'] : null,
+ isset($params['sord']) ? $params['sord'] : null);
+
+ // DEBUG PURPOSES ONLY!
+ $params['query'] = $query;
+
+ return $this->gridDataRecordsExecute($params, $model, $query);
+ }
+
+ function gridDataRecordsExecute(&$params, &$model, $query) {
+ return $model->find('all', $query);
+ }
+
+ function gridDataTables(&$params, &$model) {
+ return array('link' => array());
+ }
+
+ function gridDataTableSet(&$params, &$model) {
+ // Preliminary set of tables
+ $query = array_intersect_key($this->gridDataTables($params, $model),
+ array('link'=>1, 'contain'=>1));
+
+ // Perform filtering based on user request: $params['post']['filter']
+ $query = array_intersect_key($this->gridDataFilterTables($params, $model, $query),
+ array('link'=>1, 'contain'=>1));
+
+ return $query;
+ }
+
+
+ function gridDataConditions(&$params, &$model) {
+ $searches = array();
+
+ if (isset($params['_search']) && $params['_search'] === 'true') {
+ if (isset($params['searchOper'])) {
+ $searches[] = array('op' => $params['searchOper'],
+ 'field' => $params['searchField'],
+ 'value' => $params['searchString']);
+ }
+ else {
+ // DOH! Crappy mechanism puts toolbar search terms
+ // directly into params as name/value pairs. No
+ // way to know which elements of params are search
+ // terms, so skipping this at the moment.
+ }
+ }
+ elseif (isset($params['filt']) && $params['filt']) {
+ $searches[] = array('op' => 'bw',
+ 'field' => $params['filtField'],
+ 'value' => $params['filtValue']);
+ }
+
+ $ops = array('eq' => array('op' => null, 'pre' => '', 'post' => ''),
+ 'ne' => array('op' => '<>', 'pre' => '', 'post' => ''),
+ 'lt' => array('op' => '<', 'pre' => '', 'post' => ''),
+ 'le' => array('op' => '<=', 'pre' => '', 'post' => ''),
+ 'gt' => array('op' => '>', 'pre' => '', 'post' => ''),
+ 'ge' => array('op' => '>=', 'pre' => '', 'post' => ''),
+ 'bw' => array('op' => 'LIKE', 'pre' => '', 'post' => '%'),
+ 'ew' => array('op' => 'LIKE', 'pre' => '%', 'post' => ''),
+ 'cn' => array('op' => 'LIKE', 'pre' => '%', 'post' => '%'),
+ );
+
+ $conditions = array();
+ foreach ($searches AS $search) {
+ $op = $ops[$search['op']];
+ $field = $search['field'] . ($op['op'] ? ' '.$op['op'] : '');
+ $value = $op['pre'] . $search['value']. $op['post'];
+ $conditions[] = array($field => $value);
+ }
+
+ if (isset($params['action']) && $params['action'] === 'idlist') {
+ if (count($params['post']['idlist']))
+ $conditions[] = array($model->alias.'.'.$model->primaryKey => $params['post']['idlist']);
+ else
+ $conditions[] = '0=1';
+ }
+
+ return $conditions;
+ }
+
+ function gridDataConditionSet(&$params, &$model) {
+ // Conditions for record retrieval
+ $conditions = $this->gridDataConditions($params, $model);
+
+ // Perform filtering based on user request: $params['post']['filter']
+ return $this->gridDataFilterConditions($params, $model, $conditions);
+ }
+
+ function gridDataFields(&$params, &$model) {
+ $db = &$model->getDataSource();
+ $fields = $db->fields($model, $model->alias);
+ return $fields;
+ }
+
+ function gridDataGroup(&$params, &$model) {
+ return $model->alias.'.'.$model->primaryKey;
+ }
+
+ function gridDataOrder(&$params, &$model, $index, $direction) {
+ return $index ? array($index .' '. $direction) : null;
+ }
+
+ function gridDataLimit(&$params, &$model, $start, $limit) {
+ return $start . ', ' . $limit;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ * gridData POST PROCESSING
+ */
+
+ function gridDataPostProcess(&$params, &$model, &$records) {
+ // Add grid IDs to each record
+ $this->gridDataPostProcessGridIDs($params, $model, $records);
+
+ // Add the calculated fields (if any), to the model fields
+ $this->gridDataPostProcessCalculatedFields($params, $model, $records);
+
+ // Perform any requested subtotaling of fields
+ $this->gridDataPostProcessSubtotal($params, $model, $records);
+
+ // Add in any needed hyperlinks
+ $this->gridDataPostProcessLinks($params, $model, $records, array());
+
+ // DEBUG PURPOSES ONLY!
+ //$params['records'] = $records;
+ }
+
+ function gridDataPostProcessGridIDs(&$params, &$model, &$records) {
+ $model_alias = $model->alias;
+ $id = $model->primaryKey;
+
+ foreach ($records AS &$record) {
+ $record['grid_id'] = $record[$model_alias][$id];
+ }
+ }
+
+ function gridDataPostProcessCalculatedFields(&$params, &$model, &$records) {
+ $model_alias = $model->alias;
+
+ foreach ($records AS &$record) {
+ // Add the calculated fields (if any), to the model fields
+ if (isset($record[0])) {
+ $record[$model_alias] = $record[0] + $record[$model_alias];
+ unset($record[0]);
+ }
+ }
+ }
+
+ function gridDataPostProcessSubtotal(&$params, &$model, &$records) {
+ $model_alias = $model->alias;
+
+ // REVISIT : 20090722
+ // Horrible solution to something that should be done
+ // in SQL. But, it works for now, so what the heck...
+
+ $subtotals = array();
+ foreach ($params['post']['fields'] AS $field) {
+ if (preg_match('/subtotal-(.*)$/', $field, $matches))
+ $subtotals[] = array('field' => $matches[1],
+ 'name' => $field,
+ 'amount' => 0);
+ }
+
+ foreach ($records AS &$record) {
+ foreach ($subtotals AS &$subtotal) {
+ $field = $subtotal['field'];
+ if (preg_match("/\./", $field)) {
+ list($tbl, $col) = explode(".", $field);
+ $record['subtotal-'.$tbl][$col] =
+ ($subtotal['amount'] += $record[$tbl][$col]);
+ }
+ else {
+ $record[$model->alias]['subtotal-'.$field] =
+ ($subtotal['amount'] += $record[$model->alias][$field]);
+ }
+ }
+ }
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ // Don't create any links if ordered not to.
+ if (isset($params['post']['nolinks']))
+ return;
+
+ App::import('Helper', 'Html');
+
+ foreach ($links AS $table => $fields) {
+ $special = array('controller', 'action', 'id');
+ $controller = Inflector::pluralize(Inflector::underscore($table));
+ $action = 'view';
+ $id = 'id';
+ extract(array_intersect_key($fields, array_flip($special)));
+ foreach ($records AS &$record) {
+ if (!isset($record[$table]))
+ continue;
+
+ foreach (array_diff_key($fields, array_flip($special)) AS $field) {
+ if (!isset($record[$table][$id]) || !isset($record[$table][$field]))
+ continue;
+
+ // DEBUG PURPOSES ONLY!
+ //$params['linkrecord'][] = compact('table', 'field', 'id', 'controller', 'record');
+ $record[$table][$field] =
+ '' .
+ $record[$table][$field] .
+ '';
+ }
+ }
+ }
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ * gridData OUTPUT
+ */
+
+ function gridDataOutput(&$params, &$model, &$records, $pagination) {
+ $this->gridDataOutputHeader($params, $model);
+ $this->gridDataOutputXMLHeader($params, $model);
+ $this->gridDataOutputRootNodeBegin($params, $model);
+ $this->gridDataOutputSummary($params, $model, $pagination);
+ $this->gridDataOutputRecords($params, $model, $records);
+ $this->gridDataOutputRootNodeEnd($params, $model);
+ }
+
+ function gridDataOutputHeader(&$params, &$model) {
+ if (!$params['debug']) {
+ header("Content-type: text/xml;charset=utf-8");
+ }
+ }
+
+ function gridDataOutputXMLHeader(&$params, &$model) {
+ echo "\n";
+ }
+
+ function gridDataOutputRootNodeBegin(&$params, &$model) {
+ echo "\n";
+ }
+
+ function gridDataOutputRootNodeEnd(&$params, &$model) {
+ echo "\n";
+ }
+
+ function gridDataOutputSummary(&$params, &$model, $pagination) {
+ echo " \n";
+ echo " {$pagination['page']}\n";
+ echo " {$pagination['total']}\n";
+ echo " {$pagination['record_count']}\n";
+
+ if (isset($params['userdata'])) {
+ foreach ($params['userdata'] AS $field => $value)
+ echo ' ' . "{$value}\n";
+ }
+ }
+
+ function gridDataOutputRecords(&$params, &$model, &$records) {
+ $id_field = 'grid_id';
+ foreach ($records AS $record) {
+ $this->gridDataOutputRecord($params, $model, $record,
+ $record[$id_field], $params['post']['fields']);
+ }
+ }
+
+ function gridDataOutputRecord(&$params, &$model, &$record, $id, $fields) {
+ echo " \n";
+ foreach ($fields AS $field) {
+ $this->gridDataOutputRecordField($params, $model, $record, $field);
+ }
+ echo "
\n";
+ }
+
+ function gridDataOutputRecordField(&$params, &$model, &$record, $field) {
+ if (preg_match("/\./", $field)) {
+ list($tbl, $col) = explode(".", $field);
+ //pr(compact('record', 'field', 'tbl', 'col'));
+ $data = $record[$tbl][$col];
+ }
+ else {
+ $data = $record[$model->alias][$field];
+ }
+ $this->gridDataOutputRecordCell($params, $model, $record, $field, $data);
+ }
+
+ function gridDataOutputRecordCell(&$params, &$model, &$record, $field, $data) {
+ // be sure to put text data in CDATA
+ if (preg_match("/^\d*$/", $data))
+ echo " $data | \n";
+ else
+ echo " | \n";
+ }
+
+ function INTERNAL_ERROR($msg, $depth = 0) {
+ INTERNAL_ERROR($msg, false, $depth+1);
+ $this->render_empty();
+ $this->_stop();
+ }
+
+ function render_empty() {
+ $this->render('/empty');
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/app_helper.php b/app_helper.php
new file mode 100644
index 0000000..1b5e0b1
--- /dev/null
+++ b/app_helper.php
@@ -0,0 +1,50 @@
+params[$mod]) && is_array($url) && !isset($url[$mod]))
+ $url[$mod] = $this->params[$mod];
+ }
+ return parent::url($url, $full);
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/app_model.php b/app_model.php
new file mode 100644
index 0000000..7eb7f10
--- /dev/null
+++ b/app_model.php
@@ -0,0 +1,491 @@
+ 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;
+
+
+ // REVISIT : 20090730
+ // Why is this constructor crashing?
+ // Clearly it's in some sort of infinite
+ // loop, but it seems the correct way
+ // to have a constructor call the parent...
+
+/* function __construct() { */
+/* parent::__construct(); */
+/* $this->prClassLevel(5, '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 = $caller['class'];
+ $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 = $caller['class'];
+ if (empty($function))
+ $function = $caller['function'];
+ $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 ''."\n";
+ echo '' . "\n"; // End pr_header_class
+
+ if (isset($show_level) && $level > $show_level)
+ $display = 'none';
+ else
+ $display = 'block';
+
+ echo '
'."\n";
+ pr($mixed, false, false);
+ echo '
' . "\n"; // End pr_output_class
+ echo '
' . "\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]);
+ }
+ pr($vars);
+ }
+
+
+ /**
+ * Get Enum Values
+ * Snippet v0.1.3
+ * http://cakeforge.org/snippet/detail.php?type=snippet&id=112
+ *
+ * Gets the enum values for MySQL 4 and 5 to use in selectTag()
+ */
+ function getEnumValues($columnName=null, $tableName=null)
+ {
+ if ($columnName==null) { return array(); } //no field specified
+
+ if (!isset($tableName)) {
+ //Get the name of the table
+ $db =& ConnectionManager::getDataSource($this->useDbConfig);
+ $tableName = $db->fullTableName($this, false);
+ }
+
+ //Get the values for the specified column (database and version specific, needs testing)
+ $result = $this->query("SHOW COLUMNS FROM {$tableName} LIKE '{$columnName}'");
+
+ //figure out where in the result our Types are (this varies between mysql versions)
+ $types = null;
+ if ( isset( $result[0]['COLUMNS']['Type'] ) ) { //MySQL 5
+ $types = $result[0]['COLUMNS']['Type']; $default = $result[0]['COLUMNS']['Default'];
+ }
+ elseif ( isset( $result[0][0]['Type'] ) ) { //MySQL 4
+ $types = $result[0][0]['Type']; $default = $result[0][0]['Default'];
+ }
+ else { //types return not accounted for
+ return array();
+ }
+
+ //Get the values
+ return array_flip(array_merge(array(''), // MySQL sets 0 to be the empty string
+ explode("','", strtoupper(preg_replace("/(enum)\('(.+?)'\)/","\\2", $types)))
+ ));
+ } //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;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: nameToID
+ * - Returns the ID of the named item
+ */
+ function nameToID($name) {
+ $this->cacheQueries = true;
+ $item = $this->find('first', array
+ ('recursive' => -1,
+ 'conditions' => compact('name'),
+ ));
+ $this->cacheQueries = false;
+ if ($item) {
+ $item = current($item);
+ return $item['id'];
+ }
+ return null;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: statMerge
+ * - Merges summary data from $b into $a
+ */
+
+ function statsMerge (&$a, $b) {
+ if (!isset($b))
+ return;
+
+ if (!isset($a)) {
+ $a = $b;
+ }
+ elseif (!is_array($a) && !is_array($b)) {
+ $a += $b;
+ }
+ elseif (is_array($a) && is_array($b)) {
+ foreach (array_intersect_key($a, $b) AS $k => $v)
+ {
+ if (preg_match("/^sp\./", $k))
+ $a[$k] .= '; ' . $b[$k];
+ else
+ $this->statsMerge($a[$k], $b[$k]);
+ }
+ $a = array_merge($a, array_diff_key($b, $a));
+ }
+ else {
+ die ("Can't yet merge array and non-array stats");
+ }
+ }
+
+
+ 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;
+
+ if (is_array($data)) {
+ foreach ($data as $key => &$value) {
+ $this->recursive_array_replace($find, $replace, $value);
+ }
+ return;
+ }
+
+ if (isset($replace))
+ $data = preg_replace($find, $replace, $data);
+ elseif (preg_match($find, $data))
+ $data = null;
+ }
+
+ function beforeSave() {
+/* pr(array('class' => $this->name, */
+/* 'alias' => $this->alias, */
+/* 'function' => 'AppModel::beforeSave')); */
+
+ // Replace all empty strings with NULL.
+ // If a particular model doesn't like this, they'll have to
+ // override the behavior, or set useNullForEmpty to false.
+ if ($this->useNullForEmpty)
+ $this->recursive_array_replace("/^\s*$/", null, $this->data);
+
+ if ($this->formatDateFields) {
+ $alias = $this->alias;
+
+ foreach ($this->_schema AS $field => $info) {
+ if ($info['type'] == 'date' || $info['type'] == 'timestamp') {
+ if (isset($this->data[$alias][$field])) {
+/* pr("Fix Date for '$alias'.'$field'; current value = " . */
+/* "'{$this->data[$alias][$field]}'"); */
+ if ($this->data[$alias][$field] === 'CURRENT_TIMESTAMP')
+ // Seems CakePHP is broken for the default timestamp.
+ // It tries to automagically set the value to CURRENT_TIMESTAMP
+ // which is wholly rejected by MySQL. Just put it back to NULL
+ // and let the SQL engine deal with the defaults... it's not
+ // Cake's place to do this anyway :-/
+ $this->data[$alias][$field] = null;
+ else
+ $this->data[$alias][$field] =
+ $this->dateFormatBeforeSave($this->data[$alias][$field]);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: dateFormatBeforeSave
+ * - convert dates to database format
+ */
+
+ function dateFormatBeforeSave($dateString) {
+/* $time = ''; */
+/* if (preg_match('/(\d+(:\d+))/', $dateString, $match)) */
+/* $time = ' '.$match[1]; */
+/* $dateString = preg_replace('/(\d+(:\d+))/', '', $dateString); */
+/* return date('Y-m-d', strtotime($dateString)) . $time; */
+
+ if (preg_match('/:/', $dateString))
+ return date('Y-m-d H:i:s', strtotime($dateString));
+ else
+ return date('Y-m-d', strtotime($dateString));
+ }
+
+ function INTERNAL_ERROR($msg, $depth = 0) {
+ INTERNAL_ERROR($msg, false, $depth+1);
+ echo $this->requestAction(array('controller' => 'accounts',
+ 'action' => 'render_empty'),
+ array('return', 'bare' => false)
+ );
+ $this->_stop();
+ }
+}
diff --git a/config/acl.ini.php b/config/acl.ini.php
new file mode 100644
index 0000000..94a9a9a
--- /dev/null
+++ b/config/acl.ini.php
@@ -0,0 +1,74 @@
+;
+; SVN FILE: $Id: acl.ini.php 7945 2008-12-19 02:16:01Z gwoo $
+;/**
+; * Short description for file.
+; *
+; *
+; * PHP versions 4 and 5
+; *
+; * CakePHP(tm) : Rapid Development Framework http://www.cakephp.org/
+; * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
+; *
+; * Licensed under The MIT License
+; * Redistributions of files must retain the above copyright notice.
+; *
+; * @filesource
+; * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
+; * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
+; * @package cake
+; * @subpackage cake.app.config
+; * @since CakePHP(tm) v 0.10.0.1076
+; * @version $Revision: 7945 $
+; * @modifiedby $LastChangedBy: gwoo $
+; * @lastmodified $Date: 2008-12-18 18:16:01 -0800 (Thu, 18 Dec 2008) $
+; * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+; */
+
+; acl.ini.php - Cake ACL Configuration
+; ---------------------------------------------------------------------
+; Use this file to specify user permissions.
+; aco = access control object (something in your application)
+; aro = access request object (something requesting access)
+;
+; User records are added as follows:
+;
+; [uid]
+; groups = group1, group2, group3
+; allow = aco1, aco2, aco3
+; deny = aco4, aco5, aco6
+;
+; Group records are added in a similar manner:
+;
+; [gid]
+; allow = aco1, aco2, aco3
+; deny = aco4, aco5, aco6
+;
+; The allow, deny, and groups sections are all optional.
+; NOTE: groups names *cannot* ever be the same as usernames!
+;
+; ACL permissions are checked in the following order:
+; 1. Check for user denies (and DENY if specified)
+; 2. Check for user allows (and ALLOW if specified)
+; 3. Gather user's groups
+; 4. Check group denies (and DENY if specified)
+; 5. Check group allows (and ALLOW if specified)
+; 6. If no aro, aco, or group information is found, DENY
+;
+; ---------------------------------------------------------------------
+
+;-------------------------------------
+;Users
+;-------------------------------------
+
+[username-goes-here]
+groups = group1, group2
+deny = aco1, aco2
+allow = aco3, aco4
+
+;-------------------------------------
+;Groups
+;-------------------------------------
+
+[groupname-goes-here]
+deny = aco5, aco6
+allow = aco7, aco8
\ No newline at end of file
diff --git a/config/bootstrap.php b/config/bootstrap.php
new file mode 100644
index 0000000..06af98a
--- /dev/null
+++ b/config/bootstrap.php
@@ -0,0 +1,84 @@
+' . "\n";
+ echo 'INTERNAL ERROR:
' . "\n";
+ echo '' . $message . '
' . "\n";
+ echo 'This error was not caused by anything that you did wrong.' . "\n";
+ echo '
It is a problem within the application itself and should be reported to the administrator.
' . "\n";
+
+ // Print out the entire stack trace
+ echo '
' . "\nStack Trace:\n";
+ echo '' . "\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;
+ }
+
+ echo("- $bfile:$bline (" . ($bclas ? "$bclas::$bfunc" : "entry point") . ")
\n");
+ }
+ echo "
\n";
+
+ echo '
' . "\nHTTP Request:\n";
+ echo '' . "\n";
+ print_r($_REQUEST);
+ echo "\n
\n";
+
+ echo '';
+ 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)
+ *
+ * $modelPaths = array('full path to models', 'second full path to models', 'etc...');
+ * $viewPaths = array('this path to views', 'second full path to views', 'etc...');
+ * $controllerPaths = array('this path to controllers', 'second full path to controllers', 'etc...');
+ *
+ */
+//EOF
+?>
\ No newline at end of file
diff --git a/config/core.php b/config/core.php
new file mode 100644
index 0000000..3add95a
--- /dev/null
+++ b/config/core.php
@@ -0,0 +1,227 @@
+ admin_index() and /admin/controller/index
+ * 'superuser' -> superuser_index() and /superuser/controller/index
+ */
+ //Configure::write('Routing.admin', 'admin');
+
+/**
+ * Turn off all caching application-wide.
+ *
+ */
+ //Configure::write('Cache.disable', true);
+/**
+ * Enable cache checking.
+ *
+ * If set to true, for view caching you must still use the controller
+ * var $cacheAction inside your controllers to define caching settings.
+ * You can either set it controller-wide by setting var $cacheAction = true,
+ * or in each action using $this->cacheAction = true.
+ *
+ */
+ //Configure::write('Cache.check', true);
+/**
+ * Defines the default error type when using the log() function. Used for
+ * differentiating error logging and debugging. Currently PHP supports LOG_DEBUG.
+ */
+ define('LOG_ERROR', 2);
+/**
+ * The preferred session handling method. Valid values:
+ *
+ * 'php' Uses settings defined in your php.ini.
+ * 'cake' Saves session files in CakePHP's /tmp directory.
+ * 'database' Uses CakePHP's database sessions.
+ *
+ * To define a custom session handler, save it at /app/config/.php.
+ * Set the value of 'Session.save' to to utilize it in CakePHP.
+ *
+ * To use database sessions, execute the SQL file found at /app/config/sql/sessions.sql.
+ *
+ */
+ Configure::write('Session.save', 'php');
+/**
+ * The name of the table used to store CakePHP database sessions.
+ *
+ * 'Session.save' must be set to 'database' in order to utilize this constant.
+ *
+ * The table name set here should *not* include any table prefix defined elsewhere.
+ */
+ //Configure::write('Session.table', 'cake_sessions');
+/**
+ * The DATABASE_CONFIG::$var to use for database session handling.
+ *
+ * 'Session.save' must be set to 'database' in order to utilize this constant.
+ */
+ //Configure::write('Session.database', 'default');
+/**
+ * The name of CakePHP's session cookie.
+ */
+ Configure::write('Session.cookie', 'CAKEPHP');
+/**
+ * Session time out time (in seconds).
+ * Actual value depends on 'Security.level' setting.
+ */
+ Configure::write('Session.timeout', '120');
+/**
+ * If set to false, sessions are not automatically started.
+ */
+ Configure::write('Session.start', true);
+/**
+ * When set to false, HTTP_USER_AGENT will not be checked
+ * in the session
+ */
+ Configure::write('Session.checkAgent', true);
+/**
+ * The level of CakePHP security. The session timeout time defined
+ * in 'Session.timeout' is multiplied according to the settings here.
+ * Valid values:
+ *
+ * 'high' Session timeout in 'Session.timeout' x 10
+ * 'medium' Session timeout in 'Session.timeout' x 100
+ * 'low' Session timeout in 'Session.timeout' x 300
+ *
+ * CakePHP session IDs are also regenerated between requests if
+ * 'Security.level' is set to 'high'.
+ */
+ Configure::write('Security.level', 'high');
+/**
+ * A random string used in security hashing methods.
+ */
+ Configure::write('Security.salt', 'fbd497077ac32a7ab159333cd7e3eeb85db5c2a5');
+/**
+ * Compress CSS output by removing comments, whitespace, repeating tags, etc.
+ * This requires a/var/cache directory to be writable by the web server for caching.
+ * and /vendors/csspp/csspp.php
+ *
+ * To use, prefix the CSS link URL with '/ccss/' instead of '/css/' or use HtmlHelper::css().
+ */
+ //Configure::write('Asset.filter.css', 'css.php');
+/**
+ * Plug in your own custom JavaScript compressor by dropping a script in your webroot to handle the
+ * output, and setting the config below to the name of the script.
+ *
+ * To use, prefix your JavaScript link URLs with '/cjs/' instead of '/js/' or use JavaScriptHelper::link().
+ */
+ //Configure::write('Asset.filter.js', 'custom_javascript_output_filter.php');
+/**
+ * The classname and database used in CakePHP's
+ * access control lists.
+ */
+ Configure::write('Acl.classname', 'DbAcl');
+ Configure::write('Acl.database', 'default');
+/**
+ *
+ * Cache Engine Configuration
+ * Default settings provided below
+ *
+ * File storage engine.
+ *
+ * Cache::config('default', array(
+ * 'engine' => 'File', //[required]
+ * 'duration'=> 3600, //[optional]
+ * 'probability'=> 100, //[optional]
+ * 'path' => CACHE, //[optional] use system tmp directory - remember to use absolute path
+ * 'prefix' => 'cake_', //[optional] prefix every cache file with this string
+ * 'lock' => false, //[optional] use file locking
+ * 'serialize' => true, [optional]
+ * ));
+ *
+ *
+ * APC (http://pecl.php.net/package/APC)
+ *
+ * Cache::config('default', array(
+ * 'engine' => 'Apc', //[required]
+ * 'duration'=> 3600, //[optional]
+ * 'probability'=> 100, //[optional]
+ * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string
+ * ));
+ *
+ * Xcache (http://xcache.lighttpd.net/)
+ *
+ * Cache::config('default', array(
+ * 'engine' => 'Xcache', //[required]
+ * 'duration'=> 3600, //[optional]
+ * 'probability'=> 100, //[optional]
+ * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string
+ * 'user' => 'user', //user from xcache.admin.user settings
+ * 'password' => 'password', //plaintext password (xcache.admin.pass)
+ * ));
+ *
+ *
+ * Memcache (http://www.danga.com/memcached/)
+ *
+ * Cache::config('default', array(
+ * 'engine' => 'Memcache', //[required]
+ * 'duration'=> 3600, //[optional]
+ * 'probability'=> 100, //[optional]
+ * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string
+ * 'servers' => array(
+ * '127.0.0.1:11211' // localhost, default port 11211
+ * ), //[optional]
+ * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory)
+ * ));
+ *
+ */
+ Cache::config('default', array('engine' => 'File'));
+?>
\ No newline at end of file
diff --git a/config/database.php b/config/database.php
new file mode 100644
index 0000000..1bccd6c
--- /dev/null
+++ b/config/database.php
@@ -0,0 +1,14 @@
+ 'mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'login' => 'pmgr',
+ 'password' => 'pmgruser',
+ 'database' => 'property_manager',
+ 'prefix' => 'pmgr_',
+ );
+}
+?>
\ No newline at end of file
diff --git a/config/database.php.default b/config/database.php.default
new file mode 100644
index 0000000..c20fd5d
--- /dev/null
+++ b/config/database.php.default
@@ -0,0 +1,101 @@
+ The name of a supported driver; valid options are as follows:
+ * mysql - MySQL 4 & 5,
+ * mysqli - MySQL 4 & 5 Improved Interface (PHP5 only),
+ * sqlite - SQLite (PHP5 only),
+ * postgres - PostgreSQL 7 and higher,
+ * mssql - Microsoft SQL Server 2000 and higher,
+ * db2 - IBM DB2, Cloudscape, and Apache Derby (http://php.net/ibm-db2)
+ * oracle - Oracle 8 and higher
+ * firebird - Firebird/Interbase
+ * sybase - Sybase ASE
+ * adodb-[drivername] - ADOdb interface wrapper (see below),
+ * odbc - ODBC DBO driver
+ *
+ * You can add custom database drivers (or override existing drivers) by adding the
+ * appropriate file to app/models/datasources/dbo. Drivers should be named 'dbo_x.php',
+ * where 'x' is the name of the database.
+ *
+ * persistent => true / false
+ * Determines whether or not the database should use a persistent connection
+ *
+ * connect =>
+ * ADOdb set the connect to one of these
+ * (http://phplens.com/adodb/supported.databases.html) and
+ * append it '|p' for persistent connection. (mssql|p for example, or just mssql for not persistent)
+ * For all other databases, this setting is deprecated.
+ *
+ * host =>
+ * the host you connect to the database. To add a socket or port number, use 'port' => #
+ *
+ * prefix =>
+ * Uses the given prefix for all the tables in this database. This setting can be overridden
+ * on a per-table basis with the Model::$tablePrefix property.
+ *
+ * schema =>
+ * For Postgres and DB2, specifies which schema you would like to use the tables in. Postgres defaults to
+ * 'public', DB2 defaults to empty.
+ *
+ * encoding =>
+ * For MySQL, MySQLi, Postgres and DB2, specifies the character encoding to use when connecting to the
+ * database. Uses database default.
+ *
+ */
+class DATABASE_CONFIG {
+
+ var $default = array(
+ 'driver' => 'mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'login' => 'user',
+ 'password' => 'password',
+ 'database' => 'database_name',
+ 'prefix' => '',
+ );
+
+ var $test = array(
+ 'driver' => 'mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'login' => 'user',
+ 'password' => 'password',
+ 'database' => 'test_database_name',
+ 'prefix' => '',
+ );
+}
+?>
\ No newline at end of file
diff --git a/config/inflections.php b/config/inflections.php
new file mode 100644
index 0000000..c0a516e
--- /dev/null
+++ b/config/inflections.php
@@ -0,0 +1,70 @@
+ value array of regex used to match words.
+ * If key matches then the value is returned.
+ *
+ * $pluralRules = array('/(s)tatus$/i' => '\1\2tatuses', '/^(ox)$/i' => '\1\2en', '/([m|l])ouse$/i' => '\1ice');
+ */
+ $pluralRules = array();
+/**
+ * This is a key only array of plural words that should not be inflected.
+ * Notice the last comma
+ *
+ * $uninflectedPlural = array('.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox');
+ */
+ $uninflectedPlural = array('.*cash');
+/**
+ * This is a key => value array of plural irregular words.
+ * If key matches then the value is returned.
+ *
+ * $irregularPlural = array('atlas' => 'atlases', 'beef' => 'beefs', 'brother' => 'brothers')
+ */
+ $irregularPlural = array();
+/**
+ * This is a key => value array of regex used to match words.
+ * If key matches then the value is returned.
+ *
+ * $singularRules = array('/(s)tatuses$/i' => '\1\2tatus', '/(matr)ices$/i' =>'\1ix','/(vert|ind)ices$/i')
+ */
+ $singularRules = array();
+/**
+ * This is a key only array of singular words that should not be inflected.
+ * You should not have to change this value below if you do change it use same format
+ * as the $uninflectedPlural above.
+ */
+ $uninflectedSingular = $uninflectedPlural;
+/**
+ * This is a key => value array of singular irregular words.
+ * Most of the time this will be a reverse of the above $irregularPlural array
+ * You should not have to change this value below if you do change it use same format
+ *
+ * $irregularSingular = array('atlases' => 'atlas', 'beefs' => 'beef', 'brothers' => 'brother')
+ */
+ $irregularSingular = array_flip($irregularPlural);
+?>
\ No newline at end of file
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 0000000..01d09ac
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,55 @@
+ '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('/', $default_path);
+
+/*
+ * Route for admin functionality
+ */
+Router::connect('/admin',
+ array('admin_route' => true) + $default_path);
+Router::connect('/admin/:controller/:action/*',
+ array('admin_route' => true, 'action' => null));
+
+/*
+ * Route for development functionality
+ */
+Router::connect('/dev',
+ array('dev_route' => true) + $default_path);
+Router::connect('/dev/:controller/:action/*',
+ array('dev_route' => true, 'action' => null));
+
+?>
\ No newline at end of file
diff --git a/config/sql/db_acl.php b/config/sql/db_acl.php
new file mode 100644
index 0000000..5f24eab
--- /dev/null
+++ b/config/sql/db_acl.php
@@ -0,0 +1,79 @@
+ array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'),
+ 'parent_id' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'model' => array('type'=>'string', 'null' => true),
+ 'foreign_key' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'alias' => array('type'=>'string', 'null' => true),
+ 'lft' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'rght' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
+ );
+
+ var $aros = array(
+ 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'),
+ 'parent_id' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'model' => array('type'=>'string', 'null' => true),
+ 'foreign_key' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'alias' => array('type'=>'string', 'null' => true),
+ 'lft' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'rght' => array('type'=>'integer', 'null' => true, 'default' => NULL, 'length' => 10),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
+ );
+
+ var $aros_acos = array(
+ 'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'),
+ 'aro_id' => array('type'=>'integer', 'null' => false, 'length' => 10, 'key' => 'index'),
+ 'aco_id' => array('type'=>'integer', 'null' => false, 'length' => 10),
+ '_create' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2),
+ '_read' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2),
+ '_update' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2),
+ '_delete' => array('type'=>'string', 'null' => false, 'default' => '0', 'length' => 2),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'ARO_ACO_KEY' => array('column' => array('aro_id', 'aco_id'), 'unique' => 1))
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/config/sql/db_acl.sql b/config/sql/db_acl.sql
new file mode 100644
index 0000000..6d507fe
--- /dev/null
+++ b/config/sql/db_acl.sql
@@ -0,0 +1,40 @@
+# $Id: db_acl.sql 7945 2008-12-19 02:16:01Z gwoo $
+#
+# Copyright 2005-2008, Cake Software Foundation, Inc.
+#
+# Licensed under The MIT License
+# Redistributions of files must retain the above copyright notice.
+# http://www.opensource.org/licenses/mit-license.php The MIT License
+
+CREATE TABLE acos (
+ id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ parent_id INTEGER(10) DEFAULT NULL,
+ model VARCHAR(255) DEFAULT '',
+ foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
+ alias VARCHAR(255) DEFAULT '',
+ lft INTEGER(10) DEFAULT NULL,
+ rght INTEGER(10) DEFAULT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE aros_acos (
+ id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ aro_id INTEGER(10) UNSIGNED NOT NULL,
+ aco_id INTEGER(10) UNSIGNED NOT NULL,
+ _create CHAR(2) NOT NULL DEFAULT 0,
+ _read CHAR(2) NOT NULL DEFAULT 0,
+ _update CHAR(2) NOT NULL DEFAULT 0,
+ _delete CHAR(2) NOT NULL DEFAULT 0,
+ PRIMARY KEY(id)
+);
+
+CREATE TABLE aros (
+ id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ parent_id INTEGER(10) DEFAULT NULL,
+ model VARCHAR(255) DEFAULT '',
+ foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
+ alias VARCHAR(255) DEFAULT '',
+ lft INTEGER(10) DEFAULT NULL,
+ rght INTEGER(10) DEFAULT NULL,
+ PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/config/sql/i18n.php b/config/sql/i18n.php
new file mode 100644
index 0000000..72233ff
--- /dev/null
+++ b/config/sql/i18n.php
@@ -0,0 +1,56 @@
+ array('type'=>'integer', 'null' => false, 'default' => NULL, 'length' => 10, 'key' => 'primary'),
+ 'locale' => array('type'=>'string', 'null' => false, 'length' => 6, 'key' => 'index'),
+ 'model' => array('type'=>'string', 'null' => false, 'key' => 'index'),
+ 'foreign_key' => array('type'=>'integer', 'null' => false, 'length' => 10, 'key' => 'index'),
+ 'field' => array('type'=>'string', 'null' => false, 'key' => 'index'),
+ 'content' => array('type'=>'text', 'null' => true, 'default' => NULL),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'locale' => array('column' => 'locale', 'unique' => 0), 'model' => array('column' => 'model', 'unique' => 0), 'row_id' => array('column' => 'foreign_key', 'unique' => 0), 'field' => array('column' => 'field', 'unique' => 0))
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/config/sql/i18n.sql b/config/sql/i18n.sql
new file mode 100644
index 0000000..484d8a2
--- /dev/null
+++ b/config/sql/i18n.sql
@@ -0,0 +1,26 @@
+# $Id: i18n.sql 7945 2008-12-19 02:16:01Z gwoo $
+#
+# Copyright 2005-2008, Cake Software Foundation, Inc.
+#
+# Licensed under The MIT License
+# Redistributions of files must retain the above copyright notice.
+# http://www.opensource.org/licenses/mit-license.php The MIT License
+
+CREATE TABLE i18n (
+ id int(10) NOT NULL auto_increment,
+ locale varchar(6) NOT NULL,
+ model varchar(255) NOT NULL,
+ foreign_key int(10) NOT NULL,
+ field varchar(255) NOT NULL,
+ content mediumtext,
+ PRIMARY KEY (id),
+# UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
+# INDEX I18N_LOCALE_ROW(locale, model, foreign_key),
+# INDEX I18N_LOCALE_MODEL(locale, model),
+# INDEX I18N_FIELD(model, foreign_key, field),
+# INDEX I18N_ROW(model, foreign_key),
+ INDEX locale (locale),
+ INDEX model (model),
+ INDEX row_id (foreign_key),
+ INDEX field (field)
+);
\ No newline at end of file
diff --git a/config/sql/sessions.php b/config/sql/sessions.php
new file mode 100644
index 0000000..7f00a26
--- /dev/null
+++ b/config/sql/sessions.php
@@ -0,0 +1,53 @@
+ array('type'=>'string', 'null' => false, 'key' => 'primary'),
+ 'data' => array('type'=>'text', 'null' => true, 'default' => NULL),
+ 'expires' => array('type'=>'integer', 'null' => true, 'default' => NULL),
+ 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1))
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/config/sql/sessions.sql b/config/sql/sessions.sql
new file mode 100644
index 0000000..23a1925
--- /dev/null
+++ b/config/sql/sessions.sql
@@ -0,0 +1,16 @@
+# $Id: sessions.sql 7118 2008-06-04 20:49:29Z gwoo $
+#
+# Copyright 2005-2008, Cake Software Foundation, Inc.
+# 1785 E. Sahara Avenue, Suite 490-204
+# Las Vegas, Nevada 89104
+#
+# Licensed under The MIT License
+# Redistributions of files must retain the above copyright notice.
+# http://www.opensource.org/licenses/mit-license.php The MIT License
+
+CREATE TABLE cake_sessions (
+ id varchar(255) NOT NULL default '',
+ data text,
+ expires int(11) default NULL,
+ PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/controllers/accounts_controller.php b/controllers/accounts_controller.php
new file mode 100644
index 0000000..0554f26
--- /dev/null
+++ b/controllers/accounts_controller.php
@@ -0,0 +1,204 @@
+ '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')),
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * override: sideMenuLinks
+ * - Generates controller specific links for the side menu
+ */
+ function sideMenuLinks() {
+ return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / asset / liability / equity / income / expense / all
+ * - Generate a chart of accounts
+ */
+
+ function index() { $this->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: gridData
+ * - With the application controller handling the gridData action,
+ * these virtual functions ensure that the correct data is passed
+ * to jqGrid.
+ */
+
+ function gridDataSetup(&$params) {
+ parent::gridDataSetup($params);
+ if (!isset($params['action']))
+ $params['action'] = 'all';
+ }
+
+ 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 gridDataTables(&$params, &$model) {
+ return array
+ ('link' =>
+ array(// Models
+ 'CurrentLedger' => array
+ (// Models
+ 'LedgerEntry'
+ ),
+ ),
+ );
+ }
+
+ function gridDataFields(&$params, &$model) {
+ $fields = parent::gridDataFields($params, $model);
+ return array_merge($fields,
+ $this->Account->Ledger->LedgerEntry->debitCreditFields(true));
+ }
+
+ 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']));
+ }
+
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ $conditions[] = array('Account.level >=' => 10);
+
+ return $conditions;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Account'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: newledger
+ * - Close the current account ledger and create a new one,
+ * carrying forward any balance if necessary.
+ */
+
+ function newledger($id = null) {
+ $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)
+ $this->redirect(array('action'=>'view', $id));
+ else
+ $this->redirect(array('action'=>'index'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: collected
+ * - Displays the items actually collected for the period
+ * e.g. How much was collected in rent from 4/1/09 - 5/1/09
+ */
+ function collected($id = null) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+
+ $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: view
+ * - Displays information about a specific account
+ */
+
+ function view($id = null) {
+ $account = $this->Account->find
+ ('first',
+ array('contain' =>
+ array(// Models
+ 'CurrentLedger' =>
+ array('fields' => array('id', 'sequence', 'name')),
+
+ 'Ledger' =>
+ array('CloseTransaction' => array
+ ('order' => array('CloseTransaction.stamp' => 'DESC'))),
+ ),
+ 'conditions' => array(array('Account.id' => $id),
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ array('Account.level >=' => 10),
+ ),
+ )
+ );
+
+ 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));
+
+ // Prepare to render
+ $title = 'Account: ' . $account['Account']['name'];
+ $this->set(compact('account', 'title', 'stats'));
+ }
+
+}
diff --git a/controllers/contacts_controller.php b/controllers/contacts_controller.php
new file mode 100644
index 0000000..ea2e20d
--- /dev/null
+++ b/controllers/contacts_controller.php
@@ -0,0 +1,202 @@
+sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / all
+ * - Generate a listing of contacts
+ */
+
+ function index() { $this->all(); }
+ function all() { $this->gridView('All Contacts', 'all'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * virtuals: gridData
+ * - With the application controller handling the gridData action,
+ * these virtual functions ensure that the correct data is passed
+ * to jqGrid.
+ */
+
+ function gridDataOrder(&$params, &$model, $index, $direction) {
+ $order = parent::gridDataOrder($params, $model, $index, $direction);
+ if ($index === 'Contact.last_name') {
+ $order[] = 'Contact.first_name ' . $direction;
+ }
+ if ($index === 'Contact.first_name') {
+ $order[] = 'Contact.last_name ' . $direction;
+ }
+ return $order;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Contact'] = array('id');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific contact
+ */
+
+ function view($id = null) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+
+ $contact = $this->Contact->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ 'ContactPhone',
+ 'ContactEmail',
+ 'ContactAddress',
+ 'Customer'),
+
+ 'conditions' => array('Contact.id' => $id),
+ ));
+
+ // Set up dynamic menu items
+ $this->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+
+ $this->sidemenu_links[] =
+ array('name' => 'Edit',
+ 'url' => array('action' => 'edit',
+ $id));
+
+ // Prepare to render.
+ $title = 'Contact: ' . $contact['Contact']['display_name'];
+ $this->set(compact('contact', 'title'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: edit
+ */
+
+ function edit($id = null, $customer_id = null) {
+ if (isset($this->data)) {
+
+ if (isset($this->params['form']['cancel'])) {
+ if (isset($this->data['Contact']['id']))
+ $this->redirect(array('action'=>'view', $this->data['Contact']['id']));
+/* else */
+/* $this->redirect(array('controller' => 'customers', */
+/* 'action'=>'add', $this->data['Customer']['id'])); */
+ return;
+ }
+
+ // Go through each contact method and strip the bogus ID if new
+ foreach (array_intersect_key($this->data,
+ array('ContactPhone'=>1,
+ 'ContactAddress'=>1,
+ 'ContactEmail'=>1)) AS $type => $arr) {
+ foreach ($arr AS $idx => $item) {
+ if (isset($item['source']) && $item['source'] === 'new')
+ unset($this->data[$type][$idx]['id']);
+ }
+ }
+
+ // Save the contact and all associated data
+ $this->Contact->saveContact($this->data['Contact']['id'], $this->data);
+
+ // Now that the work is done, let the user view the updated contact
+ $this->redirect(array('action'=>'view', $this->data['Contact']['id']));
+ }
+
+ if ($id) {
+ $this->data = $this->Contact->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ 'ContactPhone',
+ 'ContactEmail',
+ 'ContactAddress',
+ 'Customer'),
+
+ 'conditions' => array('Contact.id' => $id),
+ ));
+
+ $title = 'Contact: ' . $this->data['Contact']['display_name'] . " : Edit";
+ }
+ else {
+ $title = "Enter New Contact";
+ $this->data = array('ContactPhone' => array(),
+ 'ContactAddress' => array(),
+ 'ContactEmail' => array());
+ }
+
+ $phone_types = array_flip($this->Contact->ContactPhone->getEnumValues('type'));
+ unset($phone_types[0]);
+ // REVISIT 20090705
+ // Use this to have a mixed case enum
+ // array_map('ucfirst', array_map('strtolower', $phone_types))
+ $phone_types = array_combine($phone_types, $phone_types);
+ $this->set(compact('phone_types'));
+
+ $method_types = array_flip($this->Contact->getEnumValues
+ ('type',
+ $this->Contact->tablePrefix . 'contacts_methods'));
+ unset($method_types[0]);
+ $method_types = array_combine($method_types, $method_types);
+ $this->set(compact('method_types'));
+
+ $method_preferences = array_flip($this->Contact->getEnumValues
+ ('preference',
+ $this->Contact->tablePrefix . 'contacts_methods'));
+ unset($method_preferences[0]);
+ $method_preferences = array_combine($method_preferences, $method_preferences);
+ $this->set(compact('method_preferences'));
+
+ $contact_phones = $this->Contact->ContactPhone->phoneList();
+ $this->set(compact('contact_phones'));
+
+ $contact_addresses = $this->Contact->ContactAddress->addressList();
+ $this->set(compact('contact_addresses'));
+
+ $contact_emails = $this->Contact->ContactEmail->emailList();
+ $this->set(compact('contact_emails'));
+
+ // Prepare to render.
+ //pr($this->data);
+ $this->set(compact('title'));
+ $this->render('edit');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: add
+ * - Adds a new contact
+ */
+
+ function add($customer_id = null) {
+ $this->edit(null, $customer_id);
+ }
+
+}
diff --git a/controllers/customers_controller.php b/controllers/customers_controller.php
new file mode 100644
index 0000000..5244feb
--- /dev/null
+++ b/controllers/customers_controller.php
@@ -0,0 +1,486 @@
+ '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');
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * override: sideMenuLinks
+ * - Generates controller specific links for the side menu
+ */
+ function sideMenuLinks() {
+ return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / current / past / all
+ * - Creates a list of customers
+ */
+
+ function index() { $this->current(); }
+ function current() { $this->gridView('Current Tenants', 'current'); }
+ function past() { $this->gridView('Past Tenants'); }
+ function all() { $this->gridView('All Customers'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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(// Models
+ 'PrimaryContact',
+ ),
+ );
+ }
+
+ 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 gridDataFields(&$params, &$model) {
+ $fields = parent::gridDataFields($params, $model);
+ return array_merge($fields,
+ $this->Customer->StatementEntry->chargeDisbursementFields(true));
+ }
+
+ function gridDataConditions(&$params, &$model) {
+ $conditions = parent::gridDataConditions($params, $model);
+
+ if ($params['action'] === 'current') {
+ $conditions[] = array('Customer.current_lease_count >' => 0);
+ }
+ elseif ($params['action'] === 'past') {
+ $conditions[] = array('Customer.current_lease_count' => 0);
+ $conditions[] = array('Customer.past_lease_count >' => 0);
+ }
+
+ return $conditions;
+ }
+
+ function gridDataOrder(&$params, &$model, $index, $direction) {
+ $order = array();
+ $order[] = parent::gridDataOrder($params, $model, $index, $direction);
+
+ if ($index !== 'PrimaryContact.last_name')
+ $order[] = parent::gridDataOrder($params, $model,
+ 'PrimaryContact.last_name', $direction);
+ if ($index !== 'PrimaryContact.first_name')
+ $order[] = parent::gridDataOrder($params, $model,
+ 'PrimaryContact.first_name', $direction);
+ if ($index !== 'Customer.id')
+ $order[] = parent::gridDataOrder($params, $model,
+ 'Customer.id', $direction);
+
+ return $order;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Customer'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_in
+ * - Sets up the move-in page for the given customer.
+ */
+
+ function move_in($id = null) {
+ $customer = array();
+ $unit = array();
+
+ if (isset($id)) {
+ $this->Customer->recursive = -1;
+ $customer = current($this->Customer->read(null, $id));
+ }
+ $this->set(compact('customer', 'unit'));
+
+ $title = 'Customer Move-In';
+ $this->set(compact('title'));
+ $this->render('/leases/move');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_out
+ * - prepare to move a customer out of one of their units
+ */
+
+ function move_out($id) {
+
+ $customer = $this->Customer->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ 'Lease' =>
+ array('conditions' => array('Lease.moveout_date' => null),
+ // Models
+ 'Unit' =>
+ array('order' => array('sort_order'),
+ 'fields' => array('id', 'name'),
+ ),
+ ),
+ ),
+
+ 'conditions' => array('Customer.id' => $id),
+ ));
+ $this->set('customer', $lease['Customer']);
+ $this->set('unit', array());
+
+ $redirect = array('controller' => 'customers',
+ 'action' => 'view',
+ $id);
+
+ $title = $customer['Customer']['name'] . ': Prepare Move-Out';
+ $this->set(compact('title', 'customer', 'redirect'));
+ $this->render('/leases/move');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific customer
+ */
+
+ function view($id = null) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+
+ // 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),
+ ));
+ //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;
+ foreach ($customer['Lease'] AS $lease) {
+ if (!isset($lease['close_date']))
+ $show_payment = true;
+ if (!isset($lease['moveout_date']))
+ $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->sidemenu_links[] =
+ array('name' => 'New Receipt',
+ 'url' => array('action' => 'receipt',
+ $id));
+
+ if (!$show_moveout && $outstanding_balance > 0)
+ $this->sidemenu_links[] =
+ array('name' => 'Write-Off',
+ 'url' => array('action' => 'bad_debt',
+ $id));
+
+ if ($outstanding_balance < 0)
+ $this->sidemenu_links[] =
+ array('name' => 'Issue Refund',
+ 'url' => array('action' => 'refund', $id));
+
+ // Prepare to render.
+ $title = 'Customer: ' . $customer['Customer']['name'];
+ $this->set(compact('customer', 'title',
+ 'outstanding_balance',
+ 'outstanding_deposit'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: edit
+ * - Edit customer information
+ */
+
+ function edit($id = null) {
+ if (isset($this->data)) {
+ // Check to see if the operation was cancelled.
+ if (isset($this->params['form']['cancel'])) {
+ if (isset($this->data['Customer']['id']))
+ $this->redirect(array('action'=>'view', $this->data['Customer']['id']));
+
+ $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']));
+ }
+
+ // 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']));
+ }
+
+ // Go through each customer and strip the bogus ID if new
+ foreach ($this->data['Contact'] AS &$contact) {
+ if (isset($contact['source']) && $contact['source'] === 'new')
+ unset($contact['id']);
+ }
+
+ // Save the customer and all associated data
+ if (!$this->Customer->saveCustomer($this->data['Customer']['id'],
+ $this->data,
+ $this->data['Customer']['primary_contact_entry'])) {
+ $this->Session->setFlash("CUSTOMER SAVE FAILED", true);
+ pr("CUSTOMER SAVE FAILED");
+ }
+
+ // If existing customer, then view it.
+ if ($this->data['Customer']['id'])
+ $this->redirect(array('action'=>'view', $this->Customer->id));
+
+ // Since this is a new customer, go to the move in screen.
+ $this->redirect(array('action'=>'move_in', $this->Customer->id));
+ }
+
+ if ($id) {
+ // REVISIT : 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 {
+ $title = "Enter New Customer";
+ $this->data = array('Contact' => array(), 'PrimaryContact' => null);
+ }
+
+ $contact_types = array_flip($this->Customer->ContactsCustomer->getEnumValues('type'));
+ unset($contact_types[0]);
+ $contact_types = array_combine($contact_types, $contact_types);
+ $this->set(compact('contact_types'));
+
+ $contacts = $this->Customer->Contact->contactList();
+ $this->set(compact('contacts'));
+
+ // Prepare to render.
+ //pr($this->data);
+ $this->set(compact('title'));
+ $this->render('edit');
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: add
+ * - Add a new customer
+ */
+
+ function add() {
+ $this->edit();
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: receipt
+ * - Sets up the receipt entry page for the given customer.
+ */
+
+ function receipt($id = null) {
+ if (isset($id)) {
+ $this->Customer->recursive = -1;
+ $customer = $this->Customer->read(null, $id);
+ $customer = $customer['Customer'];
+ }
+ else {
+ $customer = null;
+ }
+
+ $TT = new TenderType();
+ $payment_types = $TT->paymentTypes();
+ $default_type = $TT->defaultPaymentType();
+ $this->set(compact('payment_types', 'default_type'));
+
+ $title = ($customer['name'] . ': Receipt Entry');
+ $this->set(compact('customer', 'title'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: refund
+ * - Refunds customer charges
+ */
+
+ function refund($id) {
+ $customer = $this->Customer->find
+ ('first', array
+ ('contain' => false,
+ 'conditions' => array(array('Customer.id' => $id),
+ ),
+ ));
+ if (empty($customer)) {
+ $this->redirect(array('action'=>'view', $id));
+ }
+
+ // 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: bad_debt
+ * - Sets up the write-off entry page, so that the
+ * user can write off remaining charges of a customer.
+ */
+
+ function bad_debt($id) {
+ $this->Customer->id = $id;
+ $customer = $this->Customer->find
+ ('first', array
+ ('contain' => false,
+ ));
+
+ // Make sure we have a valid customer to write off
+ if (empty($customer))
+ $this->redirect(array('action' => 'index'));
+
+ // Get the customer balance
+ $balance = $this->Customer->balance($id);
+
+ // Prepare to render
+ $title = ($customer['Customer']['name'] . ': Write Off Bad Debt');
+ $this->set(compact('title', 'customer', 'balance'));
+ $this->render('/transactions/bad_debt');
+ }
+
+}
diff --git a/controllers/double_entries_controller.php b/controllers/double_entries_controller.php
new file mode 100644
index 0000000..42a61a9
--- /dev/null
+++ b/controllers/double_entries_controller.php
@@ -0,0 +1,66 @@
+sidemenu_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 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['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['CreditLedger'] = $entry['Ledger'];
+ unset($entry['Ledger']);
+
+ // Prepare to render.
+ $title = "Double Ledger Entry #{$entry['DoubleEntry']['id']}";
+ $this->set(compact('entry', 'title'));
+ }
+
+}
diff --git a/controllers/leases_controller.php b/controllers/leases_controller.php
new file mode 100644
index 0000000..ed501c7
--- /dev/null
+++ b/controllers/leases_controller.php
@@ -0,0 +1,524 @@
+ 'Leases', 'header' => true),
+ array('name' => 'Active', 'url' => array('controller' => 'leases', 'action' => 'active')),
+ array('name' => 'Closed', 'url' => array('controller' => 'leases', 'action' => 'closed')),
+ array('name' => 'Delinquent', 'url' => array('controller' => 'leases', 'action' => 'delinquent')),
+ array('name' => 'All', 'url' => array('controller' => 'leases', 'action' => 'all')),
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * override: sideMenuLinks
+ * - Generates controller specific links for the side menu
+ */
+ function sideMenuLinks() {
+ return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / active / closed / all
+ * - Generate a listing of leases
+ */
+
+ function index() { $this->all(); }
+ function active() { $this->gridView('Active Leases'); }
+ function delinquent() { $this->gridView('Delinquent Leases'); }
+ function closed() { $this->gridView('Closed Leases'); }
+ function all() { $this->gridView('All Leases', 'all'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * virtuals: gridData
+ * - With the application controller handling the gridData action,
+ * these virtual functions ensure that the correct data is passed
+ * to jqGrid.
+ */
+
+ function gridDataSetup(&$params) {
+ parent::gridDataSetup($params);
+ if (!isset($params['action']))
+ $params['action'] = 'all';
+ }
+
+ function gridDataCountTables(&$params, &$model) {
+ return array
+ ('link' => array('Unit' => array('fields' => array('id', 'name')),
+ 'Customer' => array('fields' => array('id', 'name'))));
+ }
+
+ function gridDataTables(&$params, &$model) {
+ $link = $this->gridDataCountTables($params, $model);
+ $link['link']['StatementEntry'] = array('fields' => array());
+ return $link;
+ }
+
+ 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 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 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')
+ $index = 'Lease.id';
+
+ // Instead of sorting by name, sort by defined order
+ if ($index === 'Unit.name')
+ $index = 'Unit.sort_order';
+
+ $order = array();
+ $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::gridDataOrder($params, $model,
+ 'Lease.id', $direction);
+
+ return $order;
+ }
+
+/* function gridDataPostProcess(&$params, &$model, &$records) { */
+/* foreach ($records AS &$record) { */
+/* $record['Lease']['through_date'] */
+/* = $this->Lease->rentChargeThrough($record['Lease']['id']); */
+/* } */
+
+/* parent::gridDataPostProcess($params, $model, $records); */
+/* } */
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Lease'] = array('number');
+ $links['Unit'] = array('name');
+ $links['Customer'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_in
+ * - execute a move in on a new lease
+ */
+
+ function move_in() {
+ if (!$this->data)
+ die("Should have some data");
+
+ // 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'],
+ $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, 'move-in'));
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_out
+ * - prepare or execute a move out on a specific lease
+ */
+
+ 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']
+ );
+
+ $this->redirect($this->data['redirect']);
+ }
+
+ if (!isset($id))
+ die("Oh Nooooo!!");
+
+ $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'),
+ ),
+ ),
+
+ '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']);
+
+ $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->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: refund
+ * - Provides lease customer with a refund
+ */
+
+ function refund($id) {
+ $lease = $this->Lease->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ '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));
+ }
+
+ // The refund will be for a positive amount
+ $balance *= -1;
+
+ // 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'] . ': Refund');
+ $this->set(compact('title', 'lease', 'balance'));
+ $this->render('/transactions/refund');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: 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) {
+ $this->Lease->id = $id;
+ $lease = $this->Lease->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ 'Unit' => array('fields' => array('id', 'name')),
+ 'Customer' => array('fields' => array('id', 'name')),
+ ),
+ ));
+
+ // Make sure we have a valid lease to write off
+ if (empty($lease))
+ $this->redirect(array('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', 'lease', 'balance'));
+ $this->render('/transactions/bad_debt');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: close
+ * - Closes a lease to any further action
+ */
+
+ // REVISIT : 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 : 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));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: invoice
+ * - Sets up the invoice entry page for the given customer.
+ */
+
+ function invoice($id = null, $type = null) {
+
+ $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'),
+ ),
+ ),
+
+ 'conditions' => array(array('Lease.id' => $id),
+ array('Lease.close_date' => null),
+ ),
+ ));
+
+ $A = new Account();
+ $charge_accounts = $A->invoiceAccounts();
+ $default_account = $A->rentAccountID();
+ $rent_account = $A->rentAccountID();
+ $security_deposit_account = $A->securityDepositAccountID();
+ $this->set(compact('charge_accounts', 'default_account',
+ 'rent_account', 'security_deposit_account'));
+
+ // REVISIT 20090705:
+ // Of course, the late charge should come from the late_schedule
+ $default_late = 10;
+ $this->set(compact('default_late'));
+
+ if ($type === 'move-in') {
+ $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'] . ': ' .
+ $lease['Customer']['name'] . ': Charge Entry');
+ $this->set(compact('title', 'lease', 'charge'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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: view
+ * - Displays information about a specific lease
+ */
+
+ function view($id = null) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+
+ // Get details about the lease and its ledgers (no ledger entries yet)
+ $lease = $this->Lease->find
+ ('first',
+ array('contain' =>
+ array(// Models
+ '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]);
+
+ // 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) {
+ $this->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+
+ if (!isset($lease['Lease']['moveout_date']))
+ $this->sidemenu_links[] =
+ array('name' => 'Move-Out', 'url' => array('action' => 'move_out',
+ $id));
+
+ if (!isset($lease['Lease']['close_date']))
+ $this->sidemenu_links[] =
+ array('name' => 'New Invoice', 'url' => array('action' => 'invoice',
+ $id));
+
+ $this->sidemenu_links[] =
+ array('name' => 'New Receipt', 'url' => array('controller' => 'customers',
+ 'action' => 'receipt',
+ $lease['Customer']['id']));
+
+/* if ($outstanding_balance < 0) */
+/* $this->sidemenu_links[] = */
+/* array('name' => 'Transfer Credit to Customer', */
+/* 'url' => array('action' => 'promote_surplus', $id)); */
+
+ // REVISIT :
+ // 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 ($outstanding_balance < 0) */
+/* $this->sidemenu_links[] = */
+/* array('name' => 'Issue Refund', */
+/* 'url' => array('action' => 'refund', $id)); */
+
+ if (isset($lease['Lease']['moveout_date']) && $outstanding_balance > 0)
+ $this->sidemenu_links[] =
+ array('name' => 'Write-Off', 'url' => array('action' => 'bad_debt',
+ $id));
+
+ if ($this->Lease->closeable($id))
+ $this->sidemenu_links[] =
+ array('name' => 'Close', 'url' => array('action' => 'close',
+ $id));
+ }
+
+ // Prepare to render
+ $title = 'Lease: #' . $lease['Lease']['id'];
+ $this->set(compact('lease', 'title',
+ 'outstanding_deposit',
+ 'outstanding_balance'));
+ }
+}
diff --git a/controllers/ledger_entries_controller.php b/controllers/ledger_entries_controller.php
new file mode 100644
index 0000000..d1e7805
--- /dev/null
+++ b/controllers/ledger_entries_controller.php
@@ -0,0 +1,217 @@
+sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / current / past / all
+ * - Creates a list of ledger entries
+ */
+
+ function index() { $this->gridView('All Ledger Entries'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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) {
+ $link =
+ array(// Models
+ 'Transaction' =>
+ array('fields' => array('id', 'stamp'),
+ ),
+
+ 'Ledger' =>
+ array('fields' => array('id', 'sequence'),
+ 'Account' =>
+ array('fields' => array('id', 'name', 'type'),
+ ),
+ ),
+
+ 'Tender' =>
+ array('fields' => array('id', 'name', 'nsf_transaction_id'),
+ ),
+
+/* 'DebitEntry', */
+/* 'CreditEntry', */
+ );
+
+ return array('link' => $link);
+ }
+
+ function gridDataFields(&$params, &$model) {
+ $fields = parent::gridDataFields($params, $model);
+ return array_merge($fields,
+ $this->LedgerEntry->debitCreditFields());
+ }
+
+ 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;
+
+ // Customer needs to be added beneath Transaction
+ if ($table == 'Customer')
+ return 'Transaction';
+
+ return $table;
+ }
+
+ 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 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 gridDataOrder(&$params, &$model, $index, $direction) {
+/* if ($index === 'balance') */
+/* return ($index .' '. $direction); */
+ $order = parent::gridDataOrder($params, $model, $index, $direction);
+
+ if ($index === 'Transaction.stamp') {
+ $order[] = 'LedgerEntry.id ' . $direction;
+ }
+
+ return $order;
+ }
+
+ function gridDataPostProcessCalculatedFields(&$params, &$model, &$records) {
+ parent::gridDataPostProcessCalculatedFields($params, $model, $records);
+ foreach ($records AS &$record) {
+ // REVISIT : 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'] =
+ '' . $record['Tender']['name'] . '';
+ }
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['LedgerEntry'] = array('id');
+ $links['Transaction'] = array('id');
+ $links['Ledger'] = array('id');
+ $links['Account'] = array('controller' => 'accounts', 'name');
+ $links['Tender'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific entry
+ */
+
+ function view($id = null) {
+ $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'),
+ 'conditions' =>
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ array('Account.level >=' => 5),
+ ),
+ ),
+
+ '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'));
+ }
+
+ 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");
+
+ if (empty($entry['DebitEntry']))
+ $entry['MatchingEntry'] = $entry['CreditEntry'][0];
+ else
+ $entry['MatchingEntry'] = $entry['DebitEntry'][0];
+
+ if (empty($entry['DebitDoubleEntry']['id']))
+ $entry['DoubleEntry'] = $entry['CreditDoubleEntry'];
+ else
+ $entry['DoubleEntry'] = $entry['DebitDoubleEntry'];
+
+ // REVISIT : 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 = "Ledger Entry #{$entry['LedgerEntry']['id']}";
+ $this->set(compact('entry', 'title'));
+ }
+
+}
diff --git a/controllers/ledgers_controller.php b/controllers/ledgers_controller.php
new file mode 100644
index 0000000..5aa073b
--- /dev/null
+++ b/controllers/ledgers_controller.php
@@ -0,0 +1,149 @@
+ '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
+ */
+ function sideMenuLinks() {
+ return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / current / closed / all
+ * - Generate a list of ledgers
+ */
+
+ function index() { $this->all(); }
+ function current() { $this->gridView('Current Ledgers'); }
+ function closed() { $this->gridView('Closed Ledgers'); }
+ function all() { $this->gridView('All Ledgers', 'all'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * virtuals: gridData
+ * - With the application controller handling the gridData action,
+ * these virtual functions ensure that the correct data is passed
+ * to jqGrid.
+ */
+
+ function gridDataSetup(&$params) {
+ parent::gridDataSetup($params);
+ if (!isset($params['action']))
+ $params['action'] = 'all';
+ }
+
+ function gridDataCountTables(&$params, &$model) {
+ return array
+ ('link' =>
+ array(// Models
+ 'Account',
+ ),
+ );
+ }
+
+ function gridDataTables(&$params, &$model) {
+ $tables = $this->gridDataCountTables($params, $model);
+ $tables['link'][] = 'LedgerEntry';
+ $tables['link'][] = 'CloseTransaction';
+ return $tables;
+ }
+
+ 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('Ledger.close_transaction_id' => null);
+ }
+ elseif ($params['action'] === 'closed') {
+ $conditions[] = array('Ledger.close_transaction_id !=' => null);
+ }
+
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ $conditions[] = array('Account.level >=' => 10);
+
+ return $conditions;
+ }
+
+ function gridDataOrder(&$params, &$model, $index, $direction) {
+ $id_sequence = false;
+ if ($index === 'id_sequence') {
+ $id_sequence = true;
+ $index = 'Ledger.account_id';
+ }
+
+ $order = parent::gridDataOrder($params, $model, $index, $direction);
+
+ if ($id_sequence) {
+ $order[] = 'Ledger.sequence ' . $direction;
+ }
+
+ return $order;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Ledger'] = array('id_sequence');
+ $links['Account'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific ledger
+ */
+
+ function view($id = null) {
+ $ledger = $this->Ledger->find
+ ('first',
+ array('contain' =>
+ array(// Models
+ 'Account',
+ ),
+ 'conditions' => array(array('Ledger.id' => $id),
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ array('Account.level >=' => 10),
+ ),
+ )
+ );
+
+ 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);
+
+ // OK, set our view variables and render!
+ $title = 'Ledger: #' . $ledger['Account']['id'] .'-'. $ledger['Ledger']['sequence'];
+ $this->set(compact('ledger', 'title', 'stats'));
+ }
+}
diff --git a/controllers/maps_controller.php b/controllers/maps_controller.php
new file mode 100644
index 0000000..4a073ad
--- /dev/null
+++ b/controllers/maps_controller.php
@@ -0,0 +1,306 @@
+ 20090528:
+ * We'll need to present only those site area maps that correspond
+ * to the users particular site.
+ */
+
+ function index() { $this->all(); }
+ function all() { $this->gridView('All Maps', '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('SiteArea' => array('fields' => array('SiteArea.id', 'SiteArea.name')),
+ ),
+ );
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Map'] = array('id');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Generates a site map page
+ */
+
+ function view($id = null, $requested_width = 800) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+ $this->set('info', $this->mapInfo($id, $requested_width));
+ $this->set('title', "Site Map");
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: map
+ * - Produces a PNG site map image
+ */
+
+ function map($id = null, $requested_width = 800) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+ $this->image($this->mapInfo($id, $requested_width));
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * mapInfo
+ */
+
+ function mapInfo($id, $requested_width) {
+ // Set up array to hold the map information
+ $info = array('map_id' => $id,
+ 'border' => true,
+ 'units' => array());
+
+ // Find all of the map/unit information from this SiteArea
+ $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
+ * to reside in the view. However, two separate views need this
+ * information. The 'view' needs it to include a clickable map
+ * that corresponds to the map image, and of course, the 'map'
+ * (or 'image') view needs it to render the image. So, in the
+ * controller for now, unless I come up with a better idea.
+ *****/
+
+ // Get the overall site limits, and then compute the
+ // actual boundary extents, adjusting for a border
+ $boundary_adjustment = 12;
+ $bottom = 2*$boundary_adjustment + $map['Map']['depth'];
+ $right = 2*$boundary_adjustment + $map['Map']['width'];
+
+ // Scale things according to desired display width
+ $screen_adjustment_factor = $requested_width / $right;
+
+ // Define the overall canvas size
+ $info['width'] = $right * $screen_adjustment_factor;
+ $info['depth'] = $bottom * $screen_adjustment_factor;
+
+ // Go through each unit in the map, calculating the map location
+ foreach ($units AS $unit) {
+ $lft = $unit['MapsUnit']['pt_left'] + $boundary_adjustment;
+ $top = $unit['MapsUnit']['pt_top'] + $boundary_adjustment;
+
+ $width =
+ $unit['MapsUnit']['transpose']
+ ? $unit['UnitSize']['depth']
+ : $unit['UnitSize']['width'];
+
+ $depth =
+ $unit['MapsUnit']['transpose']
+ ? $unit['UnitSize']['width']
+ : $unit['UnitSize']['depth'];
+
+ $lft *= $screen_adjustment_factor;
+ $top *= $screen_adjustment_factor;
+ $width *= $screen_adjustment_factor;
+ $depth *= $screen_adjustment_factor;
+
+ $info['units'][] =
+ array( 'id' => $unit['Unit']['id'],
+ 'name' => $unit['Unit']['name'],
+ 'left' => $lft,
+ 'right' => $lft + $width,
+ 'top' => $top,
+ 'bottom' => $top + $depth,
+ 'width' => $width,
+ 'depth' => $depth,
+ 'n-s' => $unit['MapsUnit']['transpose'] ? 0 : 1,
+ 'status' => (($unit['Unit']['status'] === 'OCCUPIED' &&
+ !empty($unit[0]['delinquent']))
+ ? 'LATE' : $unit['Unit']['status']),
+ 'data' => $unit,
+ );
+ }
+
+/* pr($info); */
+/* $this->render('/empty'); exit(); */
+ return $info;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: legend
+ * - Produces a PNG color legend image
+ */
+
+ function legend($id = null, $requested_width = 400) {
+ $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);
+
+ $info = array('units' => array());
+
+ // Get the overall site limits, and then compute the
+ // actual boundary extents, adjusting for a border
+ $boundary_adjustment = 1;
+ $item_width = 40; // Absolute values are irrelevant, as they
+ $item_depth = 10; // will be scaled in the end anyway.
+ $bottom = 2*$boundary_adjustment + $rows*$item_depth;
+ $right = 2*$boundary_adjustment + $cols*$item_width;
+
+ // Scale things according to desired display width
+ $screen_adjustment_factor = $requested_width / $right;
+
+ // Define the overall canvas size
+ $info['width'] = $right * $screen_adjustment_factor;
+ $info['depth'] = $bottom * $screen_adjustment_factor;
+
+ // Get a starting point for our top left position.
+ $top = $lft = $boundary_adjustment;
+
+ // Scale it appropriately.
+ $top *= $screen_adjustment_factor;
+ $lft *= $screen_adjustment_factor;
+ $item_width *= $screen_adjustment_factor;
+ $item_depth *= $screen_adjustment_factor;
+
+ foreach ($status AS $code) {
+ $info['units'][] = array('name' => $code,
+ 'status' => $code,
+ 'width' => $item_width,
+ 'depth' => $item_depth,
+ 'left' => $lft,
+ 'right' => $lft + $item_width,
+ 'top' => $top,
+ 'bottom' => $top + $item_depth);
+ $top += $item_depth;
+ if ($top >= $item_depth * $rows) {
+ $top = $boundary_adjustment * $screen_adjustment_factor;
+ $lft += $item_width;
+ }
+ }
+
+ $this->image($info, true);
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * helper: image
+ * - used by actions map & legend to set up unit information and
+ * color palates before rendering the PNG image.
+ */
+
+ function image($info, $legend = false) {
+ $debug = false;
+
+ if (!$debug) {
+ $this->layout = null;
+ $this->autoLayout = false;
+ Configure::write('debug', '0');
+ }
+
+ // Define our color palate
+ // REVISIT : 20090513
+ // Get colors from DB option tables
+ $info['palate']['main']['layout']['bg'] = array('red' => 255, 'green' => 255, 'blue' => 255);
+ $info['palate']['main']['layout']['border'] = array('red' => 192, 'green' => 192, 'blue' => 192);
+ $info['palate']['main']['layout']['wall'] = array('red' => 0, 'green' => 0, 'blue' => 0);
+ $info['palate']['unit']['DELETED']['bg'] = array('red' => 0, 'green' => 0, 'blue' => 0);
+ $info['palate']['unit']['DAMAGED']['bg'] = array('red' => 192, 'green' => 128, 'blue' => 128);
+ $info['palate']['unit']['COMPANY']['bg'] = array('red' => 128, 'green' => 192, 'blue' => 128);
+ $info['palate']['unit']['UNAVAILABLE']['bg'] = array('red' => 128, 'green' => 128, 'blue' => 192);
+ $info['palate']['unit']['RESERVED']['bg'] = array('red' => 192, 'green' => 192, 'blue' => 128);
+ $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' => 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) {
+ $component = $code['bg'];
+ $method = 3;
+ if ($method == 1) {
+ foreach (array('red', 'green', 'blue') AS $prim)
+ $component[$prim] = 255 - $component[$prim];
+ } elseif ($method == 2) {
+ foreach (array('red', 'green', 'blue') AS $prim)
+ $component[$prim] = ($component[$prim]) >= 128 ? 0 : 255;
+ } elseif ($method == 3) {
+ $val = (sqrt(pow($component['red'], 2) +
+ pow($component['green'], 2) +
+ pow($component['blue'], 2)) >= sqrt(3) * 128) ? 0 : 255;
+ foreach (array('red', 'green', 'blue') AS $prim)
+ $component[$prim] = $val;
+ }
+
+ $code['fg'] = $component;
+ }
+
+ $this->set(compact('info', 'debug'));
+ $this->render('image');
+ }
+
+
+
+}
+
+?>
\ No newline at end of file
diff --git a/controllers/statement_entries_controller.php b/controllers/statement_entries_controller.php
new file mode 100644
index 0000000..b260699
--- /dev/null
+++ b/controllers/statement_entries_controller.php
@@ -0,0 +1,321 @@
+sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / current / past / all
+ * - Creates a list of statement entries
+ */
+
+ function index() { $this->gridView('All Statement Entries'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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();
+ $link['DisbursementEntry'] = array();
+ }
+
+ 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)));
+ }
+
+ if ($params['action'] === 'unreconciled') {
+ $query = array('conditions' => $conditions);
+ $set = $this->StatementEntry->reconciledSet('CHARGE', $query, true);
+
+ $entries = array();
+ foreach ($set['entries'] AS $entry)
+ $entries[] = $entry['StatementEntry']['id'];
+
+ $conditions[] = array('StatementEntry.id' => $entries);
+ $params['userdata']['balance'] = $set['summary']['balance'];
+ }
+
+ return $conditions;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['StatementEntry'] = array('id');
+ $links['Transaction'] = array('id');
+ $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?)
+ $order[] = 'Transaction.stamp ' . $direction;
+ $order[] = 'StatementEntry.effective_date ' . $direction;
+ $order[] = 'StatementEntry.id ' . $direction;
+
+ return $order;
+ }
+
+ function gridDataRecordsExecute(&$params, &$model, $query) {
+/* if ($params['action'] === '???') { */
+/* $tquery = array_diff_key($query, array('fields'=>1,'group'=>1,'limit'=>1,'order'=>1)); */
+/* $tquery['fields'] = array("IF(StatementEntry.type = 'CHARGE'," . */
+/* " SUM(COALESCE(DisbursementEntry.amount,0))," . */
+/* " SUM(COALESCE(ChargeEntry.amount,0)))" . */
+/* " AS 'applied'", */
+
+/* "StatementEntry.amount - (" . */
+/* "IF(StatementEntry.type = 'CHARGE'," . */
+/* " SUM(COALESCE(DisbursementEntry.amount,0))," . */
+/* " SUM(COALESCE(ChargeEntry.amount,0)))" . */
+/* ") AS 'balance'", */
+/* ); */
+
+/* //pr(compact('tquery')); */
+/* $total = $model->find('first', $tquery); */
+/* $params['userdata']['total'] = $total[0]['applied']; */
+/* $params['userdata']['balance'] = $total[0]['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: 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'),
+ 'Customer' => array('fields' => array('id', 'name')),
+ 'Lease' => array('fields' => array('id')),
+ ),
+
+ 'conditions' => array(array('StatementEntry.id' => $id),
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ array('Account.level >=' => 5)
+ ),
+ ));
+
+ if (empty($entry)) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('controller' => 'accounts', 'action'=>'index'));
+ }
+
+ $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') {
+
+ $reversable = $this->StatementEntry->reversable($id);
+
+ // Set up dynamic menu items
+ if ($reversable || $stats['balance'] > 0)
+ $this->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+
+ if ($reversable)
+ $this->sidemenu_links[] =
+ array('name' => 'Reverse',
+ 'url' => array('action' => 'reverse',
+ $id));
+
+ if ($stats['balance'] > 0)
+ $this->sidemenu_links[] =
+ array('name' => 'Waive Balance',
+ 'url' => array('action' => 'waive',
+ $id));
+ }
+
+ // Prepare to render.
+ $title = "Statement Entry #{$entry['StatementEntry']['id']}";
+ $this->set(compact('entry', 'title', 'stats'));
+ }
+
+}
diff --git a/controllers/tenders_controller.php b/controllers/tenders_controller.php
new file mode 100644
index 0000000..8e8be18
--- /dev/null
+++ b/controllers/tenders_controller.php
@@ -0,0 +1,258 @@
+sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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 : 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'] =
+ '' . $record['Tender']['name'] . '';
+ }
+ }
+
+ 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')),
+ ));
+
+
+ // Set up dynamic menu items
+ $this->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+
+ // Watch out for the special "Closing" entries
+ if (!empty($tender['TenderType']['id']))
+ $this->sidemenu_links[] =
+ array('name' => 'Edit',
+ 'url' => array('action' => 'edit',
+ $id));
+
+ 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->sidemenu_links[] =
+ array('name' => 'NSF',
+ 'url' => array('action' => 'nsf',
+ $id));
+ }
+
+ // 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 : 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'));
+ }
+}
diff --git a/controllers/transactions_controller.php b/controllers/transactions_controller.php
new file mode 100644
index 0000000..eca9677
--- /dev/null
+++ b/controllers/transactions_controller.php
@@ -0,0 +1,496 @@
+ 'Transactions', 'header' => true),
+ array('name' => 'All', 'url' => array('controller' => 'transactions', 'action' => 'all')),
+ array('name' => 'Invoices', 'url' => array('controller' => 'transactions', 'action' => 'invoice')),
+ array('name' => 'Receipts', 'url' => array('controller' => 'transactions', 'action' => 'receipt')),
+ array('name' => 'Deposits', 'url' => array('controller' => 'transactions', 'action' => 'deposit')),
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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 transactions
+ */
+
+ function index() { $this->all(); }
+ function all() { $this->gridView('All Transactions', 'all'); }
+ function invoice() { $this->gridView('Invoices'); }
+ function receipt() { $this->gridView('Receipts'); }
+ function deposit() {
+ $this->sidemenu_links = array
+ (array('name' => 'Operations', 'header' => true),
+ array('name' => 'New Deposit', 'url' => array('controller' => 'tenders',
+ 'action' => 'deposit')));
+ $this->gridView('Deposits');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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(// Models
+ 'Account' => array('fields' => array()),
+ ),
+ );
+ }
+
+ function gridDataTables(&$params, &$model) {
+ $link = $this->gridDataCountTables($params, $model);
+ $link['link']['StatementEntry'] = array('fields' => array());
+ $link['link']['DepositTender'] = array('fields' => array());
+ return $link;
+ }
+
+ 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;
+ }
+
+ function gridDataConditions(&$params, &$model) {
+ $conditions = parent::gridDataConditions($params, $model);
+
+ if (in_array($params['action'], array('invoice', 'receipt', 'deposit')))
+ $conditions[] = array('Transaction.type' => strtoupper($params['action']));
+
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ $conditions[] = array('Account.level >=' => 5);
+
+ return $conditions;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Transaction'] = array('id', 'action' => ($params['action'] == 'deposit'
+ ? 'deposit_slip' : 'view'));
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: postInvoice
+ * - handles the creation of a charge invoice
+ */
+
+ function postInvoice() {
+ if (!$this->RequestHandler->isPost()) {
+ echo('THIS IS NOT A POST FOR SOME REASON
');
+ return;
+ }
+
+ if (!$this->Transaction->addInvoice($this->data, null,
+ $this->data['Lease']['id'])) {
+ $this->Session->setFlash("INVOICE FAILED", true);
+ // REVISIT 20090706:
+ // Until we can work out the session problems,
+ // just die.
+ die("INVOICE FAILED
");
+ }
+
+ $this->layout = null;
+ $this->autoLayout = false;
+ $this->autoRender = false;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: postReceipt
+ * - handles the creation of a receipt
+ */
+
+ function postReceipt() {
+ if (!$this->RequestHandler->isPost()) {
+ echo('THIS IS NOT A POST FOR SOME REASON
');
+ return;
+ }
+
+ foreach($this->data['Entry'] AS &$entry) {
+ $entry['Tender'] = $entry['type'][$entry['tender_type_id']];
+ unset($entry['type']);
+ unset($entry['tender_type_id']);
+ }
+
+ if (!$this->Transaction->addReceipt($this->data,
+ $this->data['Customer']['id'],
+ (isset($this->data['Lease']['id'])
+ ? $this->data['Lease']['id']
+ : null ))) {
+ $this->Session->setFlash("RECEIPT FAILED", true);
+ // REVISIT 20090706:
+ // Until we can work out the session problems,
+ // just die.
+ die("RECEIPT FAILED
");
+ }
+
+ $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('THIS IS NOT A POST FOR SOME REASON
');
+ 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('THIS IS NOT A POST FOR SOME REASON
');
+ 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 20090706:
+ // Until we can work out the session problems,
+ // just die.
+ die("WRITE-OFF FAILED
");
+ }
+
+ // 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('THIS IS NOT A POST FOR SOME REASON
');
+ 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 20090706:
+ // Until we can work out the session problems,
+ // just die.
+ die("REFUND FAILED
");
+ }
+
+ // 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 = null) {
+ $this->Transaction->destroy($id);
+ //$this->redirect(array('action' => 'index'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific transaction
+ */
+
+ function view($id = null) {
+ $transaction = $this->Transaction->find
+ ('first',
+ array('contain' =>
+ array(// Models
+ 'Account(id,name)',
+ 'Ledger(id,name)',
+ 'NsfTender(id,name)',
+ ),
+ 'conditions' => array(array('Transaction.id' => $id),
+ // REVISIT : 20090811
+ // No security issues have been worked out yet
+ array('OR' =>
+ array(array('Account.level >=' => 5),
+ array('Account.id' => null))),
+ ),
+ ));
+
+ // REVISIT : 20090815
+ // for debug purposes only (pr output)
+ $this->Transaction->stats($id);
+
+ if (empty($transaction)) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>'index'));
+ }
+
+ if ($transaction['Transaction']['type'] === 'DEPOSIT' || $this->params['dev']) {
+ $this->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+
+ if ($transaction['Transaction']['type'] === 'DEPOSIT')
+ $this->sidemenu_links[] =
+ array('name' => 'View Slip', 'url' => array('action' => 'deposit_slip', $id));
+ if ($this->params['dev'])
+ $this->sidemenu_links[] =
+ array('name' => 'Destroy', 'url' => array('action' => 'destroy', $id),
+ 'confirmMessage' => ("This may leave the database in an unstable state." .
+ " Do NOT do this unless you know what you're doing." .
+ " Proceed anyway?"));
+ }
+
+ // 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) {
+ // Build a container for the deposit slip data
+ $deposit = array('types' => array());
+
+ $this->id = $id;
+ $deposit +=
+ $this->Transaction->find('first', array('contain' => false));
+
+ // Get a summary of all forms of tender in the deposit
+ $result = $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',
+ ));
+
+ if (empty($result)) {
+ die();
+ $this->Session->setFlash(__('Invalid Deposit.', true));
+ $this->redirect(array('action'=>'deposit'));
+ }
+
+ // Add the summary to our deposit slip data container
+ foreach ($result AS $type) {
+ $deposit['types'][$type['TenderType']['id']] =
+ $type['TenderType'] + $type[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->sidemenu_links[] =
+ array('name' => 'Operations', 'header' => true);
+ $this->sidemenu_links[] =
+ array('name' => 'View Transaction', 'url' => array('action' => 'view', $id));
+
+ $title = 'Deposit Slip';
+ $this->set(compact('title', 'deposit'));
+ $this->render('deposit_slip');
+ return;
+ }
+
+}
diff --git a/controllers/units_controller.php b/controllers/units_controller.php
new file mode 100644
index 0000000..50f15a0
--- /dev/null
+++ b/controllers/units_controller.php
@@ -0,0 +1,336 @@
+ '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
+ */
+ function sideMenuLinks() {
+ return array_merge(parent::sideMenuLinks(), $this->sidemenu_links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: index / unavailable / vacant / occupied / all
+ * - Generate a listing of units
+ */
+
+ function index() { $this->all(); }
+ function unavailable() { $this->gridView('Unavailable Units'); }
+ function vacant() { $this->gridView('Vacant Units'); }
+ function occupied() { $this->gridView('Occupied Units'); }
+ function all() { $this->gridView('All Units', 'all'); }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * virtuals: gridData
+ * - With the application controller handling the gridData action,
+ * these virtual functions ensure that the correct data is passed
+ * to jqGrid.
+ */
+
+ function gridDataSetup(&$params) {
+ parent::gridDataSetup($params);
+ if (!isset($params['action']))
+ $params['action'] = 'all';
+ }
+
+ 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', */
+/* ), */
+/* ); */
+
+ }
+
+ function gridDataTables(&$params, &$model) {
+ $link = $this->gridDataCountTables($params, $model);
+ $link['link']['CurrentLease']['StatementEntry'] = array('fields' => array());
+ 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);
+
+ return array_merge($fields,
+ $this->Unit->Lease->StatementEntry->chargeDisbursementFields(true));
+ }
+
+ function gridDataConditions(&$params, &$model) {
+ $conditions = parent::gridDataConditions($params, $model);
+
+ if ($params['action'] === 'unavailable') {
+ $conditions[] = $this->Unit->conditionUnavailable();
+ }
+ elseif ($params['action'] === 'vacant') {
+ $conditions[] = $this->Unit->conditionVacant();
+ }
+ elseif ($params['action'] === 'occupied') {
+ $conditions[] = $this->Unit->conditionOccupied();
+ }
+ elseif ($params['action'] === 'unoccupied') {
+ $conditions[] = array('NOT' => array($this->Unit->conditionOccupied()));
+ }
+
+ return $conditions;
+ }
+
+ 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::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::gridDataOrder($params, $model,
+ 'Unit.sort_order', $direction);
+
+ return $order;
+ }
+
+ function gridDataPostProcessLinks(&$params, &$model, &$records, $links) {
+ $links['Unit'] = array('name');
+ $links['UnitSize'] = array('name');
+ return parent::gridDataPostProcessLinks($params, $model, $records, $links);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_in
+ * - Sets up the move-in page for the given unit.
+ */
+
+ function move_in($id = null) {
+ $customer = array();
+ $unit = array();
+
+ if (isset($id)) {
+ $this->Unit->recursive = -1;
+ $unit = current($this->Unit->read(null, $id));
+ }
+
+ $title = 'Unit Move-In';
+ $this->set(compact('customer', 'unit', 'title'));
+ $this->render('/leases/move');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: move_out
+ * - prepare or execute a move out on a specific lease
+ */
+
+ function move_out($id) {
+
+ $unit = $this->Unit->find
+ ('first', array
+ ('contain' => array
+ (// Models
+ 'CurrentLease' =>
+ array(//'conditions' => array('Lease.moveout_date' => null),
+ // Models
+ 'Customer' =>
+ array('fields' => array('id', 'name'),
+ ),
+ ),
+ ),
+ 'conditions' => array('Unit.id' => $id),
+ ));
+ $this->set('customer', $unit['CurrentLease']['Customer']);
+ $this->set('unit', $unit['Unit']);
+ $this->set('lease', $unit['CurrentLease']);
+
+ $redirect = array('controller' => 'units',
+ 'action' => 'view',
+ $id);
+
+ $title = ('Lease #' . $unit['CurrentLease']['number'] . ': ' .
+ $unit['Unit']['name'] . ': ' .
+ $unit['CurrentLease']['Customer']['name'] . ': Prepare Move-Out');
+ $this->set(compact('title', 'redirect'));
+ $this->render('/leases/move');
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * action: view
+ * - Displays information about a specific unit
+ */
+
+ function view($id = null) {
+ if (!$id) {
+ $this->Session->setFlash(__('Invalid Item.', true));
+ $this->redirect(array('action'=>''));
+ }
+
+ $unit = $this->Unit->find
+ ('first',
+ array('contain' =>
+ array(// Models
+ 'UnitSize',
+ 'Lease' => array('Customer'),
+ 'CurrentLease' => array('Customer')
+ ),
+ 'conditions' => array('Unit.id' => $id),
+ ));
+
+ // Get the balance on each lease.
+ foreach ($unit['Lease'] AS &$lease) {
+ $stats = $this->Unit->Lease->stats($lease['id']);
+ $lease['balance'] = $stats['balance'];
+ }
+
+ $outstanding_balance = 0;
+ $outstanding_deposit = 0;
+ if (isset($unit['CurrentLease']['id'])) {
+ // Figure out the outstanding balance of the current lease.
+ $stats = $this->Unit->stats($id);
+ $outstanding_balance =
+ $stats['CurrentLease']['balance'];
+
+ // Figure out the total security deposit for the current lease.
+ $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);
+
+ $this->sidemenu_links[] =
+ array('name' => 'Edit', 'url' => array('action' => 'edit',
+ $id));
+
+ if (isset($unit['CurrentLease']['id']) &&
+ !isset($unit['CurrentLease']['moveout_date'])) {
+ $this->sidemenu_links[] =
+ array('name' => 'Move-Out', 'url' => array('action' => 'move_out',
+ $id));
+ } elseif ($this->Unit->available($unit['Unit']['status'])) {
+ $this->sidemenu_links[] =
+ array('name' => 'Move-In', 'url' => array('action' => 'move_in',
+ $id));
+ } else {
+ // Unit is unavailable (dirty, damaged, reserved, business-use, etc)
+ }
+
+ if (isset($unit['CurrentLease']['id']) &&
+ !isset($unit['CurrentLease']['close_date'])) {
+ $this->sidemenu_links[] =
+ array('name' => 'New Invoice', 'url' => array('controller' => 'leases',
+ 'action' => 'invoice',
+ $unit['CurrentLease']['id']));
+ $this->sidemenu_links[] =
+ array('name' => 'New Receipt', 'url' => array('controller' => 'customers',
+ 'action' => 'receipt',
+ $unit['CurrentLease']['customer_id']));
+ }
+
+ // 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.
+ pr($this->data);
+ $this->set(compact('title'));
+ }
+
+
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..4e60b84
--- /dev/null
+++ b/index.php
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/models/account.php b/models/account.php
new file mode 100644
index 0000000..36a9d2b
--- /dev/null
+++ b/models/account.php
@@ -0,0 +1,389 @@
+ array(
+ 'className' => 'Ledger',
+ // REVISIT 20090702:
+ // I would prefer this statement, which has no
+ // engine specific code. However, it doesn't
+ // work with the Linkable behavior. I need to
+ // look into that, just not right now.
+ //'conditions' => array(array('CurrentLedger.close_transaction_id' => null)),
+ 'conditions' => array('CurrentLedger.close_transaction_id IS NULL'),
+ ),
+ );
+
+ var $hasMany = array(
+ 'Ledger',
+ 'LedgerEntry',
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: type
+ * - Returns the type of this account
+ */
+ function type($id) {
+ $this->cacheQueries = true;
+ $account = $this->find('first', array
+ ('recursive' => -1,
+ 'fields' => array('type'),
+ 'conditions' => array(array('Account.id' => $id)),
+ ));
+ $this->cacheQueries = false;
+
+ return $account['Account']['type'];
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: fundamentalType
+ * - Returns the fundmental type of the account, credit or debit
+ */
+ function fundamentalType($id_or_type) {
+ if (is_numeric($id_or_type))
+ $type = $this->type($id_or_type);
+ else
+ $type = $id_or_type;
+
+ // Asset and Expense accounts are debit accounts
+ if (in_array(strtoupper($type), array('ASSET', 'EXPENSE')))
+ return 'debit';
+
+ // Otherwise, it's a credit account
+ return 'credit';
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: fundamentalOpposite
+ * - Returns the opposite fundmental type of the account, credit or debit
+ */
+ function fundamentalOpposite($id_or_type) {
+ if (in_array(strtolower($id_or_type), array('credit', 'debit')))
+ $fund = $id_or_type;
+ else
+ $fund = $this->fundamentalType($id_or_type);
+
+ if (strtolower($fund) == 'debit')
+ return 'credit';
+
+ return 'debit';
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: name
+ * - Returns the name of this account
+ */
+ function name($id) {
+ $this->cacheQueries = true;
+ $account = $this->find('first', array
+ ('recursive' => -1,
+ 'fields' => array('name'),
+ 'conditions' => array(array('Account.id' => $id)),
+ ));
+ $this->cacheQueries = false;
+
+ return $account['Account']['name'];
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: Account IDs
+ * - Returns the ID of the desired account
+ */
+
+ 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 : 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'
+ ); }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: fundamentalAccounts
+ * - Returns an array of accounts by their fundamental type
+ */
+
+ function fundamentalAccounts($ftype) {
+ $this->cacheQueries = true;
+ $account = $this->find('all', array
+ ('contain' => array('CurrentLedger'),
+ 'fields' => array('Account.id', 'Account.type', 'Account.name', 'CurrentLedger.id'),
+ 'conditions' => array('Account.type' => strtoupper($ftype))
+ ));
+ $this->cacheQueries = false;
+
+ return $account;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: relatedAccounts
+ * - Returns an array of accounts related by similar attributes
+ */
+
+ function relatedAccounts($attribute, $extra = null) {
+ $this->cacheQueries = true;
+ $accounts = $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())
+ );
+ $this->cacheQueries = false;
+
+ // 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: xxxAccounts
+ * - Returns an array of accounts suitable for activity xxx
+ */
+
+ function invoiceAccounts() {
+ return $this->relatedAccounts('invoices', array('order' => 'name'));
+ }
+
+ function receiptAccounts() {
+ return $this->relatedAccounts('receipts', array('order' => 'name'));
+ }
+
+ function depositAccounts() {
+ return $this->relatedAccounts('deposits', array('order' => 'name'));
+ }
+
+ function refundAccounts() {
+ return $this->relatedAccounts('refunds', array('order' => 'name'));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: collectableAccounts
+ * - Returns an array of accounts suitable to show income collection
+ */
+
+ function collectableAccounts() {
+ $accounts = $this->receiptAccounts();
+
+ 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;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: currentLedgerID
+ * - Returns the current ledger ID of the account
+ */
+ function currentLedgerID($id) {
+ $this->cacheQueries = true;
+ $item = $this->find('first', array
+ ('contain' => 'CurrentLedger',
+ 'conditions' => array('Account.id' => $id),
+ ));
+ $this->cacheQueries = false;
+ return $item['CurrentLedger']['id'];
+ }
+
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: ledgers
+ * - Returns an array of ledger ids from the given account
+ */
+ function ledgers($id, $all = false) {
+ if ($all) {
+ $contain = array('Ledger' => array('fields' => array('Ledger.id')));
+ } else {
+ $contain = array('CurrentLedger' => array('fields' => array('CurrentLedger.id')));
+ }
+
+ $this->cacheQueries = true;
+ $account = $this->find('first', array
+ ('contain' => $contain,
+ 'fields' => array(),
+ 'conditions' => array(array('Account.id' => $id)),
+ ));
+ $this->cacheQueries = false;
+
+ if ($all) {
+ $ledger_ids = array();
+ foreach ($account['Ledger'] AS $ledger)
+ array_push($ledger_ids, $ledger['id']);
+ }
+ else {
+ $ledger_ids = array($account['CurrentLedger']['id']);
+ }
+
+/* pr(array('function' => 'Account::ledgers', */
+/* 'args' => compact('id', 'all'), */
+/* 'return' => $ledger_ids)); */
+
+ return $ledger_ids;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: closeCurrentLedger
+ * - Closes the current account ledger, and opens a new one
+ * with the old balance carried forward.
+ */
+ function closeCurrentLedgers($ids = null) {
+
+ $this->cacheQueries = true;
+ $account = $this->find('all', array
+ ('contain' => array('CurrentLedger.id'),
+ 'fields' => array(),
+ 'conditions' => (empty($ids)
+ ? array()
+ : array(array('Account.id' => $ids)))
+ ));
+ $this->cacheQueries = false;
+ //pr(compact('id', 'account'));
+
+ $ledger_ids = array();
+ foreach ($account AS $acct)
+ $ledger_ids[] = $acct['CurrentLedger']['id'];
+
+ return $this->Ledger->closeLedgers($ledger_ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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 ledgerEntries($id, $all = false, $cond = null, $link = null) {
+ $ledgers = $this->ledgers($id, $all);
+ return $this->Ledger->ledgerEntries($ledgers, $cond, $link);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested account.
+ */
+
+ function stats($id = null, $all = false, $query = null) {
+ if (!$id)
+ return null;
+
+ $this->queryInit($query);
+ $query['link'] = array('Account' => $query['link']);
+
+ $stats = array();
+ foreach ($this->ledgers($id, $all) AS $ledger)
+ $this->statsMerge($stats['Ledger'],
+ $this->Ledger->stats($ledger, $query));
+
+ return $stats;
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/behaviors/linkable.php b/models/behaviors/linkable.php
new file mode 100644
index 0000000..2a60128
--- /dev/null
+++ b/models/behaviors/linkable.php
@@ -0,0 +1,486 @@
+find('all', array('link' => 'User', 'conditions' => 'project_id = 1'))
+ * - Won't produce the desired result as data came from users table will be lost.
+ * $User->find('all', array('link' => 'Project', 'conditions' => 'project_id = 1'))
+ * - Will fetch all users related to the specified project in one query
+ *
+ * - On data mining as a much lighter approach - can reduce 300+ query find operations
+ * in one single query with joins; "or your money back!" ;-)
+ *
+ * - Has the 'fields' param enabled to make it easy to replace Containable usage,
+ * only change the 'contain' param to 'link'.
+ *
+ * Linkable Behavior. Taking it easy in your DB.
+ * RafaelBandeira
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @version 1.0;
+ */
+
+
+/**********************************************************************
+ * NOTE TO 20090615:
+ * This structure should be linkable (it was my test case).
+
+ $entry = $this->LedgerEntry->find
+ ('first',
+ array('link' => array('DebitLedger' =>
+ array(
+ 'fields' => array('id'),
+ 'alias' => 'MyDebitLedger',
+ 'Account' =>
+ array(
+ 'fields' => array('id'),
+ 'alias' => 'MyDebitAccount'),
+ ),
+
+ 'MyCreditLedger' =>
+ array(
+ 'fields' => array('id'),
+ 'class' => 'CreditLedger',
+ 'MyCreditAccount' =>
+ array(
+ 'fields' => array('id'),
+ 'class' => 'Account'),
+ ),
+
+ 'MyOtherLedger' =>
+ array(
+ 'fields' => array('id'),
+ 'class' => 'Ledger',
+ 'alias' => 'AliasToMyOtherLedger',
+ 'Account' =>
+ array(
+ 'fields' => array('id'),
+ 'alias' => 'AliasToMyOtherAccount'),
+ ),
+ ),
+
+ 'conditions' => array('LedgerEntry.id' => $id),
+ ));
+**********************************************************************/
+
+
+class LinkableBehavior extends ModelBehavior {
+
+ protected $_key = 'link';
+
+ protected $_options = array(
+ 'type' => true, 'table' => true, 'alias' => true, 'joins' => true,
+ 'conditions' => true, 'fields' => true, 'reference' => true,
+ 'class' => true, 'defaults' => true, 'linkalias' => true,
+ );
+
+ protected $_defaults = array('type' => 'LEFT');
+
+ function pr($lev, $mixed) {
+ if ($lev >= 3)
+ return;
+
+ pr($mixed);
+ return;
+
+ $trace = debug_backtrace(false);
+ //array_shift($trace);
+ $calls = array();
+ foreach ($trace AS $call) {
+ $call = array_intersect_key($call,
+ array('file'=>1,
+ 'line'=>1,
+ //'class'=>1,
+ 'function'=>1,
+ ));
+ $calls[] = implode("; ", $call);
+ }
+ pr(array('debug' => $mixed, 'stack' => $calls));
+ }
+
+ /*
+ * This is a function for made recursive str_replaces in an array
+ * NOTE: The palacement of this function is terrible, but I don't
+ * know if I really want to go down this path or not.
+ */
+ function recursive_array_replace($find, $replace, &$data) {
+ if (!isset($data))
+ return;
+
+ if (is_array($data)) {
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $this->recursive_array_replace($find, $replace, $data[$key]);
+ } else {
+ $data[$key] = str_replace($find, $replace, $value);
+ }
+ }
+ } else {
+ $data = str_replace($find, $replace, $data);
+ }
+ }
+
+ public function beforeFind(&$Model, $query) {
+ if (!isset($query[$this->_key]))
+ return $query;
+
+ if (!isset($query['fields']) || $query['fields'] === true) {
+ $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));
+
+ $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'),
+ ));
+ }
+}
\ No newline at end of file
diff --git a/models/contact.php b/models/contact.php
new file mode 100644
index 0000000..b691823
--- /dev/null
+++ b/models/contact.php
@@ -0,0 +1,129 @@
+ array(
+ 'joinTable' => 'contacts_methods',
+ 'associationForeignKey' => 'method_id',
+ 'unique' => true,
+ 'conditions' => "method = 'ADDRESS'",
+ ),
+ 'ContactPhone' => array(
+ 'joinTable' => 'contacts_methods',
+ 'associationForeignKey' => 'method_id',
+ 'unique' => true,
+ 'conditions' => "method = 'PHONE'",
+ ),
+ 'ContactEmail' => array(
+ 'joinTable' => 'contacts_methods',
+ 'associationForeignKey' => 'method_id',
+ 'unique' => true,
+ 'conditions' => "method = 'EMAIL'",
+ ),
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: saveContact
+ * - Saves the contact and related data
+ */
+
+ function saveContact($id, $data) {
+
+ // Establish a display name if not already given
+ if (!$data['Contact']['display_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']));
+
+ // Save the contact data
+ $this->create();
+ if ($id)
+ $this->id = $id;
+ if (!$this->save($data, false)) {
+ return false;
+ }
+ $id = $this->id;
+
+ // Remove all associated ContactMethods, as it ensures
+ // any entries deleted by the user actually get deleted
+ // in the system. We'll recreate the needed ones anyway.
+ // REVISIT : 20090706
+ // Appears that $this->save() is already doing the
+ // delete. I would have thought this would only happen
+ // on a saveAll??
+/* $this->ContactsMethod->deleteAll */
+/* (array('contact_id' => $id), false); */
+
+ // At this point, since we've saved data to contact,
+ // we'll proceed forward as much as possible, even
+ // if we encounter an error. For now, we'll assume
+ // the operation will succeed.
+ $ret = true;
+
+ // Iterate each type of contact method, adding them into
+ // the database as needed and associating with this contact.
+ foreach (array('phone', 'address', 'email') AS $type) {
+ $class = 'Contact' . ucfirst($type);
+ $enum = strtoupper($type);
+
+ // Nothing to do if this contact method isn't used
+ if (!isset($data[$class]))
+ continue;
+
+ // Go through each entry of this contact method
+ foreach ($data[$class] AS &$item) {
+
+ // If the user has entered all new data, we need to
+ // save that as a brand new entry.
+ if (!isset($item['id'])) {
+ $I = new $class();
+ $I->create();
+ if (!$I->save($item, false)) {
+ $ret = false;
+ continue;
+ }
+ $item['id'] = $I->id;
+ }
+
+ // Update the ContactsMethod to reflect the appropriate IDs
+ $item['ContactsMethod']['contact_id'] = $id;
+ $item['ContactsMethod']['method_id'] = $item['id'];
+ $item['ContactsMethod']['method'] = $enum;
+
+ // Save the relationship between contact and phone/email/address
+ $CM = new ContactsMethod();
+ if (!$CM->save($item['ContactsMethod'], false)) {
+ $ret = false;
+ }
+ }
+ }
+
+ // Return the result
+ return $ret;
+ }
+
+ function contactList() {
+ return $this->find('list',
+ array('order' =>
+ //array('last_name', 'first_name', 'middle_name'),
+ array('display_name'),
+ ));
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/contact_address.php b/models/contact_address.php
new file mode 100644
index 0000000..22d4002
--- /dev/null
+++ b/models/contact_address.php
@@ -0,0 +1,46 @@
+ array('numeric'),
+ 'postcode' => array('postal')
+ );
+
+ var $hasMany = array(
+ 'ContactsMethod' => array(
+ 'foreignKey' => 'method_id',
+ 'conditions' => "method = 'ADDRESS'",
+ )
+ );
+
+ var $hasAndBelongsToMany = array(
+ 'Contact' => array(
+ 'className' => 'Contact',
+ 'joinTable' => 'contacts_methods',
+ 'foreignKey' => 'method_id',
+ 'associationForeignKey' => 'contact_id',
+ 'unique' => true,
+ 'conditions' => "method = 'ADDRESS'",
+ )
+ );
+
+ function addressList() {
+ $results = $this->find('all',
+ array('contain' => false,
+ 'fields' => array('id', 'address', 'city', 'state', 'postcode'),
+ 'order' => array('state', 'city', 'postcode', 'address')));
+
+ $list = array();
+ foreach ($results as $key => $val) {
+ $list[$val['ContactAddress']['id']]
+ = preg_replace("/\n/", ", ", $val['ContactAddress']['address'])
+ . ', ' . $val['ContactAddress']['city']
+ . ', ' . $val['ContactAddress']['state']
+ . ' ' . $val['ContactAddress']['postcode']
+ ;
+ }
+ return $list;
+ }
+}
+?>
\ No newline at end of file
diff --git a/models/contact_email.php b/models/contact_email.php
new file mode 100644
index 0000000..6a8e4a1
--- /dev/null
+++ b/models/contact_email.php
@@ -0,0 +1,27 @@
+ array('numeric'),
+ 'email' => array('email')
+ );
+
+ var $hasAndBelongsToMany = array(
+ 'Contact' => array(
+ 'className' => 'Contact',
+ 'joinTable' => 'contacts_methods',
+ 'foreignKey' => 'method_id',
+ 'associationForeignKey' => 'contact_id',
+ 'unique' => true,
+ 'conditions' => "method = 'EMAIL'",
+ )
+ );
+
+ function emailList() {
+ return $this->find('list');
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/contact_phone.php b/models/contact_phone.php
new file mode 100644
index 0000000..38e6169
--- /dev/null
+++ b/models/contact_phone.php
@@ -0,0 +1,46 @@
+ array('numeric'),
+ //'type' => array('inlist'),
+ 'phone' => array('phone'),
+ 'ext' => array('numeric')
+ );
+
+ var $hasMany = array(
+ 'ContactsMethod' => array(
+ 'foreignKey' => 'method_id',
+ 'conditions' => "method = 'PHONE'",
+ )
+ );
+
+ var $hasAndBelongsToMany = array(
+ 'Contact' => array(
+ 'className' => 'Contact',
+ 'joinTable' => 'contacts_methods',
+ 'foreignKey' => 'method_id',
+ 'associationForeignKey' => 'contact_id',
+ 'unique' => true,
+ 'conditions' => "method = 'PHONE'",
+ )
+ );
+
+ function phoneList() {
+ $results = $this->find('all',
+ array('contain' => false,
+ 'fields' => array('id', 'phone', 'ext'),
+ 'order' => array('phone', 'ext')));
+
+ App::Import('Helper', 'Format');
+ $list = array();
+ foreach ($results as $key => $val) {
+ $list[$val['ContactPhone']['id']]
+ = FormatHelper::phone($val['ContactPhone']['phone'],
+ $val['ContactPhone']['ext']);
+ }
+ return $list;
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/contacts_customer.php b/models/contacts_customer.php
new file mode 100644
index 0000000..515744a
--- /dev/null
+++ b/models/contacts_customer.php
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/models/contacts_method.php b/models/contacts_method.php
new file mode 100644
index 0000000..ea07338
--- /dev/null
+++ b/models/contacts_method.php
@@ -0,0 +1,25 @@
+ array(
+ 'foreignKey' => 'method_id',
+ 'conditions' => "method = 'ADDRESS'",
+ //'unique' => true,
+ ),
+ 'ContactPhone' => array(
+ 'foreignKey' => 'method_id',
+ 'conditions' => "method = 'PHONE'",
+ //'unique' => true,
+ ),
+ 'ContactEmail' => array(
+ 'foreignKey' => 'method_id',
+ 'conditions' => "method = 'EMAIL'",
+ //'unique' => true,
+ ),
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/customer.php b/models/customer.php
new file mode 100644
index 0000000..3df1ebb
--- /dev/null
+++ b/models/customer.php
@@ -0,0 +1,377 @@
+ array('numeric'),
+ 'name' => array('notempty'),
+ );
+
+ var $belongsTo = array(
+ 'PrimaryContact' => array(
+ 'className' => 'Contact',
+ ),
+ );
+
+ var $hasMany = array(
+ 'CurrentLease' => array(
+ 'className' => 'Lease',
+ 'conditions' => 'CurrentLease.close_date IS NULL',
+ ),
+ 'Lease',
+ 'StatementEntry',
+ 'ContactsCustomer',
+
+ 'Transaction',
+ );
+
+ var $hasAndBelongsToMany = array(
+ 'Contact',
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: accountId
+ * - Returns the account ID for the given customer
+ */
+ function accountId($id) {
+ $this->cacheQueries = true;
+ $customer = $this->find('first',
+ array('contain' => false,
+ 'fields' => array('account_id'),
+ 'conditions' => array(array('Customer.id' => $id))));
+ $this->cacheQueries = false;
+
+ return $customer['Customer']['account_id'];
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: leaseIds
+ * - Returns the lease IDs for the given customer
+ */
+ function leaseIds($id) {
+ $this->cacheQueries = true;
+ $customer = $this->find('first',
+ array('contain' =>
+ array('Lease' => array('fields' => array('id'))),
+ 'fields' => array(),
+ 'conditions' => array(array('Customer.id' => $id))));
+ $this->cacheQueries = false;
+
+ $ids = array();
+ foreach ($customer['Lease'] AS $lease)
+ $ids[] = $lease['id'];
+
+ return $ids;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: securityDeposits
+ * - Returns an array of security deposit entries
+ */
+ function securityDeposits($id, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ $this->queryInit($query);
+
+ $query['conditions'][] = array('StatementEntry.customer_id' => $id);
+ $query['conditions'][] = array('StatementEntry.account_id' =>
+ $this->StatementEntry->Account->securityDepositAccountID());
+
+ $set = $this->StatementEntry->reconciledSet('CHARGE', $query, false, true);
+ return $this->prReturn($set);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: securityDepositBalance
+ * - Returns the balance of the customer security deposit(s)
+ */
+
+ function securityDepositBalance($id, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ $this->queryInit($query);
+
+ $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: unreconciledCharges
+ * - Returns charges have not yet been fully paid
+ */
+
+ function unreconciledCharges($id, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ $this->queryInit($query);
+
+ $query['conditions'][] = array('StatementEntry.customer_id' => $id);
+ $set = $this->StatementEntry->reconciledSet('CHARGE', $query, true);
+ return $this->prReturn($set);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: saveCustomer
+ * - Saves the customer and related data
+ */
+
+ function saveCustomer($id, $data, $primary_contact_entry) {
+
+ // Go through each contact, and create new ones as needed
+ foreach ($data['Contact'] AS &$contact) {
+ if (isset($contact['id']))
+ continue;
+
+ $I = new Contact();
+ if (!$I->saveContact(null, array('Contact' => $contact)))
+ return false;
+ $contact['id'] = $I->id;
+ }
+
+ // Set the primary contact ID based on caller selection
+ $data['Customer']['primary_contact_id']
+ = $data['Contact'][$primary_contact_entry]['id'];
+
+ // Provide a default customer name if not specified
+ if (!$data['Customer']['name']) {
+ $this->Contact->recursive = -1;
+ $pcontact = $this->Contact->read(null, $data['Customer']['primary_contact_id']);
+ $data['Customer']['name'] = $pcontact['Contact']['display_name'];
+ }
+
+ // Save the customer data
+ $this->create();
+ if ($id)
+ $this->id = $id;
+ if (!$this->save($data, false)) {
+ return false;
+ }
+ $id = $this->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.
+ // REVISIT : 20090706
+ // Appears that $this->save() is already doing the
+ // delete. I would have thought this would only happen
+ // on a saveAll??
+/* $this->ContactsCustomer->deleteAll */
+/* (array('customer_id' => $id), false); */
+
+ // At this point, since we've saved data to customer,
+ // we'll proceed forward as much as possible, even
+ // if we encounter an error. For now, we'll assume
+ // the operation will succeed.
+ $ret = true;
+
+ // Go through each entry of this customer method
+ foreach ($data['Contact'] AS &$contact) {
+ // Update the ContactsCustomer to reflect the appropriate IDs
+ $contact['ContactsCustomer']['customer_id'] = $id;
+ $contact['ContactsCustomer']['contact_id'] = $contact['id'];
+
+ // Save the relationship between customer and contact
+ $CM = new ContactsCustomer();
+ if (!$CM->save($contact['ContactsCustomer'], false)) {
+ $ret = false;
+ }
+ }
+
+ // Return the result
+ return $ret;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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;
+ }
+
+ // REVISIT : 20090812
+ // updateLeaseCount is handled directly when needed.
+ // Should we simplify by just doing it anyway?
+ //$this->updateLeaseCount($id);
+
+ $current_leases =
+ $this->find('all',
+ // REVISIT : 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: 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'];
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested customer.
+ */
+
+ function stats($id = null, $query = null) {
+ //$this->prFunctionLevel(20);
+ $this->prEnter(compact('id', 'query'));
+ if (!$id)
+ return $this->prExit(null);
+
+ $this->queryInit($query);
+
+ // REVISIT : 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);
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/double_entry.php b/models/double_entry.php
new file mode 100644
index 0000000..999b283
--- /dev/null
+++ b/models/double_entry.php
@@ -0,0 +1,109 @@
+ 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));
+ }
+}
diff --git a/models/lease.php b/models/lease.php
new file mode 100644
index 0000000..fd43dd3
--- /dev/null
+++ b/models/lease.php
@@ -0,0 +1,849 @@
+ 30, 'show' => 30);
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: securityDeposits
+ * - Returns an array of security deposit entries
+ */
+ function securityDeposits($id, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ $this->queryInit($query);
+
+ $query['conditions'][] = array('StatementEntry.lease_id' => $id);
+ $query['conditions'][] = array('StatementEntry.account_id' =>
+ $this->StatementEntry->Account->securityDepositAccountID());
+
+ $set = $this->StatementEntry->reconciledSet('CHARGE', $query, false, true);
+ return $this->prReturn($set);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: securityDepositBalance
+ * - Returns the balance of the lease security deposit(s)
+ */
+
+ function securityDepositBalance($id, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ $this->queryInit($query);
+
+ // REVISIT : 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: 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'));
+
+ $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);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: rentLastCharges
+ * - Returns a list of rent charges from this lease that
+ * do not have sequential followup charges. Under normal
+ * circumstances, there would only be one entry, which is
+ * the most recent rent charge. However, it's possible
+ * that there are several, indicating a problem with lease.
+ */
+
+ function rentLastCharges($id) {
+ $this->prEnter(compact('id'));
+ $rent_account_id = $this->StatementEntry->Account->rentAccountID();
+ $entries = $this->find
+ ('all',
+ array('link' =>
+ array(// Models
+ 'StatementEntry',
+
+ 'SEx' =>
+ array('class' => 'StatementEntry',
+ 'fields' => array(),
+ 'conditions' => array
+ ('SEx.effective_date = DATE_ADD(StatementEntry.through_date, INTERVAL 1 day)',
+ 'SEx.lease_id = StatementEntry.lease_id',
+ 'SEx.reverse_transaction_id IS NULL',
+ ),
+ ),
+ ),
+
+ //'fields' => array('id', 'amount', 'effective_date', 'through_date'),
+ 'fields' => array(),
+ 'conditions' => array(array('Lease.id' => $id),
+ array('StatementEntry.type' => 'CHARGE'),
+ array('StatementEntry.account_id' => $rent_account_id),
+ array('StatementEntry.reverse_transaction_id IS NULL'),
+ array('SEx.id' => null),
+ ),
+ )
+ );
+
+ 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);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: rentChargeGaps
+ * - Checks for gaps in rent charges
+ */
+
+ function rentChargeGaps($id) {
+ $this->prEnter(compact('id'));
+ $entries = $this->rentLastCharges($id);
+ if ($entries && count($entries) > 1)
+ return $this->prReturn(true);
+ return $this->prReturn(false);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: rentChargeThrough
+ * - Determines the date that rent has been charged through
+ * Returns one of:
+ * null: There are gaps in the charges
+ * false: There are not yet any charges
+ * date: The date rent has been charged through
+ */
+
+ function rentChargeThrough($id) {
+ $this->prEnter(compact('id'));
+ $entries = $this->rentLastCharges($id);
+ if (!$entries)
+ return $this->prReturn(false);
+ if (count($entries) != 1)
+ return $this->prReturn(null);
+ return $this->prReturn($entries[0]['StatementEntry']['through_date']);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: rentPaidThrough
+ * - Determines the date of the first unpaid rent
+ */
+
+ function rentPaidThrough($id) {
+ $this->prEnter(compact('id'));
+ $rent_account_id = $this->StatementEntry->Account->rentAccountID();
+
+ // 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',
+ ),
+
+ 'conditions' =>
+ array(array('StatementEntry.lease_id' => $id),
+ array('StatementEntry.account_id' => $rent_account_id),
+ array('StatementEntry.reverse_transaction_id IS NULL'),
+ ),
+
+ 'order' => array('StatementEntry.effective_date'),
+ ),
+ true);
+ $this->pr(20, $rent, "Unpaid rent");
+
+ if ($rent['entries'])
+ return $this->prReturn($rent['entries'][0]['StatementEntry']['paid_through']);
+
+
+ // 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'),
+ ),
+
+ 'order' => array('StatementEntry.through_date DESC'),
+ ),
+ false);
+ $this->pr(20, $rent, "Paid rent");
+
+ if ($rent['entries'])
+ return $this->prReturn($rent['entries'][0]['StatementEntry']['through_date']);
+
+
+ // 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 : 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 : 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 : 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 : 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 : 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 10 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'");
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: moveIn
+ * - Moves the specified customer into the specified lease
+ */
+
+ function moveIn($customer_id, $unit_id,
+ $deposit = null, $rent = null,
+ $stamp = null, $comment = null)
+ {
+ $this->prEnter(compact('customer_id', 'unit_id',
+ 'deposit', 'rent', 'stamp', 'comment'));
+
+ $lt = $this->LeaseType->find('first',
+ array('conditions' =>
+ array('code' => 'SL')));
+
+ // Use NOW if not given a movein date
+ if (!isset($stamp))
+ $stamp = date('Y-m-d G:i:s');
+
+ if (!$comment)
+ $comment = null;
+
+ if (!isset($deposit) || !isset($rent)) {
+ $rates = $this->Unit->find
+ ('first',
+ array('contain' =>
+ array('UnitSize' =>
+ array('deposit', 'rent'),
+ ),
+ 'fields' => array('deposit', 'rent'),
+ 'conditions' => array('Unit.id' => $unit_id),
+ ));
+
+ $deposit =
+ (isset($deposit)
+ ? $deposit
+ : (isset($rates['Unit']['deposit'])
+ ? $rates['Unit']['deposit']
+ : (isset($rates['UnitSize']['deposit'])
+ ? $rates['UnitSize']['deposit']
+ : 0)));
+
+ $rent =
+ (isset($rent)
+ ? $rent
+ : (isset($rates['Unit']['rent'])
+ ? $rates['Unit']['rent']
+ : (isset($rates['UnitSize']['rent'])
+ ? $rates['UnitSize']['rent']
+ : 0)));
+ }
+
+
+ // Save this new lease.
+ $this->create();
+ if (!$this->save(array('lease_type_id' => $lt['LeaseType']['id'],
+ 'unit_id' => $unit_id,
+ 'customer_id' => $customer_id,
+ 'lease_date' => $stamp,
+ 'movein_date' => $stamp,
+ 'deposit' => $deposit,
+ 'rent' => $rent,
+ 'comment' => $comment), false)) {
+ 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');
+
+ // REVISIT : 20090702
+ // We need to assess the security deposit charge,
+ // and probably rent as well. Rent, however, will
+ // require user parameters to indicate whether it
+ // was waived, pro-rated, etc.
+
+ // Return the new lease ID
+ return $this->prReturn($this->id);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: moveOut
+ * - Moves the customer out of the specified lease
+ */
+
+ function moveOut($id, $status = 'VACANT',
+ $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');
+
+ // Reset the data
+ $this->create();
+ $this->id = $id;
+
+ // Set the customer move-out date
+ $this->data['Lease']['moveout_date'] = $stamp;
+
+ // 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();
+ $this->Unit->updateStatus($this->data['Lease']['unit_id'], $status);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: close
+ * - Closes the lease to further action
+ */
+
+ function close($id, $stamp = null) {
+ $this->prEnter(compact('id', 'stamp'));
+
+ if (!$this->closeable($id))
+ return $this->prReturn(false);
+
+ // Reset the data
+ $this->create();
+ $this->id = $id;
+
+ // Use NOW if not given a moveout date
+ if (!isset($stamp))
+ $stamp = date('Y-m-d G:i:s');
+
+ // Set the close date
+ $this->data['Lease']['close_date'] = $stamp;
+
+ // Save it!
+ $this->save($this->data, false);
+
+ // Update the current lease count for the customer
+ $this->Customer->updateLeaseCount($this->field('customer_id'));
+
+ return $this->prReturn(true);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: closeable
+ * - Indicates whether or not the lease can be closed
+ */
+
+ 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 $this->prReturn(false);
+
+ // We can't close a lease that's already closed
+ if (isset($this->data['Lease']['close_date']))
+ return $this->prReturn(false);
+
+ // A lease can only be closed if there are no outstanding
+ // 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 $this->prReturn(true);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: refund
+ * - Marks any lease balance as payable to the customer.
+ */
+
+ 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']);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested lease.
+ */
+
+ function stats($id = null, $query = null) {
+ $this->prEnter(compact('id', 'query'));
+ if (!$id)
+ return $this->prReturn(null);
+
+ $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);
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/lease_type.php b/models/lease_type.php
new file mode 100644
index 0000000..e606b80
--- /dev/null
+++ b/models/lease_type.php
@@ -0,0 +1,15 @@
+ array('numeric'),
+ 'name' => array('notempty')
+ );
+
+ var $hasMany = array(
+ 'Lease',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/ledger.php b/models/ledger.php
new file mode 100644
index 0000000..4213351
--- /dev/null
+++ b/models/ledger.php
@@ -0,0 +1,179 @@
+ array('className' => 'Ledger'),
+ 'CloseTransaction' => array('className' => 'Transaction'),
+ );
+
+ var $hasMany = array(
+ 'Transaction',
+ 'LedgerEntry',
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: accountID
+ * - Returns the account ID for the given ledger
+ */
+ function accountID($id) {
+ $this->cacheQueries = true;
+ $item = $this->find('first', array
+ ('link' => array('Account'),
+ 'conditions' => array('Ledger.id' => $id),
+ ));
+ $this->cacheQueries = false;
+ //pr(compact('id', 'item'));
+ return $item['Account']['id'];
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: currentLedgerID
+ * - Returns the current ledger ID of the account for the given ledger.
+ */
+ function currentLedgerID($id) {
+ return $this->Account->currentLedgerID($this->accountID($id));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: closeLedger
+ * - Closes the current ledger, and returns a fresh one
+ */
+ function closeLedgers($ids) {
+ $ret = array('new_ledger_ids' => array());
+
+ $entries = array();
+ foreach ($ids AS $id) {
+ // Query stats to get the balance forward
+ $stats = $this->stats($id);
+
+ // Populate fields from the current ledger
+ $this->recursive = -1;
+ $this->id = $id;
+ $this->read();
+
+ // 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']);
+
+ // 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;
+
+ $entries[] = array('old_ledger_id' => $id,
+ 'new_ledger_id' => $this->id,
+ 'amount' => $stats['balance']);
+ }
+
+ // Perform the close
+ $result = $this->Transaction->addClose(array('Transaction' => array(),
+ 'Ledger' => $entries));
+ $ret['Transaction'] = $result;
+ if ($result['error'])
+ return array('error' => true) + $ret;
+
+ return $ret + array('error' => false);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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 debitCreditFields($sum = false, $balance = true,
+ $entry_name = 'LedgerEntry', $account_name = 'Account') {
+ return $this->LedgerEntry->debitCreditFields
+ ($sum, $balance, $entry_name, $account_name);
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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;
+
+ $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),
+ ));
+
+ //pr(compact('entries'));
+ return $entries;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested ledger.
+ */
+ function stats($id, $query = null) {
+ if (!$id)
+ return null;
+
+ $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;
+
+ return $stats;
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/models/ledger_entry.php b/models/ledger_entry.php
new file mode 100644
index 0000000..096ca19
--- /dev/null
+++ b/models/ledger_entry.php
@@ -0,0 +1,177 @@
+ 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(
+ );
+
+ var $hasAndBelongsToMany = array(
+ // The Debit half of the double entry matching THIS Credit (if it is one)
+ 'DebitEntry' => array(
+ 'className' => 'LedgerEntry',
+ 'joinTable' => 'double_entries',
+ 'linkalias' => 'DDE',
+ 'foreignKey' => 'credit_entry_id',
+ 'associationForeignKey' => 'debit_entry_id',
+ ),
+
+ // The Credit half of the double entry matching THIS Debit (if it is one)
+ 'CreditEntry' => array(
+ 'className' => 'LedgerEntry',
+ 'joinTable' => 'double_entries',
+ 'linkalias' => 'CDE',
+ 'foreignKey' => 'debit_entry_id',
+ 'associationForeignKey' => 'credit_entry_id',
+ ),
+ );
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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 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' : ''),
+
+ ($sum ? 'SUM(' : '') .
+ "IF({$entry_name}.crdr = 'CREDIT'," .
+ " {$entry_name}.amount, NULL)" .
+ ($sum ? ')' : '') . ' AS credit' . ($sum ? 's' : ''),
+ );
+
+ 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';
+
+ if ($sum)
+ $fields[] = "COUNT({$entry_name}.id) AS entries";
+
+ return $fields;
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * 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;
+ }
+
+ 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 = null, $query = null, $set = null) {
+ $this->queryInit($query);
+
+ // REVISIT : 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');
+ }
+
+}
diff --git a/models/map.php b/models/map.php
new file mode 100644
index 0000000..2e14359
--- /dev/null
+++ b/models/map.php
@@ -0,0 +1,23 @@
+ array('numeric'),
+ 'site_id' => array('numeric'),
+ 'site_area_id' => array('numeric'),
+ 'name' => array('notempty'),
+ 'width' => array('numeric'),
+ 'depth' => array('numeric')
+ );
+
+ var $belongsTo = array(
+ 'SiteArea',
+ );
+
+ var $hasAndBelongsToMany = array(
+ 'Unit',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/site.php b/models/site.php
new file mode 100644
index 0000000..4825faa
--- /dev/null
+++ b/models/site.php
@@ -0,0 +1,16 @@
+ array('numeric'),
+ 'name' => array('notempty')
+ );
+
+ var $hasMany = array(
+ 'SiteArea',
+ 'SiteOption',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/site_area.php b/models/site_area.php
new file mode 100644
index 0000000..2303946
--- /dev/null
+++ b/models/site_area.php
@@ -0,0 +1,20 @@
+ array('numeric'),
+ 'site_id' => array('numeric'),
+ 'name' => array('notempty')
+ );
+
+ var $belongsTo = array(
+ 'Site',
+ );
+
+ var $hasOne = array(
+ 'Map',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/statement_entry.php b/models/statement_entry.php
new file mode 100644
index 0000000..9d58f56
--- /dev/null
+++ b/models/statement_entry.php
@@ -0,0 +1,725 @@
+ 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: assignCredits
+ * - Assigns all credits to existing charges
+ *
+ * REVISIT : 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($customer_id))
+ $query['conditions'][] = array('StatementEntry.customer_id' => $customer_id);
+
+ 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;
+ $lquery['conditions'][] = array('StatementEntry.type' => 'SURPLUS');
+ // REVISIT : 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
+ if (isset($charge_ids)) {
+ $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);
+ }
+ $lquery['order'] = 'StatementEntry.effective_date ASC';
+ $charges = array();
+ foreach ($this->debitTypes() AS $dtype) {
+ $rset = $this->reconciledSet($dtype, $lquery, true);
+ $entries = $rset['entries'];
+ $charges = array_merge($charges, $entries);
+ $this->pr(18, compact('dtype', 'entries'), "Outstanding Debit Entries");
+ }
+
+ // 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 : 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 = min($charge['balance'], $credit['balance']);
+ if (!isset($credit['applied']))
+ $credit['applied'] = 0;
+
+ $credit['applied'] += $disbursement_amount;
+ $credit['balance'] -= $disbursement_amount;
+
+ $this->pr(20, compact('credit'),
+ ($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'] -= $disbursement_amount;
+ if ($charge['balance'] < 0)
+ die("HOW DID WE GET A NEGATIVE CHARGE AMOUNT?");
+
+ if ($charge['balance'] <= 0)
+ $this->pr(20, 'Fully Paid Charge');
+ }
+ // 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 : 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);
+ }
+
+}
\ No newline at end of file
diff --git a/models/tender.php b/models/tender.php
new file mode 100644
index 0000000..37a127a
--- /dev/null
+++ b/models/tender.php
@@ -0,0 +1,166 @@
+ 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 : 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 : 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);
+ }
+
+
+}
+?>
\ No newline at end of file
diff --git a/models/tender_type.php b/models/tender_type.php
new file mode 100644
index 0000000..5d70cf0
--- /dev/null
+++ b/models/tender_type.php
@@ -0,0 +1,115 @@
+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];
+ }
+
+}
\ No newline at end of file
diff --git a/models/transaction.php b/models/transaction.php
new file mode 100644
index 0000000..de8923a
--- /dev/null
+++ b/models/transaction.php
@@ -0,0 +1,1344 @@
+ array(
+ 'className' => 'Tender',
+ 'foreignKey' => 'nsf_transaction_id',
+ ),
+ );
+
+ var $hasMany = array(
+ 'LedgerEntry' => array(
+ 'dependent' => true,
+ ),
+ 'StatementEntry' => array(
+ 'dependent' => true,
+ ),
+
+ 'DepositTender' => array(
+ 'className' => 'Tender',
+ 'foreignKey' => 'deposit_transaction_id',
+ ),
+
+ 'Charge' => array(
+ 'className' => 'StatementEntry',
+ 'conditions' => array('Charge.type' => 'CHARGE')
+ ),
+
+ 'Disbursement' => array(
+ 'className' => 'StatementEntry',
+ 'conditions' => array('Disbursement.type' => 'DISBURSEMENT')
+ ),
+
+ 'Debit' => array(
+ 'className' => 'LedgerEntry',
+ 'conditions' => array('Debit.crdr' => 'DEBIT')
+ ),
+
+ 'Credit' => array(
+ 'className' => 'LedgerEntry',
+ 'conditions' => array('Credit.crdr' => 'CREDIT')
+ ),
+
+ );
+
+
+ //var $default_log_level = array('log' => 30, 'show' => 15);
+ //var $max_log_level = 10;
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addInvoice
+ * - Adds a new invoice invoice
+ */
+
+ function addInvoice($data, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'customer_id', 'lease_id'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => true,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => true,
+ );
+
+ // Establish the transaction as an invoice
+ $data['Transaction'] +=
+ array('type' => 'INVOICE',
+ 'crdr' => 'DEBIT',
+ 'account_id' => $this->Account->accountReceivableAccountID(),
+ 'customer_id' => $customer_id,
+ 'lease_id' => $lease_id,
+ );
+
+ // Go through the statement entries and flag as charges
+ foreach ($data['Entry'] AS &$entry)
+ $entry += array('type' => 'CHARGE',
+ );
+
+ $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
+ if (isset($ids['transaction_id']))
+ $ids['invoice_id'] = $ids['transaction_id'];
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addReceipt
+ * - Adds a new receipt
+ */
+
+ function addReceipt($data, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'customer_id', 'lease_id'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => true,
+ 'assign_receipt' => true,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => false,
+ );
+
+ // Establish the transaction as a receipt
+ $data['Transaction'] +=
+ array('type' => 'RECEIPT',
+ 'crdr' => 'CREDIT',
+ 'account_id' => $this->Account->accountReceivableAccountID(),
+ 'customer_id' => $customer_id,
+ 'lease_id' => $lease_id,
+ );
+
+ // Go through the statement entries, making sure the tender
+ // is recorded into the correct account, and then performing
+ // an auto-deposit if necessary.
+ $deposit = array();
+ foreach ($data['Entry'] AS &$entry) {
+ if (empty($entry['Tender']['tender_type_id']))
+ continue;
+
+ $ttype = $this->LedgerEntry->Tender->TenderType->find
+ ('first', array('contain' => false,
+ 'conditions' =>
+ array('id' => $entry['Tender']['tender_type_id'])));
+ $ttype = $ttype['TenderType'];
+
+ // Set the account for posting.
+ $entry += array('account_id' => $ttype['account_id']);
+
+/* // Check for auto deposit */
+/* if (!empty($ttype['auto_deposit'])) { */
+/* $deposit[] = array('id' => 0, */
+/* 'account_id' => $ttype['deposit_account_id']); */
+/* } */
+ }
+ unset($entry); // prevent trouble since $entry is reference
+
+ $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
+ if (isset($ids['transaction_id']))
+ $ids['receipt_id'] = $ids['transaction_id'];
+ if (!empty($ids['error']))
+ return $this->prReturn(array('error' => true) + $ids);
+
+ $tender_ids = array();
+ foreach ($ids['entries'] AS $entry) {
+ $entry1 = $entry['DoubleEntry']['Entry1'];
+ if (!empty($entry1['Tender']['tender_id']))
+ $tender_ids[] = $entry1['Tender']['tender_id'];
+ }
+
+ $ids = $this->_autoDeposit($tender_ids, $ids);
+
+ return $this->prReturn($ids);
+ }
+
+ // REVISIT : 20090817
+ // Delete after rolling up the old items
+ function _autoDeposit($tender_ids, $ids) {
+ $deposit_tenders = $this->LedgerEntry->Tender->find
+ ('all', array('contain' => array('TenderType' => array('fields' => array()),
+ 'LedgerEntry' => array('fields' => array()),
+ ),
+ 'fields' => array('TenderType.deposit_account_id',
+ 'TenderType.account_id',
+ 'CONCAT("CREDIT") AS crdr',
+ 'CONCAT("Auto Deposit") AS comment',
+ 'SUM(LedgerEntry.amount) AS amount'),
+ 'conditions' => array('Tender.id' => $tender_ids,
+ 'TenderType.auto_deposit' => true,
+ ),
+ 'group' => 'TenderType.deposit_account_id',
+ ));
+
+ if (!empty($deposit_tenders)) {
+ foreach ($deposit_tenders AS &$tender)
+ $tender = $tender[0] + array_diff_key($tender['TenderType'], array('id'=>1));
+
+ $this->pr(10, compact('tender_ids', 'deposit_tenders'));
+
+ // REVISIT : 20090817
+ // Multiple tenders could result in deposits to more than one
+ // account. We're already mucking with things by having a
+ // ledger entry that's not involved with the account_id of the
+ // transaction. We could handle this by not using the helper
+ // _splitEntries function, and just building or individual
+ // entries right here (which we should probably do anyway).
+ // However, I'm ignoring the issue for now...
+ if (count($deposit_tenders) > 1)
+ $this->INTERNAL_ERROR("Only expecting one tender type");
+
+ $deposit_ids = $this->addTransactionEntries
+ (array('include_ledger_entry' => true,
+ 'include_statement_entry' => false,
+ ),
+
+ array('id' => $ids['transaction_id'],
+ // REVISIT : 20090817
+ // This is an awful cheat, and we're going to
+ // get burned from it someday.
+ 'type' => 'DEPOSIT',
+ 'crdr' => 'DEBIT',
+ 'account_id' => $deposit_tenders[0]['deposit_account_id'],
+ ),
+
+ $deposit_tenders);
+
+ $ids['deposit'] = $deposit_ids;
+ if (!empty($deposit_ids['error']))
+ return $this->prReturn(array('error' => true) + $ids);
+
+ if (!empty($tender_ids)) {
+ $entry_id = $deposit_ids['entries'][0]['DoubleEntry']['Entry2']['ledger_entry_id'];
+ $this->pr(10, compact('tender_ids', 'entry_id'));
+ $this->LedgerEntry->Tender->updateAll
+ (array('Tender.deposit_transaction_id' => $ids['transaction_id'],
+ 'Tender.deposit_ledger_entry_id' => $entry_id),
+ array('Tender.id' => $tender_ids)
+ );
+ }
+ }
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addWaiver
+ * - Adds a new waiver
+ */
+
+ function addWaiver($data, $charge_id, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'charge_id', 'customer_id', 'lease_id'));
+
+ if (count($data['Entry']) != 1)
+ $this->INTERNAL_ERROR("Should be one Entry for addWaiver");
+
+ // No assignment of credits, as we'll manually assign
+ // using charge_entry_id as part of the entry (below).
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => false,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => true,
+ );
+
+ // Just make sure the disbursement(s) are marked as waivers
+ // and that they go to cover the specific charge.
+ $data['Entry'][0] +=
+ array('type' => 'WAIVER',
+ 'account_id' => $this->Account->waiverAccountID(),
+ 'charge_entry_id' => $charge_id);
+
+ // In all other respects this is just a receipt.
+ $ids = $this->addReceipt($data, $customer_id, $lease_id);
+ if (isset($ids['transaction_id']))
+ $ids['waiver_id'] = $ids['transaction_id'];
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addWriteOff
+ * - Adds a new write off of bad debt
+ */
+
+ function addWriteOff($data, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'customer_id', 'lease_id'));
+
+ if (count($data['Entry']) != 1)
+ $this->INTERNAL_ERROR("Should be one Entry for addWriteOff");
+
+ // Just make sure the disbursement(s) are marked as write offs
+ // and that the write-off account is used for the charge.
+ $data['Entry'][0] +=
+ array('type' => 'WRITEOFF',
+ 'account_id' => $this->Account->badDebtAccountID());
+
+ // In all other respects this is just a receipt.
+ $ids = $this->addReceipt($data, $customer_id, $lease_id);
+ if (isset($ids['transaction_id']))
+ $ids['writeoff_id'] = $ids['transaction_id'];
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addDeposit
+ * - Adds a new bank deposit
+ */
+
+ function addDeposit($data, $account_id) {
+ $this->prEnter(compact('data', 'account_id'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => false,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => false,
+ 'update_tender' => true,
+ );
+
+ // Establish the transaction as a deposit
+ $data['Transaction'] +=
+ array('type' => 'DEPOSIT',
+ 'crdr' => 'DEBIT',
+ 'account_id' => $account_id,
+ 'customer_id' => null,
+ 'lease_id' => null,
+ );
+
+ // Save the list of IDs, so that we can mark their
+ // deposit transaction after it has been created.
+ $tender_ids = array_map(create_function('$item', 'return $item["tender_id"];'),
+ $data['Entry']);
+
+ // Go through the statement entries and re-group by account id
+ $group = array();
+ $tender_groups = array();
+ foreach ($data['Entry'] AS $entry) {
+ if (!isset($group[$entry['account_id']]))
+ $group[$entry['account_id']] =
+ array('account_id' => $entry['account_id'],
+ 'amount' => 0);
+ $group[$entry['account_id']]['amount'] += $entry['amount'];
+ $tender_groups[$entry['account_id']][] = $entry['tender_id'];
+ }
+ $data['Entry'] = $group;
+
+ $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
+ if (isset($ids['transaction_id']))
+ $ids['deposit_id'] = $ids['transaction_id'];
+
+ if (!empty($ids['deposit_id']) && !empty($control['update_tender'])) {
+ foreach ($tender_groups AS $group => $tender_ids) {
+ $entry_id = $ids['entries'][$group]['DoubleEntry']['Entry2']['ledger_entry_id'];
+ $this->pr(10, compact('group', 'tender_ids', 'entry_id'));
+ $this->LedgerEntry->Tender->updateAll
+ (array('Tender.deposit_transaction_id' => $ids['deposit_id'],
+ 'Tender.deposit_ledger_entry_id' => $entry_id),
+ array('Tender.id' => $tender_ids)
+ );
+ }
+ }
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addClose
+ * - Adds a new transaction for closing ledgers
+ */
+
+ function addClose($data) {
+ $this->prEnter(compact('data'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => false,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => false,
+ 'allow_no_entries' => true,
+ );
+
+ // Establish the transaction as a close
+ $data['Transaction'] +=
+ array('type' => 'CLOSE',
+ 'crdr' => null,
+ 'account_id' => null,
+ 'customer_id' => null,
+ 'lease_id' => null,
+ );
+
+ $ledger_ids = array();
+ $data['Entry'] = array();
+ foreach ($data['Ledger'] AS $ledger) {
+ $ledger_id = $ledger['old_ledger_id'];
+ $new_ledger_id = $ledger['new_ledger_id'];
+ $amount = $ledger['amount'];
+ $account_id = $this->Account->Ledger->accountID($ledger_id);
+ $crdr = strtoupper($this->Account->fundamentalOpposite($account_id));
+ $comment = "Ledger Carry Forward (c/f)";
+
+ // Save the ledger ID for later, to mark it as closed
+ $ledger_ids[] = $ledger_id;
+
+ // No need to generate ledger entries if there is no balance
+ if (empty($ledger['amount']) || $ledger['amount'] == 0)
+ continue;
+
+ // Add an entry to carry the ledger balance forward
+ $data['Entry'][] = compact('account_id', 'ledger_id', 'new_ledger_id',
+ 'crdr', 'amount', 'comment');
+ }
+ unset($data['Ledger']);
+
+ // Add the transaction and carry forward balances
+ $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
+ if (isset($ids['transaction_id']))
+ $ids['close_id'] = $ids['transaction_id'];
+
+ // Mark the older ledgers as closed
+ if (!empty($ids['close_id'])) {
+ $this->LedgerEntry->Ledger->updateAll
+ (array('Ledger.close_transaction_id' => $ids['close_id']),
+ array('Ledger.id' => $ledger_ids)
+ );
+ }
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addRefund
+ * - Adds a new refund
+ */
+
+ function addRefund($data, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'customer_id', 'lease_id'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => true,
+ );
+
+ // Establish the transaction as a Refund. This is just like a
+ // Payment, except instead of paying out of the account payable,
+ // it comes from the customer credit in the account receivable.
+ // Someday, perhaps we'll just issue a Credit Note or similar,
+ // but for now, a refund means it's time to actually PAY.
+ $data['Transaction'] +=
+ array('account_id' => $this->Account->accountReceivableAccountID());
+
+ // Also, to make it clear to the user, we flag as a REFUND
+ // even though that type works and operates just as PAYMENT
+ foreach ($data['Entry'] AS &$entry)
+ $entry += array('type' => 'REFUND');
+
+ $ids = $this->addPayment($data, $customer_id, $lease_id);
+ if (isset($ids['transaction_id']))
+ $ids['refund_id'] = $ids['transaction_id'];
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addPayment
+ * - Adds a new payment transaction, which is money outflow
+ */
+
+ function addPayment($data, $customer_id, $lease_id = null) {
+ $this->prEnter(compact('data', 'customer_id', 'lease_id'));
+
+ // Set up control parameters
+ $data += array('control' => array());
+ $data['control'] +=
+ array('assign' => false,
+ 'include_ledger_entry' => true,
+ 'include_statement_entry' => true,
+ );
+
+ // Establish the transaction as an payment
+ $data['Transaction'] +=
+ array('type' => 'PAYMENT',
+ 'crdr' => 'DEBIT',
+ 'account_id' => $this->Account->accountPayableAccountID(),
+ 'customer_id' => $customer_id,
+ 'lease_id' => $lease_id,
+ );
+
+ // Go through the statement entries and flag as payments
+ foreach ($data['Entry'] AS &$entry)
+ $entry += array('type' => 'PAYMENT',
+ );
+
+ $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
+ if (isset($ids['transaction_id']))
+ $ids['payment_id'] = $ids['transaction_id'];
+
+ return $this->prReturn($ids);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: verifyTransaction
+ * - Verifies consistenty of new transaction data
+ * (not in a pre-existing transaction)
+ */
+ function verifyTransaction($transaction, $entries) {
+ //$this->prFunctionLevel(10);
+ $this->prEnter(compact('transaction', 'entries'));
+
+ // Verify required Transaction data is present
+ if (empty($transaction['type']) ||
+ ($transaction['type'] != 'CLOSE'
+ && (empty($transaction['account_id']) ||
+ empty($transaction['crdr']))) ||
+ (in_array($transaction['type'], array('INVOICE', 'RECEIPT'))
+ && empty($transaction['customer_id']))
+ ) {
+ return $this->prReturn(false);
+ }
+
+ // Verify all entries
+ foreach ($entries AS $entry) {
+ // Ensure these items are null'ed out so we don't
+ // accidentally pick up stale data.
+ $le1 = $le1_tender = $le2 = $se = null;
+ extract($entry);
+ if (!empty($le1) && !empty($le2) &&
+ !$this->LedgerEntry->DoubleEntry->verifyDoubleEntry($le1, $le2, $le1_tender)) {
+ return $this->prReturn(false);
+ }
+ if (!empty($se) &&
+ !$this->StatementEntry->verifyStatementEntry($se)) {
+ return $this->prReturn(false);
+ }
+ }
+
+ return $this->prReturn(true);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addTransaction
+ * - Adds a new transaction, and the appropriate ledger and statement
+ * entries, as layed out in the $data['Entry'] array. The array is
+ * overloaded, since it is used to create both ledger _and_ statement
+ * entries.
+ */
+
+ function addTransaction($control, $transaction, $entries) {
+ $this->prEnter(compact('control', 'transaction', 'entries'));
+
+ $result = $this->_splitEntries($control, $transaction, $entries);
+ if (!empty($result['error']))
+ return $this->prReturn(array('error' => true));
+
+ // Make use of the work done by splitEntries
+ $transaction = $this->filter_null($transaction) + $result['transaction'];
+ $entries = $result['entries'];
+ extract($result['vars']);
+
+ $this->pr(20, compact('transaction', 'entries'));
+
+ // Move forward, verifying and saving everything.
+ $ret = array('data' => $transaction);
+ if (!$this->verifyTransaction($transaction, $entries))
+ return $this->prReturn(array('error' => true) + $ret);
+
+ // Save transaction to the database
+ $this->create();
+ if (!$this->save($transaction))
+ return $this->prReturn(array('error' => true) + $ret);
+ $ret['transaction_id'] = $transaction['id'] = $this->id;
+
+ // Add the entries
+ $ret += $this->addTransactionEntries($control, $transaction, $entries, false);
+
+ // If the caller requests 'assign'=>true, they really
+ // want to do a credit assignment, and _then_ create
+ // an explicit credit with any leftover. If an array
+ // is specified, they get full control of the order.
+ if (empty($control['assign']))
+ $assign_ops = array();
+ elseif (is_array($control['assign']))
+ $assign_ops = $control['assign'];
+ elseif (is_bool($control['assign']))
+ $assign_ops = (empty($control['assign_receipt'])
+ ? array('assign')
+ : array('assign', 'create'));
+ else
+ $this->INTERNAL_ERROR('Invalid control[assign] parameter');
+
+ $this->pr(17, compact('assign_ops'), 'Credit operations');
+
+ // Go through the requested assignment mechanisms
+ foreach ($assign_ops AS $method) {
+ if (!empty($ret['error']))
+ break;
+
+ $this->pr(17, compact('method'), 'Handling credits');
+
+ if ($method === 'assign') {
+ $result = $this->StatementEntry->assignCredits
+ (null,
+ (empty($control['assign_receipt']) ? null
+ : $ret['transaction_id']),
+ null,
+ $assign_disbursement_type,
+ $transaction['customer_id'],
+ $transaction['lease_id']
+ );
+ }
+ elseif ($method === 'create' || is_numeric($method)) {
+ if (is_numeric($method))
+ $credit_amount = $method;
+ else {
+ $stats = $this->stats($transaction['id']);
+ $credit_amount = $stats['undisbursed'];
+ }
+
+ if ($credit_amount < 0)
+ $this->INTERNAL_ERROR('Receipt has negative undisbursed balance');
+
+ if (empty($credit_amount))
+ continue;
+
+ $result = $this->addTransactionEntries
+ (array('include_ledger_entry' => true,
+ 'include_statement_entry' => true),
+ array('crdr' => 'DEBIT') + $transaction,
+ array(array('type' => 'SURPLUS',
+ 'account_id' => $this->Account->customerCreditAccountID(),
+ 'amount' => $credit_amount,
+ ),
+ ));
+ }
+ else
+ $this->INTERNAL_ERROR('Invalid assign method');
+
+ $ret['credit'][$method] = $result;
+ if (!empty($result['error']))
+ $ret['error'] = true;
+ }
+
+ if (!empty($transaction['customer_id'])) {
+ $this->Customer->update($transaction['customer_id']);
+ }
+ return $this->prReturn($ret);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addTransactionEntries
+ * - Largely a helper function to addTransaction, this function is
+ * responsible for adding ledger/statement entries to an existing
+ * transaction. If needed, this function can also be called outside
+ * of addTransaction, although it's not clear where that would be
+ * appropriate, since transactions are really snapshots of some
+ * event, and shouldn't be mucked with after creation.
+ */
+
+ function addTransactionEntries($control, $transaction, $entries, $split = true) {
+ $this->prEnter(compact('control', 'transaction', 'entries', 'split'));
+
+ // Verify that we have a transaction
+ if (empty($transaction['id']))
+ return $this->prReturn(array('error' => true));
+
+ // If the entries are not already split, do so now.
+ if ($split) {
+ $result = $this->_splitEntries($control, $transaction, $entries);
+ if (!empty($result['error']))
+ return $this->prReturn(array('error' => true));
+
+ // Make use of the work done by splitEntries
+ $transaction = $this->filter_null($transaction) + $result['transaction'];
+ $entries = $result['entries'];
+ extract($result['vars']);
+
+/* // Verify the entries */
+/* $ret = array(); */
+/* if (!$this->verifyTransaction($transaction, $entries)) */
+/* return $this->prReturn(array('error' => true) + $ret); */
+ }
+
+ $this->id = $transaction['id'];
+ $transaction['stamp'] = $this->field('stamp');
+ $transaction['customer_id'] = $this->field('customer_id');
+
+ // Set up our return array
+ $ret = array();
+ $ret['entries'] = array();
+ $ret['error'] = false;
+
+ // Go through the entries
+ foreach ($entries AS $e_index => &$entry) {
+ // Ensure these items are null'ed out so we don't
+ // accidentally pick up stale data.
+ $le1 = $le1_tender = $le2 = $se = null;
+ extract($entry);
+
+ if (!empty($le1) && !empty($le2)) {
+ $le1['transaction_id'] = $le2['transaction_id'] = $transaction['id'];
+ if (isset($le1_tender))
+ $le1_tender['customer_id'] = $transaction['customer_id'];
+ $result = $this->LedgerEntry->DoubleEntry->addDoubleEntry($le1, $le2, $le1_tender);
+ $ret['entries'][$e_index]['DoubleEntry'] = $result;
+ if ($result['error']) {
+ $ret['error'] = true;
+ continue;
+ }
+ }
+
+ if (!empty($se)) {
+ $se['transaction_id'] = $transaction['id'];
+ if (empty($se['effective_date']))
+ $se['effective_date'] = $transaction['stamp'];
+ $result = $this->StatementEntry->addStatementEntry($se);
+ $ret['entries'][$e_index]['StatementEntry'] = $result;
+ if ($result['error']) {
+ $ret['error'] = true;
+ continue;
+ }
+ }
+ }
+
+ return $this->prReturn($ret);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: _splitEntries
+ * - An internal helper function capable of splitting an array of
+ * combined ledger/statement entries into their indivdual entry
+ * components (2 ledger entries, and a statement entry), based
+ * on the typical requirements. Any custom split will have to
+ * be done outside of this function.
+ */
+
+ function _splitEntries($control, $transaction, $entries) {
+ $this->prEnter(compact('control', 'transaction', 'entries'));
+
+ // Verify that we have a transaction and entries
+ if (empty($transaction) ||
+ (empty($entries) && empty($control['allow_no_entries'])))
+ return $this->prReturn(array('error' => true));
+
+ // set ledger ID as the current ledger of the specified account
+ if (empty($transaction['ledger_id']))
+ $transaction['ledger_id'] =
+ $this->Account->currentLedgerID($transaction['account_id']);
+
+ // Automatically figure out the customer if we have the lease
+ if (!empty($transaction['lease_id']) && empty($transaction['customer_id'])) {
+ $L = new Lease();
+ $L->id = $transaction['lease_id'];
+ $transaction['customer_id'] = $L->field('customer_id');
+ }
+
+ if (!empty($transaction['account_id'])) {
+ if (empty($transaction['ledger_id']))
+ $transaction['ledger_id'] =
+ $this->Account->currentLedgerID($transaction['account_id']);
+
+ if (empty($transaction['crdr']))
+ $transaction['crdr'] = strtoupper($this->Account->fundamentalType
+ ($transaction['account_id']));
+ }
+
+ // Some transactions do not have their statement entries
+ // generated directly as part of the transaction, but are
+ // created in the final steps during the reconciliation
+ // phase by the assignCredits function. Keep track of
+ // what type the statement entries _would_ have been, so
+ // that the assignCredits function can do the same.
+ $assign_disbursement_type = null;
+
+ // Break each entry out of the combined statement/ledger entry
+ // and into individual entries appropriate for saving. While
+ // we're at it, calculate the transaction total as well.
+ $transaction['amount'] = 0;
+ foreach ($entries AS &$entry) {
+ // Ensure these items are null'ed out so we don't
+ // accidentally pick up stale data.
+ $le1 = $le1_tender = $le2 = $se = null;
+
+ // Really, data should be sanitized at the controller,
+ // and not here. However, it's a one stop cleanup.
+ $entry['amount'] = str_replace('$', '', $entry['amount']);
+
+ // Set up our comments, possibly using the default 'comment' field
+ if (empty($entry['ledger_entry_comment'])) {
+ if ($transaction['type'] != 'INVOICE' && !empty($entry['comment']))
+ $entry['ledger_entry_comment'] = $entry['comment'];
+ else
+ $entry['ledger_entry_comment'] = null;
+ }
+ if (empty($entry['statement_entry_comment'])) {
+ if ($transaction['type'] == 'INVOICE' && !empty($entry['comment']))
+ $entry['statement_entry_comment'] = $entry['comment'];
+ else
+ $entry['statement_entry_comment'] = null;
+ }
+
+ if (empty($entry['crdr']) && !empty($transaction['crdr']))
+ $entry['crdr'] = strtoupper($this->Account->fundamentalOpposite
+ ($transaction['crdr']));
+
+ // Priority goes to settings defined in $entry, but
+ // use the control information as defaults.
+ $entry += $control;
+
+ if (!empty($entry['include_ledger_entry'])) {
+ // Create one half of the Double Ledger Entry (and the Tender)
+ $le1 =
+ array_intersect_key($entry,
+ array_flip(array('ledger_id', 'account_id', 'crdr', 'amount')));
+ $le1['comment'] = $entry['ledger_entry_comment'];
+ $le1_tender = isset($entry['Tender']) ? $entry['Tender'] : null;
+
+ // Create the second half of the Double Ledger Entry
+ if ($transaction['type'] == 'CLOSE') {
+ $le2 =
+ array_intersect_key($entry,
+ array_flip(array('account_id', 'amount')));
+ $le2['ledger_id'] = $entry['new_ledger_id'];
+ $le2['crdr'] = strtoupper($this->Account->fundamentalType($le2['account_id']));
+ $le2['comment'] = "Ledger Balance Forward (b/f)";
+ }
+ else {
+ $le2 =
+ array_intersect_key($entry,
+ array_flip(array('amount'))) +
+ array_intersect_key($transaction,
+ array_flip(array('ledger_id', 'account_id', 'crdr')));
+ }
+
+ if ($entry['amount'] < 0 && !empty($entry['force_positive'])) {
+ $le1['amount'] *= -1;
+ $le2['amount'] *= -1;
+ $entry += array('swap_crdr' => true);
+ }
+
+ if (!empty($entry['swap_crdr']))
+ list($le1['crdr'], $le2['crdr']) = array($le2['crdr'], $le1['crdr']);
+ }
+ else
+ $le1 = $le1_tender = $le2 = null;
+
+ // Now that the ledger entries are in place, respect the 'negative' flag
+ if (!empty($entry['negative']))
+ $entry['amount'] *= -1;
+
+ if (!empty($entry['include_statement_entry'])) {
+ // Create the statement entry
+ $se =
+ array_intersect_key($entry,
+ array_flip(array('type', 'account_id', 'amount',
+ 'effective_date', 'through_date', 'due_date',
+ 'customer_id', 'lease_id',
+ 'charge_entry_id'))) +
+ array_intersect_key($transaction,
+ array_flip(array('customer_id', 'lease_id')));
+ $se['comment'] = $entry['statement_entry_comment'];
+ }
+ else {
+ if (!empty($entry['assign']) &&
+ !empty($entry['type']) &&
+ empty($entry['charge_entry_id'])) {
+ if (empty($assign_disbursement_type))
+ $assign_disbursement_type = $entry['type'];
+ elseif ($entry['type'] != $assign_disbursement_type)
+ $this->INTERNAL_ERROR('Multiple disbursement types for this transaction');
+ }
+
+ $se = null;
+ }
+
+ // Add entry amount into the transaction total
+ $transaction['amount'] += $entry['amount'];
+
+ // Replace combined entry with our new individual entries
+ $entry = compact('le1', 'le1_tender', 'le2', 'se');
+ }
+
+ return $this->prReturn(compact('transaction', 'entries')
+ + array('vars' => compact('assign_disbursement_type'))
+ + array('error' => false));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addNsf
+ * - Adds NSF transaction
+ */
+
+ function addNsf($tender, $stamp = null, $comment = null) {
+ $this->prEnter(compact('tender', 'stamp', 'comment'));
+
+ $ret = array();
+
+ // Enter the NSF
+ // This is the transaction pulling money from the bank account
+ // and recording it in the NSF account. It has nothing to do
+ // with the customer statement (charges, disbursements, credits, etc).
+ $bounce_result = $this->addDeposit
+ (array('control' =>
+ // This is not a "normal" deposit, so we don't
+ // want to update the tender deposit transaction id
+ // (it already has the correct one).
+ array('update_tender' => false),
+
+ 'Transaction' =>
+ array('stamp' => $stamp,
+ 'type' => 'WITHDRAWAL',
+ 'crdr' => 'CREDIT'),
+
+ 'Entry' =>
+ array(array('tender_id' => null,
+ 'account_id' => $this->Account->nsfAccountID(),
+ 'amount' => $tender['LedgerEntry']['amount'],
+ ))),
+ $tender['DepositLedgerEntry']['account_id']);
+
+ $this->pr(20, compact('bounce_result'));
+ $ret['bounce'] = $bounce_result;
+ if ($bounce_result['error'])
+ return $this->prReturn(array('error' => true) + $ret);
+
+ // Since we may have saved the nsf transaction with a null
+ // timestamp, query it back out of the database to find out
+ // what timestamp was _really_ specified, for later use.
+ $bounce = $this->find
+ ('first', array('contain' => false, 'id' => $bounce_result['transaction_id']));
+ $this->pr(20, compact('bounce'));
+ $stamp = $bounce['Transaction']['stamp'];
+
+ // OK, now move into customer realm, finding all statement
+ // entries that were affected by the bad payment (tender).
+ $nsf_ledger_entry = $this->LedgerEntry->find
+ ('first', array
+ ('contain' => array('Transaction' =>
+ array(//'fields' => array(),
+ 'StatementEntry' =>
+ array(//'fields' => array(),
+ ),
+ ),
+ ),
+ 'conditions' => array('LedgerEntry.id' => $tender['LedgerEntry']['id']),
+ ));
+
+ $this->pr(20, compact('nsf_ledger_entry'));
+ if (!$nsf_ledger_entry)
+ return $this->prReturn(array('error' => true) + $ret);
+
+ // Build a transaction to adjust all of the statement entries
+ $rollback =
+ array('control' =>
+ array('assign' => false,
+ 'include_ledger_entry' => false,
+ 'include_statement_entry' => true,
+ ),
+
+ 'Transaction' =>
+ array('stamp' => $stamp,
+ 'type' => 'RECEIPT',
+ 'crdr' => 'CREDIT',
+ 'account_id' => $this->Account->nsfAccountID(),
+ 'customer_id' => $tender['Tender']['customer_id'],
+ 'comment' => $comment,
+ ),
+
+ 'Entry' => array());
+
+ $rollback['Transaction']['amount'] = 0;
+ foreach ($nsf_ledger_entry['Transaction']['StatementEntry'] AS $disbursement) {
+ if ($disbursement['type'] === 'SURPLUS') {
+ $disbursement['type'] = 'VOID';
+ $this->StatementEntry->id = $disbursement['id'];
+ $this->StatementEntry->saveField('type', $disbursement['type']);
+ }
+ else {
+ $rollback['Entry'][] =
+ array('type' => $disbursement['type'],
+ 'amount' => -1 * $disbursement['amount'],
+ 'account_id' => $this->Account->nsfAccountID(),
+ 'customer_id' => $disbursement['customer_id'],
+ 'lease_id' => $disbursement['lease_id'],
+ 'charge_entry_id' => $disbursement['charge_entry_id'],
+ );
+ $rollback['Transaction']['amount'] += $disbursement['amount'];
+ }
+ }
+
+ // Add the sole ledger entry for this transaction. If there
+ // is not a transaction amount, then there is no point in
+ // recording a ledger entry of $0.00
+ if (!empty($rollback['Transaction']['amount'])) {
+ $rollback['Entry'][] =
+ array('include_ledger_entry' => true,
+ 'include_statement_entry' => false,
+ 'amount' => $rollback['Transaction']['amount'],
+ 'account_id' => $this->Account->accountReceivableAccountID(),
+ );
+
+ // Set the transaction amount to be negative
+ $rollback['Transaction']['amount'] *= -1;
+ }
+
+ // Record the transaction, which will un-pay previously paid
+ // charges, void any credits, and other similar work.
+ if (count($rollback['Entry'])) {
+ $rollback_result = $this->addTransaction($rollback['control'],
+ $rollback['Transaction'],
+ $rollback['Entry']);
+ $this->pr(20, compact('rollback', 'rollback_result'));
+ $ret['rollback'] = $rollback_result;
+ if ($rollback_result['error'])
+ return $this->prReturn(array('error' => true) + $ret);
+ }
+
+ // Add NSF Charge
+ $charge_result = $this->addInvoice
+ (array('Transaction' => compact('stamp'),
+
+ 'Entry' =>
+ array
+ (array('account_id' => $this->Account->nsfChargeAccountID(),
+ 'effective_date' => $stamp,
+ // REVISIT : 20090730
+ // BAD, BAD, BAD... who would actually
+ // hardcode a value like this???? ;-)
+ 'amount' => 35,
+ 'comment' => "NSF: " . $tender['Tender']['name'],
+ ),
+ ),
+ ),
+ $tender['Tender']['customer_id']);
+
+ $this->pr(20, compact('charge_result'));
+ $ret['charge'] = $charge_result;
+ if ($charge_result['error'])
+ return $this->prReturn(array('error' => true) + $ret);
+
+ if (!empty($ret['rollback'])) {
+ foreach ($ret['rollback']['entries'] AS $rentry) {
+ if (!empty($rentry['DoubleEntry'])) {
+ if (!empty($rentry['DoubleEntry']['error']))
+ continue;
+
+ foreach (array('Entry1', 'Entry2') AS $n) {
+ $entry = $rentry['DoubleEntry'][$n];
+ if ($entry['data']['account_id'] == $this->Account->nsfAccountID()) {
+ if (!empty($ret['nsf_ledger_entry_id']))
+ $this->INTERNAL_ERROR("More than one NSF LE ID");
+
+ $ret['nsf_ledger_entry_id'] = $entry['ledger_entry_id'];
+ }
+ }
+ }
+ }
+ }
+ if (empty($ret['rollback']['error']) && empty($ret['nsf_ledger_entry_id'])) {
+ //$this->INTERNAL_ERROR("NSF LE ID not found under rollback entries");
+ // Actually, this can happen if an item is NSF without having ever
+ // been applied to any charges.
+ $ret['nsf_ledger_entry_id'] = null;
+ }
+
+ $ret['nsf_transaction_id'] = $ret['bounce']['transaction_id'];
+ return $this->prReturn($ret + array('error' => false));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: addReversal
+ * - Adds a new charge reversal
+ */
+
+ function addReversal($charge, $stamp = null, $comment = null) {
+ $this->prEnter(compact('charge', 'stamp', 'comment'));
+
+ $ret = array();
+
+ // Finding all statement entries affected by reversing this charge.
+ $disb_entries = $this->StatementEntry->find
+ ('first', array
+ ('contain' => array('DisbursementEntry' =>
+ array(//'fields' => array(),
+ ),
+ ),
+ 'conditions' => array('StatementEntry.id' => $charge['StatementEntry']['id']),
+ ));
+
+ $this->pr(20, compact('disb_entries'));
+ if (!$disb_entries)
+ return $this->prReturn(array('error' => true) + $ret);
+
+ // Build a transaction to adjust all of the statement entries
+ // These are all disbursements against the charge we're reversing
+ $rollback =
+ array('control' =>
+ array('include_ledger_entry' => false,
+ 'include_statement_entry' => true,
+ ),
+
+ 'Transaction' =>
+ array('stamp' => $stamp,
+ 'type' => 'CREDIT_NOTE',
+ 'crdr' => 'CREDIT',
+ 'account_id' => $this->Account->accountReceivableAccountID(),
+ 'amount' => $charge['StatementEntry']['amount'],
+ 'customer_id' => $charge['StatementEntry']['customer_id'],
+ 'lease_id' => null,
+ 'comment' => $comment,
+ ),
+
+ 'Entry' => array());
+
+ // Reverse the charge
+ $rollback['Entry'][] =
+ array('include_ledger_entry' => true,
+ 'include_statement_entry' => true,
+ 'type' => 'REVERSAL',
+ 'account_id' => $charge['StatementEntry']['account_id'],
+ 'amount' => $charge['StatementEntry']['amount'],
+ 'customer_id' => $charge['StatementEntry']['customer_id'],
+ 'lease_id' => $charge['StatementEntry']['lease_id'],
+ 'charge_entry_id' => $charge['StatementEntry']['id'],
+ );
+
+ $customer_credit = 0;
+ foreach ($disb_entries['DisbursementEntry'] AS $disbursement) {
+ $rollback['Entry'][] =
+ array(
+ 'include_ledger_entry' =>
+ ($disbursement['type'] !== 'DISBURSEMENT'),
+
+ 'force_positive' => true,
+ 'type' => $disbursement['type'],
+ 'amount' => -1 * $disbursement['amount'],
+ 'account_id' =>
+ ($disbursement['type'] === 'DISBURSEMENT'
+ ? $this->Account->customerCreditAccountID()
+ : $disbursement['account_id']),
+ 'customer_id' => $disbursement['customer_id'],
+ 'lease_id' => $disbursement['lease_id'],
+ 'charge_entry_id' => $disbursement['charge_entry_id'],
+ );
+
+ if ($disbursement['type'] === 'DISBURSEMENT')
+ $customer_credit += $disbursement['amount'];
+ }
+
+ // Create an explicit surplus entry for the customer credit.
+ // Do it BEFORE assigning credits to outstanding charges to
+ // ensure that those charges are paid from the customer surplus
+ // account thus and we don't end up with bizarre disbursements,
+ // like having Rent paid from Damage (or whatever account the
+ // reversed charge came from).
+ $rollback['control']['assign'] = array($customer_credit, 'assign');
+
+ // Record the transaction, which will un-disburse previously
+ // disbursed payments, and other similar work.
+ if (count($rollback['Entry'])) {
+ $rollback_result = $this->addTransaction($rollback['control'],
+ $rollback['Transaction'],
+ $rollback['Entry']);
+ $this->pr(20, compact('rollback', 'rollback_result'));
+ $ret = $rollback_result;
+ if ($rollback_result['error'])
+ return $this->prReturn(array('error' => true) + $ret);
+ }
+
+ return $this->prReturn($ret + array('error' => false));
+ }
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - 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,
+ * and can leave the database in an inconsistent state. Expected
+ * scenario is to remove a bad transaction directly after creation,
+ * before it gets tied to anything else in the system (such as
+ * a late charge invoice for a customer that isn't actually late).
+ */
+
+ function destroy($id) {
+ $this->prFunctionLevel(30);
+ $this->prEnter(compact('id'));
+/* $transaction = $this->find */
+/* ('first', */
+/* array('contain' => */
+/* array(// Models */
+/* 'StatementEntry', */
+/* 'LedgerEntry' => array('Tender'), */
+/* ), */
+/* 'conditions' => array(array('Transaction.id' => $id)), */
+/* )); */
+/* pr($transaction); */
+
+ $this->id = $id;
+ $customer_id = $this->field('customer_id');
+ $result = $this->delete($id);
+
+ if (!empty($customer_id)) {
+ $this->StatementEntry->assignCredits
+ (null, null, null, null, $customer_id, null);
+ //$this->Customer->update($customer_id);
+ }
+
+ return $this->prReturn($result);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: update
+ * - Update any cached or calculated fields
+ */
+ function update($id) {
+ $this->INTERNAL_ERROR("Transaction::update not yet implemented");
+
+ $result = $this->find
+ ('first',
+ array('link' => array('StatementEntry'),
+ 'fields' => array("SUM(LedgerEntry.amount) AS total"),
+ 'conditions' => array(array('LedgerEntry.account_id = Transaction.account_id'),
+ ),
+ ));
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested transaction
+ */
+ function stats($id = null, $query = null, $balance_account_id = null) {
+ $this->prEnter(compact('id', 'query'));
+
+ $this->queryInit($query);
+ unset($query['group']);
+
+ if (isset($id)) {
+ $query['conditions'][] = array('Transaction.id' => $id);
+ $query['group'] = 'Transaction.id';
+ }
+ else
+ // CakePHP seems to automagically add in our ID as a part
+ // of the query conditions, but only on a 'first' query,
+ // not an 'all'. I suppose this is helpful :-/
+ unset($this->id);
+
+ if (empty($query['fields']))
+ $query['fields'] = array();
+
+ // Get the overall total
+ $squery = $query;
+ $squery['fields'][] = "SUM(Transaction.amount) AS total";
+ $squery['fields'][] = "COUNT(Transaction.id) AS count";
+ $stats = $this->find('first', $squery);
+ if (empty($stats))
+ return $this->prReturn(array());
+ $stats = $stats[0];
+ unset($stats[0]);
+
+
+ foreach ($this->hasMany AS $table => $association) {
+ // Only calculate stats for *Entry types
+ if (!preg_match("/Entry$/", $table) &&
+ !preg_match("/Entry$/", $association['className']))
+ continue;
+
+ $squery = $query;
+ $squery['link'][$table] = array('fields' => array());
+
+ if ($table == 'LedgerEntry') {
+ if (isset($balance_account_id)) {
+ $squery['link']['LedgerEntry']['Account'] = array('fields' => array());
+ $squery['conditions'][] = array("Account.id" => $balance_account_id);
+ }
+
+ $squery['fields'] = array_merge($squery['fields'],
+ $this->LedgerEntry->debitCreditFields(true, $balance_account_id != null));
+ }
+ elseif ($table == 'StatementEntry') {
+ $squery['fields'] = array_merge($squery['fields'],
+ $this->StatementEntry->chargeDisbursementFields(true));
+ }
+ else {
+ $squery['fields'][] = "SUM({$table}.amount) AS total";
+ $squery['fields'][] = "COUNT({$table}.id) AS entries";
+ }
+ $stats[$table] = $this->find('first', $squery);
+ // REVISIT : 20090724
+ // [0][0] is for when we do an 'all' query. This can
+ // be removed at some point, but I'm keeping it while
+ // toggling between 'all' and 'first' (testing).
+ if (isset($stats[$table][0][0]))
+ $stats[$table] += $stats[$table][0][0];
+ else
+ $stats[$table] += $stats[$table][0];
+ unset($stats[$table][0]);
+ }
+
+ // Add summary data, which may or may not be useful
+ // or even meaningful, depending on what the caller
+ // has queried (it's up to them to make that decision).
+ $stats['undisbursed'] = $stats['total'] - $stats['StatementEntry']['disbursements'];
+
+ return $this->prReturn($stats);
+ }
+}
+?>
\ No newline at end of file
diff --git a/models/unit.php b/models/unit.php
new file mode 100644
index 0000000..602460a
--- /dev/null
+++ b/models/unit.php
@@ -0,0 +1,230 @@
+ array('numeric'),
+ 'unit_size_id' => array('numeric'),
+ 'name' => array('notempty'),
+ 'sort_order' => array('numeric'),
+ 'walk_order' => array('numeric'),
+ 'deposit' => array('money'),
+ 'amount' => array('money')
+ );
+
+ var $belongsTo = array(
+ 'UnitSize',
+ );
+
+ var $hasOne = array(
+ 'CurrentLease' => array(
+ 'className' => 'Lease',
+ 'conditions' => 'CurrentLease.moveout_date IS NULL',
+ ),
+ );
+
+ var $hasMany = array(
+ 'Lease',
+ );
+
+ //var $default_log_level = array('log' => 30, 'show' => 15);
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * helpers: status enumerations
+ */
+
+ function statusEnums() {
+ static $status_enums;
+ if (!isset($status_enums))
+ $status_enums = $this->getEnumValues('status');
+ return $status_enums;
+ }
+
+ function activeStatusEnums() {
+ return array_diff_key($this->statusEnums(), array(''=>1, 'DELETED'=>1));
+ }
+
+ function statusValue($enum) {
+ $enums = $this->statusEnums();
+ return $enums[$enum];
+ }
+
+ function occupiedEnumValue() {
+ 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 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) .
+ ' AND ' .
+ ($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: 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, $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;
+ $this->saveField('status', $status);
+ return $this->prReturn(true);
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: update
+ * - Update any cached or calculated fields
+ */
+ function update($id) {
+ }
+
+
+ /**************************************************************************
+ **************************************************************************
+ **************************************************************************
+ * function: stats
+ * - Returns summary data from the requested customer.
+ */
+
+ function stats($id = null) {
+ if (!$id)
+ return null;
+
+ // Get the basic information necessary
+ $unit = $this->find('first',
+ array('contain' => array
+ ('Lease' => array
+ ('fields' => array('Lease.id')),
+
+ 'CurrentLease' => array
+ ('fields' => array('CurrentLease.id'))),
+
+ 'conditions' => array
+ (array('Unit.id' => $id)),
+ ));
+
+ // Get the stats for the current lease
+ $stats['CurrentLease'] = $this->Lease->stats($unit['CurrentLease']['id']);
+
+ // Sum the stats for all leases together
+ foreach ($unit['Lease'] AS $lease) {
+ $this->statsMerge($stats['Lease'], $this->Lease->stats($lease['id']));
+ }
+
+ // Return the collection
+ return $stats;
+ }
+
+}
diff --git a/models/unit_size.php b/models/unit_size.php
new file mode 100644
index 0000000..ca06b60
--- /dev/null
+++ b/models/unit_size.php
@@ -0,0 +1,25 @@
+ 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 $hasMany = array(
+ 'Unit',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/models/unit_type.php b/models/unit_type.php
new file mode 100644
index 0000000..d2bd90d
--- /dev/null
+++ b/models/unit_type.php
@@ -0,0 +1,16 @@
+ array('numeric'),
+ 'code' => array('notempty'),
+ 'name' => array('notempty')
+ );
+
+ var $hasMany = array(
+ 'UnitSize',
+ );
+
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/README b/plugins/debug_kit/README
new file mode 100644
index 0000000..4f31f67
--- /dev/null
+++ b/plugins/debug_kit/README
@@ -0,0 +1,5 @@
+To install copy the debug_kit directory to the plugins folder and include the toolbar component in your app_controller.php:
+
+$components = array('DebugKit.Toolbar');
+
++ Set debug mode to at least 1.
\ No newline at end of file
diff --git a/plugins/debug_kit/controllers/components/empty b/plugins/debug_kit/controllers/components/empty
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/debug_kit/controllers/components/toolbar.php b/plugins/debug_kit/controllers/components/toolbar.php
new file mode 100644
index 0000000..0b72acb
--- /dev/null
+++ b/plugins/debug_kit/controllers/components/toolbar.php
@@ -0,0 +1,471 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage cake.cake.libs.
+ * @since CakePHP v 1.2.0.4487
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+class ToolbarComponent extends Object {
+/**
+ * Controller instance reference
+ *
+ * @var object
+ */
+ var $controller;
+/**
+ * Components used by DebugToolbar
+ *
+ * @var array
+ */
+ var $components = array('RequestHandler');
+/**
+ * The default panels the toolbar uses.
+ * which panels are used can be configured when attaching the component
+ *
+ * @var array
+ */
+ var $_defaultPanels = array('session', 'request', 'sqlLog', 'timer', 'log', 'memory', 'variables');
+/**
+ * Loaded panel objects.
+ *
+ * @var array
+ */
+ var $panels = array();
+
+/**
+ * fallback for javascript settings
+ *
+ * @var array
+ **/
+ var $_defaultJavascript = array(
+ 'behavior' => '/debug_kit/js/js_debug_toolbar'
+ );
+/**
+ * javascript files component will be using.
+ *
+ * @var array
+ **/
+ var $javascript = array();
+/**
+ * initialize
+ *
+ * If debug is off the component will be disabled and not do any further time tracking
+ * or load the toolbar helper.
+ *
+ * @return bool
+ **/
+ function initialize(&$controller, $settings) {
+ if (Configure::read('debug') == 0) {
+ $this->enabled = false;
+ return false;
+ }
+ App::import('Vendor', 'DebugKit.DebugKitDebugger');
+
+ DebugKitDebugger::startTimer('componentInit', __('Component initialization and startup', true));
+ if (!isset($settings['panels'])) {
+ $settings['panels'] = $this->_defaultPanels;
+ }
+
+ if (isset($settings['javascript'])) {
+ $settings['javascript'] = $this->_setJavascript($settings['javascript']);
+ } else {
+ $settings['javascript'] = $this->_defaultJavascript;
+ }
+ $this->_loadPanels($settings['panels']);
+ unset($settings['panels']);
+
+ $this->_set($settings);
+ $this->controller =& $controller;
+ return false;
+ }
+
+/**
+ * Component Startup
+ *
+ * @return bool
+ **/
+ function startup(&$controller) {
+ $currentViewClass = $controller->view;
+ $this->_makeViewClass($currentViewClass);
+ $controller->view = 'DebugKit.Debug';
+ if (!isset($controller->params['url']['ext']) || (isset($controller->params['url']['ext']) && $controller->params['url']['ext'] == 'html')) {
+ $format = 'Html';
+ } else {
+ $format = 'FirePhp';
+ }
+ $controller->helpers['DebugKit.Toolbar'] = array('output' => sprintf('DebugKit.%sToolbar', $format));
+ $panels = array_keys($this->panels);
+ foreach ($panels as $panelName) {
+ $this->panels[$panelName]->startup($controller);
+ }
+ DebugKitDebugger::stopTimer('componentInit');
+ DebugKitDebugger::startTimer('controllerAction', __('Controller Action', true));
+ }
+/**
+ * beforeRender callback
+ *
+ * Calls beforeRender on all the panels and set the aggregate to the controller.
+ *
+ * @return void
+ **/
+ function beforeRender(&$controller) {
+ DebugKitDebugger::stopTimer('controllerAction');
+ $vars = array();
+ $panels = array_keys($this->panels);
+
+ foreach ($panels as $panelName) {
+ $panel =& $this->panels[$panelName];
+ $vars[$panelName]['content'] = $panel->beforeRender($controller);
+ $elementName = Inflector::underscore($panelName) . '_panel';
+ if (isset($panel->elementName)) {
+ $elementName = $panel->elementName;
+ }
+ $vars[$panelName]['elementName'] = $elementName;
+ $vars[$panelName]['plugin'] = $panel->plugin;
+ $vars[$panelName]['disableTimer'] = true;
+ }
+
+ $controller->set(array('debugToolbarPanels' => $vars, 'debugToolbarJavascript' => $this->javascript));
+ DebugKitDebugger::startTimer('controllerRender', __('Render Controller Action', true));
+ }
+
+/**
+ * Load Panels used in the debug toolbar
+ *
+ * @return void
+ * @access protected
+ **/
+ function _loadPanels($panels) {
+ foreach ($panels as $panel) {
+ $className = $panel . 'Panel';
+ if (!class_exists($className) && !App::import('Vendor', $className)) {
+ trigger_error(sprintf(__('Could not load DebugToolbar panel %s', true), $panel), E_USER_WARNING);
+ continue;
+ }
+ $panelObj =& new $className();
+ if (is_subclass_of($panelObj, 'DebugPanel') || is_subclass_of($panelObj, 'debugpanel')) {
+ $this->panels[$panel] =& $panelObj;
+ }
+ }
+ }
+
+/**
+ * Set the javascript to user scripts.
+ *
+ * Set either script key to false to exclude it from the rendered layout.
+ *
+ * @param array $scripts Javascript config information
+ * @return array
+ * @access protected
+ **/
+ function _setJavascript($scripts) {
+ $behavior = false;
+ if (!is_array($scripts)) {
+ $scripts = (array)$scripts;
+ }
+ if (isset($scripts[0])) {
+ $behavior = $scripts[0];
+ }
+ if (isset($scripts['behavior'])) {
+ $behavior = $scripts['behavior'];
+ }
+ if (!$behavior) {
+ return array();
+ } elseif ($behavior === true) {
+ $behavior = 'js';
+ }
+ if (strpos($behavior, '/') !== 0) {
+ $behavior .= '_debug_toolbar';
+ }
+ $pluginFile = APP . 'plugins' . DS . 'debug_kit' . DS . 'vendors' . DS . 'js' . DS . $behavior . '.js';
+ if (file_exists($pluginFile)) {
+ $behavior = '/debug_kit/js/' . $behavior . '.js';
+ }
+ return compact('behavior');
+ }
+/**
+ * Makes the DoppleGangerView class if it doesn't already exist.
+ * This allows DebugView to be compatible with all view classes.
+ *
+ * @param string $baseClassName
+ * @access protected
+ * @return void
+ */
+ function _makeViewClass($baseClassName) {
+ if (!class_exists('DoppelGangerView')) {
+ App::import('View', $baseClassName);
+ if (strpos('View', $baseClassName) === false) {
+ $baseClassName .= 'View';
+ }
+ $class = "class DoppelGangerView extends $baseClassName {}";
+ eval($class);
+ }
+ }
+}
+
+/**
+ * Debug Panel
+ *
+ * Abstract class for debug panels.
+ *
+ * @package cake.debug_kit
+ */
+class DebugPanel extends Object {
+/**
+ * Defines which plugin this panel is from so the element can be located.
+ *
+ * @var string
+ */
+ var $plugin = null;
+/**
+ * startup the panel
+ *
+ * Pull information from the controller / request
+ *
+ * @param object $controller Controller reference.
+ * @return void
+ **/
+ function startup(&$controller) { }
+
+/**
+ * Prepare output vars before Controller Rendering.
+ *
+ * @param object $controller Controller reference.
+ * @return void
+ **/
+ function beforeRender(&$controller) { }
+}
+
+/**
+ * Variables Panel
+ *
+ * Provides debug information on the View variables.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class VariablesPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+}
+
+/**
+ * Session Panel
+ *
+ * Provides debug information on the Session contents.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class SessionPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+/**
+ * beforeRender callback
+ *
+ * @param object $controller
+ * @access public
+ * @return array
+ */
+ function beforeRender(&$controller) {
+ return $controller->Session->read();
+ }
+}
+
+/**
+ * Request Panel
+ *
+ * Provides debug information on the Current request params.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class RequestPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+/**
+ * beforeRender callback - grabs request params
+ *
+ * @return array
+ **/
+ function beforeRender(&$controller) {
+ $out = array();
+ $out['params'] = $controller->params;
+ if (isset($controller->Cookie)) {
+ $out['cookie'] = $controller->Cookie->read();
+ }
+ $out['get'] = $_GET;
+ $out['currentRoute'] = Router::currentRoute();
+ return $out;
+ }
+}
+
+/**
+ * Timer Panel
+ *
+ * Provides debug information on all timers used in a request.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class TimerPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+/**
+ * startup - add in necessary helpers
+ *
+ * @return void
+ **/
+ function startup(&$controller) {
+ if (!in_array('Number', $controller->helpers)) {
+ $controller->helpers[] = 'Number';
+ }
+ }
+}
+
+/**
+ * Memory Panel
+ *
+ * Provides debug information on the memory consumption.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class MemoryPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+/**
+ * startup - add in necessary helpers
+ *
+ * @return void
+ **/
+ function startup(&$controller) {
+ if (!in_array('Number', $controller->helpers)) {
+ $controller->helpers[] = 'Number';
+ }
+ }
+}
+
+/**
+ * sqlLog Panel
+ *
+ * Provides debug information on the SQL logs and provides links to an ajax explain interface.
+ *
+ * @package cake.debug_kit.panels
+ **/
+class sqlLogPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+
+ var $dbConfigs = array();
+/**
+ * get db configs.
+ *
+ * @param string $controller
+ * @access public
+ * @return void
+ */
+ function startUp(&$controller) {
+ if (!class_exists('ConnectionManager')) {
+ $this->dbConfigs = array();
+ return false;
+ }
+ $this->dbConfigs = ConnectionManager::sourceList();
+ return true;
+ }
+/**
+ * Get Sql Logs for each DB config
+ *
+ * @param string $controller
+ * @access public
+ * @return void
+ */
+ function beforeRender(&$controller) {
+ $queryLogs = array();
+ if (!class_exists('ConnectionManager')) {
+ return array();
+ }
+ foreach ($this->dbConfigs as $configName) {
+ $db =& ConnectionManager::getDataSource($configName);
+ if ($db->isInterfaceSupported('showLog')) {
+ ob_start();
+ $db->showLog();
+ $queryLogs[$configName] = ob_get_clean();
+ }
+ }
+ return $queryLogs;
+ }
+}
+
+/**
+ * Log Panel - Reads log entries made this request.
+ *
+ * @package cake.debug_kit.panels
+ */
+class LogPanel extends DebugPanel {
+ var $plugin = 'debug_kit';
+/**
+ * Log files to scan
+ *
+ * @var array
+ */
+ var $logFiles = array('error.log', 'debug.log');
+/**
+ * startup
+ *
+ * @return void
+ **/
+ function startup(&$controller) {
+ if (!class_exists('CakeLog')) {
+ App::import('Core', 'Log');
+ }
+ }
+/**
+ * beforeRender Callback
+ *
+ * @return array
+ **/
+ function beforeRender(&$controller) {
+ $this->startTime = DebugKitDebugger::requestStartTime();
+ $this->currentTime = DebugKitDebugger::requestTime();
+ $out = array();
+ foreach ($this->logFiles as $log) {
+ $file = LOGS . $log;
+ if (!file_exists($file)) {
+ continue;
+ }
+ $out[$log] = $this->_parseFile($file);
+ }
+ return $out;
+ }
+/**
+ * parse a log file and find the relevant entries
+ *
+ * @param string $filename Name of file to read
+ * @access protected
+ * @return array
+ */
+ function _parseFile($filename) {
+ $file =& new File($filename);
+ $contents = $file->read();
+ $timePattern = '/(\d{4}-\d{2}\-\d{2}\s\d{1,2}\:\d{1,2}\:\d{1,2})/';
+ $chunks = preg_split($timePattern, $contents, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+ for ($i = 0, $len = count($chunks); $i < $len; $i += 2) {
+ if (strtotime($chunks[$i]) < $this->startTime) {
+ unset($chunks[$i], $chunks[$i + 1]);
+ }
+ }
+ return array_values($chunks);
+ }
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/debug_kit_app_controller.php b/plugins/debug_kit/debug_kit_app_controller.php
new file mode 100644
index 0000000..0a63efb
--- /dev/null
+++ b/plugins/debug_kit/debug_kit_app_controller.php
@@ -0,0 +1,32 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage cake.cake.libs.
+ * @since CakePHP v 1.2.0.4487
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+class DebugKitAppController extends AppController {
+
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/debug_kit_app_model.php b/plugins/debug_kit/debug_kit_app_model.php
new file mode 100644
index 0000000..ee7af0b
--- /dev/null
+++ b/plugins/debug_kit/debug_kit_app_model.php
@@ -0,0 +1,32 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage cake.cake.libs.
+ * @since CakePHP v 1.2.0.4487
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+class DebugKitAppModel extends AppModel {
+
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/models/empty b/plugins/debug_kit/models/empty
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/debug_kit/tests/cases/controllers/components/toolbar.test.php b/plugins/debug_kit/tests/cases/controllers/components/toolbar.test.php
new file mode 100644
index 0000000..1435303
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/controllers/components/toolbar.test.php
@@ -0,0 +1,323 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage cake.cake.libs.
+ * @since CakePHP v 1.2.0.4487
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+App::import('Component', 'DebugKit.Toolbar');
+
+class TestToolbarComponent extends ToolbarComponent {
+
+ function loadPanels($panels) {
+ $this->_loadPanels($panels);
+ }
+
+}
+
+Mock::generate('DebugPanel');
+
+/**
+* DebugToolbar Test case
+*/
+class DebugToolbarTestCase extends CakeTestCase {
+
+ function setUp() {
+ Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
+ Router::parse('/');
+ $this->Controller =& ClassRegistry::init('Controller');
+ $this->Controller->Component =& ClassRegistry::init('Component');
+ $this->Controller->Toolbar =& ClassRegistry::init('TestToolBarComponent', 'Component');
+ }
+
+/**
+ * test Loading of panel classes
+ *
+ * @return void
+ **/
+ function testLoadPanels() {
+ $this->Controller->Toolbar->loadPanels(array('session', 'request'));
+ $this->assertTrue(is_a($this->Controller->Toolbar->panels['session'], 'SessionPanel'));
+ $this->assertTrue(is_a($this->Controller->Toolbar->panels['request'], 'RequestPanel'));
+
+ $this->expectError();
+ $this->Controller->Toolbar->loadPanels(array('randomNonExisting', 'request'));
+ }
+
+/**
+ * test loading of vendor panels from test_app folder
+ *
+ * @access public
+ * @return void
+ */
+ function testVendorPanels() {
+ $_back = Configure::read('vendorPaths');
+ Configure::write('vendorPaths', array(APP . 'plugins' . DS . 'debug_kit' . DS . 'tests' . DS . 'test_app' . DS . 'vendors' . DS));
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'panels' => array('test'),
+ )
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->assertTrue(isset($this->Controller->Toolbar->panels['test']));
+ $this->assertTrue(is_a($this->Controller->Toolbar->panels['test'], 'TestPanel'));
+
+ Configure::write('vendorPaths', $_back);
+ }
+
+/**
+ * test initialize
+ *
+ * @return void
+ * @access public
+ **/
+ function testInitialize() {
+ $this->Controller->components = array('DebugKit.Toolbar');
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+
+ $this->assertFalse(empty($this->Controller->Toolbar->panels));
+
+ $timers = DebugKitDebugger::getTimers();
+ $this->assertTrue(isset($timers['componentInit']));
+ }
+
+/**
+ * test startup
+ *
+ * @return void
+ **/
+ function testStartup() {
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'panels' => array('MockDebug')
+ )
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Toolbar->panels['MockDebug']->expectOnce('startup');
+ $this->Controller->Toolbar->startup($this->Controller);
+
+ $this->assertEqual(count($this->Controller->Toolbar->panels), 1);
+ $this->assertTrue(isset($this->Controller->helpers['DebugKit.Toolbar']));
+ $this->assertEqual($this->Controller->helpers['DebugKit.Toolbar'], array('output' => 'DebugKit.HtmlToolbar'));
+
+ $timers = DebugKitDebugger::getTimers();
+ $this->assertTrue(isset($timers['controllerAction']));
+ }
+
+/**
+ * Test Before Render callback
+ *
+ * @return void
+ **/
+ function testBeforeRender() {
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'panels' => array('MockDebug', 'session')
+ )
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Toolbar->panels['MockDebug']->expectOnce('beforeRender');
+ $this->Controller->Toolbar->beforeRender($this->Controller);
+
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarPanels']));
+ $vars = $this->Controller->viewVars['debugToolbarPanels'];
+
+ $expected = array(
+ 'plugin' => 'debug_kit',
+ 'elementName' => 'session_panel',
+ 'content' => $this->Controller->Session->read(),
+ 'disableTimer' => true,
+ );
+ $this->assertEqual($expected, $vars['session']);
+ }
+
+/**
+ * test alternate javascript library use
+ *
+ * @return void
+ **/
+ function testAlternateJavascript() {
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar'
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => '/debug_kit/js/js_debug_toolbar',
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => 'jquery',
+ ),
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => '/debug_kit/js/jquery_debug_toolbar.js',
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => false
+ )
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array();
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => array('my_library'),
+ ),
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => 'my_library_debug_toolbar'
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => array('/my/path/to/file')
+ ),
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => '/my/path/to/file',
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => '/js/custom_behavior',
+ ),
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => '/js/custom_behavior',
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+ }
+/**
+ * Test alternate javascript existing in the plugin.
+ *
+ * @return void
+ **/
+ function testExistingAlterateJavascript() {
+ $filename = APP . 'plugins' . DS . 'debug_kit' . DS . 'vendors' . DS . 'js' . DS . 'test_alternate_debug_toolbar.js';
+ $this->skipIf(!is_writable(dirname($filename)), 'Skipping existing javascript test, debug_kit/vendors/js must be writable');
+
+ @touch($filename);
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'javascript' => 'test_alternate',
+ ),
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $this->assertTrue(isset($this->Controller->viewVars['debugToolbarJavascript']));
+ $expected = array(
+ 'behavior' => '/debug_kit/js/test_alternate_debug_toolbar.js',
+ );
+ $this->assertEqual($this->Controller->viewVars['debugToolbarJavascript'], $expected);
+ @unlink($filename);
+ }
+/**
+ * test the Log panel log reading.
+ *
+ * @return void
+ **/
+ function testLogPanel() {
+ usleep(20);
+ $this->Controller->log('This is a log I made this request');
+ $this->Controller->log('This is the second log I made this request');
+ $this->Controller->log('This time in the debug log!', LOG_DEBUG);
+
+ $this->Controller->components = array(
+ 'DebugKit.Toolbar' => array(
+ 'panels' => array('log', 'session')
+ )
+ );
+ $this->Controller->Component->init($this->Controller);
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $result = $this->Controller->viewVars['debugToolbarPanels']['log'];
+
+ $this->assertEqual(count($result['content']), 2);
+ $this->assertEqual(count($result['content']['error.log']), 4);
+ $this->assertEqual(count($result['content']['debug.log']), 2);
+
+ $this->assertEqual(trim($result['content']['debug.log'][1]), 'Debug: This time in the debug log!');
+ $this->assertEqual(trim($result['content']['error.log'][1]), 'Error: This is a log I made this request');
+ }
+
+
+/**
+ * teardown
+ *
+ * @return void
+ **/
+ function tearDown() {
+ unset($this->Controller);
+ if (class_exists('DebugKitDebugger')) {
+ DebugKitDebugger::clearTimers();
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/tests/cases/empty b/plugins/debug_kit/tests/cases/empty
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/debug_kit/tests/cases/test_objects.php b/plugins/debug_kit/tests/cases/test_objects.php
new file mode 100644
index 0000000..d7a91dc
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/test_objects.php
@@ -0,0 +1,62 @@
+
+ * Copyright 2005-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The Open Group Test Suite License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2005-2008, Cake Software Foundation, Inc.
+ * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests
+ * @package cake.tests
+ * @subpackage cake.tests.cases.libs
+ * @since CakePHP(tm) v 1.2.0.5432
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License
+ */
+/**
+ * TestFireCake class allows for testing of FireCake
+ *
+ * @package debug_kit.tests.
+ */
+class TestFireCake extends FireCake {
+ var $sentHeaders = array();
+
+ function _sendHeader($name, $value) {
+ $_this = FireCake::getInstance();
+ $_this->sentHeaders[$name] = $value;
+ }
+/**
+ * skip client detection as headers are not being sent.
+ *
+ * @access public
+ * @return void
+ */
+ function detectClientExtension() {
+ return true;
+ }
+/**
+ * Reset the fireCake
+ *
+ * @return void
+ **/
+ function reset() {
+ $_this = FireCake::getInstance();
+ $_this->sentHeaders = array();
+ $_this->_messageIndex = 1;
+ }
+}
+
+?>
diff --git a/plugins/debug_kit/tests/cases/vendors/debug_kit_debugger.test.php b/plugins/debug_kit/tests/cases/vendors/debug_kit_debugger.test.php
new file mode 100644
index 0000000..79bbe8b
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/vendors/debug_kit_debugger.test.php
@@ -0,0 +1,157 @@
+
+ * Copyright 2005-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The Open Group Test Suite License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2005-2008, Cake Software Foundation, Inc.
+ * @link https://trac.cakephp.org/wiki/Developement/TestSuite CakePHP(tm) Tests
+ * @package cake.tests
+ * @subpackage cake.tests.cases.libs
+ * @since CakePHP(tm) v 1.2.0.5432
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/opengroup.php The Open Group Test Suite License
+ */
+App::import('Core', 'Debugger');
+App::import('Vendor', 'DebugKit.DebugKitDebugger');
+
+require_once APP . 'plugins' . DS . 'debug_kit' . DS . 'tests' . DS . 'cases' . DS . 'test_objects.php';
+
+/**
+ * Short description for class.
+ *
+ * @package cake.tests
+ * @subpackage cake.tests.cases.libs
+ */
+class DebugKitDebuggerTest extends CakeTestCase {
+/**
+ * setUp method
+ *
+ * @access public
+ * @return void
+ */
+ function setUp() {
+ Configure::write('log', false);
+ if (!defined('SIMPLETESTVENDORPATH')) {
+ if (file_exists(APP . DS . 'vendors' . DS . 'simpletest' . DS . 'reporter.php')) {
+ define('SIMPLETESTVENDORPATH', 'APP' . DS . 'vendors');
+ } else {
+ define('SIMPLETESTVENDORPATH', 'CORE' . DS . 'vendors');
+ }
+ }
+ }
+
+/**
+ * Start Timer test
+ *
+ * @return void
+ **/
+ function testTimers() {
+ $this->assertTrue(DebugKitDebugger::startTimer('test1', 'this is my first test'));
+ usleep(5000);
+ $this->assertTrue(DebugKitDebugger::stopTimer('test1'));
+ $elapsed = DebugKitDebugger::elapsedTime('test1');
+ $this->assertTrue($elapsed > 0.0050);
+
+ $this->assertTrue(DebugKitDebugger::startTimer('test2', 'this is my second test'));
+ sleep(1);
+ $this->assertTrue(DebugKitDebugger::stopTimer('test2'));
+ $elapsed = DebugKitDebugger::elapsedTime('test2');
+ $this->assertTrue($elapsed > 1);
+
+ DebugKitDebugger::startTimer('test3');
+ $this->assertFalse(DebugKitDebugger::elapsedTime('test3'));
+ $this->assertFalse(DebugKitDebugger::stopTimer('wrong'));
+ }
+
+/**
+ * testRequestTime
+ *
+ * @access public
+ * @return void
+ */
+ function testRequestTime() {
+ $result1 = DebugKitDebugger::requestTime();
+ usleep(50);
+ $result2 = DebugKitDebugger::requestTime();
+ $this->assertTrue($result1 < $result2);
+ }
+
+/**
+ * test getting all the set timers.
+ *
+ * @return void
+ **/
+ function testGetTimers() {
+ DebugKitDebugger::clearTimers();
+ DebugKitDebugger::startTimer('test1', 'this is my first test');
+ DebugKitDebugger::stopTimer('test1');
+ usleep(50);
+ DebugKitDebugger::startTimer('test2');
+ DebugKitDebugger::stopTimer('test2');
+ $timers = DebugKitDebugger::getTimers();
+
+ $this->assertEqual(count($timers), 2);
+ $this->assertTrue(is_float($timers['test1']['time']));
+ $this->assertTrue(isset($timers['test1']['message']));
+ $this->assertTrue(isset($timers['test2']['message']));
+ }
+
+/**
+ * test memory usage
+ *
+ * @return void
+ **/
+ function testMemoryUsage() {
+ $result = DebugKitDebugger::getMemoryUse();
+ $this->assertTrue(is_int($result));
+
+ $result = DebugKitDebugger::getPeakMemoryUse();
+ $this->assertTrue(is_int($result));
+ }
+/**
+ * test _output switch to firePHP
+ *
+ * @return void
+ */
+ function testOutput() {
+ $firecake =& FireCake::getInstance('TestFireCake');
+ Debugger::invoke(DebugKitDebugger::getInstance('DebugKitDebugger'));
+ Debugger::output('fb');
+ $foo .= '';
+ $result = $firecake->sentHeaders;
+
+ $this->assertPattern('/GROUP_START/', $result['X-Wf-1-1-1-1']);
+ $this->assertPattern('/ERROR/', $result['X-Wf-1-1-1-2']);
+ $this->assertPattern('/GROUP_END/', $result['X-Wf-1-1-1-5']);
+
+ Debugger::invoke(Debugger::getInstance('Debugger'));
+ Debugger::output();
+ }
+
+/**
+ * tearDown method
+ *
+ * @access public
+ * @return void
+ */
+ function tearDown() {
+ Configure::write('log', true);
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/tests/cases/vendors/fire_cake.test.php b/plugins/debug_kit/tests/cases/vendors/fire_cake.test.php
new file mode 100644
index 0000000..513fb96
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/vendors/fire_cake.test.php
@@ -0,0 +1,336 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package debug_kit
+ * @subpackage cake.debug_kit.tests
+ * @since CakePHP v 1.2.0.4487
+ * @version
+ * @modifiedby
+ * @lastmodified
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+App::import('Vendor', 'DebugKit.FireCake');
+
+require_once APP . 'plugins' . DS . 'debug_kit' . DS . 'tests' . DS . 'cases' . DS . 'test_objects.php';
+/**
+ * Test Case For FireCake
+ *
+ * @package debug_kit.tests
+ */
+class FireCakeTestCase extends CakeTestCase {
+/**
+ * setup test
+ *
+ * Fill FireCake with TestFireCake instance.
+ *
+ * @access public
+ * @return void
+ */
+ function setUp() {
+ $this->firecake =& FireCake::getInstance('TestFireCake');
+ }
+/**
+ * test getInstance cheat.
+ *
+ * If this fails the rest of the test is going to fail too.
+ *
+ * @return void
+ **/
+ function testGetInstanceOverride() {
+ $instance =& FireCake::getInstance();
+ $instance2 =& FireCake::getInstance();
+ $this->assertReference($instance, $instance2);
+ $this->assertIsA($instance, 'FireCake');
+ $this->assertIsA($instance, 'TestFireCake', 'Stored instance is not a copy of TestFireCake, test case is broken.');
+ }
+/**
+ * testsetoption
+ *
+ * @return void
+ **/
+ function testSetOptions() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ $this->assertEqual($this->firecake->options['includeLineNumbers'], false);
+ }
+/**
+ * test Log()
+ *
+ * @access public
+ * @return void
+ */
+ function testLog() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::log('Testing');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-Protocol-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Plugin-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-1']));
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 1);
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '26|[{"Type":"LOG"},"Testing"]|');
+
+ FireCake::log('Testing', 'log-info');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-2'], '45|[{"Type":"LOG","Label":"log-info"},"Testing"]|');
+ }
+/**
+ * test info()
+ *
+ * @access public
+ * @return void
+ */
+ function testInfo() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::info('I have information');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-Protocol-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Plugin-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-1']));
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 1);
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '38|[{"Type":"INFO"},"I have information"]|');
+
+ FireCake::info('I have information', 'info-label');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-2'], '59|[{"Type":"INFO","Label":"info-label"},"I have information"]|');
+ }
+/**
+ * test info()
+ *
+ * @access public
+ * @return void
+ */
+ function testWarn() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::warn('A Warning');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-Protocol-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Plugin-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-1']));
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 1);
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '29|[{"Type":"WARN"},"A Warning"]|');
+
+ FireCake::warn('A Warning', 'Bzzz');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-2'], '44|[{"Type":"WARN","Label":"Bzzz"},"A Warning"]|');
+ }
+/**
+ * test error()
+ *
+ * @access public
+ * @return void
+ **/
+ function testError() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::error('An error');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-Protocol-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Plugin-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-1']));
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 1);
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '29|[{"Type":"ERROR"},"An error"]|');
+
+ FireCake::error('An error', 'wonky');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-2'], '45|[{"Type":"ERROR","Label":"wonky"},"An error"]|');
+ }
+/**
+ * test dump()
+ *
+ * @return void
+ **/
+ function testDump() {
+ FireCake::dump('mydump', array('one' => 1, 'two' => 2));
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-2-1-1'], '28|{"mydump":{"one":1,"two":2}}|');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-2']));
+ }
+/**
+ * test table() generation
+ *
+ * @return void
+ **/
+ function testTable() {
+ $table[] = array('Col 1 Heading','Col 2 Heading');
+ $table[] = array('Row 1 Col 1','Row 1 Col 2');
+ $table[] = array('Row 2 Col 1','Row 2 Col 2');
+ $table[] = array('Row 3 Col 1','Row 3 Col 2');
+ FireCake::table('myTrace', $table);
+ $expected = '162|[{"Type":"TABLE","Label":"myTrace"},[["Col 1 Heading","Col 2 Heading"],["Row 1 Col 1","Row 1 Col 2"],["Row 2 Col 1","Row 2 Col 2"],["Row 3 Col 1","Row 3 Col 2"]]]|';
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], $expected);
+ }
+/**
+ * testStringEncoding
+ *
+ * @return void
+ **/
+ function testStringEncode() {
+ $result = $this->firecake->stringEncode(array(1,2,3));
+ $this->assertEqual($result, array(1,2,3));
+
+ $this->firecake->setOptions(array('maxArrayDepth' => 3));
+ $deep = array(1 => array(2 => array(3)));
+ $result = $this->firecake->stringEncode($deep);
+ $this->assertEqual($result, array(1 => array(2 => '** Max Array Depth (3) **')));
+
+ $obj =& FireCake::getInstance();
+ $result = $this->firecake->stringEncode($obj);
+ $this->assertTrue(is_array($result));
+ $this->assertEqual($result['_defaultOptions']['useNativeJsonEncode'], true);
+ $this->assertEqual($result['_log'], null);
+ $this->assertEqual($result['_encodedObjects'][0], '** Recursion (TestFireCake) **');
+ }
+/**
+ * test trace()
+ *
+ * @return void
+ **/
+ function testTrace() {
+ FireCake::trace('myTrace');
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-Protocol-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Plugin-1']));
+ $this->assertTrue(isset($this->firecake->sentHeaders['X-Wf-1-Structure-1']));
+ $dump = $this->firecake->sentHeaders['X-Wf-1-1-1-1'];
+ $this->assertPattern('/"Message":"myTrace"/', $dump);
+ $this->assertPattern('/"Trace":\[/', $dump);
+ }
+/**
+ * test enabling and disabling of FireCake output
+ *
+ * @return void
+ **/
+ function testEnableDisable() {
+ FireCake::disable();
+ FireCake::trace('myTrace');
+ $this->assertTrue(empty($this->firecake->sentHeaders));
+
+ FireCake::enable();
+ FireCake::trace('myTrace');
+ $this->assertFalse(empty($this->firecake->sentHeaders));
+ }
+/**
+ * test correct line continuation markers on multi line headers.
+ *
+ * @access public
+ * @return void
+ */
+ function testMultiLineOutput() {
+ FireCake::trace('myTrace');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 3);
+ $header = $this->firecake->sentHeaders['X-Wf-1-1-1-1'];
+ $this->assertEqual(substr($header, -2), '|\\');
+
+ $header = $this->firecake->sentHeaders['X-Wf-1-1-1-2'];
+ $this->assertEqual(substr($header, -2), '|\\');
+
+ $header = $this->firecake->sentHeaders['X-Wf-1-1-1-3'];
+ $this->assertEqual(substr($header, -1), '|');
+ }
+
+/**
+ * test inclusion of line numbers
+ *
+ * @return void
+ **/
+ function testIncludeLineNumbers() {
+ FireCake::setOptions(array('includeLineNumbers' => true));
+ FireCake::info('Testing');
+ $result = $this->firecake->sentHeaders['X-Wf-1-1-1-1'];
+ $this->assertPattern('/"File"\:"APP.*fire_cake.test.php/', $result);
+ $this->assertPattern('/"Line"\:\d+/', $result);
+ }
+/**
+ * test Group messages
+ *
+ * @return void
+ **/
+ function testGroup() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::group('test');
+ FireCake::info('my info');
+ FireCake::groupEnd();
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '44|[{"Type":"GROUP_START","Label":"test"},null]|');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-3'], '27|[{"Type":"GROUP_END"},null]|');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 3);
+ }
+/**
+ * test fb() parameter parsing
+ *
+ * @return void
+ **/
+ function testFbParameterParsing() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::fb('Test');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '23|[{"Type":"LOG"},"Test"]|');
+
+ FireCake::fb('Test', 'warn');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-2'], '24|[{"Type":"WARN"},"Test"]|');
+
+ FireCake::fb('Test', 'Custom label', 'warn');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-3'], '47|[{"Type":"WARN","Label":"Custom label"},"Test"]|');
+
+ $this->expectError();
+ $this->assertFalse(FireCake::fb('Test', 'Custom label', 'warn', 'more parameters'));
+
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-Index'], 3);
+ }
+/**
+ * Test defaulting to log if incorrect message type is used
+ *
+ * @return void
+ **/
+ function testIncorrectMessageType() {
+ FireCake::setOptions(array('includeLineNumbers' => false));
+ FireCake::fb('Hello World', 'foobared');
+ $this->assertEqual($this->firecake->sentHeaders['X-Wf-1-1-1-1'], '30|[{"Type":"LOG"},"Hello World"]|');
+ }
+/**
+ * testClientExtensionDetection.
+ *
+ * @return void
+ **/
+ function testDetectClientExtension() {
+ $back = env('HTTP_USER_AGENT');
+ $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4 FirePHP/0.2.1';
+ $this->assertTrue(FireCake::detectClientExtension());
+
+ $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4 FirePHP/0.0.4';
+ $this->assertFalse(FireCake::detectClientExtension());
+
+ $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4';
+ $this->assertFalse(FireCake::detectClientExtension());
+ $_SERVER['HTTP_USER_AGENT'] = $back;
+ }
+/**
+ * test of Non Native JSON encoding.
+ *
+ * @return void
+ **/
+ function testNonNativeEncoding() {
+ FireCake::setOptions(array('useNativeJsonEncode' => false));
+ $json = FireCake::jsonEncode(array('one' => 1, 'two' => 2));
+ $this->assertEqual($json, '{"one":1,"two":2}');
+
+ $json = FireCake::jsonEncode(array(1,2,3));
+ $this->assertEqual($json, '[1,2,3]');
+
+ $json = FireCake::jsonEncode(FireCake::getInstance());
+ $this->assertPattern('/"options"\:\{"maxObjectDepth"\:\d*,/', $json);
+ }
+/**
+ * reset the FireCake counters and headers.
+ *
+ * @access public
+ * @return void
+ */
+ function tearDown() {
+ TestFireCake::reset();
+ }
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/tests/cases/views/debug.test.php b/plugins/debug_kit/tests/cases/views/debug.test.php
new file mode 100644
index 0000000..916a67d
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/views/debug.test.php
@@ -0,0 +1,144 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage cake.cake.libs.
+ * @since CakePHP v 1.2.0.4487
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+App::import('Core', 'View');
+
+if (!class_exists('DoppelGangerView')) {
+ class DoppelGangerView extends View {}
+}
+
+App::import('View', 'DebugKit.Debug');
+App::import('Vendor', 'DebugKit.DebugKitDebugger');
+/**
+ * Debug View Test Case
+ *
+ * @package debug_kit.tests
+ */
+class DebugViewTestCase extends CakeTestCase {
+/**
+ * set Up test case
+ *
+ * @return void
+ **/
+ function setUp() {
+ Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
+ Router::parse('/');
+ $this->Controller =& ClassRegistry::init('Controller');
+ $this->View =& new DebugView($this->Controller, false);
+ $this->_debug = Configure::read('debug');
+ }
+
+/**
+ * start Case - switch view paths
+ *
+ * @return void
+ **/
+ function startCase() {
+ $this->_viewPaths = Configure::read('viewPaths');
+ Configure::write('viewPaths', array(
+ TEST_CAKE_CORE_INCLUDE_PATH . 'tests' . DS . 'test_app' . DS . 'views'. DS,
+ APP . 'plugins' . DS . 'debug_kit' . DS . 'views'. DS,
+ ROOT . DS . LIBS . 'view' . DS
+ ));
+ }
+
+/**
+ * test that element timers are working
+ *
+ * @return void
+ **/
+ function testElementTimers() {
+ $result = $this->View->element('test_element');
+ $this->assertPattern('/^this is the test element$/', $result);
+
+ $result = DebugKitDebugger::getTimers();
+ $this->assertTrue(isset($result['render_test_element.ctp']));
+ }
+
+/**
+ * test rendering and ensure that timers are being set.
+ *
+ * @access public
+ * @return void
+ */
+ function testRenderTimers() {
+ $this->Controller->viewPath = 'posts';
+ $this->Controller->action = 'index';
+ $this->Controller->params = array(
+ 'action' => 'index',
+ 'controller' => 'posts',
+ 'plugin' => null,
+ 'url' => array('url' => 'posts/index'),
+ 'base' => null,
+ 'here' => '/posts/index',
+ );
+ $this->Controller->layout = 'default';
+ $View =& new DebugView($this->Controller, false);
+ $View->render('index');
+
+ $result = DebugKitDebugger::getTimers();
+ $this->assertEqual(count($result), 3);
+ $this->assertTrue(isset($result['viewRender']));
+ $this->assertTrue(isset($result['render_default.ctp']));
+ $this->assertTrue(isset($result['render_index.ctp']));
+ }
+
+/**
+ * Test for correct loading of helpers into custom view
+ *
+ * @return void
+ */
+ function testLoadHelpers() {
+ $loaded = array();
+ $result = $this->View->_loadHelpers($loaded, array('Html', 'Javascript', 'Number'));
+ $this->assertTrue(is_object($result['Html']));
+ $this->assertTrue(is_object($result['Javascript']));
+ $this->assertTrue(is_object($result['Number']));
+ }
+
+/**
+ * reset the view paths
+ *
+ * @return void
+ **/
+ function endCase() {
+ Configure::write('viewPaths', $this->_viewPaths);
+ }
+
+/**
+ * tear down function
+ *
+ * @return void
+ **/
+ function tearDown() {
+ unset($this->View, $this->Controller);
+ DebugKitDebugger::clearTimers();
+ Configure::write('debug', $this->_debug);
+ }
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/tests/cases/views/helpers/fire_php_toobar.test.php b/plugins/debug_kit/tests/cases/views/helpers/fire_php_toobar.test.php
new file mode 100644
index 0000000..60206dc
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/views/helpers/fire_php_toobar.test.php
@@ -0,0 +1,137 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage debug_kit.tests.views.helpers
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+App::import('Helper', 'DebugKit.FirePhpToolbar');
+App::import('Core', array('View', 'Controller'));
+require_once APP . 'plugins' . DS . 'debug_kit' . DS . 'tests' . DS . 'cases' . DS . 'test_objects.php';
+
+FireCake::getInstance('TestFireCake');
+
+class FirePhpToolbarHelperTestCase extends CakeTestCase {
+/**
+ * setUp
+ *
+ * @return void
+ **/
+ function setUp() {
+ Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
+ Router::parse('/');
+
+ $this->Toolbar =& new ToolbarHelper(array('output' => 'DebugKit.FirePhpToolbar'));
+ $this->Toolbar->FirePhpToolbar =& new FirePhpToolbarHelper();
+
+ $this->Controller =& ClassRegistry::init('Controller');
+ if (isset($this->_debug)) {
+ Configure::write('debug', $this->_debug);
+ }
+ }
+/**
+ * start Case - switch view paths
+ *
+ * @return void
+ **/
+ function startCase() {
+ $this->_viewPaths = Configure::read('viewPaths');
+ Configure::write('viewPaths', array(
+ TEST_CAKE_CORE_INCLUDE_PATH . 'tests' . DS . 'test_app' . DS . 'views'. DS,
+ APP . 'plugins' . DS . 'debug_kit' . DS . 'views'. DS,
+ ROOT . DS . LIBS . 'view' . DS
+ ));
+ $this->_debug = Configure::read('debug');
+ $this->firecake =& FireCake::getInstance();
+ }
+/**
+ * test neat array (dump)creation
+ *
+ * @return void
+ */
+ function testMakeNeatArray() {
+ $this->Toolbar->makeNeatArray(array(1,2,3));
+ $result = $this->firecake->sentHeaders;
+ $this->assertTrue(isset($result['X-Wf-1-1-1-1']));
+ $this->assertPattern('/\[1,2,3\]/', $result['X-Wf-1-1-1-1']);
+ }
+/**
+ * testAfterlayout element rendering
+ *
+ * @return void
+ */
+ function testAfterLayout(){
+ $this->Controller->viewPath = 'posts';
+ $this->Controller->action = 'index';
+ $this->Controller->params = array(
+ 'action' => 'index',
+ 'controller' => 'posts',
+ 'plugin' => null,
+ 'url' => array('url' => 'posts/index', 'ext' => 'xml'),
+ 'base' => null,
+ 'here' => '/posts/index',
+ );
+ $this->Controller->layout = 'default';
+ $this->Controller->uses = null;
+ $this->Controller->components = array('DebugKit.Toolbar');
+ $this->Controller->constructClasses();
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $result = $this->Controller->render();
+ $this->assertNoPattern('/debug-toolbar/', $result);
+ $result = $this->firecake->sentHeaders;
+ $this->assertTrue(is_array($result));
+
+ }
+/**
+ * endTest()
+ *
+ * @return void
+ */
+ function endTest() {
+ TestFireCake::reset();
+ }
+/**
+ * reset the view paths
+ *
+ * @return void
+ **/
+ function endCase() {
+ Configure::write('viewPaths', $this->_viewPaths);
+ }
+
+/**
+ * tearDown
+ *
+ * @access public
+ * @return void
+ */
+ function tearDown() {
+ unset($this->Toolbar, $this->Controller);
+ ClassRegistry::removeObject('view');
+ ClassRegistry::flush();
+ Router::reload();
+ }
+}
+?>
\ No newline at end of file
diff --git a/plugins/debug_kit/tests/cases/views/helpers/html_toolbar.test.php b/plugins/debug_kit/tests/cases/views/helpers/html_toolbar.test.php
new file mode 100644
index 0000000..d924c9e
--- /dev/null
+++ b/plugins/debug_kit/tests/cases/views/helpers/html_toolbar.test.php
@@ -0,0 +1,358 @@
+
+ * Copyright 2006-2008, Cake Software Foundation, Inc.
+ * 1785 E. Sahara Avenue, Suite 490-204
+ * Las Vegas, Nevada 89104
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @filesource
+ * @copyright Copyright 2006-2008, Cake Software Foundation, Inc.
+ * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
+ * @package cake
+ * @subpackage debug_kit.tests.views.helpers
+ * @version $Revision$
+ * @modifiedby $LastChangedBy$
+ * @lastmodified $Date$
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ */
+App::import('Helper', array('DebugKit.HtmlToolbar', 'Html', 'Javascript'));
+App::import('Core', array('View', 'Controller'));
+
+class HtmlToolbarHelperTestCase extends CakeTestCase {
+/**
+ * setUp
+ *
+ * @return void
+ **/
+ function setUp() {
+ Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
+ Router::parse('/');
+
+ $this->Toolbar =& new ToolbarHelper(array('output' => 'DebugKit.HtmlToolbar'));
+ $this->Toolbar->HtmlToolbar =& new HtmlToolbarHelper();
+ $this->Toolbar->HtmlToolbar->Html =& new HtmlHelper();
+ $this->Toolbar->HtmlToolbar->Javascript =& new JavascriptHelper();
+
+ $this->Controller =& ClassRegistry::init('Controller');
+ if (isset($this->_debug)) {
+ Configure::write('debug', $this->_debug);
+ }
+ }
+
+/**
+ * start Case - switch view paths
+ *
+ * @return void
+ **/
+ function startCase() {
+ $this->_viewPaths = Configure::read('viewPaths');
+ Configure::write('viewPaths', array(
+ TEST_CAKE_CORE_INCLUDE_PATH . 'tests' . DS . 'test_app' . DS . 'views'. DS,
+ APP . 'plugins' . DS . 'debug_kit' . DS . 'views'. DS,
+ ROOT . DS . LIBS . 'view' . DS
+ ));
+ $this->_debug = Configure::read('debug');
+ }
+
+/**
+ * test Neat Array formatting
+ *
+ * @return void
+ **/
+ function testMakeNeatArray() {
+ $in = false;
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = null;
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = true;
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = array('key' => 'value');
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = array('key' => null);
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = array('key' => 'value', 'foo' => 'bar');
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+
+ $in = array(
+ 'key' => 'value',
+ 'foo' => array(
+ 'this' => 'deep',
+ 'another' => 'value'
+ )
+ );
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ ' array('class' => 'neat-array depth-1')),
+ 'assertTags($result, $expected);
+
+ $in = array(
+ 'key' => 'value',
+ 'foo' => array(
+ 'this' => 'deep',
+ 'another' => 'value'
+ ),
+ 'lotr' => array(
+ 'gandalf' => 'wizard',
+ 'bilbo' => 'hobbit'
+ )
+ );
+ $result = $this->Toolbar->makeNeatArray($in, 1);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0 expanded'),
+ ' array('class' => 'neat-array depth-1')),
+ ' array('class' => 'neat-array depth-1')),
+ 'assertTags($result, $expected);
+
+ $result = $this->Toolbar->makeNeatArray($in, 2);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0 expanded'),
+ ' array('class' => 'neat-array depth-1 expanded')),
+ ' array('class' => 'neat-array depth-1 expanded')),
+ 'assertTags($result, $expected);
+
+ $in = array('key' => 'value', 'array' => array());
+ $result = $this->Toolbar->makeNeatArray($in);
+ $expected = array(
+ 'ul' => array('class' => 'neat-array depth-0'),
+ 'assertTags($result, $expected);
+ }
+
+/**
+ * Test injection of toolbar
+ *
+ * @return void
+ **/
+ function testInjectToolbar() {
+ $this->Controller->viewPath = 'posts';
+ $this->Controller->action = 'index';
+ $this->Controller->params = array(
+ 'action' => 'index',
+ 'controller' => 'posts',
+ 'plugin' => null,
+ 'url' => array('url' => 'posts/index'),
+ 'base' => null,
+ 'here' => '/posts/index',
+ );
+ $this->Controller->helpers = array('Html', 'Javascript', 'DebugKit.Toolbar');
+ $this->Controller->layout = 'default';
+ $this->Controller->uses = null;
+ $this->Controller->components = array('DebugKit.Toolbar');
+ $this->Controller->constructClasses();
+ $this->Controller->Component->initialize($this->Controller);
+ $this->Controller->Component->startup($this->Controller);
+ $this->Controller->Component->beforeRender($this->Controller);
+ $result = $this->Controller->render();
+ $result = str_replace(array("\n", "\r"), '', $result);
+ $this->assertPattern('#.+