PWA (neue Dateien): - Vollständige Progressive Web App mit Token-basierter Auth - 4 Swipe-Panels: Alle STZ, Stundenzettel, Produktliste, Lieferauflistung - Kundensuche, Leistungen-Accordion, Mehraufwand-Sektion - Produkt-Übernahme aus Auftrag + Mehraufwand in STZ - Service Worker, Manifest, App-Icons für Installation Desktop-Änderungen: - Produktliste: Checkboxen immer sichtbar (außer bereits auf STZ) - Lieferauflistung: Vereinfachte Ansicht (nur Verbaut-Spalte) - Admin: PWA-Link in Einstellungen - Sprachdatei: PWA-Übersetzungen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
983 lines
32 KiB
PHP
Executable file
983 lines
32 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* Stundenzettel - Business Object Klasse
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
|
|
|
/**
|
|
* Class Stundenzettel
|
|
*/
|
|
class Stundenzettel extends CommonObject
|
|
{
|
|
/**
|
|
* @var string ID to identify managed object
|
|
*/
|
|
public $element = 'stundenzettel';
|
|
|
|
/**
|
|
* @var string Name of table without prefix
|
|
*/
|
|
public $table_element = 'stundenzettel';
|
|
|
|
/**
|
|
* @var string Picto
|
|
*/
|
|
public $picto = 'clock';
|
|
|
|
// Status constants
|
|
const STATUS_DRAFT = 0;
|
|
const STATUS_VALIDATED = 1;
|
|
const STATUS_INVOICED = 2;
|
|
const STATUS_CANCELED = 9;
|
|
|
|
/**
|
|
* @var string Ref
|
|
*/
|
|
public $ref;
|
|
|
|
/**
|
|
* @var int Auftrag ID
|
|
*/
|
|
public $fk_commande;
|
|
|
|
/**
|
|
* @var int Rechnung ID (nach Übertrag)
|
|
*/
|
|
public $fk_facture;
|
|
|
|
/**
|
|
* @var int Kunde ID
|
|
*/
|
|
public $fk_soc;
|
|
|
|
/**
|
|
* @var int Ersteller
|
|
*/
|
|
public $fk_user_author;
|
|
|
|
/**
|
|
* @var int Freigebender User
|
|
*/
|
|
public $fk_user_valid;
|
|
|
|
/**
|
|
* @var int|string Datum des Stundenzettels
|
|
*/
|
|
public $date_stundenzettel;
|
|
|
|
/**
|
|
* @var int|string Erstelldatum
|
|
*/
|
|
public $datec;
|
|
|
|
/**
|
|
* @var int|string Freigabedatum
|
|
*/
|
|
public $date_valid;
|
|
|
|
/**
|
|
* @var int Status
|
|
*/
|
|
public $status;
|
|
|
|
/**
|
|
* @var string Private Notizen
|
|
*/
|
|
public $note_private;
|
|
|
|
/**
|
|
* @var string Öffentliche Notizen
|
|
*/
|
|
public $note_public;
|
|
|
|
/**
|
|
* @var float|null Stundenpreis (NULL = Standard verwenden)
|
|
*/
|
|
public $hourly_rate;
|
|
|
|
/**
|
|
* @var int 1 = Stundenpreis wurde manuell geändert
|
|
*/
|
|
public $hourly_rate_is_custom = 0;
|
|
|
|
/**
|
|
* @var array Leistungen
|
|
*/
|
|
public $leistungen = array();
|
|
|
|
/**
|
|
* @var array Produkte
|
|
*/
|
|
public $products = array();
|
|
|
|
/**
|
|
* @var array Notizen (abhakbare Merkzettel)
|
|
*/
|
|
public $notes = array();
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
/**
|
|
* Create object into database
|
|
*
|
|
* @param User $user User that creates
|
|
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
|
* @return int <0 if KO, Id of created object if OK
|
|
*/
|
|
public function create($user, $notrigger = false)
|
|
{
|
|
global $conf;
|
|
|
|
$error = 0;
|
|
|
|
// Generate ref
|
|
if (empty($this->ref)) {
|
|
$this->ref = $this->getNextNumRef();
|
|
}
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
|
$sql .= "ref, entity, fk_commande, fk_soc, fk_user_author,";
|
|
$sql .= "date_stundenzettel, datec, status, note_private, note_public,";
|
|
$sql .= "hourly_rate, hourly_rate_is_custom";
|
|
$sql .= ") VALUES (";
|
|
$sql .= "'".$this->db->escape($this->ref)."',";
|
|
$sql .= ((int)$conf->entity).",";
|
|
$sql .= ((int)$this->fk_commande).",";
|
|
$sql .= ((int)$this->fk_soc).",";
|
|
$sql .= ((int)$user->id).",";
|
|
$sql .= "'".$this->db->idate($this->date_stundenzettel)."',";
|
|
$sql .= "'".$this->db->idate(dol_now())."',";
|
|
$sql .= "0,"; // STATUS_DRAFT
|
|
$sql .= ($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
|
$sql .= ($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL").",";
|
|
$sql .= (isset($this->hourly_rate) && $this->hourly_rate !== null ? ((float)$this->hourly_rate) : "NULL").",";
|
|
$sql .= ((int)$this->hourly_rate_is_custom);
|
|
$sql .= ")";
|
|
|
|
dol_syslog(get_class($this)."::create", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
$error++;
|
|
$this->errors[] = "Error ".$this->db->lasterror();
|
|
}
|
|
|
|
if (!$error) {
|
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
|
}
|
|
|
|
if (!$error) {
|
|
$this->db->commit();
|
|
return $this->id;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load object in memory from the database
|
|
*
|
|
* @param int $id Id object
|
|
* @param string $ref Ref
|
|
* @return int <0 if KO, 0 if not found, >0 if OK
|
|
*/
|
|
public function fetch($id, $ref = null)
|
|
{
|
|
$sql = "SELECT s.rowid, s.ref, s.entity, s.fk_commande, s.fk_facture, s.fk_soc,";
|
|
$sql .= " s.fk_user_author, s.fk_user_valid, s.date_stundenzettel, s.datec,";
|
|
$sql .= " s.date_valid, s.status, s.note_private, s.note_public, s.tms,";
|
|
$sql .= " s.hourly_rate, s.hourly_rate_is_custom";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as s";
|
|
if ($ref) {
|
|
$sql .= " WHERE s.ref = '".$this->db->escape($ref)."'";
|
|
} else {
|
|
$sql .= " WHERE s.rowid = ".((int)$id);
|
|
}
|
|
|
|
dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
if ($this->db->num_rows($resql)) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
|
|
$this->id = $obj->rowid;
|
|
$this->ref = $obj->ref;
|
|
$this->entity = $obj->entity;
|
|
$this->fk_commande = $obj->fk_commande;
|
|
$this->fk_facture = $obj->fk_facture;
|
|
$this->fk_soc = $obj->fk_soc;
|
|
$this->fk_user_author = $obj->fk_user_author;
|
|
$this->fk_user_valid = $obj->fk_user_valid;
|
|
$this->date_stundenzettel = $this->db->jdate($obj->date_stundenzettel);
|
|
$this->datec = $this->db->jdate($obj->datec);
|
|
$this->date_valid = $this->db->jdate($obj->date_valid);
|
|
$this->status = $obj->status;
|
|
$this->note_private = $obj->note_private;
|
|
$this->note_public = $obj->note_public;
|
|
$this->hourly_rate = $obj->hourly_rate;
|
|
$this->hourly_rate_is_custom = $obj->hourly_rate_is_custom;
|
|
|
|
// Load lines
|
|
$this->fetchLeistungen();
|
|
$this->fetchProducts();
|
|
$this->fetchNotes();
|
|
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
$this->error = "Error ".$this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update object into database
|
|
*
|
|
* @param User $user User that modifies
|
|
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function update($user, $notrigger = false)
|
|
{
|
|
$error = 0;
|
|
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
|
$sql .= " date_stundenzettel = '".$this->db->idate($this->date_stundenzettel)."',";
|
|
$sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
|
$sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL").",";
|
|
$sql .= " hourly_rate = ".(isset($this->hourly_rate) && $this->hourly_rate !== null && $this->hourly_rate !== '' ? ((float)$this->hourly_rate) : "NULL").",";
|
|
$sql .= " hourly_rate_is_custom = ".((int)$this->hourly_rate_is_custom);
|
|
$sql .= " WHERE rowid = ".((int)$this->id);
|
|
|
|
dol_syslog(get_class($this)."::update", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
$error++;
|
|
$this->errors[] = "Error ".$this->db->lasterror();
|
|
}
|
|
|
|
if (!$error) {
|
|
return 1;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate object
|
|
*
|
|
* @param User $user User that validates
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function validate($user)
|
|
{
|
|
$error = 0;
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
|
$sql .= " status = ".self::STATUS_VALIDATED.",";
|
|
$sql .= " fk_user_valid = ".((int)$user->id).",";
|
|
$sql .= " date_valid = '".$this->db->idate(dol_now())."'";
|
|
$sql .= " WHERE rowid = ".((int)$this->id);
|
|
|
|
dol_syslog(get_class($this)."::validate", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
$error++;
|
|
$this->errors[] = "Error ".$this->db->lasterror();
|
|
}
|
|
|
|
if (!$error) {
|
|
$this->status = self::STATUS_VALIDATED;
|
|
$this->fk_user_valid = $user->id;
|
|
$this->date_valid = dol_now();
|
|
|
|
// Update tracking table
|
|
$this->updateTracking();
|
|
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set to draft
|
|
*
|
|
* @param User $user User that reopens
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function setDraft($user)
|
|
{
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
|
$sql .= " status = ".self::STATUS_DRAFT.",";
|
|
$sql .= " fk_user_valid = NULL,";
|
|
$sql .= " date_valid = NULL";
|
|
$sql .= " WHERE rowid = ".((int)$this->id);
|
|
|
|
dol_syslog(get_class($this)."::setDraft", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$this->status = self::STATUS_DRAFT;
|
|
return 1;
|
|
} else {
|
|
$this->error = "Error ".$this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete object in database
|
|
*
|
|
* @param User $user User that deletes
|
|
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function delete($user, $notrigger = false)
|
|
{
|
|
$error = 0;
|
|
|
|
$this->db->begin();
|
|
|
|
// Delete notes
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) $error++;
|
|
|
|
// Delete leistungen
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) $error++;
|
|
|
|
// Delete products
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) $error++;
|
|
|
|
// Delete main record
|
|
if (!$error) {
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) $error++;
|
|
}
|
|
|
|
if (!$error) {
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load leistungen
|
|
*
|
|
* @return int <0 if KO, number of lines if OK
|
|
*/
|
|
public function fetchLeistungen()
|
|
{
|
|
$this->leistungen = array();
|
|
|
|
$sql = "SELECT l.rowid, l.fk_user, l.fk_product, l.date_leistung, l.time_start, l.time_end, l.duration, l.description, l.rang,";
|
|
$sql .= " p.ref as product_ref, p.label as product_label";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung as l";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON l.fk_product = p.rowid";
|
|
$sql .= " WHERE l.fk_stundenzettel = ".((int)$this->id);
|
|
$sql .= " ORDER BY l.rang, l.date_leistung, l.time_start";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$this->leistungen[] = $obj;
|
|
}
|
|
return count($this->leistungen);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Load products
|
|
*
|
|
* @return int <0 if KO, number of lines if OK
|
|
*/
|
|
public function fetchProducts()
|
|
{
|
|
$this->products = array();
|
|
|
|
// Produkte laden, bei Freitext-Produkten Beschreibung aus commandedet holen falls leer
|
|
$sql = "SELECT sp.rowid, sp.fk_product, sp.fk_commandedet, sp.fk_manager_line,";
|
|
$sql .= " sp.product_ref, sp.product_label,";
|
|
$sql .= " CASE WHEN sp.description IS NULL OR sp.description = '' THEN cd.description ELSE sp.description END as description,";
|
|
$sql .= " sp.qty_original, sp.qty_done, sp.qty_cumulated, sp.origin, sp.rang";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = sp.fk_commandedet";
|
|
$sql .= " WHERE sp.fk_stundenzettel = ".((int)$this->id);
|
|
$sql .= " ORDER BY sp.rang";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$this->products[] = $obj;
|
|
}
|
|
return count($this->products);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Add a leistung
|
|
*
|
|
* @param User $user User
|
|
* @param string $date Date
|
|
* @param string $time_start Start time
|
|
* @param string $time_end End time
|
|
* @param string $description Description
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function addLeistung($user, $date, $time_start = null, $time_end = null, $description = '', $fk_product = null)
|
|
{
|
|
global $langs;
|
|
|
|
// Calculate duration
|
|
$duration = 0;
|
|
if ($time_start && $time_end) {
|
|
$start = strtotime($time_start);
|
|
$end = strtotime($time_end);
|
|
$duration = ($end - $start) / 60; // in minutes
|
|
}
|
|
|
|
// Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine Leistung existiert
|
|
if ($time_start && $time_end) {
|
|
$sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL";
|
|
$resqlCheck = $this->db->query($sqlCheck);
|
|
if ($resqlCheck) {
|
|
$newStart = strtotime($time_start);
|
|
$newEnd = strtotime($time_end);
|
|
while ($objCheck = $this->db->fetch_object($resqlCheck)) {
|
|
$existStart = strtotime($objCheck->time_start);
|
|
$existEnd = strtotime($objCheck->time_end);
|
|
// Überlappung: Start1 < End2 UND Start2 < End1
|
|
if ($newStart < $existEnd && $existStart < $newEnd) {
|
|
$this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end);
|
|
return -2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get next rang
|
|
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
$rang = 0;
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$rang = $obj->maxrang + 1;
|
|
}
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_leistung (";
|
|
$sql .= "fk_stundenzettel, fk_user, fk_product, date_leistung, time_start, time_end, duration, description, rang";
|
|
$sql .= ") VALUES (";
|
|
$sql .= ((int)$this->id).",";
|
|
$sql .= ((int)$user->id).",";
|
|
$sql .= ($fk_product > 0 ? ((int)$fk_product) : "NULL").",";
|
|
$sql .= "'".$this->db->idate($date)."',";
|
|
$sql .= ($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").",";
|
|
$sql .= ($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").",";
|
|
$sql .= ((int)$duration).",";
|
|
$sql .= "'".$this->db->escape($description)."',";
|
|
$sql .= ((int)$rang);
|
|
$sql .= ")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_leistung");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Update a leistung
|
|
*
|
|
* @param int $leistung_id Leistung ID
|
|
* @param string $date Date
|
|
* @param string $time_start Start time
|
|
* @param string $time_end End time
|
|
* @param string $description Description
|
|
* @param int $fk_product Product/Service ID
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function updateLeistung($leistung_id, $date, $time_start = null, $time_end = null, $description = '', $fk_product = null)
|
|
{
|
|
global $langs;
|
|
|
|
// Calculate duration
|
|
$duration = 0;
|
|
if ($time_start && $time_end) {
|
|
$start = strtotime($time_start);
|
|
$end = strtotime($time_end);
|
|
$duration = ($end - $start) / 60; // in minutes
|
|
}
|
|
|
|
// Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine andere Leistung existiert
|
|
if ($time_start && $time_end) {
|
|
$sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$sqlCheck .= " AND rowid != ".((int)$leistung_id); // Aktuelle Leistung ausschließen
|
|
$sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL";
|
|
$resqlCheck = $this->db->query($sqlCheck);
|
|
if ($resqlCheck) {
|
|
$newStart = strtotime($time_start);
|
|
$newEnd = strtotime($time_end);
|
|
while ($objCheck = $this->db->fetch_object($resqlCheck)) {
|
|
$existStart = strtotime($objCheck->time_start);
|
|
$existEnd = strtotime($objCheck->time_end);
|
|
// Überlappung: Start1 < End2 UND Start2 < End1
|
|
if ($newStart < $existEnd && $existStart < $newEnd) {
|
|
$this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end);
|
|
return -2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_leistung SET";
|
|
$sql .= " fk_product = ".($fk_product > 0 ? ((int)$fk_product) : "NULL").",";
|
|
$sql .= " date_leistung = '".$this->db->idate($date)."',";
|
|
$sql .= " time_start = ".($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").",";
|
|
$sql .= " time_end = ".($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").",";
|
|
$sql .= " duration = ".((int)$duration).",";
|
|
$sql .= " description = '".$this->db->escape($description)."'";
|
|
$sql .= " WHERE rowid = ".((int)$leistung_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
return 1;
|
|
}
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Delete a leistung
|
|
*
|
|
* @param int $leistung_id Leistung ID
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function deleteLeistung($leistung_id)
|
|
{
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
|
$sql .= " WHERE rowid = ".((int)$leistung_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
return 1;
|
|
}
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Add a product
|
|
*
|
|
* @param int $fk_product Product ID
|
|
* @param int $fk_commandedet Commandedet ID
|
|
* @param int $fk_manager_line Manager line ID
|
|
* @param float $qty_original Original qty
|
|
* @param float $qty_done Done qty
|
|
* @param string $origin Origin (order or added)
|
|
* @param string $description Description (for free-text products)
|
|
* @param string $product_label_override Label für Freitext-Produkte (ohne fk_product)
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function addProduct($fk_product, $fk_commandedet = null, $fk_manager_line = null, $qty_original = 0, $qty_done = 0, $origin = 'order', $description = '', $product_label_override = '')
|
|
{
|
|
global $db;
|
|
|
|
// Get product info
|
|
$product_ref = '';
|
|
$product_label = '';
|
|
if ($fk_product > 0) {
|
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
|
$prod = new Product($this->db);
|
|
if ($prod->fetch($fk_product) > 0) {
|
|
$product_ref = $prod->ref;
|
|
$product_label = $prod->label;
|
|
}
|
|
}
|
|
|
|
// Override für Freitext-Produkte (kein fk_product, aber Label vorhanden)
|
|
if (!empty($product_label_override) && empty($product_label)) {
|
|
$product_label = $product_label_override;
|
|
}
|
|
|
|
// Get next rang
|
|
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
$rang = 0;
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$rang = $obj->maxrang + 1;
|
|
}
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_product (";
|
|
$sql .= "fk_stundenzettel, fk_product, fk_commandedet, fk_manager_line,";
|
|
$sql .= "product_ref, product_label, description, qty_original, qty_done, origin, rang";
|
|
$sql .= ") VALUES (";
|
|
$sql .= ((int)$this->id).",";
|
|
$sql .= ($fk_product > 0 ? (int)$fk_product : "NULL").",";
|
|
$sql .= ($fk_commandedet > 0 ? (int)$fk_commandedet : "NULL").",";
|
|
$sql .= ($fk_manager_line > 0 ? (int)$fk_manager_line : "NULL").",";
|
|
$sql .= "'".$this->db->escape($product_ref)."',";
|
|
$sql .= "'".$this->db->escape($product_label)."',";
|
|
$sql .= "'".$this->db->escape($description)."',";
|
|
$sql .= ((float)$qty_original).",";
|
|
$sql .= ((float)$qty_done).",";
|
|
$sql .= "'".$this->db->escape($origin)."',";
|
|
$sql .= ((int)$rang);
|
|
$sql .= ")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_product");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Update product qty_done
|
|
*
|
|
* @param int $line_id Line ID
|
|
* @param float $qty_done Done qty
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function updateProductQty($line_id, $qty_done)
|
|
{
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product SET";
|
|
$sql .= " qty_done = ".((float)$qty_done);
|
|
$sql .= " WHERE rowid = ".((int)$line_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
return $resql ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Delete product from stundenzettel
|
|
*
|
|
* @param int $line_id Line ID
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function deleteProduct($line_id)
|
|
{
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sql .= " WHERE rowid = ".((int)$line_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
return $resql ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Update tracking table
|
|
*
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function updateTracking()
|
|
{
|
|
// Sum up all products for this order across all stundenzettels
|
|
foreach ($this->products as $prod) {
|
|
if (!$prod->fk_commandedet) continue;
|
|
|
|
// Get total done qty for this commandedet
|
|
$sql = "SELECT SUM(qty_done) as total_done FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sql .= " WHERE sp.fk_commandedet = ".((int)$prod->fk_commandedet);
|
|
$sql .= " AND s.status >= ".self::STATUS_VALIDATED;
|
|
|
|
$resql = $this->db->query($sql);
|
|
$total_done = 0;
|
|
if ($resql && ($obj = $this->db->fetch_object($resql))) {
|
|
$total_done = $obj->total_done;
|
|
}
|
|
|
|
// Update or insert tracking
|
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_tracking";
|
|
$sql .= " WHERE fk_commande = ".((int)$this->fk_commande);
|
|
$sql .= " AND fk_commandedet = ".((int)$prod->fk_commandedet);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
|
// Update
|
|
$obj = $this->db->fetch_object($resql);
|
|
$remaining = $prod->qty_original - $total_done;
|
|
$status = 'open';
|
|
if ($total_done >= $prod->qty_original) {
|
|
$status = 'done';
|
|
} elseif ($total_done > 0) {
|
|
$status = 'partial';
|
|
}
|
|
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_tracking SET";
|
|
$sql .= " qty_delivered = ".((float)$total_done).",";
|
|
$sql .= " qty_remaining = ".((float)$remaining).",";
|
|
$sql .= " status = '".$this->db->escape($status)."'";
|
|
$sql .= " WHERE rowid = ".((int)$obj->rowid);
|
|
$this->db->query($sql);
|
|
} else {
|
|
// Insert
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_tracking (";
|
|
$sql .= "fk_commande, fk_product, fk_commandedet, fk_manager_line,";
|
|
$sql .= "product_ref, product_label, qty_ordered, qty_delivered, qty_remaining, status";
|
|
$sql .= ") VALUES (";
|
|
$sql .= ((int)$this->fk_commande).",";
|
|
$sql .= ($prod->fk_product > 0 ? (int)$prod->fk_product : "NULL").",";
|
|
$sql .= ((int)$prod->fk_commandedet).",";
|
|
$sql .= ($prod->fk_manager_line > 0 ? (int)$prod->fk_manager_line : "NULL").",";
|
|
$sql .= "'".$this->db->escape($prod->product_ref)."',";
|
|
$sql .= "'".$this->db->escape($prod->product_label)."',";
|
|
$sql .= ((float)$prod->qty_original).",";
|
|
$sql .= ((float)$total_done).",";
|
|
$sql .= ((float)($prod->qty_original - $total_done)).",";
|
|
$sql .= "'open'";
|
|
$sql .= ")";
|
|
$this->db->query($sql);
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get next reference number
|
|
*
|
|
* @return string Next ref
|
|
*/
|
|
public function getNextNumRef()
|
|
{
|
|
global $conf;
|
|
|
|
$prefix = 'SZ';
|
|
$year = date('Y');
|
|
|
|
$sql = "SELECT MAX(CAST(SUBSTRING(ref, ".(strlen($prefix.$year.'-') + 1).") AS UNSIGNED)) as maxnum";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
|
|
$sql .= " WHERE ref LIKE '".$this->db->escape($prefix.$year)."-%'";
|
|
$sql .= " AND entity = ".((int)$conf->entity);
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$num = $obj->maxnum + 1;
|
|
} else {
|
|
$num = 1;
|
|
}
|
|
|
|
return $prefix.$year.'-'.sprintf('%05d', $num);
|
|
}
|
|
|
|
/**
|
|
* Get status label
|
|
*
|
|
* @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
|
|
* @return string Label
|
|
*/
|
|
public function getLibStatut($mode = 0)
|
|
{
|
|
return $this->LibStatut($this->status, $mode);
|
|
}
|
|
|
|
/**
|
|
* Return label of status
|
|
*
|
|
* @param int $status Status
|
|
* @param int $mode Mode
|
|
* @return string Label
|
|
*/
|
|
public function LibStatut($status, $mode = 0)
|
|
{
|
|
global $langs;
|
|
$langs->load("stundenzettel@stundenzettel");
|
|
|
|
if ($status == self::STATUS_DRAFT) {
|
|
$statusType = 'status0';
|
|
$label = $langs->trans("StatusDraft");
|
|
} elseif ($status == self::STATUS_VALIDATED) {
|
|
$statusType = 'status4';
|
|
$label = $langs->trans("StatusValidated");
|
|
} elseif ($status == self::STATUS_INVOICED) {
|
|
$statusType = 'status6';
|
|
$label = $langs->trans("StatusInvoiced");
|
|
} elseif ($status == self::STATUS_CANCELED) {
|
|
$statusType = 'status9';
|
|
$label = $langs->trans("StatusCanceled");
|
|
}
|
|
|
|
return dolGetStatus($label, '', '', $statusType, $mode);
|
|
}
|
|
|
|
/**
|
|
* Return URL link
|
|
*
|
|
* @param int $withpicto Picto
|
|
* @param string $option Option
|
|
* @return string Link
|
|
*/
|
|
public function getNomUrl($withpicto = 0, $option = '')
|
|
{
|
|
$result = '';
|
|
$url = dol_buildpath('/stundenzettel/card.php?id='.$this->id, 1);
|
|
|
|
$label = '<u>'.$this->ref.'</u>';
|
|
|
|
if ($withpicto) {
|
|
$result .= img_object('', $this->picto, 'class="pictofixedwidth"');
|
|
}
|
|
$result .= '<a href="'.$url.'" title="'.dol_escape_htmltag($label).'">'.$this->ref.'</a>';
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Load notes (checkable memos)
|
|
*
|
|
* @return int <0 if KO, number of notes if OK
|
|
*/
|
|
public function fetchNotes()
|
|
{
|
|
$this->notes = array();
|
|
|
|
$sql = "SELECT rowid, fk_user, note, checked, rang, datec";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
|
$sql .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$sql .= " ORDER BY rang, datec";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$this->notes[] = $obj;
|
|
}
|
|
return count($this->notes);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Add a note
|
|
*
|
|
* @param User $user User
|
|
* @param string $note Note text
|
|
* @return int <0 if KO, >0 if OK (rowid)
|
|
*/
|
|
public function addNote($user, $note)
|
|
{
|
|
if (empty($note)) {
|
|
return -1;
|
|
}
|
|
|
|
// Get next rang
|
|
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$resql = $this->db->query($sql);
|
|
$rang = 0;
|
|
if ($resql) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
$rang = $obj->maxrang + 1;
|
|
}
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_note (";
|
|
$sql .= "fk_stundenzettel, fk_user, note, checked, rang, datec";
|
|
$sql .= ") VALUES (";
|
|
$sql .= ((int)$this->id).",";
|
|
$sql .= ((int)$user->id).",";
|
|
$sql .= "'".$this->db->escape($note)."',";
|
|
$sql .= "0,"; // not checked
|
|
$sql .= ((int)$rang).",";
|
|
$sql .= "'".$this->db->idate(dol_now())."'";
|
|
$sql .= ")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_note");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Update note checked status
|
|
*
|
|
* @param int $note_id Note ID
|
|
* @param int $checked 0=unchecked, 1=checked
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function updateNoteStatus($note_id, $checked)
|
|
{
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_note SET";
|
|
$sql .= " checked = ".((int)$checked);
|
|
$sql .= " WHERE rowid = ".((int)$note_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
return $resql ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Delete a note
|
|
*
|
|
* @param int $note_id Note ID
|
|
* @return int <0 if KO, >0 if OK
|
|
*/
|
|
public function deleteNote($note_id)
|
|
{
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
|
$sql .= " WHERE rowid = ".((int)$note_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
|
|
|
$resql = $this->db->query($sql);
|
|
return $resql ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Get unchecked notes (for display on order page)
|
|
*
|
|
* @return array Array of unchecked notes
|
|
*/
|
|
public function getUncheckedNotes()
|
|
{
|
|
$notes = array();
|
|
|
|
$sql = "SELECT rowid, fk_user, note, rang, datec";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
|
$sql .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
|
$sql .= " AND checked = 0";
|
|
$sql .= " ORDER BY rang, datec";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$notes[] = $obj;
|
|
}
|
|
}
|
|
return $notes;
|
|
}
|
|
}
|