IR_Neatly_Reporting - Version 1.0.1.0

Version Notes

This latest release allows the extension to now support PHP 5.4.0

Download this release

Release Info

Developer Jon Leighton
Extension IR_Neatly_Reporting
Version 1.0.1.0
Comparing to
See all releases


Code changes from version 1.0.0.2 to 1.0.1.0

app/code/community/IR/Neatly/.DS_Store ADDED
Binary file
app/code/community/IR/Neatly/Exception/Api.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Neatly API Exception handler.
4
+ *
5
+ * @author Magento
6
+ */
7
+ class IR_Neatly_Exception_Api extends Exception
8
+ {
9
+ /**
10
+ * An array of error messages created during the API call.
11
+ *
12
+ * @var array
13
+ */
14
+ protected $errors = array();
15
+
16
+ /**
17
+ * Throw an API exception.
18
+ *
19
+ * @param mixed $errors
20
+ * @param mixed $code
21
+ */
22
+ public function __construct($errors, $code = 0)
23
+ {
24
+ // if response code passed as first argument
25
+ if (is_int($errors)) {
26
+ $code = $errors;
27
+ $errors = array();
28
+ }
29
+
30
+ parent::__construct('', $code);
31
+
32
+ $this->errors = (is_array($errors) ? $errors : array($errors));
33
+ }
34
+
35
+ /**
36
+ * Get an array of error messages.
37
+ *
38
+ * @return array
39
+ */
40
+ public function getErrors()
41
+ {
42
+ return $this->errors;
43
+ }
44
+ }
app/code/community/IR/Neatly/Helper/Data.php ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Neatly data helper.
4
+ *
5
+ * @author iResources
6
+ */
7
+ class IR_Neatly_Helper_Data extends Mage_Core_Helper_Data
8
+ {
9
+ const VERSION = '1.0.0.2';
10
+
11
+ /**
12
+ * Checks whether JSON can be rendered at the /neatly endpoint.
13
+ *
14
+ * @param integer|string|Mage_Core_Model_Store $store
15
+ * @return boolean
16
+ */
17
+ public function isEnabled($store = null)
18
+ {
19
+ $isActive = Mage::getConfig()->getModuleConfig('IR_Neatly')->is('active', 'true');
20
+ $isEnabled = !Mage::getStoreConfig('advanced/modules_disable_output/IR_Neatly');
21
+
22
+ return $isActive && $isEnabled;
23
+ }
24
+
25
+ /**
26
+ * Checks whether the passed API token is valid.
27
+ *
28
+ * @param string $passedApiToken
29
+ * @return bool
30
+ */
31
+ public function isApiTokenValid($passedApiToken = null)
32
+ {
33
+ if (!$passedApiToken) {
34
+ return false;
35
+ }
36
+
37
+ if (!$apiToken = Mage::helper('core')->decrypt(Mage::getStoreConfig('ir_neatly/security/neatly_api_token'))) {
38
+ return false;
39
+ }
40
+
41
+ if ($passedApiToken !== $apiToken) {
42
+ return false;
43
+ }
44
+
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Get the plugin version.
50
+ *
51
+ * @return string
52
+ */
53
+ public function getVersion()
54
+ {
55
+ return self::VERSION;
56
+ }
57
+ }
app/code/community/IR/Neatly/Model/Reports/Abstract.php ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class IR_Neatly_Model_Reports_Abstract
4
+ {
5
+ /**
6
+ * @var Mage_Core_Model_Resource
7
+ */
8
+ protected $resource;
9
+
10
+ /**
11
+ * @var Magento_Db_Adapter_Pdo_Mysql
12
+ */
13
+ protected $readConnection;
14
+
15
+ /**
16
+ * @var array
17
+ */
18
+ public $options;
19
+
20
+ /**
21
+ * Set resource and read connection properties.
22
+ */
23
+ public function __construct($options = array())
24
+ {
25
+ $this->resource = Mage::getSingleton('core/resource');
26
+ $this->readConnection = $this->resource->getConnection('core_read');
27
+
28
+ if (!$this->readConnection instanceof Magento_Db_Adapter_Pdo_Mysql &&
29
+ !$this->readConnection instanceof Varien_Db_Adapter_Pdo_Mysql) {
30
+ throw new Exception('The Neatly Magento extension only supports MySQL databases.');
31
+ }
32
+
33
+ // set options.
34
+ $this->setOptions($options);
35
+
36
+ // if a valid order not set.
37
+ if (!in_array($this->options['order'], array('ASC', 'DESC'))) {
38
+ $this->options['order'] = 'DESC';
39
+ }
40
+
41
+ $groups = array('DAY', 'MONTH', 'YEAR');
42
+
43
+ if (!in_array(strtoupper($this->options['group_by']), $groups)) {
44
+ $this->options['group_by'] = 'DAY';
45
+ }
46
+
47
+ switch ($this->options['group_by']) {
48
+ case 'YEAR':
49
+ $this->options['date_format'] = '%Y';
50
+ break;
51
+ case 'MONTH':
52
+ $this->options['date_format'] = '%Y-%m';
53
+ default:
54
+ $this->options['date_format'] = '%Y-%m-%d';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get pagination data for a given total.
60
+ *
61
+ * @param int $total
62
+ * @param array $data
63
+ * @return stdClass
64
+ */
65
+ public function getPagination($total, $data)
66
+ {
67
+ $lastPage = 0;
68
+
69
+ $from = (($this->options['page'] - 1) * $this->options['page_size']) + 1;
70
+ $to = ($from - 1) + count($data);
71
+
72
+ $lastPage = ceil($total / $this->options['page_size']);
73
+
74
+ return (object)array(
75
+ 'total'=> (int)$total,
76
+ 'total_on_this_page' => (int)count($data),
77
+ 'per_page'=> (int)$this->options['page_size'],
78
+ 'current_page'=> (int)$this->options['page'],
79
+ 'last_page'=> (int)$lastPage,
80
+ 'from'=> (int)$from,
81
+ 'to'=> (int)$to,
82
+ 'data' => $data
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Get a set of dates between 2 given ranges.
88
+ *
89
+ * @param string|int $from
90
+ * @param string|int $to
91
+ * @param string $range
92
+ */
93
+ public function getDates($from, $to, $range = 'DAY')
94
+ {
95
+ if (!in_array($range, array(
96
+ 'DAY',
97
+ 'WEEK',
98
+ 'MONTH',
99
+ 'YEAR',
100
+ ))) {
101
+ $range = 'DAY';
102
+ }
103
+
104
+ $from = gmdate("Y-m-d", strtotime($from));
105
+ $to = gmdate("Y-m-d", strtotime($to));
106
+
107
+ $dates[] = $from;
108
+
109
+ $currentDate = $from;
110
+
111
+ // While the current date is less than the end date
112
+ while ($currentDate < $to) {
113
+ $currentDate = gmdate("Y-m-d", strtotime("+1 {$range}", strtotime($currentDate)));
114
+ $dates[] = $currentDate;
115
+ }
116
+
117
+ return $dates;
118
+ }
119
+
120
+ /**
121
+ * Combine a set of dates and a set of objects from a database query. The
122
+ * objects passed must contain a "date" property.
123
+ *
124
+ * @param array $dateRange
125
+ * @param array $objects
126
+ * @param array $keys The properties you want to use from the objects.
127
+ * @return array
128
+ */
129
+ public function datesCombine($dateRange, $objects, $keys)
130
+ {
131
+ $dates = array();
132
+
133
+ // remove date from keys.
134
+ unset($keys[array_search('date', $keys)]);
135
+
136
+ foreach ($dateRange as $i => $date) {
137
+ $from = strtotime($date);
138
+ $to = strtotime("{$dateRange[$i]} 23:59:59");
139
+
140
+ foreach ($objects as $object) {
141
+ $period = strtotime($object['date']);
142
+ if ($period >= $from && $period < $to) {
143
+ foreach ($keys as $key) {
144
+ $dates[$date][$key] = is_numeric($object[$key]) ? floatval($object[$key]) : $object[$key];
145
+ }
146
+ } elseif (!isset($dates[$date])) { // if date has not already been set by a previous object.
147
+ foreach ($keys as $key) {
148
+ $dates[$date][$key] = 0;
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return $dates;
155
+ }
156
+
157
+ /**
158
+ * Append other.
159
+ *
160
+ * @param int $total
161
+ * @param array $objects
162
+ * @return array
163
+ */
164
+ public function appendOther($totals, $objects)
165
+ {
166
+ $total['total'] = 0;
167
+ $total['percentage'] = 0;
168
+ $total['value'] = 0;
169
+
170
+ foreach ($objects as $object) {
171
+ $total['total'] += $object['total'];
172
+ $total['percentage'] += $object['total_percentage'];
173
+ $total['value'] += $object['total_value'];
174
+ }
175
+
176
+ // if there are no "other" values there is no need to append.
177
+ if (($totals['total'] - $total['total']) == 0) {
178
+ return $objects;
179
+ }
180
+
181
+ $objects[] = array(
182
+ 'total' => $totals['total'] - $total['total'],
183
+ 'total_percentage' => 100 - $total['percentage'],
184
+ 'total_value' => $totals['total_value'] - $total['value'],
185
+ 'name' => 'Other',
186
+ );
187
+
188
+ return $objects;
189
+ }
190
+
191
+ /**
192
+ * Set report options.
193
+ *
194
+ * @param array $options
195
+ */
196
+ public function setOptions($options = array())
197
+ {
198
+ // set global default options.
199
+ $this->options = array_merge(array(
200
+ 'from' => null,
201
+ 'to' => null,
202
+ 'sort' => null,
203
+ 'order' => 'DESC',
204
+ 'page_size' => 10,
205
+ 'page' => 1,
206
+ 'group_by' => 'DAY',
207
+ 'date_format' => '%Y-%m-%d',
208
+ 'store_id' => null,
209
+ ), $options);
210
+ }
211
+ }
app/code/community/IR/Neatly/Model/Reports/Customers.php ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class IR_Neatly_Model_Reports_Customers extends IR_Neatly_Model_Reports_Abstract
4
+ {
5
+ /**
6
+ * Get total new vs exsiting customers grouped by dates.
7
+ *
8
+ * @return array
9
+ */
10
+ public function getNewVsReturningCustomers()
11
+ {
12
+ $options = array_merge(array(
13
+ 'status' => 'complete'
14
+ ), $this->options);
15
+
16
+ $query = $this->readConnection->select();
17
+
18
+ $columns = array(
19
+ // where customer entity doesn't exist or difference between order and customer created date is 0
20
+ 'total_new' => 'SUM(IF(c.entity_id IS NULL OR DATEDIFF(sfo.created_at, c.created_at) = 0, 1, 0))',
21
+ // where customer entity exist AND difference between order and customer created date is greater than 0
22
+ 'total_existing' => 'SUM(IF(c.entity_id IS NOT NULL AND DATEDIFF(sfo.created_at, c.created_at) > 0, 1, 0))',
23
+ );
24
+
25
+ // add period column
26
+ $columns['date'] = 'DATE(sfo.created_at)';
27
+
28
+ // add group by clause.
29
+ # $query->group("{$options['group_by']}(sfo.created_at)");
30
+ $query->group("DATE_FORMAT(sfo.created_at, '{$options['date_format']}')");
31
+
32
+ if ($this->options['from']) {
33
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
34
+ }
35
+
36
+ if ($this->options['to']) {
37
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
38
+ }
39
+
40
+ if ($options['status']) {
41
+ $query->where('sfo.status = ?', $options['status']);
42
+ }
43
+
44
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
45
+
46
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
47
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
48
+
49
+ $customersTbl = $this->resource->getTableName('customer/entity');
50
+ $query->joinLeft(array('c' => $customersTbl), 'c.entity_id = sfo.customer_id', array());
51
+
52
+ $groups = $this->readConnection->fetchAll($query);
53
+
54
+ $dateRange = $this->getDates($options['from'], $options['to'], $options['group_by']);
55
+
56
+ return $this->datesCombine($dateRange, $groups, array_keys($columns));
57
+ }
58
+
59
+ /**
60
+ * Find out how many customers have ordered a total number of items. For
61
+ * example "2 customers ordered 35 products, 10 customers ordered 2
62
+ * products, etc".
63
+ *
64
+ * @return array
65
+ */
66
+ public function getProductsPerCustomer()
67
+ {
68
+ $options = array_merge(array(
69
+ 'status' => 'complete'
70
+ ), $this->options);
71
+
72
+ $query = $this->readConnection->select();
73
+
74
+ $columns = array(
75
+ 'total_qty_ordered',
76
+ 'total_customers' => 'COUNT(*)',
77
+ 'total_invoiced' => 'SUM(total_invoiced)',
78
+ 'total_refunded' => 'SUM(total_refunded)',
79
+ 'total' => 'IF(SUM(total_refunded), SUM(total_invoiced) - SUM(total_refunded), SUM(total_invoiced))'
80
+ );
81
+
82
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
83
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
84
+
85
+ if ($this->options['from']) {
86
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
87
+ }
88
+
89
+ if ($this->options['to']) {
90
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
91
+ }
92
+
93
+ if ($options['status']) {
94
+ $query->where('sfo.status = ?', $options['status']);
95
+ }
96
+
97
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
98
+
99
+ $query->limitPage($this->options['page'], $this->options['page_size']);
100
+ $query->order("total_qty_ordered {$options['order']}");
101
+ $query->group('total_qty_ordered');
102
+
103
+ $data = $this->readConnection->fetchAll($query);
104
+
105
+ return $data;
106
+ }
107
+
108
+ /**
109
+ * Get total orders by customer groups.
110
+ *
111
+ * @return array
112
+ */
113
+ public function getOrdersByCustomerGroup($count = false)
114
+ {
115
+ $options = array_merge(array(
116
+ 'status' => 'complete'
117
+ ), $this->options);
118
+
119
+ $query = $this->readConnection->select();
120
+
121
+ $columns = array(
122
+ 'total' => 'COUNT(*)',
123
+ );
124
+
125
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
126
+ $customersTbl = $this->resource->getTableName('customer/entity');
127
+ $customerGroupTbl = $this->resource->getTableName('customer_group');
128
+
129
+ if ($count) {
130
+ $columns = array(
131
+ 'total' => 'COUNT(sfo.entity_id)',
132
+ 'total_value' => 'SUM(sfo.grand_total)'
133
+ );
134
+
135
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
136
+ $query->join(array('c' => $customersTbl), 'c.entity_id = sfo.customer_id', array());
137
+ $query->join(array('cg' => $customerGroupTbl), 'cg.customer_group_id = c.group_id');
138
+
139
+ } else {
140
+ // get totals.
141
+ $totals = $this->getOrdersByCustomerGroup($options, true);
142
+
143
+ $columns = array(
144
+ 'total' => 'COUNT(sfo.entity_id)',
145
+ 'total_percentage' => "ROUND((COUNT(sfo.entity_id) / {$totals['total']}) * 100, 2)",
146
+ 'total_value' => 'SUM(sfo.grand_total)',
147
+ );
148
+
149
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
150
+ $query->join(array('c' => $customersTbl), 'c.entity_id = sfo.customer_id', array());
151
+ $query->join(array('cg' => $customerGroupTbl), 'cg.customer_group_id = c.group_id', array('name' => 'cg.customer_group_code'));
152
+
153
+ $query->limitPage($this->options['page'], 24);
154
+ $query->group('cg.customer_group_id');
155
+ $query->order('total DESC');
156
+ }
157
+
158
+ if ($this->options['from']) {
159
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
160
+ }
161
+
162
+ if ($this->options['to']) {
163
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
164
+ }
165
+
166
+ if ($options['status']) {
167
+ $query->where('sfo.status = ?', $options['status']);
168
+ }
169
+
170
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
171
+
172
+ $data = $this->readConnection->fetchAll($query);
173
+
174
+ if ($count) {
175
+ return $data[0];
176
+ } else {
177
+ return $this->appendOther($totals, $data);
178
+ }
179
+ }
180
+ }
app/code/community/IR/Neatly/Model/Reports/Reporting.php ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class IR_Neatly_Model_Reports_Reporting extends IR_Neatly_Model_Reports_Abstract
4
+ {
5
+ /**
6
+ * Get customers.
7
+ *
8
+ * @return mixed
9
+ */
10
+ public function getCustomers($count = false, $options = array())
11
+ {
12
+ $options = array_merge(array(
13
+ 'status' => 'complete',
14
+ 'item_total' => null,
15
+ 'item_total_operator' => null,
16
+ 'order_value' => null,
17
+ 'order_value_operator' => null,
18
+ 'sku' => null,
19
+ 'customer_email' => null,
20
+ 'city' => null,
21
+ 'region' => null,
22
+ 'postcode' => null,
23
+ 'country_id' => null,
24
+ ), $this->options, $options);
25
+
26
+ // if asking for "show me customers who have not placed an order between these dates".
27
+ if ((in_array($options['item_total_operator'], array('=', '<', '<=')) && $options['item_total'] === '0') ||
28
+ (in_array($options['order_value_operator'], array('=', '<', '<=')) && $options['order_value'] === '0')) {
29
+ return $this->getCustomersWithoutOrder($count, $options);
30
+ }
31
+
32
+ // new query.
33
+ $query = $this->readConnection->select();
34
+
35
+ // get table names.
36
+ $customersTbl = $this->resource->getTableName('customer_entity');
37
+ $addressTbl = $this->resource->getTableName('sales/order_address');
38
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
39
+ $itemsTbl = $this->resource->getTableName('sales/order_item');
40
+
41
+ if ($count) {
42
+ $columns = array(
43
+ 'total' => 'COUNT(DISTINCT(sfo.customer_email))',
44
+ );
45
+
46
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
47
+ $query->joinLeft(array('sfoa' => $addressTbl), 'sfoa.entity_id = sfo.billing_address_id', array());
48
+ } else {
49
+ $columns = array(
50
+ 'customer_email' => 'DISTINCT(customer_email)',
51
+ 'customer_firstname',
52
+ 'customer_lastname',
53
+ 'customer_middlename',
54
+ 'customer_prefix',
55
+ 'customer_suffix',
56
+ 'customer_is_guest',
57
+ );
58
+
59
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
60
+
61
+ $columns = array(
62
+ 'street',
63
+ 'city',
64
+ 'region',
65
+ 'postcode',
66
+ 'country_id',
67
+ 'telephone',
68
+ );
69
+
70
+ $query->joinLeft(array('sfoa' => $addressTbl), 'sfoa.entity_id = sfo.billing_address_id', $columns);
71
+
72
+ $query->limitPage($options['page'], $options['page_size']);
73
+ }
74
+
75
+ # $query->where('sfo.store_id = ?', $this->options['store_id']);
76
+
77
+ if ($options['status']) {
78
+ $query->where('status = ?', $options['status']);
79
+ }
80
+
81
+ if ($options['from']) {
82
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
83
+ }
84
+
85
+ if ($options['to']) {
86
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
87
+ }
88
+
89
+ if ($options['order_value']) {
90
+ $operator = $this->validOperator($options['order_value_operator']);
91
+ $query->where("sfo.grand_total {$operator} ?", $options['order_value']);
92
+ }
93
+
94
+ if ($options['item_total']) {
95
+ $operator = $this->validOperator($options['item_total_operator']);
96
+ $query->where("sfo.total_qty_ordered {$operator} ?", $options['item_total']);
97
+ }
98
+
99
+ if ($options['customer_email']) {
100
+ $query->where('sfo.customer_email = ?', $options['customer_email']);
101
+ }
102
+
103
+ if ($options['city']) {
104
+ $query->where('sfoa.city = ?', $options['city']);
105
+ }
106
+
107
+ if ($options['region']) {
108
+ $query->where('sfoa.region = ?', $options['region']);
109
+ }
110
+
111
+ if ($options['postcode']) {
112
+ $query->where('sfoa.postcode = ?', $options['postcode']);
113
+ }
114
+
115
+ if ($options['country_id']) {
116
+ $query->where('sfoa.country_id = ?', $options['country_id']);
117
+ }
118
+
119
+ if ($options['sku']) {
120
+ // if sku is not an array.
121
+ if (!is_array($options['sku'])) {
122
+ $options['sku'] = array($options['sku']);
123
+ }
124
+
125
+ $marks = array();
126
+ foreach ($options['sku'] as $sku) {
127
+ $marks[] = "?";
128
+ }
129
+
130
+ $in = implode(',', $marks);
131
+
132
+ $sql = "EXISTS (SELECT
133
+ sku
134
+ FROM
135
+ {$itemsTbl} i
136
+ WHERE
137
+ i.order_id = sfo.entity_id
138
+ AND parent_item_id IS NULL
139
+ AND sku IN ({$in}))";
140
+
141
+ $query->where($sql, $options['sku']);
142
+ }
143
+
144
+ $data = $this->readConnection->fetchAll($query);
145
+
146
+ if ($count) {
147
+ return $data[0]['total'];
148
+ } else {
149
+ return $data;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Search for customers who have not placed an order.
155
+ *
156
+ * @param bool $count
157
+ * @param array $options
158
+ * @return int|array.
159
+ */
160
+ protected function getCustomersWithoutOrder($count = false, $options = array())
161
+ {
162
+ $c = Mage::getSingleton('core/resource')->getConnection('default_write');
163
+
164
+ $onWhere = array('sfo.status = "complete"');
165
+
166
+ if ($options['from']) {
167
+ $onWhere[] = sprintf('DATE(sfo.created_at) >= %s', $c->quote($this->options['from']));
168
+ }
169
+
170
+ if ($options['to']) {
171
+ $onWhere[] = sprintf('DATE(sfo.created_at) <= %s', $c->quote($this->options['to']));
172
+ }
173
+
174
+ $collection = Mage::getResourceModel('customer/customer_collection')
175
+ ->addAttributeToSelect('customer_id')
176
+ ->addNameToSelect()
177
+ ->addAttributeToSelect('email')
178
+ ->addAttributeToSelect('created_at')
179
+ ->addAttributeToSelect('group_id')
180
+ ->joinAttribute('billing_postcode', 'customer_address/postcode', 'default_billing', null, 'left')
181
+ ->joinAttribute('billing_city', 'customer_address/city', 'default_billing', null, 'left')
182
+ ->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left')
183
+ ->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left')
184
+ ->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left')
185
+ ->joinTable(
186
+ array('sfo' => 'sales/order'),
187
+ 'customer_id = entity_id',
188
+ array('customer_id'),
189
+ implode(" AND ", $onWhere) ?: null,
190
+ 'left'
191
+ )
192
+ ->distinct(true);
193
+
194
+ $collection->getSelect()->where('sfo.customer_id IS NULL');
195
+
196
+ if ($options['customer_email']) {
197
+ $collection->addAttributeToFilter('email', array('eq' => $options['customer_email']));
198
+ }
199
+
200
+ if ($options['city']) {
201
+ $collection->addAttributeToFilter('billing_city', array('eq' => $options['city']));
202
+ }
203
+
204
+ if ($options['region']) {
205
+ $collection->addAttributeToFilter('billing_region', array('eq' => $options['region']));
206
+ }
207
+
208
+ if ($options['postcode']) {
209
+ $collection->addAttributeToFilter('billing_postcode', array('eq' => $options['postcode']));
210
+ }
211
+
212
+ if ($options['country_id']) {
213
+ $collection->addAttributeToFilter('billing_country_id', array('eq' => $options['country_id']));
214
+ }
215
+
216
+ if ($count) {
217
+ return $collection->getSize();
218
+ }
219
+
220
+ // set limit.
221
+ $collection->getSelect()->limit($options['page_size'], ($options['page_size'] * ($options['page'] - 1)));
222
+
223
+ $customers = array();
224
+
225
+ foreach ($collection as $customer) {
226
+ $customers[] = array(
227
+ 'customer_email' => isset($customer['email']) ? $customer['email'] : null,
228
+ 'customer_firstname' => isset($customer['firstname']) ? $customer['firstname'] : null,
229
+ 'customer_lastname' => isset($customer['lastname']) ? $customer['lastname'] : null,
230
+ 'customer_middlename' => isset($customer['middlename']) ? $customer['middlename'] : null,
231
+ 'customer_prefix' => isset($customer['prefix']) ? $customer['prefix'] : null,
232
+ 'customer_suffix' => isset($customer['suffix']) ? $customer['suffix'] : null,
233
+ 'customer_is_guest' => 0,
234
+ 'street' => isset($customer['billing_street']) ? $customer['billing_street'] : null,
235
+ 'city' => isset($customer['billing_city']) ? $customer['billing_city'] : null,
236
+ 'region' => isset($customer['billing_region']) ? $customer['billing_region'] : null,
237
+ 'postcode' => isset($customer['billing_postcode']) ? $customer['billing_postcode'] : null,
238
+ 'country_id' => isset($customer['billing_country_id']) ? $customer['billing_country_id'] : null,
239
+ 'telephone' => isset($customer['telephone']) ? $customer['telephone'] : '',
240
+ );
241
+ }
242
+
243
+ return $customers;
244
+ }
245
+
246
+ /**
247
+ * Check to see whether the passed comparison operator is valid.
248
+ *
249
+ * @param string $operator
250
+ * @return string
251
+ */
252
+ protected function validOperator($operator)
253
+ {
254
+ $operators = array(
255
+ ">",
256
+ ">=",
257
+ "<=",
258
+ "=",
259
+ "!="
260
+ );
261
+
262
+ // if operator is not valid.
263
+ if (!in_array($operator, $operators)) {
264
+ // return default operator.
265
+ return '=';
266
+ }
267
+
268
+ return $operator;
269
+ }
270
+ }
app/code/community/IR/Neatly/Model/Reports/Sales.php ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class IR_Neatly_Model_Reports_Sales extends IR_Neatly_Model_Reports_Abstract
4
+ {
5
+ /**
6
+ * Get total orders for a given date range aggregated by status.
7
+ *
8
+ * @return mixed
9
+ */
10
+ public function getTotalOrders()
11
+ {
12
+ $options = array_merge(array(
13
+ 'status' => 'complete',
14
+ ), $this->options);
15
+
16
+ // new query.
17
+ $query = $this->readConnection->select();
18
+
19
+ $columns = array(
20
+ 'total_orders' => 'COUNT(*)',
21
+ 'total_orders_value' => 'SUM(grand_total)',
22
+ 'date' => 'created_at'
23
+ );
24
+
25
+ if ($this->options['from']) {
26
+ $query->where('DATE(created_at) >= ?', $this->options['from']);
27
+ }
28
+
29
+ if ($this->options['to']) {
30
+ $query->where('DATE(created_at) <= ?', $this->options['to']);
31
+ }
32
+
33
+ if ($options['status']) {
34
+ $query->where('status = ?', $options['status']);
35
+ }
36
+
37
+ $query->group("DATE_FORMAT(`created_at`, '{$options['date_format']}')");
38
+
39
+ $query->where('store_id = ?', $this->options['store_id']);
40
+
41
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
42
+
43
+ $query->from($salesOrderTbl, $columns);
44
+
45
+ $data = $this->readConnection->fetchAll($query);
46
+
47
+ $dateRange = $this->getDates($options['from'], $options['to'], $options['group_by']);
48
+
49
+ return $this->datesCombine($dateRange, $data, array_keys($columns));
50
+ }
51
+
52
+ /**
53
+ * Get aggregated total sales data.
54
+ *
55
+ * @return array
56
+ */
57
+ public function getAggregatedTotals()
58
+ {
59
+ $options = array_merge(array(
60
+ 'status' => 'complete',
61
+ ), $this->options);
62
+
63
+ // new query.
64
+ $query = $this->readConnection->select();
65
+
66
+ $columns = array(
67
+ 'total_orders' => 'COUNT(*)',
68
+ 'total_items_ordered' => 'SUM(total_qty_ordered)',
69
+ 'total_tax' => 'SUM(tax_amount) - SUM(tax_canceled)',
70
+ #'total_tax_amount_actual' => 'SUM(tax_invoiced) - SUM(tax_refunded)',
71
+ 'total_discounts' => 'SUM(discount_amount) - SUM(discount_canceled)',
72
+ #'total_discounts_actual' => 'SUM(discount_invoiced) - SUM(discount_refunded)',
73
+ 'total_shipping' => 'SUM(shipping_amount) - SUM(shipping_canceled)',
74
+ #'total_shipping_actual' => 'SUM(shipping_invoiced) - SUM(shipping_refunded)',
75
+ 'total_revenue' => 'SUM(total_paid) - SUM(total_refunded)',
76
+ 'total_paid_amount' => 'SUM(total_paid)',
77
+ 'total_refunded' => 'SUM(total_refunded)',
78
+ 'total_cancelled' => 'SUM(total_canceled)',
79
+ 'date' => 'created_at'
80
+ );
81
+
82
+ $query = $this->readConnection->select();
83
+
84
+ if ($this->options['from']) {
85
+ $query->where('DATE(created_at) >= ?', $this->options['from']);
86
+ }
87
+
88
+ if ($this->options['to']) {
89
+ $query->where('DATE(created_at) <= ?', $this->options['to']);
90
+ }
91
+
92
+ if ($options['status']) {
93
+ $query->where('status = ?', $options['status']);
94
+ }
95
+
96
+ $query->where('store_id = ?', $this->options['store_id']);
97
+
98
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
99
+
100
+ $query->from($salesOrderTbl, $columns);
101
+
102
+ $data = $this->readConnection->fetchAll($query);
103
+
104
+ foreach ($data[0] as $key => $val) {
105
+ if (!$data[0][$key]) {
106
+ $data[0][$key] = 0;
107
+ } elseif (is_numeric($data[0][$key])) {
108
+ $data[0][$key] = floatval($data[0][$key]);
109
+ }
110
+ }
111
+
112
+ return $data[0];
113
+ }
114
+
115
+ /**
116
+ * Get best selling products.
117
+ *
118
+ * @return array
119
+ */
120
+ public function getBestSellingProducts($count = false)
121
+ {
122
+ $options = array_merge(array(
123
+ 'status' => 'complete'
124
+ ), $this->options);
125
+
126
+ $query = $this->readConnection->select();
127
+
128
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
129
+ $salesOrderItemTbl = $this->resource->getTableName('sales/order_item');
130
+
131
+ if ($count) {
132
+ $columns = array(
133
+ 'total' => 'COUNT(sfoi.product_id)',
134
+ 'total_value' => 'SUM(sfoi.price)'
135
+ );
136
+
137
+ $query->from(array('sfoi' => $salesOrderItemTbl), $columns);
138
+ $query->join(array('sfo' => $salesOrderTbl), 'sfo.entity_id = sfoi.order_id', array());
139
+ } else {
140
+
141
+ // get totals.
142
+ $totals = $this->getBestSellingProducts(true);
143
+
144
+ $columns = array(
145
+ 'total' => 'COUNT(sfoi.item_id)',
146
+ 'total_percentage' => "ROUND((COUNT(sfoi.item_id) / {$totals['total']}) * 100, 2)",
147
+ 'total_value' => 'SUM(sfoi.price)',
148
+ 'name' => 'CONCAT(sfoi.name, " (", sfoi.sku, ")")',
149
+ );
150
+
151
+ $query->limitPage($this->options['page'], $this->options['page_size']);
152
+ $query->from(array('sfoi' => $salesOrderItemTbl), $columns);
153
+ $query->join(array('sfo' => $salesOrderTbl), 'sfo.entity_id = sfoi.order_id', array());
154
+ $query->group("sfoi.product_id");
155
+ $query->order('total DESC');
156
+ }
157
+
158
+ if ($this->options['from']) {
159
+ $query->where('DATE(sfoi.created_at) >= ?', $this->options['from']);
160
+ }
161
+
162
+ if ($this->options['to']) {
163
+ $query->where('DATE(sfoi.created_at) <= ?', $this->options['to']);
164
+ }
165
+
166
+ if ($options['status']) {
167
+ $query->where('sfo.status = ?', $options['status']);
168
+ }
169
+
170
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
171
+
172
+ $data = $this->readConnection->fetchAll($query);
173
+
174
+ if ($count) {
175
+ return $data[0];
176
+ } else {
177
+ return $this->appendOther($totals, $data);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Get total orders by group type.
183
+ *
184
+ * @param array $options
185
+ * @param bool $count
186
+ * @return mixed
187
+ */
188
+ public function getOrdersByGroup($options, $count = false)
189
+ {
190
+ $options = array_merge(array(
191
+ 'status' => 'complete',
192
+ 'group' => 'sfoa.country_id'
193
+ ), $this->options, $options);
194
+
195
+ $query = $this->readConnection->select();
196
+
197
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
198
+ $salesOrderAddressTbl = $this->resource->getTableName('sales/order_address');
199
+ $salesOrderPaymentTbl = $this->resource->getTableName('sales/order_payment');
200
+ $query->join(array('sfoa' => $salesOrderAddressTbl), 'sfoa.parent_id = sfo.entity_id AND sfoa.address_type = "billing"', array());
201
+ $query->join(array('sfop' => $salesOrderPaymentTbl), 'sfop.parent_id = sfo.entity_id', array());
202
+
203
+ if ($count) {
204
+ $columns = array(
205
+ 'total' => 'COUNT(*)',
206
+ 'total_value' => 'SUM(sfo.grand_total)'
207
+ );
208
+
209
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
210
+
211
+ } else {
212
+ // get totals.
213
+ $totals = $this->getOrdersByGroup($options, true);
214
+
215
+ $columns = array(
216
+ 'total' => 'COUNT(*)',
217
+ 'total_percentage' => "ROUND((COUNT(sfo.entity_id) / {$totals['total']}) * 100, 2)",
218
+ 'total_value' => 'SUM(sfo.grand_total)',
219
+ 'name' => $options['group'],
220
+ );
221
+
222
+ $query->limitPage($this->options['page'], $this->options['page_size']);
223
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
224
+ $query->group($options['group']);
225
+ $query->order('total DESC');
226
+ }
227
+
228
+ if ($this->options['from']) {
229
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
230
+ }
231
+
232
+ if ($this->options['to']) {
233
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
234
+ }
235
+
236
+ if ($options['status']) {
237
+ $query->where('sfo.status = ?', $options['status']);
238
+ }
239
+
240
+ $query->where('sfo.status != ?', 'canceled');
241
+
242
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
243
+
244
+ $data = $this->readConnection->fetchAll($query);
245
+
246
+ if ($count) {
247
+ return $data[0];
248
+ } else {
249
+ return $this->appendOther($totals, $data);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Get total orders by group type.
255
+ *
256
+ * @param array $options
257
+ * @param bool $count
258
+ * @return mixed
259
+ */
260
+ public function getOrdersByPeriod($options, $count = false)
261
+ {
262
+ $options = array_merge(array(
263
+ 'status' => 'complete',
264
+ 'period' => 'hour'
265
+ ), $this->options, $options);
266
+
267
+ $periods = array(
268
+ 'hour',
269
+ 'dayname'
270
+ );
271
+
272
+ if (!in_array($options['period'], $periods)) {
273
+ $options['period'] = 'hour';
274
+ }
275
+
276
+ $query = $this->readConnection->select();
277
+
278
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
279
+
280
+ if ($count) {
281
+ $columns = array(
282
+ 'total' => 'COUNT(*)',
283
+ 'total_value' => 'SUM(sfo.grand_total)'
284
+ );
285
+
286
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
287
+
288
+ } else {
289
+ // get totals.
290
+ $totals = $this->getOrdersByPeriod($options, true);
291
+
292
+ $columns = array(
293
+ 'total' => 'COUNT(*)',
294
+ 'total_percentage' => "ROUND((COUNT(sfo.entity_id) / {$totals['total']}) * 100, 2)",
295
+ 'total_value' => 'SUM(sfo.grand_total)',
296
+ 'name' => "{$options['period']}(created_at)",
297
+ );
298
+
299
+ if ($options['period'] === 'hour') {
300
+ $columns['name'] = "DATE_FORMAT(created_at, '%k')";
301
+ }
302
+
303
+ // limit to 24 results (covers 7 days, covers 24 hours)
304
+ $query->limitPage($this->options['page'], 24);
305
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
306
+ $query->group("name");
307
+ $query->order('total DESC');
308
+ }
309
+
310
+ if ($this->options['from']) {
311
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
312
+ }
313
+
314
+ if ($this->options['to']) {
315
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
316
+ }
317
+
318
+ if ($options['status']) {
319
+ $query->where('sfo.status = ?', $options['status']);
320
+ }
321
+
322
+ $query->where('sfo.status != ?', 'canceled');
323
+
324
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
325
+
326
+ $data = $this->readConnection->fetchAll($query);
327
+
328
+ if ($count) {
329
+ return $data[0];
330
+ } else {
331
+ // if hour, get all 24 hours.
332
+ if ($options['period'] === 'hour') {
333
+ return $this->parseHours($data);
334
+ }
335
+ return $data;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Get total orders.
341
+ *
342
+ * @return int
343
+ */
344
+ public function getTotal()
345
+ {
346
+ $options = array_merge(array(
347
+ 'status' => null
348
+ ), $this->options);
349
+
350
+ $query = $this->readConnection->select();
351
+
352
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
353
+ $salesOrderItemTbl = $this->resource->getTableName('sales/order_item');
354
+
355
+ $columns = array('total' => 'COUNT(*)');
356
+
357
+ $query->from(array('sfoi' => $salesOrderItemTbl), $columns);
358
+ $query->join(array('sfo' => $salesOrderTbl), 'sfo.entity_id = sfoi.order_id', array());
359
+
360
+ if ($this->options['from']) {
361
+ $query->where('DATE(sfoi.created_at) >= ?', $this->options['from']);
362
+ }
363
+
364
+ if ($this->options['to']) {
365
+ $query->where('DATE(sfoi.created_at) <= ?', $this->options['to']);
366
+ }
367
+
368
+ if ($options['status']) {
369
+ $query->where('sfo.status = ?', $options['status']);
370
+ }
371
+
372
+ $query->where('sfo.status != ?', 'canceled');
373
+
374
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
375
+
376
+ $data = $this->readConnection->fetchAll($query);
377
+
378
+ return $data[0]['total'];
379
+ }
380
+
381
+ /**
382
+ * Get detailed information about orders.
383
+ *
384
+ * @param bool $count
385
+ * @return array|int
386
+ */
387
+ public function getDetailedOrders($count = false)
388
+ {
389
+ $options = array_merge(array(
390
+ 'entity_id' => null,
391
+ 'status' => null,
392
+ 'sort' => 'sfo.increment_id'
393
+ ), $this->options);
394
+
395
+ // get total orders.
396
+ $total = $this->getTotal();
397
+
398
+ $query = $this->readConnection->select();
399
+
400
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
401
+ $salesOrderAddressTbl = $this->resource->getTableName('sales/order_address');
402
+ $customersTbl = $this->resource->getTableName('customer/entity');
403
+ $customerGroupsTbl = $this->resource->getTableName('customer_group');
404
+
405
+ if ($count) {
406
+ $query->from(array('sfo' => $salesOrderTbl), array('total' => 'COUNT(DISTINCT sfo.entity_id)'));
407
+ $query->join(array('sfoa' => $salesOrderAddressTbl), 'sfoa.parent_id = sfo.entity_id', array());
408
+ } else {
409
+ // order columns.
410
+ $columns = array(
411
+ 'entity_id',
412
+ 'increment_id',
413
+ 'status',
414
+ 'created_at',
415
+ 'customer_email',
416
+ 'total_qty_ordered',
417
+ 'discount_amount',
418
+ 'shipping_amount',
419
+ 'tax_amount',
420
+ 'subtotal',
421
+ 'grand_total',
422
+ 'customer_is_guest',
423
+ );
424
+
425
+ $query->from(array('sfo' => $salesOrderTbl), $columns);
426
+
427
+ // address columns.
428
+ $columns = array(
429
+ 'region',
430
+ 'city',
431
+ 'country_id',
432
+ 'postcode'
433
+ );
434
+
435
+ $query->join(array('sfoa' => $salesOrderAddressTbl), 'sfoa.parent_id = sfo.entity_id', $columns);
436
+
437
+ // left join customers table.
438
+ $query->joinLeft(array('c' => $customersTbl), 'c.entity_id = sfoa.customer_id', array());
439
+ $query->joinLeft(array('cg' => $customerGroupsTbl), 'cg.customer_group_id = c.group_id', array('customer_group_code'));
440
+
441
+ $sorts = array(
442
+ 'sfo.increment_id',
443
+ 'sfo.created_at',
444
+ 'sfo.status',
445
+ );
446
+
447
+ if (!in_array($options['sort'], $sorts)) {
448
+ $options['sort'] = 'increment_id';
449
+ }
450
+
451
+ $query->limitPage($this->options['page'], $this->options['page_size']);
452
+ $query->order("{$options['sort']} {$options['order']}");
453
+ $query->group('sfo.entity_id');
454
+ }
455
+
456
+ if ($options['entity_id']) {
457
+ $query->where('sfo.entity_id = ?', $options['entity_id']);
458
+ }
459
+
460
+ if ($this->options['from']) {
461
+ $query->where('DATE(sfo.created_at) >= ?', $this->options['from']);
462
+ }
463
+
464
+ if ($this->options['to']) {
465
+ $query->where('DATE(sfo.created_at) <= ?', $this->options['to']);
466
+ }
467
+
468
+ if ($options['status']) {
469
+ $query->where('sfo.status = ?', $options['status']);
470
+ }
471
+
472
+ $query->where('sfo.status != ?', 'canceled');
473
+
474
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
475
+
476
+ $data = $this->readConnection->fetchAll($query);
477
+
478
+ if ($count) {
479
+ return $data[0]['total'];
480
+ }
481
+
482
+ return $data;
483
+ }
484
+
485
+ /**
486
+ * Get items for a given order.
487
+ *
488
+ * @param int $orderId
489
+ * @param return array
490
+ */
491
+ public function getOrderItems()
492
+ {
493
+ $options = array_merge(array(
494
+ 'entity_id' => null,
495
+ ), $this->options);
496
+
497
+ $query = $this->readConnection->select();
498
+
499
+ $columns = array(
500
+ 'item_id',
501
+ 'sku',
502
+ 'name',
503
+ 'created_at',
504
+ 'qty_ordered',
505
+ 'base_price',
506
+ 'tax_amount',
507
+ 'tax_percent',
508
+ 'discount_amount',
509
+ 'discount_percent',
510
+ 'price_incl_tax'
511
+ );
512
+
513
+ $salesOrderItemsTbl = $this->resource->getTableName('sales/order_item');
514
+ $query->from(array('sfoi' => $salesOrderItemsTbl), $columns);
515
+
516
+ if ($this->options['entity_id']) {
517
+ $query->where('sfoi.order_id = ?', $this->options['entity_id']);
518
+ }
519
+
520
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
521
+
522
+ $data = $this->readConnection->fetchAll($query);
523
+
524
+ return $data;
525
+ }
526
+
527
+ /**
528
+ * Get total orders by product category.
529
+ *
530
+ * @return array
531
+ */
532
+ public function getOrdersByProductCategory($count = false)
533
+ {
534
+ $options = array_merge(array(
535
+ 'status' => 'complete'
536
+ ), $this->options);
537
+
538
+ $query = $this->readConnection->select();
539
+
540
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
541
+ $salesOrderItemTbl = $this->resource->getTableName('sales/order_item');
542
+ $catalogCategoryProductTbl = $this->resource->getTableName('catalog/category_product');
543
+ $catalogCategoryTbl = $this->resource->getTableName('catalog/category');
544
+
545
+ if ($count) {
546
+ $columns = array(
547
+ 'total' => 'COUNT(sfoi.item_id)',
548
+ 'total_value' => 'SUM(sfoi.price)'
549
+ );
550
+ } else {
551
+ // get totals.
552
+ $totals = $this->getOrdersByProductCategory(true);
553
+
554
+ $columns = array(
555
+ 'total' => 'COUNT(sfoi.item_id)',
556
+ 'total_percentage' => "ROUND((COUNT(sfoi.item_id) / {$totals['total']}) * 100, 2)",
557
+ 'total_value' => 'SUM(sfoi.price)',
558
+ 'name' => 'CONCAT(sfoi.name, " (", sfoi.sku, ")")',
559
+ );
560
+
561
+ $query->limitPage($this->options['page'], $this->options['page_size']);
562
+ $query->group('ccev.entity_id');
563
+ $query->order('total DESC');
564
+ }
565
+
566
+ $query->from(array('sfoi' => $salesOrderItemTbl), $columns);
567
+ $query->join(array('sfo' => $salesOrderTbl), 'sfo.entity_id = sfoi.order_id', array());
568
+ $query->join(array('ccp' => $catalogCategoryProductTbl), 'ccp.product_id = sfoi.product_id', array());
569
+ $query->join(array('cc' => $catalogCategoryTbl), 'cc.entity_id = ccp.category_id', array());
570
+ $attributeId = $this->getCategoryNameAttributeId();
571
+
572
+ $tblName = Mage::getConfig()->getTablePrefix() . 'catalog_category_entity_varchar';
573
+ $query->join(array('ccev' => $tblName), "ccev.entity_id = cc.entity_id AND attribute_id={$attributeId}", array('name' => 'value'));
574
+
575
+ if ($this->options['from']) {
576
+ $query->where('DATE(sfoi.created_at) >= ?', $this->options['from']);
577
+ }
578
+
579
+ if ($this->options['to']) {
580
+ $query->where('DATE(sfoi.created_at) <= ?', $this->options['to']);
581
+ }
582
+
583
+ if ($options['status']) {
584
+ $query->where('sfo.status = ?', $options['status']);
585
+ }
586
+
587
+ $query->where('sfo.status != ?', 'canceled');
588
+
589
+ $query->where('sfo.store_id = ?', $this->options['store_id']);
590
+
591
+ $data = $this->readConnection->fetchAll($query);
592
+
593
+ if ($count) {
594
+ return $data[0];
595
+ } else {
596
+ return $this->appendOther($totals, $data);
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Get the ID of the category name attribute.
602
+ *
603
+ * @return int
604
+ */
605
+ protected function getCategoryNameAttributeId()
606
+ {
607
+ $eavAttributeTbl = $this->resource->getTableName('eav/attribute');
608
+ $eavEntityTypeTbl = $this->resource->getTableName('eav/entity_type');
609
+ $query = $this->readConnection->select();
610
+ $query->from(array('eat' => $eavEntityTypeTbl), array());
611
+ $query->join(array('ea' => $eavAttributeTbl), 'ea.entity_type_id = eat.entity_type_id', array('attribute_id'));
612
+ $query->where('eat.entity_type_code = ?', 'catalog_category');
613
+ $query->where('ea.attribute_code = ?', 'name');
614
+
615
+ $data = $this->readConnection->fetchAll($query);
616
+
617
+ if (!isset($data[0]['attribute_id'])) {
618
+ throw new Exception("Could not find category name attribute ID.");
619
+ }
620
+
621
+ return (int)$data[0]['attribute_id'];
622
+ }
623
+
624
+ /**
625
+ * Get a distinct list of all the order statuses set in the database.
626
+ *
627
+ * @return array
628
+ */
629
+ public function getDistinctOrderStatuses()
630
+ {
631
+ $salesOrderTbl = $this->resource->getTableName('sales/order');
632
+ $query = $this->readConnection->select();
633
+ $query->from($salesOrderTbl, array('status' => 'DISTINCT(status)'));
634
+
635
+ $data = $this->readConnection->fetchAll($query);
636
+
637
+ $statuses = array();
638
+
639
+ foreach ($data as $status) {
640
+ // if status not set.
641
+ if (!$status['status']) {
642
+ continue;
643
+ }
644
+
645
+ $statuses[] = (object)array(
646
+ 'name' => ucwords(str_replace('_', ' ', $status['status'])),
647
+ 'value' => $status['status']
648
+ );
649
+ }
650
+
651
+ $query->where('store_id = ?', $this->options['store_id']);
652
+
653
+ return $statuses;
654
+ }
655
+
656
+ /**
657
+ * Get all Magento stores.
658
+ *
659
+ * @return array
660
+ */
661
+ public function getStores()
662
+ {
663
+ $mage = Mage::app();
664
+
665
+ $stores = array();
666
+
667
+ // if stores returned.
668
+ if ($data = $mage->getStores()) {
669
+ foreach ($data as $store) {
670
+ if (!$store->getIsActive()) {
671
+ continue;
672
+ }
673
+
674
+ $stores[] = (object)array(
675
+ "id" => $store->store_id,
676
+ "name" => $store->getName()
677
+ );
678
+ }
679
+ }
680
+
681
+ return $stores;
682
+ }
683
+
684
+ /**
685
+ * Get meta information about the store and options.
686
+ *
687
+ * @return stdClass
688
+ */
689
+ public function getMeta()
690
+ {
691
+ $store = Mage::getModel('core/store')
692
+ ->load($this->options['store_id']);
693
+
694
+ $currencyCode = $store->getCurrentCurrencyCode();
695
+ $currencySymbol = Mage::app()
696
+ ->getLocale()
697
+ ->currency($currencyCode)
698
+ ->getSymbol();
699
+
700
+ return (object)array(
701
+ 'store' => (object)array(
702
+ 'id' => $this->options['store_id'],
703
+ 'name' => $store->getName(),
704
+ ),
705
+ 'currency' => (object)array(
706
+ 'code' => $currencyCode,
707
+ 'symbol' => $currencySymbol
708
+ )
709
+ );
710
+ }
711
+
712
+ /**
713
+ * Parse a set of 24 hours.
714
+ *
715
+ * @param array $objects
716
+ * @return array
717
+ */
718
+ protected function parseHours($objects = array())
719
+ {
720
+ $hours = array();
721
+ foreach ($objects as $object) {
722
+ $hours[$object['name']] = $object;
723
+ }
724
+
725
+ $resp = array();
726
+ foreach (range(0, 23) as $hour) {
727
+ $resp[] = array(
728
+ 'total' => isset($hours[$hour]) ? $hours[$hour]['total'] : 0,
729
+ 'total_percentage' => isset($hours[$hour]) ? $hours[$hour]['total_percentage'] : 0,
730
+ 'total_value' => isset($hours[$hour]) ? $hours[$hour]['total_value'] : 0,
731
+ 'name' => sprintf('%02d:00', $hour)
732
+ );
733
+ }
734
+
735
+ // sort by total desc.
736
+ usort($resp, function ($a, $b) {
737
+ return $b['total'] - $a['total'];
738
+ });
739
+
740
+ return $resp;
741
+ }
742
+ }
app/code/community/IR/Neatly/controllers/IndexController.php ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ use IR_Neatly_Exception_Api as ApiException;
4
+
5
+ class IR_Neatly_IndexController extends Mage_Core_Controller_Front_Action
6
+ {
7
+ /**
8
+ * @var Mage_Core_Controller_Response_Http
9
+ */
10
+ protected $resp;
11
+
12
+ /**
13
+ * @var array
14
+ */
15
+ protected $stores = array();
16
+
17
+ /**
18
+ * @var array
19
+ */
20
+ protected $data = array();
21
+
22
+ /**
23
+ * @var IR_Neatly_Model_Reports_Sales
24
+ */
25
+ protected $salesReport;
26
+
27
+ /**
28
+ * @var IR_Neatly_Model_Reports_Reporting
29
+ */
30
+ protected $reporting;
31
+
32
+ /**
33
+ * @var array
34
+ */
35
+ protected $actions = array(
36
+ 'sales_totals_by_date' => 'getTotalOrders',
37
+ 'new_vs_returning_customers' => 'getNewVsReturningCustomers',
38
+ 'aggregated_totals' => 'getAggregatedTotals',
39
+ 'best_selling_products' => 'getBestSellingProducts',
40
+ 'sales_by_country' => 'getOrdersByCountry',
41
+ 'sales_by_city' => 'getOrdersByCity',
42
+ 'sales_by_payment_method' => 'getOrdersByPaymentMethod',
43
+ 'sales_by_hour' => 'getOrdersByHour',
44
+ 'sales_by_week_day' => 'getOrdersByWeekDay',
45
+ 'items_sold_by_product_category' => 'getOrdersByProductCategory',
46
+ 'sales_by_customer_group' => 'getOrdersByCustomerGroup',
47
+ 'distinct_order_statuses' => 'getDistinctOrderStatuses',
48
+ 'customers' => 'getCustomers',
49
+ 'stores' => 'getStores',
50
+ 'meta' => 'getMeta',
51
+ );
52
+
53
+ /**
54
+ * GET /neatly
55
+ */
56
+ public function indexAction()
57
+ {
58
+ $this->resp = $this->getResponse();
59
+
60
+ $this->resp->setHeader('Content-type', 'application/json');
61
+
62
+ $errors = array();
63
+
64
+ $helper = Mage::helper('ir_neatly');
65
+
66
+ // if extension is not active and enabled.
67
+ if (!$helper->isEnabled()) {
68
+ $errors[] = 'Neatly extension disabled.';
69
+ }
70
+
71
+ // if api_token not set.
72
+ if (!$passedApiToken = $this->getRequest()->getParam('api_token')) {
73
+ $errors[] = '"api_token" required.';
74
+ } elseif (!$helper->isApiTokenValid($passedApiToken)) {
75
+ $errors[] = 'Incorrect API Token.';
76
+ }
77
+
78
+ if (!empty($errors)) {
79
+ $this->resp->setHttpResponseCode(403);
80
+ $this->resp->setBody(json_encode(array('errors' => $errors)));
81
+ return;
82
+ }
83
+
84
+ $this->options = array_merge(array(
85
+ 'to' => null,
86
+ 'from' => null,
87
+ 'action' => null,
88
+ 'store_id' => null,
89
+ 'page' => true,
90
+ ), $this->getRequest()->getParams());
91
+
92
+ $this->salesReport = Mage::getModel('ir_neatly/reports_sales', $this->options);
93
+
94
+ $this->reporting = Mage::getModel('ir_neatly/reports_reporting', $this->options);
95
+
96
+ // get all stores.
97
+ $this->stores = $this->salesReport->getStores();
98
+
99
+ // if only 1 store returned or no store_id passed in request.
100
+ if (count($this->stores) == 1 || empty($this->options['store_id'])) {
101
+ $this->options['store_id'] = $this->stores[0]->id;
102
+ }
103
+
104
+ $this->salesReport->setOptions($this->options);
105
+ $this->customersReport = Mage::getModel('ir_neatly/reports_customers', $this->options);
106
+
107
+ try {
108
+ if (empty($this->options['to']) || empty($this->options['from'])) {
109
+ throw new ApiException('"to" and "from" dates required.', 400);
110
+ }
111
+
112
+ $resp = array('version' => $helper->getVersion());
113
+
114
+ // if action is an array and all requested actions exist.
115
+ if (is_array($this->options['action']) &&
116
+ array_intersect($this->options['action'], array_keys($this->actions)) === $this->options['action']) {
117
+ foreach ($this->options['action'] as $action) {
118
+ $method = $this->actions[$action];
119
+ $resp[$action] = $this->{$method}();
120
+ }
121
+ } elseif (isset($this->actions[$this->options['action']])) {
122
+ // if action is not an array but exists.
123
+ $method = $this->actions[$this->options['action']];
124
+ $resp[$this->options['action']] = $this->{$method}();
125
+ } else {
126
+ // get default actions (expect "customers").
127
+ unset($this->actions['customers']);
128
+ foreach ($this->actions as $action => $method) {
129
+ $resp[$action] = $this->{$method}();
130
+ }
131
+ }
132
+
133
+ $this->resp->setBody(json_encode($resp));
134
+ } catch (ApiException $e) {
135
+ $this->resp->setHttpResponseCode($e->getCode());
136
+ $this->resp->setBody(json_encode(array('errors' => $e->getErrors())));
137
+ } catch (Exception $e) {
138
+ // throw exceptions while in development.
139
+ if (isset($_SERVER['MAGE_IS_DEVELOPER_MODE'])) {
140
+ throw $e;
141
+ }
142
+
143
+ $msg = 'There is a problem with the installed Neatly extension. Please check your log files.';
144
+ Mage::logException($e);
145
+ $this->resp->setHttpResponseCode(500);
146
+ $this->resp->setBody(json_encode(array('errors' => array($msg))));
147
+ }
148
+ }
149
+
150
+ protected function getTotalOrders()
151
+ {
152
+ return $this->salesReport->getTotalOrders();
153
+ }
154
+
155
+ protected function getNewVsReturningCustomers()
156
+ {
157
+ return $this->customersReport->getNewVsReturningCustomers();
158
+ }
159
+
160
+ protected function getAggregatedTotals()
161
+ {
162
+ return $this->salesReport->getAggregatedTotals();
163
+ }
164
+
165
+ protected function getBestSellingProducts()
166
+ {
167
+ return $this->salesReport->getBestSellingProducts();
168
+ }
169
+
170
+ protected function getOrdersByCountry()
171
+ {
172
+ return $this->salesReport->getOrdersByGroup(array('group' => 'sfoa.country_id'));
173
+ }
174
+
175
+ protected function getOrdersByCity()
176
+ {
177
+ return $this->salesReport->getOrdersByGroup(array('group' => 'sfoa.city'));
178
+ }
179
+
180
+ protected function getOrdersByPaymentMethod()
181
+ {
182
+ return $this->salesReport->getOrdersByGroup(array('group' => 'sfop.method'));
183
+ }
184
+
185
+ protected function getOrdersByHour()
186
+ {
187
+ return $this->salesReport->getOrdersByPeriod(array('period' => 'hour'));
188
+ }
189
+
190
+ protected function getOrdersByWeekDay()
191
+ {
192
+ return $this->salesReport->getOrdersByPeriod(array('period' => 'dayname'));
193
+ }
194
+
195
+ protected function getOrdersByProductCategory()
196
+ {
197
+ return $this->salesReport->getOrdersByProductCategory();
198
+ }
199
+
200
+ protected function getOrdersByCustomerGroup()
201
+ {
202
+ return $this->customersReport->getOrdersByCustomerGroup();
203
+ }
204
+
205
+ protected function getDistinctOrderStatuses()
206
+ {
207
+ return $this->salesReport->getDistinctOrderStatuses();
208
+ }
209
+
210
+ public function getCustomers()
211
+ {
212
+ // if page not set.
213
+ if (!$this->options['page']) {
214
+ // get entire result set.
215
+ return $this->reporting->getCustomers();
216
+ }
217
+
218
+ $count = $this->reporting->getCustomers(true);
219
+
220
+ $options = array();
221
+
222
+ // if results set is 1000 or less.
223
+ if ($count <= 1000) {
224
+ // return entire resp.
225
+ $options['page_size'] = 1000;
226
+ }
227
+
228
+ $customers = $this->reporting->getCustomers(false, $options);
229
+ return $this->reporting->getPagination($count, $customers);
230
+ }
231
+
232
+ protected function getStores()
233
+ {
234
+ return $this->stores;
235
+ }
236
+
237
+ protected function getMeta()
238
+ {
239
+ return $this->salesReport->getMeta();
240
+ }
241
+ }
app/code/community/IR/Neatly/etc/adminhtml.xml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <acl>
4
+ <resources>
5
+ <all>
6
+ <title>Allow Everything</title>
7
+ </all>
8
+ <admin>
9
+ <children>
10
+ <system>
11
+ <children>
12
+ <config>
13
+ <children>
14
+ <ir_neatly translate="title">
15
+ <title>An Example Section</title>
16
+ <sort_order>100</sort_order>
17
+ </ir_neatly>
18
+ </children>
19
+ </config>
20
+ </children>
21
+ </system>
22
+ </children>
23
+ </admin>
24
+ </resources>
25
+ </acl>
26
+ </config>
app/code/community/IR/Neatly/etc/config.xml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <config>
2
+ <modules>
3
+ <IR_Neatly>
4
+ <version>0.1.0</version>
5
+ </IR_Neatly>
6
+ </modules>
7
+ <global>
8
+ <models>
9
+ <ir_neatly>
10
+ <class>IR_Neatly_Model</class>
11
+ </ir_neatly>
12
+ </models>
13
+ <helpers>
14
+ <ir_neatly>
15
+ <class>IR_Neatly_Helper</class>
16
+ </ir_neatly>
17
+ </helpers>
18
+ </global>
19
+ <frontend>
20
+ <routers>
21
+ <neatly>
22
+ <use>standard</use>
23
+ <args>
24
+ <module>IR_Neatly</module>
25
+ <frontName>neatly</frontName>
26
+ </args>
27
+ </neatly>
28
+ </routers>
29
+ </frontend>
30
+ </config>
app/code/community/IR/Neatly/etc/system.xml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <tabs>
4
+ <nwall translate="label" module="ir_neatly">
5
+ <label>Neatly</label>
6
+ <sort_order>300</sort_order>
7
+ </nwall>
8
+ </tabs>
9
+ <sections>
10
+ <ir_neatly translate="label">
11
+ <label>API Configuration</label>
12
+ <tab>nwall</tab>
13
+ <frontend_type>text</frontend_type>
14
+ <sort_order>1000</sort_order>
15
+ <show_in_default>1</show_in_default>
16
+ <show_in_website>1</show_in_website>
17
+ <show_in_store>1</show_in_store>
18
+ <groups>
19
+ <security translate="label">
20
+ <label>API Configuration</label>
21
+ <frontend_type>text</frontend_type>
22
+ <sort_order>1</sort_order>
23
+ <show_in_default>1</show_in_default>
24
+ <show_in_website>1</show_in_website>
25
+ <show_in_store>1</show_in_store>
26
+ <fields>
27
+ <neatly_api_token translate="label">
28
+ <label>API Token</label>
29
+ <comment>
30
+ <![CDATA[Your neatly.io API Token.]]>
31
+ </comment>
32
+ <sort_order>20</sort_order>
33
+ <show_in_default>1</show_in_default>
34
+ <show_in_website>0</show_in_website>
35
+ <show_in_store>0</show_in_store>
36
+ <can_be_empty>0</can_be_empty>
37
+ <backend_model>adminhtml/system_config_backend_encrypted</backend_model>
38
+ </neatly_api_token>
39
+ </fields>
40
+ </security>
41
+ </groups>
42
+ </ir_neatly>
43
+ </sections>
44
+ </config>
app/etc/modules/IR_Neatly.xml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <!--
3
+ /**
4
+ * Module initial config
5
+ *
6
+ * @author iResources
7
+ */
8
+ -->
9
+ <config>
10
+ <modules>
11
+ <IR_Neatly>
12
+ <active>true</active>
13
+ <codePool>community</codePool>
14
+ <depends>
15
+ <Mage_Adminhtml />
16
+ </depends>
17
+ </IR_Neatly>
18
+ </modules>
19
+ </config>
package.xml CHANGED
@@ -1,15 +1,13 @@
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>IR_Neatly_Reporting</name>
4
- <version>1.0.0.2</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.gnu.org/copyleft/gpl.html">GNU General Public License (GPL)</license>
7
  <channel>community</channel>
8
  <extends/>
9
  <summary>View a range of detailed reports about your sales, products and customers from your Neatly dashboard.</summary>
10
- <description>This extension allows Neatly to pull and generate detailed reports about your sales, customers and products.&#xD;
11
- &#xD;
12
- Simply install the extension and enter your API key and you will be able to view the following data:&#xD;
13
  &#xD;
14
  &lt;ul&gt;&#xD;
15
  &lt;li&gt;Sales Overview&lt;/li&gt;&#xD;
@@ -25,11 +23,11 @@ Simply install the extension and enter your API key and you will be able to view
25
  &lt;/ul&gt;&#xD;
26
  &#xD;
27
  You can also compare data against previous period of custom your date range comparison.</description>
28
- <notes>Initial extension release.</notes>
29
  <authors><author><name>Jon Leigton</name><user>iResources</user><email>j.leighton@i-resources.co.uk</email></author></authors>
30
- <date>2015-05-21</date>
31
- <time>13:53:24</time>
32
- <contents><target name="mageetc"><dir name="app"><file name="etc" hash=""/></dir></target></contents>
33
  <compatible/>
34
- <dependencies><required><php><min>5.6.8</min><max>5.6.9</max></php></required></dependencies>
35
  </package>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>IR_Neatly_Reporting</name>
4
+ <version>1.0.1.0</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.gnu.org/copyleft/gpl.html">GNU General Public License (GPL)</license>
7
  <channel>community</channel>
8
  <extends/>
9
  <summary>View a range of detailed reports about your sales, products and customers from your Neatly dashboard.</summary>
10
+ <description>Simply install the extension and enter your API key and you will be able to view the following data:&#xD;
 
 
11
  &#xD;
12
  &lt;ul&gt;&#xD;
13
  &lt;li&gt;Sales Overview&lt;/li&gt;&#xD;
23
  &lt;/ul&gt;&#xD;
24
  &#xD;
25
  You can also compare data against previous period of custom your date range comparison.</description>
26
+ <notes>This latest release allows the extension to now support PHP 5.4.0</notes>
27
  <authors><author><name>Jon Leigton</name><user>iResources</user><email>j.leighton@i-resources.co.uk</email></author></authors>
28
+ <date>2015-06-03</date>
29
+ <time>11:05:27</time>
30
+ <contents><target name="magecommunity"><dir name="IR"><dir name="Neatly"><dir name="Exception"><file name="Api.php" hash="079a77634e425c151fcc97e9d67a2ebf"/></dir><dir name="Helper"><file name="Data.php" hash="ad424a7ecee2e205356678d233908207"/></dir><dir name="Model"><dir name="Reports"><file name="Abstract.php" hash="5b908c879ffa6974085fedb19b530959"/><file name="Customers.php" hash="f6af2cf717dbb7d121d643d1171d6ccf"/><file name="Reporting.php" hash="1415934625ff2b9963d3d87cf1851201"/><file name="Sales.php" hash="6b0fb8adb0ec9cbf283508fe59adb797"/></dir></dir><dir name="controllers"><file name="IndexController.php" hash="a419ada141895a71c2337aa4e10ac24d"/></dir><dir name="etc"><file name="adminhtml.xml" hash="98b2b738856967354adade75f1830a9c"/><file name="config.xml" hash="33268e3951cce566d47534e7114cbe60"/><file name="system.xml" hash="3f13f3b44d36643f8bb9822a64ce4c29"/></dir><file name=".DS_Store" hash="601020d0d1c6b7a09b8efd396666f583"/></dir></dir></target><target name="mageetc"><dir name="modules"><file name="IR_Neatly.xml" hash="8cfc6d5b71ff25155628d065d6a7335b"/></dir></target></contents>
31
  <compatible/>
32
+ <dependencies><required><php><min>5.4.0</min><max>5.6.9</max></php></required></dependencies>
33
  </package>