partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 269a208ce350dc71ef1bd470b7d2049cc7cb3ae8
parent 3f63042c65cb7247ba39adb4b780ec9218da037f
Author: Felicitus <felicitus@felicitus.org>
Date:   Sun,  1 Jan 2012 23:58:36 +0100

Added the stock history view

Diffstat:
Msrc/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php | 2+-
Msrc/backend/de/RaumZeitLabor/PartKeepr/Stock/StockEntry.php | 18+++++++++++++-----
Asrc/backend/de/RaumZeitLabor/PartKeepr/Stock/StockManager.php | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/de/RaumZeitLabor/PartKeepr/Stock/StockService.php | 50+++++++++++++++++++++++++-------------------------
Msrc/frontend/js/Components/MenuBar.js | 15+++++++++++++++
Msrc/frontend/js/Components/Part/PartStockHistory.js | 132+++++--------------------------------------------------------------------------
Asrc/frontend/js/Components/StockReport/AbstractStockHistoryGrid.js | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/frontend/js/Components/StockReport/StockHistoryGrid.js | 31+++++++++++++++++++++++++++++++
Msrc/frontend/js/Models/StockEntry.js | 8++++----
10 files changed, 406 insertions(+), 206 deletions(-)

diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Manager/AbstractManager.php @@ -1,9 +1,12 @@ <?php namespace de\RaumZeitLabor\PartKeepr\Manager; +use Doctrine\ORM\Query; + declare(encoding = 'UTF-8'); use de\RaumZeitLabor\PartKeepr\Util\Singleton, de\RaumZeitLabor\PartKeepr\PartKeepr, + Doctrine\ORM\QueryBuilder, de\RaumZeitLabor\PartKeepr\Manager\Exceptions\EntityInUseException; /** @@ -91,62 +94,125 @@ abstract class AbstractManager extends Singleton { public function getList (ManagerFilter $filter) { $qb = PartKeepr::getEM()->createQueryBuilder(); - $qb->select("COUNT(q.id)"); + $qb->select("COUNT(q.id)")->from($this->getEntityName(),"q"); - $qb->where("1=1"); + $this->applyFiltering($qb, $filter); + $this->applyCustomQuery($qb, $filter); - if ($filter->getFilter() !== null && $filter->getFilterField() !== null) { - $aOrWhereFields = array(); - - if (is_array($filter->getFilterField())) { - foreach ($filter->getFilterField() as $field) { - $aOrWhereFields[] = "q.".$field." = :filter"; - } - } else { - $aOrWhereFields[] = "q.".$filter->getFilterField()." = :filter"; - } + $totalQuery = $qb->getQuery(); + + $this->applyResultFields($qb, $filter); + $this->applyPagination($qb, $filter); + $this->applySorting($qb, $filter); + + $query = $qb->getQuery(); + + return array("data" => $this->getResult($query), "totalCount" => $totalQuery->getSingleScalarResult()); + } + + /** + * Processes the result after it was retrieved. In the default configuration, it returns an array result, or + * if no query fields are specified, it tries to serialize all objects. + */ + protected function getResult (Query $query) { + if (count($this->getQueryFields()) == 0) { + $aSerializedResult = array(); + foreach ($query->getResult() as $result) { + $aSerializedResult[] = $result->serialize(); + } - foreach ($aOrWhereFields as $or) { - $qb->orWhere($or); + return $aSerializedResult; + } else { + return $query->getArrayResult(); + } + } + + /** + * Applies pagination to the query + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applyResultFields (QueryBuilder $qb, ManagerFilter $filter) { + if (count($this->getQueryFields()) == 0) { + $qb->select("q"); + } else { + // Prepend a prefix to each field + $aQueryFields = array(); + foreach ($this->getQueryFields() as $field) { + $aQueryFields[] = "q.".$field; } - $qb->setParameter("filter", "%".$filter->getFilter()."%"); + $qb->select($aQueryFields); } - - if ($filter->getFilterCallback() !== null) { - call_user_func($filter->getFilterCallback(), $qb); - } - - $qb->from($this->getEntityName(),"q"); - - $totalQuery = $qb->getQuery(); - - // Prepend a prefix to each field - $aQueryFields = array(); - foreach ($this->getQueryFields() as $field) { - $aQueryFields[] = "q.".$field; + } + + /** + * Applies filtering to the query and calls back the custom filtering function, if required. + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applyFiltering (QueryBuilder $qb, ManagerFilter $filter) { + if ($filter->getFilter() !== null && $filter->getFilterField() !== null) { + $aOrWhereFields = array(); + + if (is_array($filter->getFilterField())) { + foreach ($filter->getFilterField() as $field) { + $aOrWhereFields[] = "q.".$field." LIKE :filter"; + } + } else { + $aOrWhereFields[] = "q.".$filter->getFilterField()." LIKE :filter"; + } + + foreach ($aOrWhereFields as $or) { + $qb->orWhere($or); + } + + $qb->setParameter("filter", "%".$filter->getFilter()."%"); } - - $qb->select($aQueryFields); - - - if ($filter->getStart() !== null && $filter->getLimit() !== null) { - $qb->setFirstResult($filter->getStart()); + + if ($filter->getFilterCallback() !== null) { + call_user_func($filter->getFilterCallback(), $qb); } + } + + + /** + * Applies a custom query to the QueryBuilder + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applyCustomQuery (QueryBuilder $qb, ManagerFilter $filter) { - if ($filter->getLimit() !== null) { - $qb->setMaxResults($filter->getLimit()); + } + + /** + * Applies pagination to the query + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applyPagination (QueryBuilder $qb, ManagerFilter $filter) { + if ($filter->getStart() !== null && $filter->getLimit() !== null) { + $qb->setFirstResult($filter->getStart()); + } + + if ($filter->getLimit() !== null) { + $qb->setMaxResults($filter->getLimit()); } - - if ($filter->getSortField() !== null) { - $qb->orderBy("q.".$filter->getSortField(), $filter->getDir()); + } + + /** + * Applies record sorting + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applySorting (QueryBuilder $qb, ManagerFilter $filter) { + if ($filter->getSortField() !== null && $filter->getSortField() != "q.") { + $qb->orderBy($filter->getSortField(), $filter->getDir()); } - - $query = $qb->getQuery(); - - $result = $query->getResult(); - - return array("data" => $result, "totalCount" => $totalQuery->getSingleScalarResult()); - } } \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php b/src/backend/de/RaumZeitLabor/PartKeepr/Manager/ManagerFilter.php @@ -225,7 +225,7 @@ class ManagerFilter { "direction" => "ASC"); } - $this->setSortField($aSortParams["property"]); + $this->setSortField("q.".$aSortParams["property"]); $this->setDirection($aSortParams["direction"]); if ($service->hasParameter("query")) { diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockEntry.php b/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockEntry.php @@ -140,6 +140,14 @@ class StockEntry extends BaseEntity implements Serializable { $this->part = $part; } + /** + * Returns the part assigned to this entry. + * @return Part $part The part + */ + public function getPart () { + return $this->part; + } + /** * Sets the user assigned to this entry. * @param User $user The user The user to set @@ -194,15 +202,15 @@ class StockEntry extends BaseEntity implements Serializable { public function serialize () { return array( "id" => $this->getId(), + "part_name" => $this->getPart()->getName(), + "part_id" => $this->getPart()->getId(), + "storageLocation_name" => $this->getPart()->getStorageLocation()->getName(), "username" => is_object($this->getUser()) ? $this->getUser()->getUsername() : PartKeepr::i18n("Unknown User"), "user_id" => is_object($this->getUser()) ? $this->getUser()->getId() : null, - "amount" => abs($this->getStockLevel()), - "datetime" => $this->getDateTime()->format("Y-m-d H:i:s"), + "stockLevel" => abs($this->getStockLevel()), + "dateTime" => $this->getDateTime()->format("Y-m-d H:i:s"), "direction" => ($this->getStockLevel() < 0) ? "out" : "in", "price" => $this->getPrice() ); } - - - } \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockManager.php @@ -0,0 +1,57 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Stock; +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Manager\AbstractManager, + Doctrine\ORM\QueryBuilder, + de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter, + de\RaumZeitLabor\PartKeepr\PartKeepr; + +class StockManager extends AbstractManager { + /** + * Returns the FQCN for the target entity to operate on. + * @return string The FQCN, e.g. de\RaumZeitLabor\PartKeepr\Part + */ + public function getEntityName () { + return 'de\RaumZeitLabor\PartKeepr\Stock\StockEntry'; + } + + /** + * Returns all fields which need to appear in the getList ResultSet. + * @return array An array of all fields which should be returned + */ + public function getQueryFields () { + return array(); + } + + /** + * Returns the default sort field + * + * @return string The default sort field + */ + public function getDefaultSortField () { + return "dateTime"; + } + + /** + * Applies a custom query to the QueryBuilder + * + * @param QueryBuilder $qb The query builder + * @param ManagerFilter $filter The query filter + */ + protected function applyCustomQuery (QueryBuilder $qb, ManagerFilter $filter) { + switch ($filter->getSortField()) { + case "q.part_name": + $qb->join("q.part", "p"); + $filter->setSortField("p.name"); + break; + case "q.user_id": + $qb->leftJoin("q.user", "u"); + $filter->setSortField("u.username"); + break; + case "q.direction": + $filter->setSortField("q.dateTime"); + break; + } + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockService.php b/src/backend/de/RaumZeitLabor/PartKeepr/Stock/StockService.php @@ -1,43 +1,43 @@ <?php -namespace de\raumzeitlabor\PartKeepr\Stock; +namespace de\RaumZeitLabor\PartKeepr\Stock; declare(encoding = 'UTF-8'); use de\RaumZeitLabor\PartKeepr\Stock\StockEntry, de\RaumZeitLabor\PartKeepr\PartKeepr, de\RaumZeitLabor\PartKeepr\User\User, + de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter, de\RaumZeitLabor\PartKeepr\Session\SessionManager, de\RaumZeitLabor\PartKeepr\Service\RestfulService, de\RaumZeitLabor\PartKeepr\Service\Service;; class StockService extends Service implements RestfulService { + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::get() + */ public function get () { - $qb = PartKeepr::getEM()->createQueryBuilder(); - - $qb->select("se")->from("de\RaumZeitLabor\PartKeepr\Stock\StockEntry","se") - ->where("se.part = :part") - ->orderBy("se.dateTime", "DESC") - ->setParameter("part", $this->getParameter("item")); - - $results = $qb->getQuery()->getResult(); - - $aData = array(); - - foreach ($results as $result) { - $aData[] = array( - "username" => is_object($result->getUser()) ? $result->getUser()->getUsername() : PartKeepr::i18n("Unknown User"), - "user_id" => is_object($result->getUser()) ? $result->getUser()->getId() : null, - "amount" => abs($result->getStockLevel()), - "datetime" => $result->getDateTime()->format("Y-m-d H:i:s"), - "id" => $result->getId(), - "direction" => ($result->getStockLevel() < 0) ? "out" : "in", - "price" => $result->getPrice() - ); + if ($this->hasParameter("id")) { + return array("data" => StockManager::getInstance()->getEntity($this->getParameter("id"))->serialize()); + } else { + $parameters = new ManagerFilter($this); + $parameters->setFilterField("name"); + + if ($this->hasParameter("part")) { + $parameters->setFilterCallback(array($this, "filterCallback")); + } + return StockManager::getInstance()->getList($parameters); } - - - return array("data" => $aData); + } + + /** + * If the "part" parameter is set, join the part into the result and filter on that + * @param QueryBuilder The $queryBuilder + */ + public function filterCallback ($queryBuilder) { + $queryBuilder->andWhere("q.part = :part"); + $queryBuilder->setParameter("part", $this->getParameter("part")); } public function create () { diff --git a/src/frontend/js/Components/MenuBar.js b/src/frontend/js/Components/MenuBar.js @@ -70,7 +70,12 @@ Ext.define('PartKeepr.MenuBar', { text: i18n("System Notices"), handler: this.showSystemNotices, icon: 'resources/fugue-icons/icons/service-bell.png' + },{ + text: i18n("Stock History"), + handler: this.showStockHistory, + icon: 'resources/fugue-icons/icons/service-bell.png' } + ] }); @@ -257,6 +262,16 @@ Ext.define('PartKeepr.MenuBar', { PartKeepr.getApplication().addItem(j); j.show(); }, + showStockHistory: function () { + var j = Ext.create("PartKeepr.StockHistoryGrid", { + title: i18n("Stock History"), + iconCls: 'icon-service-bell', + closable: true + }); + + PartKeepr.getApplication().addItem(j); + j.show(); + }, displayComponent: function (component) { var j = Ext.create(component.type, { title: component.title, diff --git a/src/frontend/js/Components/Part/PartStockHistory.js b/src/frontend/js/Components/Part/PartStockHistory.js @@ -1,136 +1,18 @@ Ext.define('PartKeepr.PartStockHistory', { - extend: 'Ext.grid.Panel', + extend: 'PartKeepr.AbstractStockHistoryGrid', alias: 'widget.PartStockHistory', - columns: [ - { - header: "", - xtype:'actioncolumn', - dataIndex: 'direction', - renderer: function (val) { - if (val == "out") - { - return '<img title="'+i18n("Parts removed")+'" src="resources/silkicons/brick_delete.png"/>'; - } else { - return '<img title="'+i18n("Parts added")+'" src="resources/silkicons/brick_add.png"/>'; - } - }, - width: 20 - }, - { - header: i18n("User"), - dataIndex: 'user_id', - flex: 0.4, - minWidth: 80, - renderer: function (val) { - var rec = PartKeepr.getApplication().getUserStore().getById(val); - - if (rec) { - return rec.get("username"); - } else { - return i18n("Unknown user"); - } - - }, - editor: { - xtype: 'UserComboBox' - } - }, - {header: i18n("Amount"), dataIndex: 'amount', width: 50, - editor: { - xtype:'numberfield', - allowBlank:false - }}, - {header: i18n("Date"), dataIndex: 'datetime', width: 120}, - { - header: i18n("Price"), - editor: { - xtype:'numberfield', - allowBlank:false - }, - dataIndex: 'price', - width: 60, - renderer: function (val, p, rec) { - if (rec.get("dir") == "out") { - return "-"; - } else { - return val; - } - } - } - ], - model: 'PartKeepr.StockEntry', - /** - * Initializes the stock history grid. - */ - initComponent: function () { - var config = { - autoLoad: false, - autoSync: true, - remoteFilter: false, - remoteSort: false, - model: 'PartKeepr.StockEntry', - pageSize: -1}; - - this.store = Ext.create('Ext.data.Store', config); - - this.editing = Ext.create('Ext.grid.plugin.CellEditing', { - clicksToEdit: 1 - }); - - this.plugins = [ this.editing ]; + initComponent: function () { + this.callParent(); - this.editing.on("beforeedit", this.onBeforeEdit, this); - - this.on("activate", this.onActivate, this); - this.callParent(); - }, - /** - * Called before editing a cell. Checks if the user may actually make the requested changes. - * - * @param e Passed from ExtJS - * @returns {Boolean} - */ - onBeforeEdit: function (e) { - - // Checks if the usernames match - var sameUser = e.record.get("username") == PartKeepr.getApplication().getUsername(); - - switch (e.field) { - case "price": - // Check the direction is "out". If yes, editing the price field is not allowed - if (e.record.get("direction") == "out") { - return false; - } - - // If it's not the same user or an admin, editing is not allowed - if ( !sameUser && !PartKeepr.getApplication().isAdmin()) { - return false; - } - break; - case "amount": - // Only an admin may edit the amount. Regular users must put the stock back in manually. - if (!PartKeepr.getApplication().isAdmin()) { - return false; - } - break; - case "user": - if (!PartKeepr.getApplication().isAdmin()) { - return false; - } - break; - default: - return true; - } - - return true; - }, - /** + this.on("activate", this.onActivate, this); + }, + /** * Called when the view is activated. */ onActivate: function () { var proxy = this.store.getProxy(); - proxy.extraParams.item = this.part; + proxy.extraParams.part = this.part; this.store.load(); } diff --git a/src/frontend/js/Components/StockReport/AbstractStockHistoryGrid.js b/src/frontend/js/Components/StockReport/AbstractStockHistoryGrid.js @@ -0,0 +1,139 @@ +/** + * Represents the stock history grid. + */ +Ext.define('PartKeepr.AbstractStockHistoryGrid', { + extend: 'Ext.grid.Panel', + + pageSize: 25, + + defineColumns: function () { + this.columns = [{ + header: "", + xtype:'actioncolumn', + dataIndex: 'direction', + renderer: function (val) { + if (val == "out") + { + return '<img title="'+i18n("Parts removed")+'" src="resources/silkicons/brick_delete.png"/>'; + } else { + return '<img title="'+i18n("Parts added")+'" src="resources/silkicons/brick_add.png"/>'; + } + }, + width: 20 + }, + { + header: i18n("User"), + dataIndex: 'user_id', + flex: 1, + minWidth: 80, + renderer: function (val, p, rec) { + return rec.get("username"); + }, + editor: { + xtype: 'UserComboBox' + } + }, + {header: i18n("Amount"), dataIndex: 'stockLevel', width: 50, + editor: { + xtype:'numberfield', + allowBlank:false + }}, + {header: i18n("Date"), dataIndex: 'dateTime', width: 120}, + { + header: i18n("Price"), + editor: { + xtype:'numberfield', + allowBlank:false + }, + dataIndex: 'price', + width: 60, + renderer: function (val, p, rec) { + if (rec.get("dir") == "out") { + return "-"; + } else { + return val; + } + } + }]; + }, + model: 'PartKeepr.StockEntry', + /** + * Initializes the stock history grid. + */ + initComponent: function () { + + this.defineColumns(); + + var config = { + autoLoad: false, + autoSync: true, + remoteFilter: true, + remoteSort: true, + proxy: PartKeepr.getRESTProxy("Stock"), + model: 'PartKeepr.StockEntry', + pageSize: this.pageSize }; + + this.store = Ext.create('Ext.data.Store', config); + + this.editing = Ext.create('Ext.grid.plugin.CellEditing', { + clicksToEdit: 1 + }); + + this.plugins = [ this.editing ]; + + this.bottomToolbar = Ext.create("Ext.toolbar.Paging", { + store: this.store, + enableOverflow: true, + dock: 'bottom', + displayInfo: false + }); + + + this.dockedItems = new Array(); + this.dockedItems.push(this.bottomToolbar); + + this.editing.on("beforeedit", this.onBeforeEdit, this); + + this.callParent(); + }, + /** + * Called before editing a cell. Checks if the user may actually make the requested changes. + * + * @param e Passed from ExtJS + * @returns {Boolean} + */ + onBeforeEdit: function (e) { + + // Checks if the usernames match + var sameUser = e.record.get("username") == PartKeepr.getApplication().getUsername(); + + switch (e.field) { + case "price": + // Check the direction is "out". If yes, editing the price field is not allowed + if (e.record.get("direction") == "out") { + return false; + } + + // If it's not the same user or an admin, editing is not allowed + if ( !sameUser && !PartKeepr.getApplication().isAdmin()) { + return false; + } + break; + case "stockLevel": + // Only an admin may edit the amount. Regular users must put the stock back in manually. + if (!PartKeepr.getApplication().isAdmin()) { + return false; + } + break; + case "user": + if (!PartKeepr.getApplication().isAdmin()) { + return false; + } + break; + default: + return true; + } + + return true; + } +});+ \ No newline at end of file diff --git a/src/frontend/js/Components/StockReport/StockHistoryGrid.js b/src/frontend/js/Components/StockReport/StockHistoryGrid.js @@ -0,0 +1,31 @@ +/** + * The stock history grid. It shows all stock transactions. + */ +Ext.define('PartKeepr.StockHistoryGrid', { + extend: 'PartKeepr.AbstractStockHistoryGrid', + alias: 'widget.PartStockHistory', + + pageSize: 25, + + defineColumns: function () { + this.callParent(); + + this.columns.splice(1, 0, { + header: i18n("Part"), + dataIndex: 'part_name', + flex: 1, + minWidth: 200 + }); + }, + initComponent: function () { + this.callParent(); + + this.on("activate", this.onActivate, this); + }, + /** + * Called when the view is activated. + */ + onActivate: function () { + this.store.load(); + } +}); diff --git a/src/frontend/js/Models/StockEntry.js b/src/frontend/js/Models/StockEntry.js @@ -4,10 +4,10 @@ Ext.define("PartKeepr.StockEntry", { { id: 'id', name: 'id', type: 'int' }, { name: 'username', type: 'string'}, { name: 'user_id', type: 'int'}, - { name: 'datetime', type: 'datetime'}, - { name: 'amount', type: 'int'}, + { name: 'dateTime', type: 'datetime'}, + { name: 'stockLevel', type: 'int'}, { name: 'direction', type: 'string'}, + { name: 'part_name', type: 'string'}, { name: 'price', type: 'float'} - ], - proxy: PartKeepr.getRESTProxy("Stock") + ] }); \ No newline at end of file