Passed
Push — master ( 906c6c...900101 )
by Andreas
19:25
created

decline()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.0058

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 4
nop 0
dl 0
loc 23
ccs 13
cts 14
cp 0.9286
crap 4.0058
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package org.openpsa.sales
4
 * @author Nemein Oy, http://www.nemein.com/
5
 * @copyright Nemein Oy, http://www.nemein.com/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
/**
10
 * MidCOM wrapped class for access to deliverables
11
 *
12
 * @property integer $up
13
 * @property integer $product
14
 * @property integer $supplier
15
 * @property integer $salesproject
16
 * @property string $title
17
 * @property string $description
18
 * @property float $price
19
 * @property float $invoiced
20
 * @property float $units
21
 * @property float $plannedUnits
22
 * @property float $uninvoiceableUnits
23
 * @property string $unit
24
 * @property float $pricePerUnit
25
 * @property boolean $invoiceByActualUnits
26
 * @property boolean $continuous
27
 * @property float $cost Actual cost of the delivery
28
 * @property float $plannedCost Original planned cost
29
 * @property float $costPerUnit Cost per unit, used as basis of calculations for the fields above
30
 * @property string $costType
31
 * @property integer $start Start can have two different meanings:
32
        		- for single deliveries, it's the time when delivery can start
33
        		- for subscriptions it's the subscription start
34
 * @property integer $end End can have two different meanings:
35
        		- for single deliveries, it's the delivery deadline
36
        		- for subscriptions it's the subscription end
37
 * @property integer $notify
38
 * @property integer $state State of the proposal/order
39
 * @property integer $orgOpenpsaObtype Used to a) distinguish OpenPSA objects in QB b) store object "subtype" (project vs task etc)
40
 * @package org.openpsa.sales
41
 */
42
class org_openpsa_sales_salesproject_deliverable_dba extends midcom_core_dbaobject
43
{
44
    public $__midcom_class_name__ = __CLASS__;
45
    public $__mgdschema_class_name__ = 'org_openpsa_salesproject_deliverable';
46
47
    const STATE_NEW = 100;
48
    const STATE_DECLINED = 300;
49
    const STATE_ORDERED = 400;
50
    const STATE_STARTED = 450;
51
    const STATE_DELIVERED = 500;
52
    const STATE_INVOICED = 600;
53
54
    /**
55
     * @var boolean
56
     */
57
    private $_update_parent_on_save = false;
58
59 31
    public function _on_creating() : bool
60
    {
61 31
        $this->calculate_price(false);
62 31
        return true;
63
    }
64
65 31
    public function _on_created()
66
    {
67 31
        $this->_update_parent();
68 31
    }
69
70 18
    public function _on_updating() : bool
71
    {
72 18
        $this->calculate_price(false);
73
74 18
        if (   $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION
75 18
            && $this->continuous) {
76 2
            $this->end = 0;
77 17
        } elseif ($this->end < $this->start) {
78 1
            $this->end = $this->start + 1;
79
        }
80 18
        return true;
81
    }
82
83 18
    public function _on_updated()
84
    {
85 18
        if ($this->_update_parent_on_save) {
86 7
            $this->_update_parent();
87
        }
88 18
    }
89
90 31
    public function _on_deleted()
91
    {
92 31
        $entries = $this->get_at_entries();
93 31
        foreach ($entries as $entry) {
94 8
            $entry->delete();
95
        }
96 31
        $this->_update_parent();
97 31
    }
98
99 31
    private function _update_parent()
100
    {
101 31
        $project = new org_openpsa_sales_salesproject_dba($this->salesproject);
102 31
        $project->calculate_price();
103 31
    }
104
105 4
    public function get_state() : string
106
    {
107 4
        switch ($this->state) {
108
            case self::STATE_NEW:
109
                return 'proposed';
110
            case self::STATE_DECLINED:
111
                return 'declined';
112
            case self::STATE_ORDERED:
113 3
                return 'ordered';
114
            case self::STATE_STARTED:
115 1
                return 'started';
116
            case self::STATE_DELIVERED:
117 1
                return 'delivered';
118
            case self::STATE_INVOICED:
119 1
                return 'invoiced';
120
        }
121 3
        return '';
122
    }
123
124
    /**
125
     * @return midcom_services_at_entry_dba[]
126
     */
127 31
    public function get_at_entries() : array
128
    {
129 31
        $mc = new org_openpsa_relatedto_collector($this->guid, midcom_services_at_entry_dba::class);
130 31
        $mc->add_object_constraint('method', '=', 'new_subscription_cycle');
131 31
        return $mc->get_related_objects();
132
    }
133
134 32
    public function calculate_price(bool $update = true)
135
    {
136 32
        $calculator_class = midcom_baseclasses_components_configuration::get('org.openpsa.sales', 'config')->get('calculator');
137 32
        $calculator = new $calculator_class();
138 32
        $calculator->run($this);
139 32
        $cost = $calculator->get_cost();
140 32
        $price = $calculator->get_price();
141 32
        if (   $price != $this->price
142 32
            || $cost != $this->cost) {
143 12
            $this->price = $price;
144 12
            $this->cost = $cost;
145 12
            $this->_update_parent_on_save = true;
146 12
            if ($update) {
147
                $this->update();
148
                $this->_update_parent_on_save = false;
149
            }
150
        }
151 32
    }
152
153
    /**
154
     * Recalculate the deliverable's unit trackers based on data form a (recently updated) task
155
     */
156 12
    public function update_units()
157
    {
158 12
        debug_add('Units before update: ' . $this->units . ", uninvoiceable: " . $this->uninvoiceableUnits);
159
160
        $hours = [
161 12
            'reported' => 0,
162
            'invoiced' => 0,
163
            'invoiceable' => 0
164
        ];
165
166
        // List hours from tasks of the agreement
167 12
        $mc = org_openpsa_projects_task_dba::new_collector('agreement', $this->id);
168 12
        $other_tasks = $mc->get_rows(['reportedHours', 'invoicedHours', 'invoiceableHours']);
169
170 12
        foreach ($other_tasks as $other_task) {
171
            // Add the hours of the other tasks to agreement's totals
172 12
            $hours['reported'] += $other_task['reportedHours'];
173 12
            $hours['invoiced'] += $other_task['invoicedHours'];
174 12
            $hours['invoiceable'] += $other_task['invoiceableHours'];
175
        }
176
177
        // Update units on the agreement with invoiceable hours
178 12
        $units = $hours['invoiceable'];
179 12
        $uninvoiceableUnits = $hours['reported'] - ($hours['invoiceable'] + $hours['invoiced']);
180
181 12
        if (   $units != $this->units
182 12
            || $uninvoiceableUnits != $this->uninvoiceableUnits) {
183 4
            debug_add("agreement values have changed, setting units to " . $units . ", uninvoiceable: " . $uninvoiceableUnits);
184 4
            $this->units = $units;
185 4
            $this->uninvoiceableUnits = $uninvoiceableUnits;
186 4
            $this->_use_rcs = false;
187
188 4
            if (!$this->update()) {
189 4
                debug_add("Agreement #{$this->id} couldn't be saved to disk, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
190
            }
191
        } else {
192 11
            debug_add("Agreement values are unchanged, no update necessary");
193
        }
194 12
    }
195
196
    /**
197
     * Manually trigger a subscription cycle run.
198
     */
199 1
    public function run_cycle() : bool
200
    {
201 1
        $at_entries = $this->get_at_entries();
202 1
        if (!isset($at_entries[0])) {
203
            debug_add('No AT entry found');
204
            return false;
205
        }
206
207 1
        $entry = $at_entries[0];
208 1
        $scheduler = new org_openpsa_invoices_scheduler($this);
209
210 1
        if (!$scheduler->run_cycle($entry->arguments['cycle'])) {
211
            debug_add('Failed to run cycle');
212
            return false;
213
        }
214 1
        if (!$entry->delete()) {
215
            debug_add('Could not delete AT entry: ' . midcom_connection::get_error_string());
216
            return false;
217
        }
218 1
        return true;
219
    }
220
221 2
    public function end_subscription() : bool
222
    {
223 2
        $this->state = self::STATE_INVOICED;
224 2
        if (!$this->update()) {
225
            return false;
226
        }
227 2
        $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
228 2
        $salesproject->mark_invoiced();
229
230 2
        return true;
231
    }
232
233 1
    public function invoice() : bool
234
    {
235 1
        if (   $this->state >= self::STATE_INVOICED
236 1
            || $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
237
            return false;
238
        }
239
240 1
        $calculator = new org_openpsa_invoices_calculator();
241 1
        $amount = $calculator->process_deliverable($this);
242
243 1
        if ($amount > 0) {
244 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
245 1
            $salesproject->mark_invoiced();
246
        }
247 1
        return true;
248
    }
249
250 1
    public function decline() : bool
251
    {
252 1
        if ($this->state >= self::STATE_DECLINED) {
253 1
            return false;
254
        }
255
256 1
        $this->state = self::STATE_DECLINED;
257
258 1
        if ($this->update()) {
259
            // Update sales project if it doesn't have any open deliverables
260 1
            $qb = self::new_query_builder();
261 1
            $qb->add_constraint('salesproject', '=', $this->salesproject);
262 1
            $qb->add_constraint('state', '<>', self::STATE_DECLINED);
263 1
            if ($qb->count() == 0) {
264
                // No proposals that are not declined
265 1
                $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
266 1
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_LOST;
267 1
                $salesproject->update();
268
            }
269
270 1
            return true;
271
        }
272
        return false;
273
    }
274
275 5
    public function order() : bool
276
    {
277 5
        if ($this->state >= self::STATE_ORDERED) {
278 1
            return false;
279
        }
280
281 4
        if ($this->invoiceByActualUnits) {
282 1
            $this->cost = 0;
283 1
            $this->units = 0;
284
        }
285
286
        // Check what kind of order this is
287 4
        $product = org_openpsa_products_product_dba::get_cached($this->product);
288 4
        $scheduler = new org_openpsa_invoices_scheduler($this);
289
290 4
        if ($product->delivery == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
0 ignored issues
show
Bug Best Practice introduced by
The property delivery does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
291
            // This is a new subscription, initiate the cycle but don't send invoice
292 1
            if (!$scheduler->run_cycle(1, false)) {
293 1
                return false;
294
            }
295 3
        } elseif ($product->orgOpenpsaObtype === org_openpsa_products_product_dba::TYPE_SERVICE) {
0 ignored issues
show
Bug Best Practice introduced by
The property orgOpenpsaObtype does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
296
            $scheduler->create_task($this->start, $this->end, $this->title);
297
        }
298
        // TODO: Warehouse management: create new order (for org_openpsa_products_product_dba::TYPE_GOODS)
299
300 4
        $this->state = self::STATE_ORDERED;
301
302 4
        if ($this->update()) {
303
            // Update sales project and mark as won
304 4
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
305 4
            if ($salesproject->state != org_openpsa_sales_salesproject_dba::STATE_WON) {
306 2
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_WON;
307 2
                $salesproject->update();
308
            }
309
310 4
            return true;
311
        }
312
313
        return false;
314
    }
315
316 3
    public function deliver(bool $update_deliveries = true) : bool
317
    {
318 3
        if ($this->state > self::STATE_DELIVERED) {
319
            return false;
320
        }
321
322 3
        $product = org_openpsa_products_product_dba::get_cached($this->product);
323 3
        if ($product->delivery == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
0 ignored issues
show
Bug Best Practice introduced by
The property delivery does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
324
            // Subscriptions are ongoing, not one delivery
325 2
            return false;
326
        }
327
328 1
        $this->state = self::STATE_DELIVERED;
329 1
        $this->end = time();
330 1
        if ($this->update()) {
331
            // Update sales project and mark as delivered (if no other deliverables are active)
332 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
333 1
            $salesproject->mark_delivered();
334
335 1
            midcom::get()->uimessages->add(midcom::get()->i18n->get_string('org.openpsa.sales', 'org.openpsa.sales'), sprintf(midcom::get()->i18n->get_string('marked deliverable "%s" delivered', 'org.openpsa.sales'), $this->title));
336
337
            // Check if we need to create task or ship goods
338 1
            if (   $update_deliveries
339 1
                && $product->orgOpenpsaObtype === org_openpsa_products_product_dba::TYPE_SERVICE) {
0 ignored issues
show
Bug Best Practice introduced by
The property orgOpenpsaObtype does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
340
                // Change status of tasks connected to the deliverable
341
                $qb = org_openpsa_projects_task_dba::new_query_builder();
342
                $qb->add_constraint('agreement', '=', $this->id);
343
                $qb->add_constraint('status', '<', org_openpsa_projects_task_status_dba::CLOSED);
344
                foreach ($qb->execute() as $task) {
345
                    org_openpsa_projects_workflow::close($task, sprintf(midcom::get()->i18n->get_string('completed from deliverable %s', 'org.openpsa.sales'), $this->title));
346
                }
347
                // TODO: Warehouse management: mark product as shipped (for org_openpsa_products_product_dba::TYPE_GOODS)
348
            }
349
350 1
            return true;
351
        }
352
        return false;
353
    }
354
}
355