@@ -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(
+ "=self::getUrl("InvoiceJob", "api")?>",
+ {
+ 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();