From 1ed2dddb97bc3420174c637758f53b0144d009a4 Mon Sep 17 00:00:00 2001 From: Frank Schubert Date: Thu, 29 Aug 2024 14:51:37 +0200 Subject: [PATCH] Added InvoiceJob --- Layout/default/Invoice/Index.php | 182 +++++++++++- application/Invoice/Invoice.php | 8 +- application/Invoice/InvoiceController.php | 94 +++++- application/InvoiceJob/InvoiceJob.php | 12 + .../InvoiceJob/InvoiceJobController.php | 51 ++++ application/InvoiceJob/InvoiceJobModel.php | 253 ++++++++++++++++ .../20240827113849_create_invoice_job.php | 43 +++ lib/FronkDB/FronkDB.php | 6 + lib/mvcfronk/mfBase/mfBaseController.php | 11 +- lib/mvcfronk/mfBase/mfBaseModel.php | 9 +- scripts/invoice/invoice-job-broker.php | 269 ++++++++++++++++++ .../invoice/job-runners/make-invoice-pdf.php | 69 +++++ .../job-runners/send-invoice-email.php | 74 +++++ 13 files changed, 1051 insertions(+), 30 deletions(-) create mode 100644 application/InvoiceJob/InvoiceJob.php create mode 100644 application/InvoiceJob/InvoiceJobController.php create mode 100644 application/InvoiceJob/InvoiceJobModel.php create mode 100644 db/migrations/20240827113849_create_invoice_job.php create mode 100755 scripts/invoice/invoice-job-broker.php create mode 100644 scripts/invoice/job-runners/make-invoice-pdf.php create mode 100644 scripts/invoice/job-runners/send-invoice-email.php diff --git a/Layout/default/Invoice/Index.php b/Layout/default/Invoice/Index.php index 20e3aea55..271736f44 100644 --- a/Layout/default/Invoice/Index.php +++ b/Layout/default/Invoice/Index.php @@ -72,13 +72,11 @@ $pagination_entity_name = "Rechnungen";
-
- - ?resetFilter=1">Filter zurücksetzen -
-
- - +
+ + ?resetFilter=1"> Filter zurücksetzen + +
@@ -126,13 +124,106 @@ $pagination_entity_name = "Rechnungen";
-
-
-

Rechnungen

-
-
-
+ +
+
+
+
+
+
+

PDFs erstellen

+
"> + +
+
+ + +
+
+ + +
+
+ + + + +
Aktuelle PDF Jobs
+ + + + + + + + + + + + + + + + + +
VonBisGestartetBeendetStatus
from_date?>to_date?>started?>finished?> + status == "timeout" ? "text-danger" : "text-info"))?>">status) ? $job->status : "neu"?>
+ getResult("created") ? $job->getResult("created")." Rechungs-PDFs erstellt" : ""?> +
+ + +
+
+
+

Rechungsemails versenden

+
"> + +
+
+ + +
+
+ + +
+
+ + + + +
Aktuelle Email Jobs
+ + + + + + + + + + + + + + + + + +
VonBisGestartetBeendetStatus
from_date?>to_date?>started?>finished?> + status == "timeout" ? "text-danger" : "text-info"))?>">status) ? $job->status : "neu"?>
+ getResult("sent") ? $job->getResult("sent")." Emails versendet" : ""?> +
+ +
+
+
+ + +
+
+
@@ -189,6 +280,12 @@ $pagination_entity_name = "Rechnungen"; +
+
+

Rechnungen

+
+
+
@@ -291,6 +388,65 @@ $pagination_entity_name = "Rechnungen"; todayBtn: 'linked', autoclose: true }); + $('.datepicker').datepicker({ + orientation: "bottom", + language: 'de', + format: "dd.mm.yyyy", + showWeekDays: true, + todayBtn: 'linked', + autoclose: true + }); + + var status_update; + + function updateStatus() { + $.post( + "", + { + do: "getActiveJobs" + }, + function(success) { + if(success.status != "OK") return; + + jobs = success.result.jobs; + jobs.forEach((job) => { + let status = JSON.parse(job.result); + if(!status) return; + + if(job.task == "make-invoice-pdf") { + $("#job-" + job.id + " .status-text").text(status.created + " Rechungs-PDFs erstellt"); + } else if(job.task == "send-invoice-email") { + $("#job-" + job.id + " .status-text").text(status.sent + " Emails versendet"); + } + + $("#job-" + job.id + " .status").text(job.status ? job.status : "neu"); + if(job.status == "timeout") { + $("#job-" + job.id + " .status").removeClass("text-info text-success").addClass("text-danger"); + } else if(job.status == "finished") { + $("#job-" + job.id + " .status").removeClass("text-info text-danger").addClass("text-success"); + } else { + $("#job-" + job.id + " .status").removeClass("text-danger text-success").addClass("text-info"); + } + + if(job.finished) { + $("#job-" + job.id + " .finished").text(job.finished); + } else { + $("#job-" + job.id + " .finished").text(""); + } + }); + + status_update = setTimeout(updateStatus, 1000); + }, + "json" + ); + } + + $(document).ready(() => { + + status_update = setTimeout(updateStatus, 1000); + + }); + \ No newline at end of file diff --git a/application/Invoice/Invoice.php b/application/Invoice/Invoice.php index 22efe830d..454099e69 100644 --- a/application/Invoice/Invoice.php +++ b/application/Invoice/Invoice.php @@ -108,7 +108,13 @@ XINON GmbH"; return false; } - $pdf_filename = $pdf->getFullPath(); + $pdf_filename = false; + try { + $pdf_filename = $pdf->getFullPath(); + } catch(\Exception $e) { + $this->log->error("File for Invoice ".$this->id." not found"); + } + if(!$pdf_filename || !file_exists($pdf_filename)) { return false; } diff --git a/application/Invoice/InvoiceController.php b/application/Invoice/InvoiceController.php index 96b2a4b8a..2b594aa8e 100644 --- a/application/Invoice/InvoiceController.php +++ b/application/Invoice/InvoiceController.php @@ -57,6 +57,17 @@ class InvoiceController extends mfBaseController { $this->layout()->set("invoices", $billings); $this->layout()->set("pagination", $pagination); + // get Jobs + $now = new DateTime("now"); + $pdf_jobs = InvoiceJobModel::search(["task" => "make-invoice-pdf", "to_date>=" => $now->format("Y-m-d")]); + $email_jobs = InvoiceJobModel::search(["task" => "send-invoice-email", "to_date>=" => $now->format("Y-m-d")]); + + $this->layout()->set("pdf_jobs", $pdf_jobs); + $this->layout()->set("email_jobs", $email_jobs); + + + // sum listing + $sum_price = InvoiceModel::getSumPrice($filter); $sum_price_gross = InvoiceModel::getSumGrossPrice($filter); $sum_price_sepa = InvoiceModel::getSumPrice(array_merge($filter, ["billing_type" => "sepa"])); @@ -202,6 +213,45 @@ class InvoiceController extends mfBaseController { } + protected function createJob() { + $r = $this->request; + //var_dump($r->get());exit; + + $task = $r->task; + if($task != "make-invoice-pdf" && $task != "send-invoice-email") { + $this->layout()->setFlash("Ungültiger Task", "error"); + $this->redirect("Invoice"); + } + + if(!$r->from_date || !$r->to_date) { + $this->layout()->setFlash("Start- und Endddatum werden benötigt", "error"); + $this->redirect("Invoice"); + } + + try { + $from_date = DateTime::createFromFormat("d.m.Y", $r->from_date, new DateTimeZone("Europe/Vienna")); + $from_date->setTime(0,0,0); + + $to_date = DateTime::createFromFormat("d.m.Y", $r->to_date, new DateTimeZone("Europe/Vienna")); + $to_date->setTime(23,59,59); + } catch(Exception $e) { + $this->layout()->setFlash("Von- oder Bisdatum ungültig", "error"); + $this->redirect("Invoice"); + } + + $job = InvoiceJobModel::create([ + "task" => $task, + "from_date" => $from_date->format("Y-m-d"), + "to_date" => $to_date->format("Y-m-d"), + ]); + if(!$job->save()) { + $this->layout()->setFlash("Fehler beim Speichern", "error"); + $this->redirect("Invoice"); + } + + $this->layout()->setFlash("Task erfolgreich gespeichert", "success"); + $this->redirect("Invoice"); + } protected function runInvoicingAction() { if(!$this->me->can("Billing")) { @@ -784,9 +834,13 @@ class InvoiceController extends mfBaseController { exit; } - public function createPDFs() { + public function createPDFs($limit = false) { $invoice_path_base = MFUPLOAD_FILE_SAVE_PATH."/".TT_INVOICE_SAVE_SUBFOLDER; + $created = 0; foreach(InvoiceModel::getAll() as $invoice) { + if($limit && $created >= $limit) { + return $created; + } if(InvoiceFileModel::getFirst(["invoice_id" => $invoice->id])) { continue; } @@ -803,11 +857,11 @@ class InvoiceController extends mfBaseController { if(!$ifile) { echo "Could not create PDF for ".$invoice->invoice_number."\n"; } - - echo "."; + $created++; + //echo "."; } - echo "\n"; - return true; + //echo "\n"; + return $created; } protected function sendInvoicesAction() { @@ -831,28 +885,40 @@ class InvoiceController extends mfBaseController { /* * Gutschriften auch an Xinon */ - public function _sendEmailInvoices() { + public function _sendEmailInvoices($limit = false) { + $sent = 0; + $defer = 0; foreach(InvoiceModel::search(["billing_delivery" => "email", "date_delivered" => false]) as $invoice) { + if($limit && $sent >= $limit) { + return ["sent" => $sent, "defer" => $defer]; + } $pdf = $invoice->pdf; if(!$pdf || !$pdf->name) { - echo "PDF für ".$invoice->invoice_number." noch nicht generiert\n"; + //echo "PDF für ".$invoice->invoice_number." noch nicht generiert\n"; + $defer++; continue; } - $pdf_file = $pdf->getFullPath(); + $pdf_file = false; + try { + $pdf_file = $pdf->getFullPath(); + } catch (Exception $e) { + $this->log->error("File for Invoice ".$invoice->id." not found"); + } + if(!file_exists($pdf_file)) { - echo "Datei ".$pdf->filename." nicht gefunden\n"; + //echo "Datei ".$pdf->filename." nicht gefunden\n"; continue; } if($invoice->total == 0) { - echo "Skipping ".$invoice->invoice_number." because total is zero\n"; + //echo "Skipping ".$invoice->invoice_number." because total is zero\n"; continue; } if(!$invoice->sendByEmail()) { - echo "Error sending ".$invoice->invoice_number." to ".$invoice->email."\n"; + $this->log->warning("Error sending ".$invoice->invoice_number." to ".$invoice->email); continue; } $invoice->date_delivered = date("U"); @@ -863,11 +929,11 @@ class InvoiceController extends mfBaseController { $invoice->sendByEmail("billing@xinon.at"); } + $sent++; + } - - - return true; + return ["sent" => $sent, "defer" => $defer]; } public function printInvoices() { diff --git a/application/InvoiceJob/InvoiceJob.php b/application/InvoiceJob/InvoiceJob.php new file mode 100644 index 000000000..f26a9c835 --- /dev/null +++ b/application/InvoiceJob/InvoiceJob.php @@ -0,0 +1,12 @@ +result); + if(json_last_error() === JSON_ERROR_NONE) { + return $result->$variable; + } + return null; + } +} \ No newline at end of file diff --git a/application/InvoiceJob/InvoiceJobController.php b/application/InvoiceJob/InvoiceJobController.php new file mode 100644 index 000000000..c382fd1cd --- /dev/null +++ b/application/InvoiceJob/InvoiceJobController.php @@ -0,0 +1,51 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me", $me); + + if (!$me->is(["Admin"])) { + $this->redirect("Dashboard"); + } + } + + protected function apiAction() { + if(!$this->me->is(["Admin"])) { + $this->redirect("Dashboard"); + } + $do = $this->request->do; + $data = []; + + switch($do) { + case "getActiveJobs": + $return = $this->getActiveJobsApi(); + break; + default: + $return = false; + } + + if(!is_array($return) || !count($return)) { + $data = ["status" => "error"]; + $this->returnJson($data); + } + $data['status'] = "OK"; + $data['result'] = $return; + $this->returnJson($data); + } + + private function getActiveJobsApi() { + $now = new DateTime("now"); + $jobs = []; + foreach(InvoiceJobModel::search(["to_date>=" => $now->format("Y-m-d")]) as $job) { + $j = $job->data; + $j->id = $job->id; + $jobs[] = $j; + } + return ["jobs" => $jobs]; + } +} \ No newline at end of file diff --git a/application/InvoiceJob/InvoiceJobModel.php b/application/InvoiceJob/InvoiceJobModel.php new file mode 100644 index 000000000..dd78fd91a --- /dev/null +++ b/application/InvoiceJob/InvoiceJobModel.php @@ -0,0 +1,253 @@ + $value) { + if(property_exists(get_called_class(), $field)) { + $model ->$field = $value; + } + } + + $me = new User(); + $me->loadMe(); + + if($model->create_by === null) { + $model->create_by = $me->id; + } + if($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getAll() { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("InvoiceJob", "*", "1 = 1 ORDER BY from_date"); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[] = new InvoiceJob($data); + } + } + return $items; + + } + + public static function getFirst($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM InvoiceJob + WHERE $where + ORDER BY from_date LIMIT 1"; + //var_dump($sql);exit; + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new InvoiceJob($data); + if($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function getLast($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM InvoiceJob + WHERE $where + ORDER BY from_date DESC LIMIT 1"; + + mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new InvoiceJob($data); + if($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function count($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT COUNT(*) as cnt FROM InvoiceJob + WHERE $where"; + + mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + return $data->cnt; + } + return 0; + } + + public static function search($filter, $limit = false, $order = false, $new_db_connection = false) { + //var_dump($filter);exit; + $items = []; + + if(!$order) { + $order = "from_date ASC"; + } + + $db = FronkDB::singleton(); + if($new_db_connection) { + $db->reconnect(); + } + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM InvoiceJob + WHERE $where + ORDER BY $order"; + + if(is_array($limit) && count($limit)) { + if(is_numeric($limit['start']) && is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['start'].", ".$limit['count']; + } elseif(is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['count']; + } + } + + mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[$data->id] = new InvoiceJob($data); + } + } + + return $items; + } + + private static function getSqlFilter($filter) { + $where = "1=1 "; + //var_dump($filter);exit; + $db = FronkDB::singleton(); + + //var_dump($filter);exit; + + if(array_key_exists("started", $filter)) { + $started = $filter['started']; + if($started === null || $started === false) { + $where .= " AND InvoiceJob.started IS NULL"; + } elseif($started === true) { + $where .= " AND InvoiceJob.started > 0"; + } + } + + if(array_key_exists("finished", $filter)) { + $finished = $filter['finished']; + if($finished === null || $finished === false) { + $where .= " AND InvoiceJob.finished IS NULL"; + } elseif($finished === true) { + $where .= " AND InvoiceJob.finished > 0"; + } + } + + if(array_key_exists("from_date>", $filter)) { + $from_date = $db->escape($filter['from_date>']); + if($from_date) { + $where .= " AND InvoiceJob.from_date > '$from_date'"; + } + } + if(array_key_exists("from_date>=", $filter)) { + $from_date = $db->escape($filter['from_date>=']); + if($from_date) { + $where .= " AND InvoiceJob.from_date >= '$from_date'"; + } + } + if(array_key_exists("from_date<", $filter)) { + $from_date = $db->escape($filter['from_date<']); + if($from_date) { + $where .= " AND InvoiceJob.from_date < '$from_date'"; + } + } + if(array_key_exists("from_date<=", $filter)) { + $from_date = $db->escape($filter['from_date<=']); + if($from_date) { + $where .= " AND InvoiceJob.from_date <= '$from_date'"; + } + } + + if(array_key_exists("to_date>", $filter)) { + $to_date = $db->escape($filter['to_date>']); + if($to_date) { + $where .= " AND InvoiceJob.to_date > '$to_date'"; + } + } + if(array_key_exists("to_date>=", $filter)) { + $to_date = $db->escape($filter['to_date>=']); + if($to_date) { + $where .= " AND InvoiceJob.to_date >= '$to_date'"; + } + } + if(array_key_exists("to_date<", $filter)) { + $to_date = $db->escape($filter['to_date<']); + if($to_date) { + $where .= " AND InvoiceJob.to_date < '$to_date'"; + } + } + if(array_key_exists("to_date<=", $filter)) { + $to_date = $db->escape($filter['to_date<=']); + if($to_date) { + $where .= " AND InvoiceJob.to_date <= '$to_date'"; + } + } + + if (array_key_exists("task", $filter)) { + $task = FronkDB::singleton()->escape($filter["task"]); + if ($task) { + $where .= " AND task='$task'"; + } + } + + if(array_key_exists("vatgroup_id", $filter)) { + $vatgroup_id = $filter['vatgroup_id']; + if(is_numeric($vatgroup_id)) { + $where .= " AND InvoiceJob.vatgroup_id = '$vatgroup_id'"; + } + } + + + if(array_key_exists("add-where", $filter)) { + $where .= " ".$filter['add-where']; + } + + + + + //var_dump($filter, $where);exit; + return $where; + } + +} diff --git a/db/migrations/20240827113849_create_invoice_job.php b/db/migrations/20240827113849_create_invoice_job.php new file mode 100644 index 000000000..dd67b784a --- /dev/null +++ b/db/migrations/20240827113849_create_invoice_job.php @@ -0,0 +1,43 @@ +getEnvironment() == "thetool") { + $table = $this->table("InvoiceJob"); + $table->addColumn("task", "string", ["null" => false, "limit" => 64]); + $table->addColumn("from_date", "date", ["null" => false]); + $table->addColumn("to_date", "date", ["null" => false]); + $table->addColumn("started", "datetime", ["null" => true, "default" => null]); + $table->addColumn("finished", "datetime", ["null" => true, "default" => null]); + $table->addColumn("status", "string", ["null" => true, "default" => true, "limit" => 64]); + $table->addColumn("result", "json", ["null" => true, "default" => null]); + + $table->addColumn("create_by", "integer", ["null" => false]); + $table->addColumn("edit_by", "integer", ["null" => false]); + $table->addColumn("create", "integer", ["null" => false]); + $table->addColumn("edit", "integer", ["null" => false]); + + $table->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("InvoiceJob")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/lib/FronkDB/FronkDB.php b/lib/FronkDB/FronkDB.php index 4c8322307..553bd838c 100644 --- a/lib/FronkDB/FronkDB.php +++ b/lib/FronkDB/FronkDB.php @@ -50,6 +50,12 @@ class FronkDB { $this->query("SET NAMES utf8mb4"); } } + + public function reconnect() { + $this->instances = []; + $this->link = false; + $this->connect(); + } public function disconnect() { if($this->link) { diff --git a/lib/mvcfronk/mfBase/mfBaseController.php b/lib/mvcfronk/mfBase/mfBaseController.php index ff8e3cad1..0fbb3dd71 100644 --- a/lib/mvcfronk/mfBase/mfBaseController.php +++ b/lib/mvcfronk/mfBase/mfBaseController.php @@ -39,7 +39,12 @@ class mfBaseController if (!defined('MFUSELOGIN')) define('MFUSELOGIN', false); if (!defined('MFUSEMENU')) define('MFUSEMENU', false); - if (MFUSELOGIN) { + $nologin = false; + if(defined("MFBASE_BYPASS_LOGIN")) { + $nologin = MFBASE_BYPASS_LOGIN; + } + + if (MFUSELOGIN && !$nologin) { // if protected area and not logged in, redirect to mfLogin if ($this->needlogin == true) { if (!mfLoginController::isLoggedIn()) { @@ -138,6 +143,10 @@ class mfBaseController } + public function reconnectDB() { + $this->db()->reconnect(); + } + public function __call($name, $params) { $methodname = false; diff --git a/lib/mvcfronk/mfBase/mfBaseModel.php b/lib/mvcfronk/mfBase/mfBaseModel.php index 206a3f9d1..3499d4cc1 100644 --- a/lib/mvcfronk/mfBase/mfBaseModel.php +++ b/lib/mvcfronk/mfBase/mfBaseModel.php @@ -32,7 +32,7 @@ class mfBaseModel { * Takes ID or DB row as arguments * @param id or table row $_ */ - public function __construct($_ = NULL) { + public function __construct($_ = NULL, $new_db_connection = false) { $this->log = mfLoghandler::singleton(); $this->table = get_class($this); $this->data = new stdClass(); @@ -46,6 +46,9 @@ class mfBaseModel { if(FRONKDB) { $this->db = FronkDB::singleton(); + if($new_db_connection) { + $this->db->reconnect(); + } } if(method_exists($this, "init")) { @@ -59,6 +62,10 @@ class mfBaseModel { } } + public function reconnectDB() { + $this->db->reconnect(); + } + public function load($row) { if(!is_object($this->data)) { $this->data = new stdClass(); diff --git a/scripts/invoice/invoice-job-broker.php b/scripts/invoice/invoice-job-broker.php new file mode 100755 index 000000000..b4d98b337 --- /dev/null +++ b/scripts/invoice/invoice-job-broker.php @@ -0,0 +1,269 @@ +#!/usr/bin/php +id); +define("INTERNAL_USER_USERNAME", $me->username); + +$request = array(); + +// Put commandline arguments into $request +if(count($argv)) { + $args=$argv; + array_shift($args); // shift scriptname off of args array + foreach($args as $i => $arg) { + if(preg_match('/^--(.+)/',$arg,$m)) { + if(isset($args[$i+1]) && !preg_match('/^-/',$args[$i+1])) { + $request[$m[1]] = $args[$i+1]; + } else { + $request[$m[1]] = true; + } + } elseif(preg_match('/^-([a-zA-Z])(.+)/', $arg, $m)) { + $request[$m[1]] = $m[2]; + } elseif(preg_match('/^-([^-])/', $arg, $m)) { + if(isset($args[$i+1]) && !preg_match('/^-/',$args[$i+1])) { + $request[$m[1]] = $args[$i + 1]; + } else { + $request[$m[1]] = true; + } + } + } +} + +require_once(LIBDIR."/mvcfronk/mfRouter/mfRouter.php"); +$log = mfLoghandler::singleton(); + +cli_set_process_title("thetool-invoice-job-broker"); +pcntl_async_signals(true); +pcntl_signal(SIGTERM, 'signalHandler'); + +$forkcount = 0; +$childpids = []; + +if(pidislocked()) { + echo "Invoice Job Broker läuft bereits (pidfile vorhanden)\n"; + exit; +} +if(!lockpid()) { + $log->error(__FILE__.": Error creating lock file!"); + die("Error creating lock file!\n"); +} + +$all_pids = []; + +$valid_tasks = [ + "make-invoice-pdf", + "send-invoice-email" +]; + +while(1) { + $processes = []; + + //sleep(5); + //echo "looking for new jobs\n"; + $now = new DateTime("now"); + $jobs = InvoiceJobModel::search(["from_date<=" => $now->format("Y-m-d"), "to_date>=" => $now->format("Y-m-d"), "finished" => null], false, false, true); + + /*if(!count($jobs)) { + echo "no more jobs. Exiting.\n"; + break; + }*/ + + foreach($jobs as $job) { + $taskname = $job->task; + + if(!array_key_exists($job->task, $childpids)) { + $childpids[$taskname] = []; + } + + if($job->started && $job->status != "defer") { + //echo "Job ".$job->id." is running already\n"; + continue; + } + + $proc = [ + "job_id" => $job->id, + "task" => $taskname, + "pid" => false, + "processtitle" => "$taskname - running job ".$job->id + ]; + + $processes[] = $proc; + } + + if(!count($processes) && !count($all_pids)) { + break; + } + + foreach($processes as $proc) { + //echo "process task ".$proc["task"]." pid: ".$proc["pid"]."\n"; + + if($proc["pid"]) { + // maybe look for new tasks here + continue; + } + + $taskname = $proc["task"]; + + if($childpids[$taskname]) { + //echo "cannot start new $taskname job, because another one is running already\n"; + continue; + } + //echo "Starting new child\n"; + $pid = pcntl_fork(); + + + if($pid === -1) { + $log->debug("error forking"); + exit; + } elseif($pid > 0) { + // in parent + $forkcount++; + $childpids[$taskname] = $pid; + $proc["pid"] = $pid; + $all_pids[$pid] = $proc; + //sleep(1); + } else { + // in child + $mypid = getmypid(); + $job_id = $proc["job_id"]; + $job = new InvoiceJob($job_id, true); + $job->status = "inprogress"; + $job->save(); + + try { + //echo "in pid $mypid\n"; + //echo "looking for runner for job $taskname\n"; + cli_set_process_title($proc["processtitle"]); + + $include_name = __DIR__ . "/job-runners/" . $taskname . ".php"; + if(!file_exists($include_name)) { + echo "[$mypid] Runner $include_name not found\n"; + } + + require($include_name); + + //echo "[$mypid] Runner $include_name is finished\n"; + + } catch(\Exception $e) { + // exit child process on error + echo "$mypid caught exception: " . $e->getMessage(); + echo $e->getTraceAsString()."\n"; + exit; + } + exit; // make sure child exits when done + } + } + + if(count($all_pids)) { + $status = false; + $return_pid = pcntl_wait($status, WNOHANG); + if($return_pid) { + echo "child $return_pid returned\n"; + $pid_proc = $all_pids[$return_pid]; + $pid_task = $pid_proc["task"]; + $childpids[$pid_task] = null; + unset($all_pids[$return_pid]); + } + } + /*echo "No more PIDs, exiting loop\n"; + break;*/ + sleep(5); +} + +unlockpid(); + +function signalHandler($sig) { + //global $continue; + global $childpids; + global $invoice_job_lock; + + //$continue = false; + //echo "in signal handler\n"; + + if(count($childpids)) { + foreach($childpids as $taskname => $pids) { + foreach($pids as $childpid) { + posix_kill($childpid, SIGTERM); + } + + } + } + unlockpid(); + exit; +} + +function client_log($pid, $text, $severity = "notice") { + global $log; + global $is_daemon; + + if($is_daemon) { + echo "[".date('Y-m-d H:i:s')."] [$pid] $text\n"; + } + $log->$severity($text); + + return true; +} + +function pidislocked() { + $pid = getmypid(); + $pidfile = __DIR__."/.invoice-job-broker.lock"; + if(file_exists($pidfile)) { + return true; + } + return false; +} +function lockpid() { + $pid = getmypid(); + $pidfile = __DIR__."/.invoice-job-broker.lock"; + file_put_contents($pidfile, $pid); + if(file_exists($pidfile)) { + return true; + } + return false; +} + +function unlockpid() { + $pid = getmypid(); + $pidfile = __DIR__."/.invoice-job-broker.lock"; + if(file_exists($pidfile)) { + unlink($pidfile); + return true; + } + return false; +} \ No newline at end of file diff --git a/scripts/invoice/job-runners/make-invoice-pdf.php b/scripts/invoice/job-runners/make-invoice-pdf.php new file mode 100644 index 000000000..6e9a05570 --- /dev/null +++ b/scripts/invoice/job-runners/make-invoice-pdf.php @@ -0,0 +1,69 @@ +started = $started->format("Y-m-d H:i:s"); +$job->reconnectDB(); +$job->save(); + +$job_return = new stdClass(); +$job_return->created = 0; + +$pdfs_created = 0; +$timeout = false; + +if($job->result) { + $job_return = json_decode($job->result); + if(json_last_error() === JSON_ERROR_NONE) { + $pdfs_created = $job_return->created; + } else { + $job_return = new stdClass(); + $job_return->created = 0; + } +} + + +$ic = new InvoiceController(false); +$ic->reconnectDB(); + +do { + $now = new DateTime("now"); + if($now->format("Y-m-d H:i:s") > $job->to_date." 23:59:59") { + $timeout = true; + break; + } + + $created = $ic->createPDFs(10); + + $pdfs_created += $created; + $job_return->created = $pdfs_created; + $job->result = json_encode($job_return); + //$job->return = json_encode(["created" => $created]); + $job->reconnectDB(); + $job->save(); +} while($created); + + +if($timeout) { + $job->status = "timeout"; +} else { + $job->status = "finished"; +} + +$finished = new DateTime("now"); +$job->finished = $finished->format("Y-m-d H:i:s"); +$job->reconnectDB(); +$job->save(); \ No newline at end of file diff --git a/scripts/invoice/job-runners/send-invoice-email.php b/scripts/invoice/job-runners/send-invoice-email.php new file mode 100644 index 000000000..66caeb9ac --- /dev/null +++ b/scripts/invoice/job-runners/send-invoice-email.php @@ -0,0 +1,74 @@ +started) { + $job->started = $started->format("Y-m-d H:i:s"); + $job->reconnectDB(); + $job->save(); +} + +$job_return = new stdClass(); +$job_return->sent = 0; + +$pdfs_sent = 0; +$defer = false; +$timeout = false; + +if($job->result) { + $job_return = json_decode($job->result); + if(json_last_error() === JSON_ERROR_NONE) { + $pdfs_sent = $job_return->sent; + } else { + $job_return = new stdClass(); + $job_return->sent = 0; + } +} + + +$ic = new InvoiceController(false); +$ic->reconnectDB(); + +// main loop +do { + $now = new DateTime("now"); + if($now->format("Y-m-d H:i:s") > $job->to_date." 23:59:59") { + $timeout = true; + break; + } + + $email_return = $ic->_sendEmailInvoices(300); + if($email_return["defer"]) { + $defer = true; + } + $sent = $email_return["sent"]; + $pdfs_sent += $sent; + $job_return->sent = $pdfs_sent; + $job->result = json_encode($job_return); + //$job->return = json_encode(["sent" => $sent]); + $job->reconnectDB(); + $job->save(); +} while($sent); + +// prepare job update +if($timeout) { + $job->status = "timeout"; +} elseif($defer) { + echo "email runner: deferring to next run\n"; + $job->status = "defer"; +} else { + $finished = new DateTime("now"); + $job->finished = $finished->format("Y-m-d H:i:s"); + $job->status = "finished"; +} + +$job->reconnectDB(); +$job->save();