{"id":18737909,"url":"https://github.com/technodelight/magento_custom_reports_example","last_synced_at":"2025-07-06T18:04:09.216Z","repository":{"id":8109218,"uuid":"9524833","full_name":"technodelight/magento_custom_reports_example","owner":"technodelight","description":"An example module about the basic of how to create a custom report in Magento","archived":false,"fork":false,"pushed_at":"2013-06-11T08:24:25.000Z","size":177,"stargazers_count":31,"open_issues_count":0,"forks_count":16,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-26T14:06:05.744Z","etag":null,"topics":["admin","grid","magento","magento1","php","tutorial","tutorial-code"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/technodelight.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-04-18T15:17:17.000Z","updated_at":"2022-07-25T17:45:08.000Z","dependencies_parsed_at":"2022-07-17T14:47:16.037Z","dependency_job_id":null,"html_url":"https://github.com/technodelight/magento_custom_reports_example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technodelight%2Fmagento_custom_reports_example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technodelight%2Fmagento_custom_reports_example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technodelight%2Fmagento_custom_reports_example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/technodelight%2Fmagento_custom_reports_example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/technodelight","download_url":"https://codeload.github.com/technodelight/magento_custom_reports_example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248621330,"owners_count":21134812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["admin","grid","magento","magento1","php","tutorial","tutorial-code"],"created_at":"2024-11-07T15:27:23.231Z","updated_at":"2025-04-12T19:32:33.019Z","avatar_url":"https://github.com/technodelight.png","language":"PHP","readme":"# Custom Reports in Magento\n\n\n Because I haven't found any detailed article on how to create a report and how it works, I decided to write\none myself, and try to give you some details, not just a plain-code-figure-out-everything-yourself stuff.\nThe example would be quite simple, but it just fits for an excersice: list the orders grand total and shipping\namount, and - to give this story a little twist - we would like to display how much percent was the shipping amount of the\norder's total. We would like to display totals too under our grid.\n Acceptance criteria for our module:\n - Ability to filter in a given date interval\n - Ability to change date interval (days, months or years)\n - Ability to filter results with a non-zero shipping percent only\n - Ability to export to CSV and MS Excel\n In the example code I would like to use some of the best practices, and to follow the conventions as much as possible. I\ncreated a public git repository from where you could download the whole source code. If you are impatient, scroll to\nthe end of this article for the link.\n\n\n## About the Reports in a Nutshell\n\n Basically it consists a grid, a collection and a form, where the form has fields to filter the displayed\nresults of the grid. The grid displays the collection's items using the applied filters. There is an enermous amount\nof entry points which we could use to change data during runtime, but we won't use many of them.\n\n\n## Creating the base module\n\n We will create some blocks, models, helpers for our module, overload a controller, define a layout, then\nplace the whole thing into the admin menu. We have to define models because we will use a collection,\nit should have blocks to display the grid and the form, while the helper will handle the translations and it's\nrequired because we make an admin module. We will place our files under the 'local' codepool, under the `My` vendor\nand the module's name would be `Reports`.\n You can notice some difference while creating a module in the admin area compared to a frontend one. We will overload\nthe controller under the config's 'admin' node instead of adding a new frontname to the system, also applying the\nlayout updates would be in a different node named `adminhtml`. You may wonder why we won't place it\nunder the same node as the controller; this could be traced back to legacy reasons. This node is also the place\nfor the admin menu configuration, but we separate it into a file named after this node (adminhtml.xml). This is\na feature of Magento, you could separate your module's configuration by the node names used. Usually we do\nthis with system.xml, adminhtml.xml and api.xml/api_v2.xml, depending on needs.\n\n\n## Configuration files\n\n First of all, we will write our module xml. Because we'll work in the `local` codepool, we should\nplace all of our files under the `app/code/local` directory.\n\napp/etc/modules/My_Reports.xml\n\n    \u003c?xml version=\"1.0\"?\u003e\n    \u003cconfig\u003e\n        \u003cmodules\u003e\n            \u003cMy_Reports\u003e\n                \u003cactive\u003etrue\u003c/active\u003e\n                \u003cversion\u003e0.1.0\u003c/version\u003e\n                \u003ccodePool\u003elocal\u003c/codePool\u003e\n            \u003c/My_Reports\u003e\n        \u003c/modules\u003e\n    \u003c/config\u003e\n\n In `config.xml`, we tell Magento's admin router to search the controller first in our module, before `Mage_Adminhtml`,\nthen add the layout update file for creating the report's user interface.\n\napp/code/local/My/Reports/etc/config.xml\n\n    \u003c?xml version=\"1.0\"?\u003e\n    \u003cconfig\u003e\n        \u003cmodules\u003e\n            \u003cMy_Reports\u003e\n                \u003cversion\u003e0.1.0\u003c/version\u003e\n                \u003cdepends\u003e\n                    \u003cMage_Adminhtml /\u003e\n                    \u003cMage_Sales /\u003e\n                \u003c/depends\u003e\n            \u003c/My_Reports\u003e\n        \u003c/modules\u003e\n    \n        \u003cglobal\u003e\n            \u003cmodels\u003e\n                \u003cmy_reports\u003e\n                    \u003cclass\u003eMy_Reports_Model\u003c/class\u003e\n                    \u003cresourceModel\u003emy_reports_mysql4\u003c/resourceModel\u003e\n                \u003c/my_reports\u003e\n                \u003cmy_reports_mysql4\u003e\n                    \u003cclass\u003eMy_Reports_Model_Mysql4\u003c/class\u003e\n                \u003c/my_reports_mysql4\u003e\n            \u003c/models\u003e\n            \u003chelpers\u003e\n                \u003cmy_reports\u003e\n                    \u003cclass\u003eMy_Reports_Helper\u003c/class\u003e\n                \u003c/my_reports\u003e\n            \u003c/helpers\u003e\n            \u003cblocks\u003e\n                \u003cmy_reports\u003e\n                    \u003cclass\u003eMy_Reports_Block\u003c/class\u003e\n                \u003c/my_reports\u003e\n            \u003c/blocks\u003e\n        \u003c/global\u003e\n    \n        \u003cadmin\u003e\n            \u003crouters\u003e\n                \u003cadminhtml\u003e\n                    \u003cargs\u003e\n                        \u003cmodules\u003e\n                            \u003cMy_Reports before=\"Mage_Adminhtml\"\u003eMy_Reports_Adminhtml\u003c/My_Reports\u003e\n                        \u003c/modules\u003e\n                    \u003c/args\u003e\n                \u003c/adminhtml\u003e\n            \u003c/routers\u003e\n        \u003c/admin\u003e\n    \n        \u003cadminhtml\u003e\n            \u003clayout\u003e\n                \u003cupdates\u003e\n                    \u003cmy_reports\u003e\n                        \u003cfile\u003emy_reports.xml\u003c/file\u003e\n                    \u003c/my_reports\u003e\n                \u003c/updates\u003e\n            \u003c/layout\u003e\n        \u003c/adminhtml\u003e\n    \n    \u003c/config\u003e\n\n After that, add our module to the admin menu under Report \u003e Sales. We define some basic ACL rule too, which\nallows every user to operate with our grid.\n\napp/code/local/My/Reports/etc/adminhtml.xml\n\n    \u003c?xml version=\"1.0\"?\u003e\n    \u003cconfig\u003e\n        \u003cmenu\u003e\n            \u003creport\u003e\n                \u003cchildren\u003e\n                    \u003csalesroot translate=\"title\"\u003e\n                        \u003cchildren\u003e\n                            \u003cmy_reports translate=\"title\"\u003e\n                                \u003ctitle\u003eMy Custom Reports\u003c/title\u003e\n                                \u003caction\u003eadminhtml/my_reports\u003c/action\u003e\n                                \u003csort_order\u003e100\u003c/sort_order\u003e\n                            \u003c/my_reports\u003e\n                        \u003c/children\u003e\n                    \u003c/salesroot\u003e\n                \u003c/children\u003e\n            \u003c/report\u003e\n        \u003c/menu\u003e\n        \u003cacl\u003e\n            \u003cresources\u003e\n                \u003cadmin\u003e\n                    \u003cchildren\u003e\n                        \u003csystem\u003e\n                            \u003cchildren\u003e\n                                \u003cconfig\u003e\n                                    \u003cchildren\u003e\n                                        \u003cmy_reports\u003e\n                                            \u003ctitle\u003eMy Reports Section\u003c/title\u003e\n                                        \u003c/my_reports\u003e\n                                    \u003c/children\u003e\n                                \u003c/config\u003e\n                            \u003c/children\u003e\n                        \u003c/system\u003e\n                        \u003creport\u003e\n                            \u003cchildren\u003e\n                                \u003csalesroot\u003e\n                                    \u003cchildren\u003e\n                                        \u003cmy_reports translate=\"title\"\u003e\n                                            \u003ctitle\u003eMy Custom Reports\u003c/title\u003e\n                                            \u003cchildren\u003e\n                                                \u003cview translate=\"title\"\u003e\n                                                    \u003ctitle\u003eView\u003c/title\u003e\n                                                \u003c/view\u003e\n                                            \u003c/children\u003e\n                                        \u003c/my_reports\u003e\n                                    \u003c/children\u003e\n                                \u003c/salesroot\u003e\n                            \u003c/children\u003e\n                        \u003c/report\u003e\n                    \u003c/children\u003e\n                \u003c/admin\u003e\n            \u003c/resources\u003e\n        \u003c/acl\u003e\n    \u003c/config\u003e\n\n To get a working admin module, we should create a helper class. Since we haven't got any logic which we should\nshare between blocks, controllers or models, we just inherit everything from `Mage_Core_Helper_Abstract` and leave the body empty.\n There is a convention to use the helper's translate method to hook translations through it, so let's follow it on our code!\n\napp/code/local/My/Reports/Helper/Data.php\n\n    \u003c?php\n    \n    /**\n     * Default helper for our Admin module\n     * \n     * Hook for translations\n     */\n    class My_Reports_Helper_Data \n        extends Mage_Core_Helper_Abstract\n    {\n        \n    }\n\n\n## Controller\n\n The controller's `_initAction()` and `_initReportAction()` methods could be familiar from the `Mage_Adminhtml_Report_SalesController`.\nIn the `indexAction` we will use these methods to pass values from the request object to the filter. The methods starting with 'export' shall\nexport our data to the appropriate formats. Luckily we don't have to code the exportation logic ourself, it's already\nimplemented by the Magento Team (at least one thing less to do). Because the export is a part of the grid, we have the opportunity\nto export anything what the grid could display.\n\napp/code/local/My/Reports/controllers/Adminhtml/My/ReportsController.php\n\n    \u003c?php\n    \n    class My_Reports_Adminhtml_My_ReportsController\n        extends Mage_Adminhtml_Controller_Action\n    {\n        /**\n         * Initialize titles and navigation breadcrumbs\n         * @return My_Reports_Adminhtml_ReportsController\n         */\n        protected function _initAction()\n        {\n            $this-\u003e_title($this-\u003e__('Reports'))\n                -\u003e_title($this-\u003e__('Sales'))\n                -\u003e_title($this-\u003e__('My Custom Reports'));\n            $this-\u003eloadLayout()\n                -\u003e_setActiveMenu('report/sales')\n                -\u003e_addBreadcrumb(Mage::helper('my_reports')-\u003e__('Reports'), Mage::helper('my_reports')-\u003e__('Reports'))\n                -\u003e_addBreadcrumb(Mage::helper('my_reports')-\u003e__('Sales'), Mage::helper('my_reports')-\u003e__('Sales'))\n                -\u003e_addBreadcrumb(Mage::helper('my_reports')-\u003e__('My Custom Reports'), Mage::helper('my_reports')-\u003e__('My Custom Reports'));\n            return $this;\n        }\n    \n        /**\n         * Prepare blocks with request data from our filter form\n         * @return My_Reports_Adminhtml_ReportsController\n         */\n        protected function _initReportAction($blocks)\n        {\n            if (!is_array($blocks)) {\n                $blocks = array($blocks);\n            }\n     \n            $requestData = Mage::helper('adminhtml')-\u003eprepareFilterString($this-\u003egetRequest()-\u003egetParam('filter'));\n            $requestData = $this-\u003e_filterDates($requestData, array('from', 'to'));\n            $params = $this-\u003e_getDefaultFilterData();\n            foreach ($requestData as $key =\u003e $value) {\n                if (!empty($value)) {\n                    $params-\u003esetData($key, $value);\n                }\n            }\n     \n            foreach ($blocks as $block) {\n                if ($block) {\n                    $block-\u003esetFilterData($params);\n                }\n            }\n            return $this;\n        }\n    \n        /**\n         * Grid action\n         */\n        public function indexAction()\n        {\n            $this-\u003e_initAction();\n    \n            $gridBlock = $this-\u003egetLayout()-\u003egetBlock('adminhtml_report.grid');\n            $filterFormBlock = $this-\u003egetLayout()-\u003egetBlock('grid.filter.form');\n            $this-\u003e_initReportAction(array(\n                $gridBlock,\n                $filterFormBlock\n            ));\n    \n            $this-\u003erenderLayout();\n        }\n    \n        /**\n         * Export reports to CSV file\n         */\n        public function exportCsvAction()\n        {\n            $fileName = 'my_reports.csv';\n            $grid = $this-\u003egetLayout()-\u003ecreateBlock('my_reports/adminhtml_report_grid');\n            $this-\u003e_initReportAction($grid);\n            $this-\u003e_prepareDownloadResponse($fileName, $grid-\u003egetCsvFile());\n        }\n    \n        /**\n         * Export reports to Excel XML file\n         */\n        public function exportExcelAction()\n        {\n            $fileName = 'my_reports.xml';\n            $grid = $this-\u003egetLayout()-\u003ecreateBlock('my_reports/adminhtml_report_grid');\n            $this-\u003e_initReportAction($grid);\n            $this-\u003e_prepareDownloadResponse($fileName, $grid-\u003egetExcelFile());\n        }\n    \n        /**\n         * Returns default filter data\n         * @return Varien_Object\n         */\n        protected function _getDefaultFilterData()\n        {\n            return new Varien_Object(array(\n                'from' =\u003e date('Y-m-d G:i:s', strtotime('-1 month -1 day')),\n                'to' =\u003e date('Y-m-d G:i:s', strtotime('-1 day'))\n            ));\n        }\n    }\n\n\n## Layout, Grid Container\n\n The `indexAction` supplies our blocks with data, therefore it's time to start creating them! Let's start right now with the `layout.xml`.\nAs you can see, we will need a container block, which would be the place of the grid and the filter form. Notice that nothing describes the grid block here. Don't worry, the container should add it later, dynamically.\n\napp/design/adminhtml/default/default/layout/my_reports.xml\n\n    \u003c?xml version=\"1.0\"?\u003e\n    \u003clayout version=\"0.1.0\"\u003e\n        \u003cadminhtml_my_reports_index\u003e\n            \u003creference name=\"content\"\u003e\n                \u003cblock type=\"my_reports/adminhtml_report\" template=\"report/grid/container.phtml\" name=\"my_reports_report_grid_container\"\u003e\n                    \u003cblock type=\"my_reports/adminhtml_filter_form\" name=\"grid.filter.form\" /\u003e\n                \u003c/block\u003e\n            \u003c/reference\u003e\n        \u003c/adminhtml_my_reports_index\u003e\n    \u003c/layout\u003e\n\n Let's go on with the container. This block should build the the grid block in it's parent class' `_prepareLayout()` method in\nthe following way: `{blockGroup}/{controller}_grid`. The {blockGroup} is the block alias (`my_reports`), which we already defined in our\n`config.xml` under the blocks node, and the {controller} is this block's identifier (`adminhtml_report`). The grid block's name\nwould be `my_reports/adminhtml_report_grid` then.\n\napp/code/local/My/Reports/Block/Adminhtml/Report.php\n\n    \u003c?php\n    \n    class My_Reports_Block_Adminhtml_Report\n        extends Mage_Adminhtml_Block_Widget_Grid_Container\n    {\n        /**\n         * This is your module alias\n         */\n        protected $_blockGroup = 'my_reports';\n\n        /**\n         * This is the controller's name (this block)\n         */\n        protected $_controller = 'adminhtml_report';\n    \n        /*\n            Note: the grid block's name would prepare from $_blockGroup and $_controller with the suffix '_grid'.\n            So the complete block would called my_reports/adminhtml_report_grid . As you already guessed it,\n            this will resolve to the class My_Reports_Adminhtml_Report_Grid .\n         */\n    \n        /**\n         * Prepare grid container, add and remove additional buttons\n         */\n        public function __construct()\n        {\n            // The head title of the grid\n            $this-\u003e_headerText = Mage::helper('my_reports')-\u003e__('My Custom Reports');\n            // Set hard-coded template. As you can see, the layout.xml \n            // attribute is ineffective, but we keep up with conventions\n            $this-\u003esetTemplate('report/grid/container.phtml');\n            // call parent constructor and let it add the buttons\n            parent::__construct();\n            // we create a report, not just a standard grid, so remove add button, we don't need it this time\n            $this-\u003e_removeButton('add');\n    \n            // add a button to our form to let the user kick-off our logic from the admin\n            $this-\u003eaddButton('filter_form_submit', array(\n                'label' =\u003e Mage::helper('my_reports')-\u003e__('Show Report'),\n                'onclick' =\u003e 'filterFormSubmit()'\n            ));\n        }\n    \n        /**\n         * This function will prepare our filter URL\n         * @return string\n         */\n        public function getFilterUrl()\n        {\n            $this-\u003egetRequest()-\u003esetParam('filter', null);\n            return $this-\u003egetUrl('*/*/index', array('_current' =\u003e true));\n        }\n    }\n\n\n## Grid\n\n The grid connects our backend data and the logic in templates to display everything on the frontend, so it's a \nbit of both worlds.\nThe original sales report grid contains an abstract and a concrete class implementation, but for the purpose\nof easy understanding, we will place everything into only one class.\n The code which deals with displaying data on the user interface should be prepared in the `_prepareColumns`. Using\nthe `type` key you can choose one column renderer from the bundled ones (you could find the full list of the renderers\nat `Mage_Adminhtml_Block_Widget_Grid_Column::_getRendererByType()`). However, there isn't one which could handle\nthe percent values, therefore we should create one by ourselves. The `index` would attach the SQL result's column\nto the column renderer (you should define the 'alias' here as you defined it in your query in the resource model,\nfor example you could see how we specified the `shipping_rate` column).\n The method which deals with supplying data from the backend is `_prepareCollection()`. Here we pass the values\nfrom the filters to the collection within the `_addCustomFilter()` method.\n\napp/code/local/My/Reports/Block/Adminhtml/Report/Grid.php\n\n    \u003c?php\n    \n    class My_Reports_Block_Adminhtml_Report_Grid\n        extends Mage_Adminhtml_Block_Widget_Grid\n    {\n        // add vars used by our methods\n    \n        /**\n         * Grouped class name of used collection by this grid\n         * @var string\n         */\n        protected $_resourceCollectionName = 'my_reports/report_collection';\n    \n        /**\n         * List of columns to aggregate by\n         * @var array\n         */\n        protected $_aggregatedColumns;\n    \n        /**\n         * Basic setup of our grid\n         */\n        public function __construct()\n        {\n            parent::__construct();\n    \n            // change behaviour of grid. This time we won't use pager and ajax functions\n            $this-\u003esetPagerVisibility(false);\n            $this-\u003esetUseAjax(false);\n            $this-\u003esetFilterVisibility(false);\n    \n            // set message for empty result\n            $this-\u003esetEmptyCellLabel(Mage::helper('my_reports')-\u003e__('No records found.'));\n    \n            // set grid ID in adminhtml\n            $this-\u003esetId('myReportsGrid');\n    \n            // set our grid to obtain totals\n            $this-\u003esetCountTotals(true);\n        }\n    \n        // add getters\n    \n        /**\n         * Returns the resource collection name which we'll apply filters and display results\n         * @return string\n         */\n        public function getResourceCollectionName()\n        {\n            return $this-\u003e_resourceCollectionName;\n        }\n    \n        /**\n         * Factory method for our resource collection\n         * @return Mage_Core_Model_Mysql4_Collection_Abstract\n         */\n        public function getResourceCollection()\n        {\n            $resourceCollection = Mage::getResourceModel($this-\u003egetResourceCollectionName());\n            return $resourceCollection;\n        }\n    \n        /**\n         * Gets the actual used currency code.\n         * We will convert every currency value to this currency.\n         * @return string\n         */\n        public function getCurrentCurrencyCode()\n        {\n            return Mage::app()-\u003egetStore()-\u003egetBaseCurrencyCode();\n        }\n    \n        /**\n         * Get currency rate, base to given currency\n         * @param string|Mage_Directory_Model_Currency $toCurrency currency code\n         * @return int\n         */\n        public function getRate($toCurrency)\n        {\n            return Mage::app()-\u003egetStore()-\u003egetBaseCurrency()-\u003egetRate($toCurrency);\n        }\n    \n        /**\n         * Return totals data\n         * Count totals if it's not previously counted and set to retrieve\n         * @return Varien_Object\n         */\n        public function getTotals()\n        {\n            $result = parent::getTotals();\n            if (!$result \u0026\u0026 $this-\u003egetCountTotals()) {\n                $filterData = $this-\u003egetFilterData();\n                $totalsCollection = $this-\u003egetResourceCollection();\n                \n                // apply our custom filters on collection\n                $this-\u003e_addCustomFilter(\n                    $totalsCollection,\n                    $filterData\n                );\n    \n                // isTotals is a flag, we will deal with this in the resource collection\n                $totalsCollection-\u003eisTotals(true);\n    \n                // set totals row even if we didn't got a result\n                if ($totalsCollection-\u003ecount() \u003c 1) {\n                    $this-\u003esetTotals(new Varien_Object);\n                } else {\n                    $this-\u003esetTotals($totalsCollection-\u003egetFirstItem());\n                }\n    \n                $result             = parent::getTotals();\n            }\n    \n            return $result;\n        }\n    \n        // prepare columns and collection\n    \n        /**\n         * Prepare our grid's columns to display\n         * @return My_Reports_Block_Adminhtml_Grid\n         */\n        protected function _prepareColumns()\n        {\n            // get currency code and currency rate for the currency renderers.\n            // our orders could be in different currencies, therefore we should convert the values to the base currency\n            $currencyCode = $this-\u003egetCurrentCurrencyCode();\n            $rate = $this-\u003egetRate($currencyCode);\n    \n            // add our first column, period which represents a date\n            $this-\u003eaddColumn('period', array(\n                'header' =\u003e Mage::helper('my_reports')-\u003e__('Period'),\n                'index' =\u003e 'created_at', // 'index' attaches a column from the SQL result set to the grid\n                'renderer' =\u003e 'adminhtml/report_sales_grid_column_renderer_date',\n                'width' =\u003e 100,\n                'sortable' =\u003e false,\n                'period_type' =\u003e $this-\u003egetFilterData()-\u003egetPeriodType() // could be day, month or year\n            ));\n    \n            // add base grand total w/ a currency renderer, and add totals\n            $this-\u003eaddColumn('base_grand_total', array(\n                'header' =\u003e Mage::helper('my_reports')-\u003e__('Grand Total'),\n                'index' =\u003e 'base_grand_total',\n                // type defines a grid column renderer; you could find the complete list \n                // and the exact aliases at Mage_Adminhtml_Block_Widget_Grid_Column::_getRendererByType()\n                'type' =\u003e 'currency',\n                'currency_code' =\u003e $currencyCode, // set currency code..\n                'rate' =\u003e $rate, // and currency rate, used by the column renderer\n                'total' =\u003e 'sum'\n            ));\n    \n            // add the next column shipping_amount, with an average on totals\n            $this-\u003eaddColumn('base_shipping_amount', array(\n                'header' =\u003e Mage::helper('my_reports')-\u003e__('Shipping Amount'),\n                'index' =\u003e 'base_shipping_amount',\n                'type' =\u003e 'currency',\n                'currency_code' =\u003e $currencyCode,\n                'rate' =\u003e $rate,\n                'total' =\u003e 'sum'\n            ));\n    \n            // rate, where base_shipping_amount/base_grand_total is a percent\n            $this-\u003eaddColumn('shipping_rate', array(\n                'header' =\u003e Mage::helper('my_reports')-\u003e__('Shipping Rate'),\n                'index' =\u003e 'shipping_rate',\n                'renderer' =\u003e 'my_reports/adminhtml_report_grid_column_renderer_percent',\n                'decimals' =\u003e 2,\n                'total' =\u003e 'avg'\n            ));\n    \n            // add export types\n            $this-\u003eaddExportType('*/*/exportCsv', Mage::helper('my_reports')-\u003e__('CSV'));\n            $this-\u003eaddExportType('*/*/exportExcel', Mage::helper('my_reports')-\u003e__('MS Excel XML'));\n    \n            return parent::_prepareColumns();\n        }\n    \n        /**\n         * Prepare our collection which we'll display in the grid\n         * First, get the resource collection we're dealing with, with our custom filters applied.\n         * In case of an export, we're done, otherwise calculate the totals\n         * @return My_Reports_Block_Adminhtml_Grid\n         */\n        protected function _prepareCollection()\n        {\n            $filterData = $this-\u003egetFilterData();\n            $resourceCollection = $this-\u003egetResourceCollection();\n    \n            // get our resource collection and apply our filters on it\n            $this-\u003e_addCustomFilter(\n                $resourceCollection,\n                $filterData\n            );\n    \n            // attach the prepared collection to our grid\n            $this-\u003esetCollection($resourceCollection);\n    \n            // skip totals if we do an export (calling getTotals would be a duplicate, because\n            // the export method calls it explicitly)\n            if ($this-\u003e_isExport) {\n                return $this;\n            }\n    \n            // count totals if needed\n            if ($this-\u003egetCountTotals()) {\n                $this-\u003egetTotals();\n            }\n    \n            return parent::_prepareCollection();\n        }\n    \n        /**\n         * Apply our custom filters on collection\n         * @param Mage_Core_Model_Mysql4_Collection_Abstract $collection\n         * @param Varien_Object $filterData\n         * @return My_Reports_Block_Adminhtml_Report_Grid\n         */\n        protected function _addCustomFilter($collection, $filterData)\n        {\n            $collection\n                -\u003esetPeriodType($filterData-\u003egetPeriodType())\n                -\u003esetDateRange($filterData-\u003egetFrom(), $filterData-\u003egetTo())\n                -\u003eisShippingRateNonZeroOnly($filterData-\u003egetShippingRate() ? true : false)\n                -\u003esetAggregatedColumns($this-\u003e_getAggregatedColumns());\n    \n            return $this;\n        }\n    \n        /**\n         * Returns the columns we specified to summarize totals\n         * \n         * Collect all columns we added totals to. \n         * The returned array would be ie. 'base_grand_total' =\u003e 'sum'\n         * @return array\n         */\n        protected function _getAggregatedColumns()\n        {\n            if (!isset($this-\u003e_aggregatedColumns) \u0026\u0026 $this-\u003egetColumns()) {\n                $this-\u003e_aggregatedColumns = array();\n                foreach ($this-\u003egetColumns() as $column) {\n                    if ($column-\u003ehasTotal()) {\n                        $this-\u003e_aggregatedColumns[$column-\u003egetId()] = $column-\u003egetTotal();\n                    }\n                }\n            }\n    \n            return $this-\u003e_aggregatedColumns;\n        }\n    \n    }\n\n We don't have a renderer to display the percent values yet, so we have to create it. Because\nevery column object inherits from `Varien_Object`, you could pass any value to your column renderer in the\ngrid's `_prepareColumns()` method. We will create our renderer by using this capability, but because we\nshould have default values, we should wrap the getters within our own methods.\n If you'd like to display the value differently in an export, you have to overwrite the `renderExport()`\nmethod (by default it returns with the `render()` method's result).\n Also, it's worth mentioning that there are two column block types, the one which we would like to create\nnow, and an other one which deals with inline filtering on values, placed on the top of the grid (we turned \nit off this time, see `setFilterVisibility` in the grid class). If you are interested, you could find everything\nin `Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Abstract`.\n\napp/code/local/My/Reports/Block/Adminhtml/Report/Grid/Column/Renderer/Percent.php\n\n    \u003c?php\n    \n    class My_Reports_Block_Adminhtml_Report_Column_Renderer_Percent\n        extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract\n    {\n        // default (fallback) values, if not specified from outside\n    \n        /**\n         * Default value for rounding value by\n         * @var int\n         */\n        const DECIMALS = 2;\n\n        // render the field\n    \n        /**\n         * Renders grid column\n         * @param Varien_Object $row\n         * @return string\n         */\n        public function render(Varien_Object $row)\n        {\n            $value          = $this-\u003e_getValue($row);\n            $decimals       = $this-\u003e_getDecimals();\n            return number_format($value, $decimals) . '%';\n        }\n    \n        // add getter for decimals\n\n        /**\n         * Get decimal to round value by\n         * The decimals value could be changed with specifying it from outside using\n         * a setter method supported by Varien_Object (ie. with setData('decimals', 2) or setDecimals(2))\n         * @return int\n         */\n        protected function _getDecimals()\n        {\n            $decimals       = $this-\u003egetDecimals(); // this is a magic getter\n            return !is_null($decimals) ? $decimals : self::DECIMALS;\n        }\n    \n    }\n\n\n## Form\n\n We are already done with almost everything in our layout, except the filter form.\n This is a block which wraps the `Varien_Data_form` with a template (`widget/grid.phtml`). We will\ncreate a fieldset and place our form elements in it, and put the options for the select elements\nto protected getters. We may have to modify the fields in runtime from outside the class, therefore we \nwill add functionality to achieve this behaviour.\n\napp/code/local/My/Reports/Block/Adminhtml/Filter/Form.php\n\n    \u003c?php\n    \n    class My_Reports_Block_Adminhtml_Filter_Form\n        extends Mage_Adminhtml_Block_Widget_Form\n    {\n        /**\n         * This will contain our form element's visibility\n         * @var array\n         */\n        protected $_fieldVisibility = array();\n    \n        /**\n         * Field options\n         * @var array\n         */\n        protected $_fieldOptions = array();\n    \n        /**\n         * Sets a form element to be visible or not\n         * @param string $fieldId\n         * @param bool $visibility\n         * @return My_Reports_Block_Adminhtml_Filter_Form\n         */\n        public function setFieldVisibility($fieldId, $visibility)\n        {\n            $this-\u003e_fieldVisibility[$fieldId] = $visibility ? true : false;\n            return $this;\n        }\n    \n        /**\n         * Returns the field is visible or not. If we hadn't set a value\n         * for the field previously, it will return the value defined in the\n         * defaultVisibility parameter (it's true by default)\n         * @param string $fieldId\n         * @param bool $defaultVisibility\n         * @return bool\n         */\n        public function getFieldVisibility($fieldId, $defaultVisibility = true)\n        {\n            if (isset($this-\u003e_fieldVisibility[$fieldId])) {\n                return $this-\u003e_fieldVisibility[$fieldId];\n            }\n            return $defaultVisibility;\n        }\n    \n        /**\n         * Set field option(s)\n         * @param string $fieldId\n         * @param string|array $option if option is an array, loop through it's keys and values\n         * @param mixed $value if option is an array this option is meaningless\n         * @return My_Reports_Block_Adminhtml_Filter_Form\n         */\n        public function setFieldOption($fieldId, $option, $value = null)\n        {\n            if (is_array($option)) {\n                $options = $option;\n            } else {\n                $options = array($option =\u003e $value);\n            }\n    \n            if (!isset($this-\u003e_fieldOptions[$fieldId])) {\n                $this-\u003e_fieldOptions[$fieldId] = array();\n            }\n    \n            foreach ($options as $key =\u003e $value) {\n                $this-\u003e_fieldOptions[$fieldId][$key] = $value;\n            }\n    \n            return $this;\n        }\n    \n        /**\n         * Prepare our form elements\n         * @return My_Reports_Block_Adminhtml_Filter_Form\n         */\n        protected function _prepareForm()\n        {\n            // inicialise our form\n            $actionUrl = $this-\u003egetCurrentUrl();\n            $form = new Varien_Data_Form(array(\n                'id' =\u003e 'filter_form',\n                'action' =\u003e $actionUrl, \n                'method' =\u003e 'get'\n            ));\n    \n            // set ID prefix for all elements in our form\n            $htmlIdPrefix = 'my_reports_';\n            $form-\u003esetHtmlIdPrefix($htmlIdPrefix);\n    \n            // create a fieldset to add elements to\n            $fieldset = $form-\u003eaddFieldset(\n                'base_fieldset',\n                array(\n                    'legend' =\u003e Mage::helper('my_reports')-\u003e__('Filter')\n                )\n            );\n    \n            // prepare our filter fields and add each to the fieldset\n    \n            // date filter\n            $dateFormatIso  = Mage::app()\n                -\u003egetLocale()\n                -\u003egetDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT);\n            $fieldset-\u003eaddField('from', 'date', array(\n                'name' =\u003e 'from',\n                'format' =\u003e $dateFormatIso,\n                'image' =\u003e $this-\u003egetSkinUrl('images/grid-cal.gif'),\n                'label' =\u003e Mage::helper('my_reports')-\u003e__('From'),\n                'title' =\u003e Mage::helper('my_reports')-\u003e__('From')\n            ));\n            $fieldset-\u003eaddField('to', 'date', array(\n                'name' =\u003e 'to',\n                'format' =\u003e $dateFormatIso,\n                'image' =\u003e $this-\u003egetSkinUrl('images/grid-cal.gif'),\n                'label' =\u003e Mage::helper('my_reports')-\u003e__('To'),\n                'title' =\u003e Mage::helper('my_reports')-\u003e__('To')\n            ));\n            $fieldset-\u003eaddField('period_type', 'select', array(\n                'name' =\u003e 'period_type',\n                'options' =\u003e $this-\u003e_getPeriodTypeOptions(),\n                'label' =\u003e Mage::helper('my_reports')-\u003e__('Period')\n            ));\n    \n            // non-zero shipping rate filter\n            $fieldset-\u003eaddField('shipping_rate', 'select', array(\n                'name' =\u003e 'shipping_rate',\n                'options' =\u003e $this-\u003e_getShippingRateSelectOptions(),\n                'label' =\u003e Mage::helper('my_reports')-\u003e__('Show values where shipping rate greater than 0')\n            ));\n    \n            $form-\u003esetUseContainer(true);\n            $this-\u003esetForm($form);\n    \n            return $this;\n        }\n    \n        /**\n         * Get period type options\n         * @return array\n         */\n        protected function _getPeriodTypeOptions()\n        {\n            $options = array(\n                'day' =\u003e Mage::helper('my_reports')-\u003e__('Day'),\n                'month' =\u003e Mage::helper('my_reports')-\u003e__('Month'),\n                'year' =\u003e Mage::helper('my_reports')-\u003e__('Year'),\n            );\n    \n            return $options;\n        }\n    \n        /**\n         * Returns options for shipping rate select\n         * @return array\n         */\n        protected function _getShippingRateSelectOptions()\n        {\n            $options = array(\n                '0' =\u003e 'Any',\n                '1' =\u003e 'Specified'\n            );\n    \n            return $options;\n        }\n    \n        /**\n         * Inicialise form values\n         * Called after prepareForm, we apply the previously set values from filter on the form\n         * @return My_Reports_Block_Adminhtml_Filter_Form\n         */\n        protected function _initFormValues()\n        {\n            $filterData = $this-\u003egetFilterData();\n            $this-\u003egetForm()-\u003eaddValues($filterData-\u003egetData());\n            return parent::_initFormValues();\n        }\n    \n        /**\n         * Apply field visibility and field options on our form fields before rendering\n         * @return My_Reports_Block_Adminhtml_Filter_Form\n         */\n        protected function _beforeHtml()\n        {\n            $result = parent::_beforeHtml();\n    \n            $elements = $this-\u003egetForm()-\u003egetElements();\n    \n            // iterate on our elements and select fieldsets\n            foreach ($elements as $element) {\n                $this-\u003e_applyFieldVisibiltyAndOptions($element);\n            }\n    \n            return $result;\n        }\n    \n        /**\n         * Apply field visibility and options on fieldset element\n         * Recursive\n         * @param Varien_Data_Form_Element_Fieldset $element\n         * @return Varien_Data_Form_Element_Fieldset\n         */\n        protected function _applyFieldVisibiltyAndOptions($element) {\n            if ($element instanceof Varien_Data_Form_Element_Fieldset) {\n                foreach ($element-\u003egetElements() as $fieldElement) {\n                    // apply recursively\n                    if ($fieldElement instanceof Varien_Data_Form_Element_Fieldset) {\n                        $this-\u003e_applyFieldVisibiltyAndOptions($fieldElement);\n                        continue;\n                    }\n    \n                    $fieldId = $fieldElement-\u003egetId();\n                    // apply field visibility\n                    if (!$this-\u003egetFieldVisibility($fieldId)) {\n                        $element-\u003eremoveField($fieldId);\n                        continue;\n                    }\n    \n                    // apply field options\n                    if (isset($this-\u003e_fieldOptions[$fieldId])) {\n                        $fieldOptions = $this-\u003e_fieldOptions[$fieldId];\n                        foreach ($fieldOptions as $k =\u003e $v) {\n                            $fieldElement-\u003esetDataUsingMethod($k, $v);\n                        }\n                    }\n                }\n            }\n    \n            return $element;\n        }\n    \n    }\n\n\n## Collection\n\n Finally arrived to the point when we will code our last class: the collection. It will\ncollect our data which we would like to display in the grid rows. We should have to write some getters,\nthose ones which we already referenced to in the `_addCustomFilter()` method. The SQL query building starts\nin the `_initSelect()` method. It is originally called from the parent class' constructor, but it\nisn't fit for us this case, because the `isTotals` flag is set after the object has been\ninstantiated, we will move the select initialisation into the `_beforeLoad()` method.\n We should define the displayed columns in the `_getSelectedColumns()` method based on the `isTotals` flag's\nvalue. The `_getAggregatedColumns()` method builds the SQL query's columns part in totals mode. In the\noriginal Sales Report the aggregated columns are prepared in the grid in this format: \n`'columnId' =\u003e '{$total}({$columnId})'`, but I think building queries are the resource model's\nresponsibility; therefore I chose a different realisation (take a look at the `_getAggregatedColumn()` method).\n If you'd like to debug and see the actual queries, overwrite the `load()` method. The method's two\nparameters explains the functionality behind them. For a little hint you could take a look\nat `Varien_Data_Collection_Db::printLogQuery()`.\n\napp/code/local/My/Reports/Model/Mysql4/Report/Collection.php\n\n    \u003c?php\n    \n    class My_Reports_Model_Mysql4_Report_Collection\n        extends Mage_Core_Model_Mysql4_Collection_Abstract\n    {\n        // vars containing our filters' data\n    \n        /**\n         * Period type to group results by\n         * Could be day, month or year\n         * @var string\n         */\n        protected $_periodType;\n    \n        /**\n         * 'From Date' filter\n         * @var string\n         */\n        protected $_from;\n    \n        /**\n         * 'To Date' filter\n         * @var string\n         */\n        protected $_to;\n    \n        /**\n         * Filter only results where shipping rate is greater than zero\n         * @var bool\n         */\n        protected $_isShippingRateNonZeroOnly = false;\n    \n        /**\n         * Count totals (aggregated columns) only\n         * @var bool\n         */\n        protected $_isTotals = false;\n    \n        /**\n         * Aggregated columns to count totals\n         * In the format of: 'columnId' =\u003e 'total'\n         * @var array\n         */\n        protected $_aggregatedColumns = array();\n    \n        // define basic setup of our collection\n    \n        /**\n         * We should overwrite constructor to allow custom resources to use\n         * The original constructor calls _initSelect by default which isn't suits our \n         * needs, because the totals mode is set after instantiation of\n         * the collection object (therefore we will handle this case right before \n         * loading our collection).\n         */\n        public function __construct($resource = null)\n        {\n            $this-\u003esetModel('adminhtml/report_item');\n            $this-\u003esetResourceModel('sales/order');\n            $this-\u003esetConnection($this-\u003egetResource()-\u003egetReadConnection());\n        }\n    \n        // add filter methods\n    \n        /**\n         * Set period type\n         * @param string $periodType\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        public function setPeriodType($periodType)\n        {\n            $this-\u003e_periodType = $periodType;\n            return $this;\n        }\n    \n        /**\n         * Set date range to filter on\n         * @param string $from\n         * @param string $to\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        public function setDateRange($from, $to)\n        {\n            $this-\u003e_from = $from;\n            $this-\u003e_to = $to;\n            return $this;\n        }\n    \n        /**\n         * Setter/getter method for filtering items only with shipping rate greater than zero\n         * @param bool $bool by default null it returns the current state flag\n         * @return bool|My_Reports_Model_Mysql4_Report_Collection\n         */\n        public function isShippingRateNonZeroOnly($bool = null)\n        {\n            if (is_null($bool)) {\n                return $this-\u003e_isShippingRateNonZeroOnly;\n            }\n            $this-\u003e_isShippingRateNonZeroOnly = $bool ? true : false;\n            return $this;\n        }\n    \n        /**\n         * Set aggregated columns used in totals mode\n         * @param array $columns\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        public function setAggregatedColumns($columns)\n        {\n            $this-\u003e_aggregatedColumns = $columns;\n            return $this;\n        }\n    \n        /**\n         * Setter/getter for setting totals mode on collection\n         * By default the collection selects columns we display in the grid,\n         * by selecting this mode we will only query the aggregated columns\n         * @param bool $bool by default null it returns the current state of flag\n         * @return bool|My_Reports_Model_Mysql4_Report_Collection\n         */\n        public function isTotals($bool = null)\n        {\n            if (is_null($bool)) {\n                return $this-\u003e_isTotals;\n            }\n            $this-\u003e_isTotals = $bool ? true : false;\n            return $this;\n        }\n    \n        // prepare select\n    \n        /**\n         * Get selected columns depending on totals mode\n         */\n        protected function _getSelectedColumns() {\n            if ($this-\u003eisTotals()) {\n                $selectedColumns = $this-\u003e_getAggregatedColumns();\n            } else {\n                $selectedColumns = array(\n                    'created_at' =\u003e $this-\u003e_getPeriodFormat(),\n                    'base_grand_total' =\u003e 'SUM(base_grand_total)',\n                    'base_shipping_amount' =\u003e 'SUM(base_shipping_amount)',\n                    'shipping_rate' =\u003e 'AVG((base_shipping_amount / base_grand_total) * 100)',\n                    'base_currency_code' =\u003e 'base_currency_code',\n                );\n            }\n    \n            return $selectedColumns;\n        }\n    \n        /**\n         * Return aggregated columns\n         * This method uses ::_getAggregatedColumn for getting the db expression for the specified columnId\n         * @return array\n         */\n        protected function _getAggregatedColumns()\n        {\n            $aggregatedColumns = array();\n            foreach ($this-\u003e_aggregatedColumns as $columnId =\u003e $total) {\n                $aggregatedColumns[$columnId] = $this-\u003e_getAggregatedColumn($columnId, $total);\n            }\n            return $aggregatedColumns;\n        }\n    \n        /**\n         * Returns the db expression based on total mode and column ID\n         * @param string $columnId the column's ID used in expression\n         * @param string $total mode of aggregation (could be sum or avg)\n         * @return string\n         */\n        protected function _getAggregatedColumn($columnId, $total)\n        {\n            switch ($columnId) {\n                case 'shipping_rate' : {\n                    $expression = \"{$total}((base_shipping_amount / base_grand_total) * 100)\";\n                } break;\n                default : {\n                    $expression = \"{$total}({$columnId})\";\n                } break;\n            }\n    \n            return $expression;\n        }\n    \n        /**\n         * Get period format based on '_periodType'\n         * @return string\n         */\n        protected function _getPeriodFormat()\n        {\n            $adapter = $this-\u003egetConnection();\n            if ('month' == $this-\u003e_periodType) {\n                $periodFormat = 'DATE_FORMAT(created_at, \\'%Y-%m\\')';\n                // From Magento EE 1.12 you should use the adapter's appropriate method:\n                // $periodFormat = $adapter-\u003egetDateFormatSql('created_at', '%Y-%m');\n            } else if ('year' == $this-\u003e_periodType) {\n                $periodFormat = 'EXTRACT(YEAR FROM created_at)';\n                // From Magento EE 1.12 you should use the adapter's appropriate method:\n                // $periodFormat = $adapter-\u003egetDateExtractSql('created_at', Varien_Db_Adapter_Interface::INTERVAL_YEAR);\n            } else {\n                $periodFormat = 'created_at';\n                // From Magento EE 1.12 you should use the adapter's appropriate method:\n                // $periodFormat = $adapter-\u003egetDateFormatSql('created_at', '%Y-%m-%d');\n            }\n    \n            return $periodFormat;\n        }\n    \n        /**\n         * Prepare select statement depending on totals is on or off\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        protected function _initSelect()\n        {\n            $this-\u003egetSelect()-\u003ereset();\n    \n            // select aggregated columns only in totals; w/o grouping by period\n            $this-\u003egetSelect()-\u003efrom($this-\u003egetResource()-\u003egetMainTable(), $this-\u003e_getSelectedColumns());\n            if (!$this-\u003eisTotals()) {\n                $this-\u003egetSelect()-\u003egroup($this-\u003e_getPeriodFormat());\n            }\n    \n            return $this;\n        }\n    \n        // render filters\n    \n        /**\n         * Apply our date range filter on select\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        protected function _applyDateRangeFilter()\n        {\n            if (!is_null($this-\u003e_from)) {\n                $this-\u003e_from = date('Y-m-d G:i:s', strtotime($this-\u003e_from));\n                $this-\u003egetSelect()-\u003ewhere('created_at \u003e= ?', $this-\u003e_from);\n            }\n            if (!is_null($this-\u003e_to)) {\n                $this-\u003e_to = date('Y-m-d G:i:s', strtotime($this-\u003e_to));\n                $this-\u003egetSelect()-\u003ewhere('created_at \u003c= ?', $this-\u003e_to);\n            }\n    \n            return $this;\n        }\n    \n        /**\n         * Apply shipping rate filter\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        protected function _applyShippingRateNonZeroOnlyFilter()\n        {\n            if ($this-\u003e_isShippingRateNonZeroOnly) {\n                $this-\u003egetSelect()\n                    -\u003ewhere('((base_shipping_amount / base_grand_total) * 100) \u003e 0');\n            }\n        }\n    \n        /**\n         * Inicialise select right before loading collection\n         * We need to fire _initSelect here, because the isTotals mode creates different results depending\n         * on it's value. The parent implementation of the collection originally fires this method in the\n         * constructor.\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        protected function _beforeLoad()\n        {\n            $this-\u003e_initSelect();\n            return parent::_beforeLoad();\n        }\n    \n        /**\n         * This would render all of our pre-set filters on collection.\n         * Calling of this method happens in Varien_Data_Collection_Db::_renderFilters(), while\n         * the _renderFilters itself is called in Varien_Data_Collection_Db::load() before calling\n         * _renderOrders() and _renderLimit() .\n         * @return My_Reports_Model_Mysql4_Report_Collection\n         */\n        protected function _renderFiltersBefore()\n        {\n            $this\n                -\u003e_applyDateRangeFilter()\n                -\u003e_applyShippingRateNonZeroOnlyFilter();\n            return $this;\n        }\n    \n    }\n\n\n## Final words\n\n As you could see, it's not rocket science to create a report. However it could be scary at first, but\nI hope I could give you a better understanding of the process. Send me a beer if I was able to help you :)\nComments and opinions are more than welcome.\n The module is available on GitHub: https://github.com/technodelight/magento_custom_reports_example\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnodelight%2Fmagento_custom_reports_example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechnodelight%2Fmagento_custom_reports_example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnodelight%2Fmagento_custom_reports_example/lists"}