diff --git a/Admin/Hooks/Cli.php b/Admin/Hooks/Cli.php new file mode 100644 index 0000000..3d371ab --- /dev/null +++ b/Admin/Hooks/Cli.php @@ -0,0 +1,16 @@ + [ + 'callback' => ['\Modules\Workflow\Controller\ApiController:hookWorkflowChangeState'], + ], + "POST:Billing:bill-update" => [ + 'callback' => ['\Modules\Workflow\Controller\ApiController:hookWorkflowChangeState'], + ], + + "POST:Billing:bill_element-create" => [ + 'callback' => ['\Modules\Workflow\Controller\ApiController:hookWorkflowChangeState'], + ], + "POST:Billing:bill_element-update" => [ + 'callback' => ['\Modules\Workflow\Controller\ApiController:hookWorkflowChangeState'], + ], +]; diff --git a/Admin/Install/Workflow.install.json b/Admin/Install/Workflow.install.json index 4a752dd..79604d3 100755 --- a/Admin/Install/Workflow.install.json +++ b/Admin/Install/Workflow.install.json @@ -1,32 +1,38 @@ { + "templates": [ + { + "name": "Billing workflow", + "path": "/Modules/Billing/Admin/Install/Workflow/bill" + } + ], "triggers": [ - "PRE:Module:Billing:bill-create", - "POST:Module:Billing:bill-create", - "PRE:Module:Billing:bill-update", - "POST:Module:Billing:bill-update", - "PRE:Module:Billing:bill-delete", - "POST:Module:Billing:bill-delete", + "PRE:Billing:bill-create", + "POST:Billing:bill-create", + "PRE:Billing:bill-update", + "POST:Billing:bill-update", + "PRE:Billing:bill-delete", + "POST:Billing:bill-delete", - "PRE:Module:Billing:bill_element-create", - "POST:Module:Billing:bill_element-create", - "PRE:Module:Billing:bill_element-update", - "POST:Module:Billing:bill_element-update", - "PRE:Module:Billing:bill_element-delete", - "POST:Module:Billing:bill_element-delete", + "PRE:Billing:bill_element-create", + "POST:Billing:bill_element-create", + "PRE:Billing:bill_element-update", + "POST:Billing:bill_element-update", + "PRE:Billing:bill_element-delete", + "POST:Billing:bill_element-delete", - "PRE:Module:Billing:bill_media-create", - "POST:Module:Billing:bill_media-create", - "PRE:Module:Billing:bill_media-update", - "POST:Module:Billing:bill_media-update", - "PRE:Module:Billing:bill_media-delete", - "POST:Module:Billing:bill_media-delete", + "PRE:Billing:bill_media-create", + "POST:Billing:bill_media-create", + "PRE:Billing:bill_media-update", + "POST:Billing:bill_media-update", + "PRE:Billing:bill_media-delete", + "POST:Billing:bill_media-delete", - "PRE:Module:Billing:bill_note-create", - "POST:Module:Billing:bill_note-create", - "PRE:Module:Billing:bill_note-update", - "POST:Module:Billing:bill_note-update", - "PRE:Module:Billing:bill_note-delete", - "POST:Module:Billing:bill_note-delete" + "PRE:Billing:bill_note-create", + "POST:Billing:bill_note-create", + "PRE:Billing:bill_note-update", + "POST:Billing:bill_note-update", + "PRE:Billing:bill_note-delete", + "POST:Billing:bill_note-delete" ], "actions": { "1005100001": { diff --git a/Admin/Install/Workflow/bill/WorkflowController.php b/Admin/Install/Workflow/bill/WorkflowController.php new file mode 100644 index 0000000..fb00a19 --- /dev/null +++ b/Admin/Install/Workflow/bill/WorkflowController.php @@ -0,0 +1,319 @@ +createdBy = new NullAccount($request->header->account); + $instance->title = $template->name . ': ' . ($request->getDataString('title') ?? ''); + $instance->template = new NullWorkflowTemplate($request->getData('template') ?? 0); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getInstanceListFromRequest(RequestAbstract $request) : array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function createTemplateViewFromRequest(View $view, RequestAbstract $request, ResponseAbstract $response) : void + { + $includes = 'Modules/Media/Files/'; + $tpl = $view->data['template']->source->findFile('template-profile.tpl.php'); + + $start = \stripos($tpl->getPath(), $includes); + + $view->setTemplate('/' . \substr($tpl->getPath(), $start, -8)); + } + + /** + * {@inheritdoc} + */ + public function createInstanceViewFromRequest(View $view, RequestAbstract $request, ResponseAbstract $response) : void + { + $includes = 'Modules/Media/Files/'; + $tpl = $view->data['template']->source->findFile('instance-profile.tpl.php'); + + $start = \stripos($tpl->getPath(), $includes); + + $view->setTemplate('/' . \substr($tpl->getPath(), $start, -8)); + } + + /** + * {@inheritdoc} + */ + public function apiChangeState(RequestAbstract $request, ResponseAbstract $response, $data = []) : void + { + // @bug what if we create two tasks for the same element (due to edits) before it got closed + // In that case the automatic task closing will only effect the most recent task + // Maybe check task existence first and close it before creating a new task + } + + /** + * {@inheritdoc} + */ + public function hookChangeState( + WorkflowTemplate $template, + int $account, + mixed $old, + mixed $new, + ?int $type = null, + string $trigger = '', + ?string $module = null, + ?string $ref = null, + ?string $content = null, + ?string $ip = null + ) : mixed + { + $old = null; + + /** @var Bill $bill */ + $bill = null; + if ($type === StringUtils::intHash(BillMapper::class)) { + $bill = $new; + } else if ($type === StringUtils::intHash(BillElementMapper::class)) { + /** @var BillElement $element */ + $element = $new; + $bill = BillMapper::get() + ->where('id', $element->bill->id) + ->executeGet(); + } + + if ($bill->status !== BillStatus::DONE) { + return null; + } + + $instance = WorkflowInstanceAbstractMapper::get() + ->with('steps') + ->with('template') + ->where('template/module', 'Billing') + ->where('template/type', $bill->client != null ? WorkflowType::SALES_BILL : WorkflowType::PURCHASE_BILL) + ->where('ref', $bill->id) + ->executeGet(); + + $instance->template = $template; + $old = clone $instance; + + if ($type === StringUtils::intHash(BillMapper::class)) { + $actionType = $this->hookChangeBillState($instance, $bill); + } else if ($type === StringUtils::intHash(BillElementMapper::class)) { + $actionType = $this->hookChangeBillElementState($instance, $bill); + } + + if ($actionType < 0) { + return null; + } + + // $actionType === 0 -> create/update instance but don't create a task + if ($instance->id === 0) { + // Create new instance if none exist for this element (ref) + $instance->template = $template; + $instance->ref = $element->bill->id; + + WorkflowInstanceAbstractMapper::create() + ->execute($instance); + + $oldSerialized = ''; + } else { + WorkflowInstanceAbstractMapper::update() + ->execute($instance); + + $oldSerialized = $old->jsonSerialize(); + } + + $audit = new Audit( + new NullAccount($account), + $oldSerialized, + $instance->jsonSerialize(), + StringUtils::intHash(WorkflowInstanceAbstract::class), + $trigger, + $module, + $ref, + $content, + (int) \ip2long($ip ?? '127.0.0.1') + ); + AuditMapper::create()->execute($audit); + + if ($actionType === 0) { + return null; + } + + // Create task for users + // @bug We are currently searching the groups by static category. This is wrong + // Workflows could be different per company (multiple levels) -> we need to be able to define the group differently + // Maybe we define the group that does a specific step as a setting either in the settings table or settings.json file of the workflow + $groups = GroupMapper::findReadPermission( + $this->app->unitId, + 'Billing', + PermissionCategory::SALES_INVOICE, + $bill->id + ); + + $task = new Task(); + $task->title = ''; + $task->description = ''; + $task->descriptionRaw = ''; + $task->createdBy = new NullAccount($account); + $task->status = TaskStatus::OPEN; + $task->type = TaskType::SINGLE; + $task->redirect = '{/base}/workflow/instance/view?{?}&id=' . $instance->id; + $task->unit = $bill->unit; + $task->priority = TaskPriority::HIGH; + + $element = new TaskElement(); + $element->createdBy = $task->createdBy; + $element->due = $task->due; + $element->priority = $task->priority; + $element->status = TaskStatus::OPEN; + + foreach ($groups as $group) { + $element->addTo(new NullGroup($group)); + } + + $task->addElement($element); + + TaskMapper::create()->execute($task); + + // TaskStep data: + /* + [ + { + 'task_id' => 0, + 'element_id' => 0, + 'level' => 1, // sometimes multiple levels exist per approval (e.g. price until x approved by cso, then cfo, then ceo) + 'approved' => true, + + 'max_price' => , + 'max_price' => , + } + ]; + */ + // @todo steps should have notes for communication (maybe use task elements for that). This way people can communicate + // Maybe create notifications whenever a new note is created? + + return $instance; + } + + private function hookChangeBillState(WorkflowInstanceAbstract $instance, Bill $bill) : bool { + // if all states approved -> set to approved + // careful, if bill create -> check if elements are approved + + $instance->data_int = WorkflowStepStatusEnum::BILL_APPROVAL; + + $foundStep = null; + + // Find matching step (if exists) + foreach ($instance->steps as $step) { + + } + + // @bug It could be multiple steps that we need to handle + // Create new step if not existing + if ($foundStep !== null) { + $foundStep = new WorkflowStep(); + $instance->steps[] = $foundStep; + } + + // new bill + // check credit limit + // check payment term + // check payment approval + // check total price + // check total gross profit + // check correctness + + return false; + } + + private function hookChangeBillElementState(WorkflowInstanceAbstract $instance, Bill $bill) : bool { + // if element within range -> set to approved (unless element exists and is not approved) + + // new element + // quantity changed + // price changed + + // check price + // check quantity + // check gross profit + + return false; + } + + /** + * {@inheritdoc} + */ + public function apiHandleState(RequestAbstract $request, ResponseAbstract $response, $data = []) : void + { + if (($data['trigger'] ?? null) === 'PRE:Billing-print') { + + } + } + + /** + * {@inheritdoc} + */ + public function createInstanceDbModel(WorkflowInstanceAbstract $instance) : void + { + } +} diff --git a/Admin/Install/Workflow/bill/WorkflowState.php b/Admin/Install/Workflow/bill/WorkflowState.php new file mode 100644 index 0000000..e69de29 diff --git a/Admin/Install/Workflow/bill/instance-list.tpl.php b/Admin/Install/Workflow/bill/instance-list.tpl.php new file mode 100644 index 0000000..9fdd481 --- /dev/null +++ b/Admin/Install/Workflow/bill/instance-list.tpl.php @@ -0,0 +1,49 @@ +data['instances']; + +$previous = empty($instances) ? 'workflows/instance/list' : '{/base}/workflows/instance/list?{?}&offset=' . \reset($instances)->id . '&ptype=p'; +$next = empty($instances) ? 'workflows/instance/list' : '{/base}/workflows/instance/list?{?}&offset=' . \end($instances)->id . '&ptype=n'; + +echo $this->data['nav']->render(); +?> +
+
+
+
getHtml('Instances'); ?>download
+
+ + + + + +
+ +
+
+ +
+
+
diff --git a/Admin/Install/Workflow/bill/instance-profile.tpl.php b/Admin/Install/Workflow/bill/instance-profile.tpl.php new file mode 100644 index 0000000..029b5e5 --- /dev/null +++ b/Admin/Install/Workflow/bill/instance-profile.tpl.php @@ -0,0 +1,54 @@ +data['nav']->render(); +?> +
+
+ +
+
+ request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+ // create colored list (grey, red, green) of the different steps + // grey = no action needed + // yellow = action needed + // green = approved + // red = declined +
+
+
+ + request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+
+
Profile
+
+
+
+
+
+
+
+
diff --git a/Admin/Install/Workflow/bill/lang.php b/Admin/Install/Workflow/bill/lang.php new file mode 100644 index 0000000..28853ba --- /dev/null +++ b/Admin/Install/Workflow/bill/lang.php @@ -0,0 +1,18 @@ + [ + 'Overview' => 'Overview', + 'Bill' => 'Bill', + 'CreditLimit' => 'Credit Limit', + 'PaymentTerm' => 'Payment Term', + 'Elements' => 'Elements', + ], + 'de' => [ + 'Overview' => 'Übersicht', + 'Bill' => 'Beleg', + 'CreditLimit' => 'Kreditlimit', + 'PaymentTerm' => 'Zahlungsbedingung', + 'Elements' => 'Belegzeilen', + ], +]; diff --git a/Admin/Install/Workflow/bill/settings.json b/Admin/Install/Workflow/bill/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/Admin/Install/Workflow/bill/settings.tpl.php b/Admin/Install/Workflow/bill/settings.tpl.php new file mode 100644 index 0000000..e69de29 diff --git a/Admin/Install/Workflow/bill/template-profile.tpl.php b/Admin/Install/Workflow/bill/template-profile.tpl.php new file mode 100644 index 0000000..a52a6bb --- /dev/null +++ b/Admin/Install/Workflow/bill/template-profile.tpl.php @@ -0,0 +1,131 @@ +data['template']; +$media = $template->source->getSources(); + +$lang = include $template->source->findFile('lang.php')->getAbsolutePath(); + +echo $this->data['nav']->render(); ?> + +
+
+ +
+
+ request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+
+
+
getHtml('General'); ?>
+
+
+ + +
+ +
+ + +
+
+
+
+
+
+
+ + request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+
+
+
+ flowchart TB; + CREATE_BILL[Create bill]-->LOCKED{Is locked?}; + LOCKED-->|TRUE|CREATE_APPROVAL_TASK[Accounting approval task]; + LOCKED-->|FALSE|PRINTABLE; + CREATE_APPROVAL_TASK-->ACCOUNTING_APPROVAL{Is ok?}; + ACCOUNTING_APPROVAL-->|FALSE|ACCOUNTING_NOT_APPROVED[Inform OP]; + ACCOUNTING_APPROVAL-->|TRUE|PRINTABLE; + CREATE_BILL-->CREATE_CHECK_TASK[Invoice validation task]; + CREATE_CHECK_TASK-->BILL_CHECK{Is correct?}; + BILL_CHECK-->|TRUE|CHECK_PRICES{High discounts?}; + CHECK_PRICES-->|FALSE|PRINTABLE; + BILL_CHECK-->|FALSE|INFO_WRITER[Inform OP]; + CHECK_PRICES-->|TRUE|CREATE_SALES_APPROVAL_TASK[Sales approval task]; + CREATE_SALES_APPROVAL_TASK-->SALES_APPROVAL{Is ok?}; + SALES_APPROVAL-->|TRUE|CHECK_PRICES_ESCALATED{Over limit?}; + SALES_APPROVAL-->|FALSE|SALES_NOT_APPROVED[Inform OP]; + CHECK_PRICES_ESCALATED-->|TRUE|CREATE_CFO_PRICE_APPROVAL[CFO approval task]; + CHECK_PRICES_ESCALATED-->|FALSE|PRINTABLE; + CREATE_CFO_PRICE_APPROVAL-->CFO_APPROVAL{Is ok?}; + CFO_APPROVAL-->|TRUE|PRINTABLE[Mark printable]; + CFO_APPROVAL-->|FALSE|CFO_NOT_APPROVED[Inform OP + Sales]; + + CLICK_PRINT[Click print]-->IS_APPROVED{Is approved}; + IS_APPROVED-->|TRUE|PRINT[Print]; + IS_APPROVED-->|FALSE|PRINT_ERROR[Show print error]; + + UPDATE_BILL[Update bill]-->CHECK_THREASHOLDS{Change above threshold}; + CHECK_THREASHOLDS-->|TRUE|OPEN_TASKS[Update & re-open tasks]; +
+
+
+
+
+
+ + request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+
+ + request->uri->fragment) || $this->request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> +
+
+
+
+
getHtml('Media'); ?>download
+ + + + + extension === 'collection' + ? UriFactory::build('{/base}/media/list?path=' . \rtrim($file->getVirtualPath(), '/') . '/' . $file->name) + : UriFactory::build('{/base}/media/view?id=' . $file->id + . '&path={?path}' . ( + $file->id === 0 + ? '/' . $file->name + : '' + ) + ); + + ?> + +
+ getHtml('Name'); ?> + getHtml('Type'); ?> +
+ name; ?> + extension; ?> + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/Admin/Install/Workflow/bill/workflow.json b/Admin/Install/Workflow/bill/workflow.json new file mode 100644 index 0000000..bc893cf --- /dev/null +++ b/Admin/Install/Workflow/bill/workflow.json @@ -0,0 +1,12 @@ +[ + { + "name": "IS_APPROVED", + "title": "Is approved?", + "hooks": [ + ], + "if": { + "isApproved": true, + "action": "print" + } + } +] \ No newline at end of file diff --git a/Admin/Install/db.json b/Admin/Install/db.json index c6d51d5..67b76ae 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -907,6 +907,11 @@ "null": false, "foreignTable": "unit", "foreignKey": "unit_id" + }, + "billing_bill_approval": { + "name": "billing_bill_approval", + "type": "INT", + "null": false } } }, @@ -1278,6 +1283,11 @@ "type": "INT", "default": null, "null": true + }, + "billing_bill_element_approval": { + "name": "billing_bill_element_approval", + "type": "INT", + "null": false } } }, diff --git a/Controller/ApiAttributeController.php b/Controller/ApiAttributeController.php index c65058d..9fdc553 100755 --- a/Controller/ApiAttributeController.php +++ b/Controller/ApiAttributeController.php @@ -243,7 +243,7 @@ final class ApiAttributeController extends Controller ->with('type') ->with('type/defaults') ->with('value') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); $new = $this->updateAttributeFromRequest($request, clone $old); @@ -291,7 +291,7 @@ final class ApiAttributeController extends Controller $billAttribute = BillAttributeMapper::get() ->with('type') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); if ($billAttribute->type->isRequired) { @@ -327,7 +327,7 @@ final class ApiAttributeController extends Controller } /** @var BaseStringL11n $old */ - $old = BillAttributeTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = BillAttributeTypeL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $new = $this->updateAttributeTypeL11nFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillAttributeTypeL11nMapper::class, 'bill_attribute_type_l11n', $request->getOrigin()); @@ -357,7 +357,7 @@ final class ApiAttributeController extends Controller } /** @var BaseStringL11n $billAttributeTypeL11n */ - $billAttributeTypeL11n = BillAttributeTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $billAttributeTypeL11n = BillAttributeTypeL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billAttributeTypeL11n, BillAttributeTypeL11nMapper::class, 'bill_attribute_type_l11n', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billAttributeTypeL11n); } @@ -385,7 +385,7 @@ final class ApiAttributeController extends Controller } /** @var AttributeType $old */ - $old = BillAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('id'))->execute(); + $old = BillAttributeTypeMapper::get()->with('defaults')->where('id', $request->getDataInt('id') ?? 0)->execute(); $new = $this->updateAttributeTypeFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillAttributeTypeMapper::class, 'bill_attribute_type', $request->getOrigin()); @@ -417,7 +417,7 @@ final class ApiAttributeController extends Controller } /** @var AttributeType $billAttributeType */ - $billAttributeType = BillAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('id'))->execute(); + $billAttributeType = BillAttributeTypeMapper::get()->with('defaults')->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billAttributeType, BillAttributeTypeMapper::class, 'bill_attribute_type', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billAttributeType); } @@ -445,7 +445,7 @@ final class ApiAttributeController extends Controller } /** @var AttributeValue $old */ - $old = BillAttributeValueMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = BillAttributeValueMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); /** @var \Modules\Attribute\Models\Attribute $attr */ $attr = BillAttributeMapper::get() @@ -485,7 +485,7 @@ final class ApiAttributeController extends Controller // } // /** @var \Modules\Billing\Models\BillAttributeValue $billAttributeValue */ - // $billAttributeValue = BillAttributeValueMapper::get()->where('id', (int) $request->getData('id'))->execute(); + // $billAttributeValue = BillAttributeValueMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); // $this->deleteModel($request->header->account, $billAttributeValue, BillAttributeValueMapper::class, 'bill_attribute_value', $request->getOrigin()); // $this->createStandardDeleteResponse($request, $response, $billAttributeValue); } @@ -513,7 +513,7 @@ final class ApiAttributeController extends Controller } /** @var BaseStringL11n $old */ - $old = BillAttributeValueL11nMapper::get()->where('id', (int) $request->getData('id')); + $old = BillAttributeValueL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0); $new = $this->updateAttributeValueL11nFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillAttributeValueL11nMapper::class, 'bill_attribute_value_l11n', $request->getOrigin()); @@ -543,7 +543,7 @@ final class ApiAttributeController extends Controller } /** @var BaseStringL11n $billAttributeValueL11n */ - $billAttributeValueL11n = BillAttributeValueL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $billAttributeValueL11n = BillAttributeValueL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billAttributeValueL11n, BillAttributeValueL11nMapper::class, 'bill_attribute_value_l11n', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billAttributeValueL11n); } diff --git a/Controller/ApiBillController.php b/Controller/ApiBillController.php index fbebea3..43ad3d6 100755 --- a/Controller/ApiBillController.php +++ b/Controller/ApiBillController.php @@ -64,6 +64,10 @@ use phpOMS\Model\Message\FormValidation; use phpOMS\Stdlib\Base\FloatInt; use phpOMS\System\MimeType; use phpOMS\Views\View; +use Modules\Billing\Models\WorkflowType; +use Modules\Workflow\Models\WorkflowTemplateMapper; +use Modules\Workflow\Models\WorkflowTemplateStatus; +use phpOMS\Message\Http\HttpResponse; /** * Billing class. @@ -278,7 +282,7 @@ final class ApiBillController extends Controller return; } - $this->app->eventManager->triggerSimilar('PRE:Module:' . self::NAME . '-bill-finalize', '', [ + $this->app->eventManager->triggerSimilar('PRE:' . self::NAME . '-bill-finalize', '', [ $request->header->account, null, $new, null, self::NAME . '-bill-finalize', @@ -317,7 +321,7 @@ final class ApiBillController extends Controller } /** @var \Modules\Billing\Models\Bill $old */ - $old = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); + $old = BillMapper::get()->where('id', $request->getDataInt('bill') ?? 0)->execute(); // @feature Allow to update internal statistical fields // Example: Referral account @@ -482,7 +486,7 @@ final class ApiBillController extends Controller $bill->accSection = empty($temp = $bill->client->getAttribute('section')->value->id) ? null : $temp; $bill->accGroup = empty($temp = $bill->client->getAttribute('client_group')->value->id) ? null : $temp; $bill->accType = empty($temp = $bill->client->getAttribute('client_type')->value->id) ? null : $temp; - $bill->rep = $request->hasData('rep') ? new NullSalesRep((int) $request->getData('rep')) : $account->rep; + $bill->rep = $request->hasData('rep') ? new NullSalesRep($request->getDataInt('re ?? 0p')) : $account->rep; } else { $bill->supplier = $account; $bill->accTaxCode = empty($temp = $bill->supplier->getAttribute('purchase_tax_code')->value->id) ? null : $temp; @@ -602,7 +606,7 @@ final class ApiBillController extends Controller $attr->value = $attrValue; $container = $request->hasData('container') - ? new NullContainer((int) $request->getData('container')) + ? new NullContainer($request->getDataInt('co ?? 0ntainer')) : null; $attr = new NullAttribute(); @@ -723,7 +727,7 @@ final class ApiBillController extends Controller ->with('attributes') ->with('attributes/type') ->with('attributes/value') - ->where('id', (int) $request->getData('client')) + ->where('id', $request->getDataInt('client') ?? 0) ->where('attributes/type/name', [ 'segment', 'section', 'client_group', 'client_type', 'sales_tax_code', @@ -740,7 +744,7 @@ final class ApiBillController extends Controller ->with('attributes') ->with('attributes/type') ->with('attributes/value') - ->where('id', (int) $request->getData('supplier')) + ->where('id', $request->getDataInt('supplier') ?? 0) ->where('attributes/type/name', [ 'purchase_tax_code', ], 'IN') @@ -797,7 +801,7 @@ final class ApiBillController extends Controller } /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('ref'))->execute(); + $bill = BillMapper::get()->where('id', $request->getDataInt('ref') ?? 0)->execute(); $path = $this->createBillDir($bill); $uploaded = new NullCollection(); @@ -864,10 +868,10 @@ final class ApiBillController extends Controller } /** @var \Modules\Media\Models\Media $media */ - $media = MediaMapper::get()->where('id', (int) $request->getData('media'))->execute(); + $media = MediaMapper::get()->where('id', $request->getDataInt('media') ?? 0)->execute(); /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute(); + $bill = BillMapper::get()->where('id', $request->getDataInt('bill') ?? 0)->execute(); // Cannot delete system generated bill if (\stripos($media->name, $bill->number) !== false) { @@ -1593,7 +1597,7 @@ final class ApiBillController extends Controller } /** @var \Modules\Billing\Models\Bill $bill */ - $bill = BillMapper::get()->where('id', (int) $request->getData('ref'))->execute(); + $bill = BillMapper::get()->where('id', $request->getDataInt('ref') ?? 0)->execute(); $request->setData('virtualpath', $this->createBillDir($bill), true); $this->app->moduleManager->get('Editor', 'Api')->apiEditorCreate($request, $response, $data); @@ -1659,7 +1663,7 @@ final class ApiBillController extends Controller } /** @var \Modules\Billing\Models\Bill $old */ - $old = BillMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = BillMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); // @todo check if bill can be deleted // @todo adjust stock transfer @@ -1730,7 +1734,7 @@ final class ApiBillController extends Controller /** @var BillElement $old */ $old = BillElementMapper::get() ->with('bill') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); if ($old->bill->status === BillStatus::ARCHIVED) { @@ -1813,7 +1817,7 @@ final class ApiBillController extends Controller // @todo handle transactions and bill update /** @var \Modules\Billing\Models\BillElement $billElement */ - $billElement = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $billElement = BillElementMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billElement, BillElementMapper::class, 'bill_element', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billElement); } diff --git a/Controller/ApiBillTypeController.php b/Controller/ApiBillTypeController.php index f588a1e..3f0aa31 100755 --- a/Controller/ApiBillTypeController.php +++ b/Controller/ApiBillTypeController.php @@ -208,7 +208,7 @@ final class ApiBillTypeController extends Controller } /** @var BillType $old */ - $old = BillTypeMapper::get()->where('id', (int) $request->getData('id')); + $old = BillTypeMapper::get()->where('id', $request->getDataInt('id') ?? 0); $new = $this->updateBillTypeFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillTypeMapper::class, 'bill_type', $request->getOrigin()); @@ -285,7 +285,7 @@ final class ApiBillTypeController extends Controller } /** @var \Modules\Billing\Models\BillType $billType */ - $billType = BillTypeMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $billType = BillTypeMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billType, BillTypeMapper::class, 'bill_type', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billType); } @@ -332,7 +332,7 @@ final class ApiBillTypeController extends Controller } /** @var BaseStringL11n $old */ - $old = BillTypeL11nMapper::get()->where('id', (int) $request->getData('id')); + $old = BillTypeL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0); $new = $this->updateBillTypeL11nFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, BillTypeL11nMapper::class, 'bill_type_l11n', $request->getOrigin()); @@ -400,7 +400,7 @@ final class ApiBillTypeController extends Controller } /** @var BaseStringL11n $billTypeL11n */ - $billTypeL11n = BillTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $billTypeL11n = BillTypeL11nMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $billTypeL11n, BillTypeL11nMapper::class, 'bill_type_l11n', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $billTypeL11n); } diff --git a/Controller/ApiPriceController.php b/Controller/ApiPriceController.php index 3f4c6c5..74843b7 100755 --- a/Controller/ApiPriceController.php +++ b/Controller/ApiPriceController.php @@ -486,7 +486,7 @@ final class ApiPriceController extends Controller } /** @var \Modules\Billing\Models\Price\Price $old */ - $old = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = PriceMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $new = $this->updatePriceFromRequest($request, clone $old); $this->app->cachePool->get()->delete( @@ -605,7 +605,7 @@ final class ApiPriceController extends Controller } /** @var \Modules\Billing\Models\Price\Price $price */ - $price = PriceMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $price = PriceMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); if ($price->name === 'default') { // default price cannot be deleted diff --git a/Controller/ApiPurchaseController.php b/Controller/ApiPurchaseController.php index ed59422..9eb94d3 100755 --- a/Controller/ApiPurchaseController.php +++ b/Controller/ApiPurchaseController.php @@ -143,6 +143,9 @@ final class ApiPurchaseController extends Controller } */ + // @question How do we allow to manually assign a bill to a person/group for approval? + // Possible solution: create general module called Approval for all kinds of approvals + $documents = $files; foreach ($documents as $file) { @@ -255,7 +258,7 @@ final class ApiPurchaseController extends Controller } $bill = BillMapper::get() - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); // After a bill is "closed" its values shouldn't change diff --git a/Controller/ApiTaxController.php b/Controller/ApiTaxController.php index f267b4b..b5a1763 100755 --- a/Controller/ApiTaxController.php +++ b/Controller/ApiTaxController.php @@ -304,7 +304,7 @@ final class ApiTaxController extends Controller } /** @var \Modules\Billing\Models\Tax\TaxCombination $old */ - $old = TaxCombinationMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $old = TaxCombinationMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $new = $this->updateTaxCombinationFromRequest($request, clone $old); $this->updateModel($request->header->account, $old, $new, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin()); @@ -463,7 +463,7 @@ final class ApiTaxController extends Controller } /** @var \Modules\Billing\Models\Tax\TaxCombination $taxCombination */ - $taxCombination = TaxCombinationMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $taxCombination = TaxCombinationMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $this->deleteModel($request->header->account, $taxCombination, TaxCombinationMapper::class, 'tax_combination', $request->getOrigin()); $this->createStandardDeleteResponse($request, $response, $taxCombination); } diff --git a/Controller/BackendController.php b/Controller/BackendController.php index 7303d70..1419be7 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -246,7 +246,7 @@ final class BackendController extends Controller ->with('files') ->with('files/tags') ->with('notes') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); $view->data['billtypes'] = BillTypeMapper::getAll() @@ -442,7 +442,7 @@ final class BackendController extends Controller ->with('files') ->with('files/tags') ->with('notes') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); $view->data['billtypes'] = BillTypeMapper::getAll() @@ -537,7 +537,7 @@ final class BackendController extends Controller $view->setTemplate('/Modules/Billing/Theme/Backend/purchase-bill'); $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1005106001, $request, $response); - $bill = BillMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $bill = BillMapper::get()->where('id', $request->getDataInt('id') ?? 0)->execute(); $view->data['bill'] = $bill; $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); @@ -647,7 +647,7 @@ final class BackendController extends Controller ->with('files') ->with('files/tags') ->with('notes') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); $tags = TagMapper::getAll() @@ -713,7 +713,7 @@ final class BackendController extends Controller $view->data['type'] = PaymentTermMapper::get() ->with('l11n') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->where('l11n/language', $response->header->l11n->language) ->execute(); @@ -775,7 +775,7 @@ final class BackendController extends Controller $view->data['type'] = ShippingTermMapper::get() ->with('l11n') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->where('l11n/language', $response->header->l11n->language) ->execute(); @@ -887,7 +887,7 @@ final class BackendController extends Controller ->with('supplierCode') ->with('itemCode') ->with('taxCode') - ->where('id', (int) $request->getData('id')) + ->where('id', $request->getDataInt('id') ?? 0) ->execute(); $view->data['client_codes'] = ClientAttributeTypeMapper::get() diff --git a/Controller/CliController.php b/Controller/CliController.php index f3cf993..5293b65 100755 --- a/Controller/CliController.php +++ b/Controller/CliController.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Modules\Billing\Controller; +use Modules\Admin\Models\AccountMapper; use Modules\Billing\Models\BillElement; use Modules\Billing\Models\BillElementMapper; use Modules\Billing\Models\BillMapper; @@ -29,6 +30,7 @@ use Modules\SupplierManagement\Models\NullSupplier; use Modules\SupplierManagement\Models\Supplier; use Modules\SupplierManagement\Models\SupplierMapper; use Modules\Tag\Models\TagMapper; +use phpOMS\Account\AccountStatus; use phpOMS\Contract\RenderableInterface; use phpOMS\Localization\ISO4217CharEnum; use phpOMS\Localization\ISO4217DecimalEnum; @@ -121,12 +123,22 @@ final class CliController extends Controller $identifiers = \json_decode($identifierContent, true); /* Supplier */ + // @performance Do we really want to select all the attributes below or only after we have found a suitable supplier + // We don't need these attributes initially, only once we found a matching supplier + + // @performance We can't select all suppliers in one go, we probably need to iterate in chunks + + // @bug We are missing the payment information here used in the matchSupplier() function + + // @performance Could it be better to first perform some parsing of the bill to get the payment information and find the supplier based on that first? + // If we find a supplier this would be much faster, if it doesn't we can still do the brute force below + /** @var \Modules\SupplierManagement\Models\Supplier[] $suppliers */ $suppliers = SupplierMapper::getAll() ->with('account') ->with('mainAddress') ->with('attributes/type') - ->where('attributes/type/name', ['bill_match_pattern', 'bill_date_format'], 'IN') + ->where('attributes/type/name', ['bill_match_pattern', 'bill_date_format', 'bill_approval'], 'IN') ->executeGetArray(); $bill->supplier = $this->matchSupplier($content, $suppliers); @@ -482,6 +494,18 @@ final class CliController extends Controller $billResponse = new HttpResponse(); $this->app->moduleManager->get('Billing', 'ApiBill')->apiBillPdfArchiveCreate($request, $billResponse); + if ($bill->supplier->id !== 0) { + // @question Do we want to also create a notification for the people in the default group + + /* + $approvalAccounts = AccountMapper::getAll() + ->with('groups') + ->where('status', AccountStatus::ACTIVE) + ->where('groups/name', $bill->supplier->getAttribute('bill_approval')->value->valueStr) + ->executeGetArray(); + */ + } + return $view; } diff --git a/Models/Bill.php b/Models/Bill.php index da0bf9e..b7713e5 100755 --- a/Models/Bill.php +++ b/Models/Bill.php @@ -19,6 +19,8 @@ use Modules\Admin\Models\NullAccount; use Modules\ClientManagement\Models\Client; use Modules\Sales\Models\SalesRep; use Modules\SupplierManagement\Models\Supplier; +use Modules\Workflow\Models\NullWorkflowStep; +use Modules\Workflow\Models\WorkflowStep; use phpOMS\Localization\BaseStringL11nType; use phpOMS\Localization\ISO4217CharEnum; use phpOMS\Localization\ISO639x1Enum; @@ -57,6 +59,8 @@ class Bill implements \JsonSerializable public int $unit = 0; + public WorkflowStep $approval; + public int $source = 0; /** @@ -418,6 +422,7 @@ class Bill implements \JsonSerializable public ?string $fiAccount = null; // @todo Implement reason for bill (especially useful for credit notes, warehouse bookings) + // @todo Implement internal notes for bill /** * Constructor. @@ -438,6 +443,8 @@ class Bill implements \JsonSerializable $this->createdBy = new NullAccount(); $this->referral = new NullAccount(); $this->type = new NullBillType(); + + $this->approval = new NullWorkflowStep(); } /** diff --git a/Models/BillElement.php b/Models/BillElement.php index 9fd2426..0c19c5c 100755 --- a/Models/BillElement.php +++ b/Models/BillElement.php @@ -18,6 +18,8 @@ use Modules\Billing\Models\Tax\TaxCombination; use Modules\ItemManagement\Models\Container; use Modules\ItemManagement\Models\Item; use Modules\ItemManagement\Models\NullItem; +use Modules\Workflow\Models\NullWorkflowStep; +use Modules\Workflow\Models\WorkflowStep; use phpOMS\Localization\ISO4217DecimalEnum; use phpOMS\Stdlib\Base\FloatInt; use phpOMS\Stdlib\Base\SmartDateTime; @@ -42,6 +44,8 @@ class BillElement implements \JsonSerializable public int $order = 0; + public WorkflowStep $approval; + public ?Item $item = null; public ?Container $container = null; @@ -201,6 +205,8 @@ class BillElement implements \JsonSerializable $this->taxP = new FloatInt(); $this->taxR = new FloatInt(); + + $this->approval = new NullWorkflowStep(); } /** diff --git a/Models/BillElementMapper.php b/Models/BillElementMapper.php index ad3300b..f4fab42 100755 --- a/Models/BillElementMapper.php +++ b/Models/BillElementMapper.php @@ -17,6 +17,7 @@ namespace Modules\Billing\Models; use Modules\Billing\Models\Tax\TaxCombinationMapper; use Modules\ItemManagement\Models\ContainerMapper; use Modules\ItemManagement\Models\ItemMapper; +use Modules\Workflow\Models\WorkflowStepMapper; use phpOMS\DataStorage\Database\Mapper\DataMapperFactory; /** @@ -80,6 +81,8 @@ final class BillElementMapper extends DataMapperFactory 'billing_bill_element_fiaccount' => ['name' => 'billing_bill_element_fiaccount', 'type' => 'string', 'internal' => 'fiAccount'], 'billing_bill_element_costcenter' => ['name' => 'billing_bill_element_costcenter', 'type' => 'string', 'internal' => 'costcenter'], 'billing_bill_element_costobject' => ['name' => 'billing_bill_element_costobject', 'type' => 'string', 'internal' => 'costobject'], + + 'billing_bill_element_approval' => ['name' => 'billing_bill_element_approval', 'type' => 'int', 'internal' => 'approval'], ]; /** @@ -118,6 +121,10 @@ final class BillElementMapper extends DataMapperFactory 'mapper' => TaxCombinationMapper::class, 'external' => 'billing_bill_element_tax_combination', ], + 'approval' => [ + 'mapper' => WorkflowStepMapper::class, + 'external' => 'billing_bill_element_approval', + ], ]; /** diff --git a/Models/BillMapper.php b/Models/BillMapper.php index 6961b48..b47e1b1 100755 --- a/Models/BillMapper.php +++ b/Models/BillMapper.php @@ -21,6 +21,7 @@ use Modules\Editor\Models\EditorDocMapper; use Modules\Media\Models\MediaMapper; use Modules\Sales\Models\SalesRepMapper; use Modules\SupplierManagement\Models\SupplierMapper; +use Modules\Workflow\Models\WorkflowStepMapper; use phpOMS\DataStorage\Database\Mapper\DataMapperFactory; /** @@ -100,6 +101,7 @@ class BillMapper extends DataMapperFactory 'billing_bill_performance_date' => ['name' => 'billing_bill_performance_date', 'type' => 'DateTime', 'internal' => 'performanceDate', 'readonly' => true], 'billing_bill_created_at' => ['name' => 'billing_bill_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], 'billing_bill_unit' => ['name' => 'billing_bill_unit', 'type' => 'int', 'internal' => 'unit'], + 'billing_bill_approval' => ['name' => 'billing_bill_approval', 'type' => 'int', 'internal' => 'approval'], ]; /** @@ -162,6 +164,10 @@ class BillMapper extends DataMapperFactory 'mapper' => ShippingTermMapper::class, 'external' => 'shippingTerms', ], + 'approval' => [ + 'mapper' => WorkflowStepMapper::class, + 'external' => 'approval', + ], ]; /** diff --git a/Models/BillStatus.php b/Models/BillStatus.php index 2c58a48..fe9cd11 100755 --- a/Models/BillStatus.php +++ b/Models/BillStatus.php @@ -32,6 +32,8 @@ abstract class BillStatus extends Enum public const DELETED = 4; + // Bill is completed and ready to be finalized + // This means the bill can now be approved (if the workflow is defined that way) public const DONE = 8; public const DRAFT = 16; diff --git a/Models/WorkflowStepStatusEnum.php b/Models/WorkflowStepStatusEnum.php new file mode 100644 index 0000000..49a5507 --- /dev/null +++ b/Models/WorkflowStepStatusEnum.php @@ -0,0 +1,61 @@ +data['media'] ?? []; /** @var \Modules\Billing\Models\Bill $bill */ -$bill = $this->getData('bill') ?? new NullBill(); +$bill = $this->data['bill'] ?? new NullBill(); $elements = $bill->elements; $billTypes = $this->data['billtypes'] ?? []; diff --git a/Theme/Cli/bill-parsed.tpl.php b/Theme/Cli/bill-parsed.tpl.php index 5dc0650..29d01a6 100755 --- a/Theme/Cli/bill-parsed.tpl.php +++ b/Theme/Cli/bill-parsed.tpl.php @@ -1,3 +1,3 @@ getData('bill') ?? null, \JSON_PRETTY_PRINT); +echo \json_encode($this->data['bill'] ?? null, \JSON_PRETTY_PRINT); diff --git a/info.json b/info.json index 56ba1c9..c391eb0 100755 --- a/info.json +++ b/info.json @@ -23,7 +23,8 @@ "Calendar": "1.0.0", "ItemManagement": "1.0.0", "ClientManagement": "1.0.0", - "SupplierManagement": "1.0.0" + "SupplierManagement": "1.0.0", + "Workflow": "1.0.0" }, "providing": { "Admin": "*",