Commit 66d6a417 authored by Mal's avatar Mal

New Invoice module to email out invoices for the week.

Added a method to Banking module to return all settings.
Payment module now calculates the surcharge.
Purchase module has new methods required by Invoice module.
Grid module now supports some selection plugins.
parent 55f685fc
......@@ -150,7 +150,7 @@ class Banking extends Base {
}
public function Factory($fn) {
if ($fn == "AllSettings") return $this->AllSettings();
}
public function Group() {
......@@ -163,7 +163,7 @@ class Banking extends Base {
public function Install($path) {
// Append dobrado.banking.js to the existing dobrado.js file.
// Note that updating the module is only available when logged in.
// Note that the module is only available when logged in.
$this->AppendScript($path, "dobrado.banking.js", false);
$mysqli = connect_db();
$query = 'CREATE TABLE IF NOT EXISTS banking ('.
......@@ -226,6 +226,28 @@ class Banking extends Base {
// Private functions below here ////////////////////////////////////////////
private function AllSettings() {
$settings = array();
$mysqli = connect_db();
$query = 'SELECT user, name, number, bsb, credit, surcharge FROM banking';
if ($result = $mysqli->query($query)) {
while ($banking = $result->fetch_assoc()) {
$settings[$banking["user"]] =
array("name" => $banking["name"],
"number" => $banking["number"],
"bsb" => $banking["bsb"],
"credit" => (int)$banking["credit"],
"surcharge" => (int)$banking["surcharge"]);
}
$result->close();
}
else {
$this->Log('Banking->AllSettings: '.$mysqli->error);
}
$mysqli->close();
return $settings;
}
}
?>
\ No newline at end of file
This diff is collapsed.
......@@ -206,6 +206,10 @@ class Payment extends Base {
$end = $fn[2];
return $this->Data($start, $end);
}
if ($name == "Surcharge" && count($fn) == 2) {
$purchases = $fn[1];
return $this->Surcharge($purchases);
}
return;
}
if ($fn == "LevelSettings") return $this->LevelSettings();
......@@ -456,6 +460,29 @@ class Payment extends Base {
$mysqli->close();
}
private function Surcharge($purchases) {
$mysqli = connect_db();
$surcharge = 0;
$query = 'SELECT surcharge FROM payment_settings';
if ($result = $mysqli->query($query)) {
if ($payment_settings = $result->fetch_assoc()) {
$surcharge = $payment_settings["surcharge"];
}
$result->close();
}
else {
$this->Log('Payment->Surcharge: '.$mysqli->error);
}
$mysqli->close();
// If the surcharge is not zero then it's a flat rate.
if ($surcharge != 0) return $surcharge;
if ($purchases < 5) return 1;
if ($purchases < 10) return $purchases * 0.2;
if ($purchases < 30) return (($purchases - 10) * 0.1) + 2;
return 4;
}
}
?>
\ No newline at end of file
......@@ -51,16 +51,18 @@ class Purchase extends Base {
$payment = new Module($this->user, $this->owner, "payment",
$this->config);
// Get lists of payment totals for all users, so that they can be
// Get a list of payment totals for all users, so that they can be
// compared to purchase totals to find outstanding debts.
$payment_totals = $payment->Factory("AllTotals");
// Also need to get the current values for the 'info' and 'warning'
// debt levels to alert the user if there is a problem.
list($object["info"], $object["warning"]) =
$payment->Factory("LevelSettings");
// Create a list of how much each user currently owes.
// Create a list of how much each user currently owes. Recent purchases
// are not included to give the user a chance to pay their account.
// Cast to object required for json in case of an empty array.
$object["outstanding"] = (object)$this->AllOutstanding($payment_totals);
$object["outstanding"] =
(object)$this->AllOutstanding($payment_totals, strtotime("-24 hours"));
// TODO: This assumes that any purchases entered in the last 6 days
// should be shown, ie don't want to show purchases from last week,
......@@ -237,15 +239,46 @@ class Purchase extends Base {
$end = $fn[2];
return $this->Data($start, $end);
}
if ($name == "Data" && count($fn) == 4) {
$start = $fn[1];
$end = $fn[2];
$user = $fn[3];
return $this->Data($start, $end, $user);
}
if ($name == "Sold" && count($fn) == 3) {
$start = $fn[1];
$end = $fn[2];
return $this->Sold($start, $end);
}
if ($name == "Sold" && count($fn) == 4) {
$start = $fn[1];
$end = $fn[2];
$user = $fn[3];
return $this->Sold($start, $end, $user);
}
if ($name == "AllSold" && count($fn) == 3) {
$start = $fn[1];
$end = $fn[2];
return $this->AllSold($start, $end);
}
if ($name == "AllTotals" && count($fn) == 3) {
$start = $fn[1];
$end = $fn[2];
return $this->AllTotals($start, $end);
}
if ($name == "AllSurcharge" && count($fn) == 3) {
$start = $fn[1];
$end = $fn[2];
return $this->AllSurcharge($start, $end);
}
if ($name == "AllOutstanding" && count($fn) == 3) {
$payment_totals = $fn[1];
$timestamp = $fn[2];
return $this->AllOutstanding($payment_totals, $timestamp);
}
return;
}
if ($fn == "Total") return $this->Total();
if ($fn == "AllTotals") return $this->AllTotals();
if ($fn == "NewUser") return $this->NewUser();
}
......@@ -346,12 +379,15 @@ class Purchase extends Base {
// Private functions below here ////////////////////////////////////////////
private function Data($start, $end) {
private function Data($start, $end, $user = "") {
$object = array();
if ($user === "") {
$user = $this->user->name;
}
$mysqli = connect_db();
$query = 'SELECT timestamp, name, quantity, price FROM purchase WHERE '.
'timestamp >='.$start.' AND timestamp <='.$end.' AND user="'.
$this->user->name.'" ORDER BY timestamp, name';
'timestamp >='.$start.' AND timestamp <='.$end.' AND user="'.$user.
'" ORDER BY timestamp, name';
if ($result = $mysqli->query($query)) {
while ($purchase = $result->fetch_assoc()) {
$quantity = (float)$purchase["quantity"];
......@@ -375,12 +411,17 @@ class Purchase extends Base {
return $object;
}
private function Sold($start, $end) {
private function Sold($start, $end, $user = "") {
$object = array();
$mysqli = connect_db();
// Get a list of the stock items this user supplies.
$stock = new Module($this->user, $this->owner, "stock", $this->config);
$supplies = $stock->Factory("Supplier");
if ($user === "") {
$supplies = $stock->Factory("Supplier");
}
else {
$supplies = $stock->Factory(array("Supplier", $user));
}
if (count($supplies) == 0) return $object;
$supply_query = "";
......@@ -461,6 +502,16 @@ class Purchase extends Base {
return $object;
}
private function AllSold($start, $end) {
$stock = new Module($this->user, $this->owner, "stock", $this->config);
$all_supplies = $stock->Factory("AllSuppliers");
$sold = array();
foreach ($all_supplies as $user => $supplies) {
$sold[$user] = $this->SupplyTotal($supplies, $start, $end);
}
return $sold;
}
private function UpdateData($user, $us_values, $current) {
$mysqli = connect_db();
$values = "";
......@@ -536,7 +587,7 @@ class Purchase extends Base {
return $total - $future - $this->SupplyTotal($supplies);
}
private function SupplyTotal($supplies) {
private function SupplyTotal($supplies, $start="", $end="") {
if (count($supplies) == 0) return 0;
$supply_total = 0;
......@@ -548,8 +599,16 @@ class Purchase extends Base {
$supply_query .= 'name="'.$supplies[$i].'"';
}
$mysqli = connect_db();
$query = 'SELECT SUM(quantity*price) AS total FROM purchase WHERE '.
'timestamp < '.time().' AND ('.$supply_query.')';
if ($start === "" && $end === "") {
$query = 'SELECT SUM(quantity*price) AS total FROM purchase WHERE '.
'timestamp < '.time().' AND ('.$supply_query.')';
}
else {
$query = 'SELECT SUM(quantity*price) as total FROM purchase WHERE '.
'timestamp >='.$start.' AND timestamp <='.$end.' AND ('.
$supply_query.')';
}
if ($result = $mysqli->query($query)) {
if ($purchase = $result->fetch_assoc()) {
$supply_total = (float)$purchase["total"];
......@@ -562,13 +621,15 @@ class Purchase extends Base {
return $supply_total;
}
private function AllTotals() {
$totals = array();
private function AllTotals($start, $end) {
$mysqli = connect_db();
$query = 'SELECT user, amount FROM purchase_totals';
$totals = array();
$query = 'SELECT user, SUM(quantity*price) as total FROM purchase '.
'WHERE timestamp >= '.$start.' AND timestamp <= '.$end.
' AND name != "surcharge" GROUP BY user';
if ($result = $mysqli->query($query)) {
while ($purchase_totals = $result->fetch_assoc()) {
$totals[$purchase_totals["user"]] = (float)$purchase_totals["amount"];
while ($purchase = $result->fetch_assoc()) {
$totals[$purchase["user"]] = (float)$purchase["total"];
}
$result->close();
}
......@@ -579,14 +640,44 @@ class Purchase extends Base {
return $totals;
}
private function AllOutstanding($payment_totals) {
private function AllSurcharge($start, $end) {
$mysqli = connect_db();
$surcharge = array();
$query = 'SELECT user, SUM(quantity*price) as total FROM purchase '.
'WHERE timestamp >= '.$start.' AND timestamp <= '.$end.
' AND name = "surcharge" GROUP BY user';
if ($result = $mysqli->query($query)) {
while ($purchase = $result->fetch_assoc()) {
$surcharge[$purchase["user"]] = (float)$purchase["total"];
}
$result->close();
}
else {
$this->Log('Purchase->AllSurcharge: '.$mysqli->error);
}
$mysqli->close();
return $surcharge;
}
private function AllOutstanding($payment_totals, $timestamp) {
$mysqli = connect_db();
// First get the array of purchase totals to compare payments to.
$purchase_totals = $this->AllTotals();
// Need to subtract purchases with future or recent timestamps from totals.
$purchase_totals = array();
$query = 'SELECT user, amount FROM purchase_totals';
if ($result = $mysqli->query($query)) {
while ($totals = $result->fetch_assoc()) {
$purchase_totals[$totals["user"]] = (float)$totals["amount"];
}
$result->close();
}
else {
$this->Log('Purchase->AllOutstanding: '.$mysqli->error);
}
// Need to subtract purchases with future timestamps from totals.
$future = array();
$mysqli = connect_db();
$query = 'SELECT user, SUM(quantity*price) as total FROM purchase '.
'WHERE timestamp > '.strtotime("-24 hours").' GROUP BY user';
'WHERE timestamp > '.$timestamp.' GROUP BY user';
if ($result = $mysqli->query($query)) {
while ($purchase = $result->fetch_assoc()) {
$future[$purchase["user"]] = (float)$purchase["total"];
......@@ -597,10 +688,10 @@ class Purchase extends Base {
$this->Log('Purchase->Outstanding: '.$mysqli->error);
}
$mysqli->close();
// Also need to get a list of suppliers so that sold goods can be deducted.
$stock = new Module($this->user, $this->owner, "stock", $this->config);
$supplies = $stock->Factory("AllSuppliers");
$outstanding = array();
foreach ($payment_totals as $user => $payments) {
$total = 0;
......
......@@ -154,6 +154,14 @@ class Stock extends Base {
}
public function Factory($fn) {
if (is_array($fn)) {
$name = $fn[0];
if ($name == "Supplier" && count($fn) == 2) {
$user = $fn[1];
return $this->Supplier($user);
}
return;
}
if ($fn == "AvailableProducts") return $this->AvailableProducts();
if ($fn == "LastUpdate") return $this->LastUpdate();
if ($fn == "AllSuppliers") return $this->AllSuppliers();
......@@ -313,10 +321,13 @@ class Stock extends Base {
return $object;
}
private function Supplier() {
private function Supplier($user = "") {
$object = array();
if ($user === "") {
$user = $this->user->name;
}
$mysqli = connect_db();
$query = 'SELECT name FROM stock WHERE user="'.$this->user->name.'"';
$query = 'SELECT name FROM stock WHERE user="'.$user.'"';
if ($result = $mysqli->query($query)) {
while ($stock = $result->fetch_assoc()) {
$object[] = $stock["name"];
......
......@@ -914,6 +914,352 @@ $special.draginit = $special.dragstart = $special.dragend = drag;
}
})(jQuery);
// File: plugins/slick.checkboxselectcolumn.js ///////////////////////////////
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"CheckboxSelectColumn": CheckboxSelectColumn
}
});
function CheckboxSelectColumn(options) {
var _grid;
var _self = this;
var _handler = new Slick.EventHandler();
var _selectedRowsLookup = {};
var _defaults = {
columnId: "_checkbox_selector",
cssClass: null,
toolTip: "Select/Deselect All",
width: 30
};
var _options = $.extend(true, {}, _defaults, options);
function init(grid) {
_grid = grid;
_handler
.subscribe(_grid.onSelectedRowsChanged, handleSelectedRowsChanged)
.subscribe(_grid.onClick, handleClick)
.subscribe(_grid.onHeaderClick, handleHeaderClick)
.subscribe(_grid.onKeyDown, handleKeyDown);
}
function destroy() {
_handler.unsubscribeAll();
}
function handleSelectedRowsChanged(e, args) {
var selectedRows = _grid.getSelectedRows();
var lookup = {}, row, i;
for (i = 0; i < selectedRows.length; i++) {
row = selectedRows[i];
lookup[row] = true;
if (lookup[row] !== _selectedRowsLookup[row]) {
_grid.invalidateRow(row);
delete _selectedRowsLookup[row];
}
}
for (i in _selectedRowsLookup) {
_grid.invalidateRow(i);
}
_selectedRowsLookup = lookup;
_grid.render();
if (selectedRows.length && selectedRows.length == _grid.getDataLength()) {
_grid.updateColumnHeader(_options.columnId, "<input type='checkbox' checked='checked'>", _options.toolTip);
} else {
_grid.updateColumnHeader(_options.columnId, "<input type='checkbox'>", _options.toolTip);
}
}
function handleKeyDown(e, args) {
if (e.which == 32) {
if (_grid.getColumns()[args.cell].id === _options.columnId) {
// if editing, try to commit
if (!_grid.getEditorLock().isActive() || _grid.getEditorLock().commitCurrentEdit()) {
toggleRowSelection(args.row);
}
e.preventDefault();
e.stopImmediatePropagation();
}
}
}
function handleClick(e, args) {
// clicking on a row select checkbox
if (_grid.getColumns()[args.cell].id === _options.columnId && $(e.target).is(":checkbox")) {
// if editing, try to commit
if (_grid.getEditorLock().isActive() && !_grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
toggleRowSelection(args.row);
e.stopPropagation();
e.stopImmediatePropagation();
}
}
function toggleRowSelection(row) {
if (_selectedRowsLookup[row]) {
_grid.setSelectedRows($.grep(_grid.getSelectedRows(), function (n) {
return n != row
}));
} else {
_grid.setSelectedRows(_grid.getSelectedRows().concat(row));
}
}
function handleHeaderClick(e, args) {
if (args.column.id == _options.columnId && $(e.target).is(":checkbox")) {
// if editing, try to commit
if (_grid.getEditorLock().isActive() && !_grid.getEditorLock().commitCurrentEdit()) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if ($(e.target).is(":checked")) {
var rows = [];
for (var i = 0; i < _grid.getDataLength(); i++) {
rows.push(i);
}
_grid.setSelectedRows(rows);
} else {
_grid.setSelectedRows([]);
}
e.stopPropagation();
e.stopImmediatePropagation();
}
}
function getColumnDefinition() {
return {
id: _options.columnId,
name: "<input type='checkbox'>",
toolTip: _options.toolTip,
field: "sel",
width: _options.width,
resizable: false,
sortable: false,
cssClass: _options.cssClass,
formatter: checkboxSelectionFormatter
};
}
function checkboxSelectionFormatter(row, cell, value, columnDef, dataContext) {
if (dataContext) {
return _selectedRowsLookup[row]
? "<input type='checkbox' checked='checked'>"
: "<input type='checkbox'>";
}
return null;
}
$.extend(this, {
"init": init,
"destroy": destroy,
"getColumnDefinition": getColumnDefinition
});
}
})(jQuery);
// File: plugins/slick.rowselectionmodel.js //////////////////////////////////
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"RowSelectionModel": RowSelectionModel
}
});
function RowSelectionModel(options) {
var _grid;
var _ranges = [];
var _self = this;
var _handler = new Slick.EventHandler();
var _inHandler;
var _options;
var _defaults = {
selectActiveRow: true
};
function init(grid) {
_options = $.extend(true, {}, _defaults, options);
_grid = grid;
_handler.subscribe(_grid.onActiveCellChanged,
wrapHandler(handleActiveCellChange));
_handler.subscribe(_grid.onKeyDown,
wrapHandler(handleKeyDown));
_handler.subscribe(_grid.onClick,
wrapHandler(handleClick));
}
function destroy() {
_handler.unsubscribeAll();
}
function wrapHandler(handler) {
return function () {
if (!_inHandler) {
_inHandler = true;
handler.apply(this, arguments);
_inHandler = false;
}
};
}
function rangesToRows(ranges) {
var rows = [];
for (var i = 0; i < ranges.length; i++) {
for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
rows.push(j);
}
}
return rows;
}
function rowsToRanges(rows) {
var ranges = [];
var lastCell = _grid.getColumns().length - 1;
for (var i = 0; i < rows.length; i++) {
ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
}
return ranges;
}
function getRowsRange(from, to) {
var i, rows = [];
for (i = from; i <= to; i++) {
rows.push(i);
}
for (i = to; i < from; i++) {
rows.push(i);
}
return rows;
}
function getSelectedRows() {
return rangesToRows(_ranges);
}
function setSelectedRows(rows) {
setSelectedRanges(rowsToRanges(rows));
}
function setSelectedRanges(ranges) {
_ranges = ranges;
_self.onSelectedRangesChanged.notify(_ranges);
}
function getSelectedRanges() {
return _ranges;
}
function handleActiveCellChange(e, data) {
if (_options.selectActiveRow) {
setSelectedRanges([new Slick.Range(data.row, 0, data.row, _grid.getColumns().length - 1)]);
}
}
function handleKeyDown(e) {
var activeRow = _grid.getActiveCell();
if (activeRow && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && (e.which == 38 || e.which == 40)) {
var selectedRows = getSelectedRows();
selectedRows.sort(function (x, y) {
return x - y
});
if (!selectedRows.length) {
selectedRows = [activeRow.row];
}
var top = selectedRows[0];
var bottom = selectedRows[selectedRows.length - 1];
var active;
if (e.which == 40) {
active = activeRow.row < bottom || top == bottom ? ++bottom : ++top;
} else {
active = activeRow.row < bottom ? --bottom : --top;
}
if (active >= 0 && active < _grid.getDataLength()) {
_grid.scrollRowIntoView(active);
_ranges = rowsToRanges(getRowsRange(top, bottom));
setSelectedRanges(_ranges);
}
e.preventDefault();
e.stopPropagation();
}
}
function handleClick(e) {
var cell = _grid.getCellFromEvent(e);
if (!cell || !_grid.canCellBeActive(cell.row, cell.cell)) {
return false;
}
var selection = rangesToRows(_ranges);
var idx = $.inArray(cell.row, selection);
if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
return false;
}
else if (_grid.getOptions().multiSelect) {
if (idx === -1 && (e.ctrlKey || e.metaKey)) {
selection.push(cell.row);
_grid.setActiveCell(cell.row, cell.cell);
} else if (idx !== -1 && (e.ctrlKey || e.metaKey)) {
selection = $.grep(selection, function (o, i) {
return (o !== cell.row);
});
_grid.setActiveCell(cell.row, cell.cell);
} else if (selection.length && e.shiftKey) {
var last = selection.pop();
var from = Math.min(cell.row, last);
var to = Math.max(cell.row, last);
selection = [];
for (var i = from; i <= to; i++) {
if (i !== last) {
selection.push(i);
}
}
selection.push(last);
_grid.setActiveCell(cell.row, cell.cell);
}
}
_ranges = rowsToRanges(selection);
setSelectedRanges(_ranges);
e.stopImmediatePropagation();
return true;
}
$.extend(this, {
"getSelectedRows": getSelectedRows,
"setSelectedRows": setSelectedRows,
"getSelectedRanges": getSelectedRanges,
"setSelectedRanges": setSelectedRanges,
"init": init,
"destroy": destroy,
"onSelectedRangesChanged": new Slick.Event()
});
}
})(jQuery);
/**
* @license
* (c) 2009-2012 Michael Leibman
......
if (!this.dobrado.invoice) {
dobrado.invoice = {};
}
(function() {
'use strict';
// This is a representation of users to send invoices to in json.