Add new products widget and dashboard integration

- Add widget box showing products starting with "New" that need review
- Add dashboard statistics for new products (small info box)
- Add new_products.php page listing all products to review
- Add CSS for dashboard icon
- Shows 5 entries on homepage, all entries on product page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-02 08:38:10 +01:00
parent 244e41c353
commit cb6bb87d60
4 changed files with 511 additions and 0 deletions

View file

@ -68,6 +68,11 @@ class ActionsImportZugferd
*/
public $result = array();
/**
* @var array Results for hooks
*/
public $results = array();
/**
* Constructor
*
@ -879,4 +884,73 @@ class ActionsImportZugferd
{
return $this->parser->getInvoiceData();
}
/**
* Hook to add dashboard line for new products
*
* @param array $parameters Parameters
* @param object $object Object
* @param string $action Action
* @param HookManager $hookmanager Hook manager
* @return int 0 = OK, >0 = number of errors
*/
public function addOpenElementsDashboardLine($parameters, &$object, &$action, $hookmanager)
{
global $langs, $user;
if (!$user->hasRight('produit', 'lire')) {
return 0;
}
require_once DOL_DOCUMENT_ROOT.'/core/class/workboardresponse.class.php';
$langs->load('importzugferd@importzugferd');
$sql = "SELECT COUNT(*) as total FROM " . MAIN_DB_PREFIX . "product";
$sql .= " WHERE entity IN (" . getEntity('product') . ")";
$sql .= " AND ref LIKE 'New%'";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$count = (int) $obj->total;
if ($count > 0) {
$response = new WorkboardResponse();
$response->warning_delay = 0;
$response->label = $langs->trans("NewProductsToReview");
$response->labelShort = $langs->trans("NewProductsToReview");
$response->url = dol_buildpath('/importzugferd/new_products.php', 1);
$response->img = img_picto('', 'product');
$response->nbtodo = $count;
$response->nbtodolate = 0;
$this->results['importzugferd_newproducts'] = $response;
}
}
return 0;
}
/**
* Hook to add dashboard group for new products
*
* @param array $parameters Parameters
* @param object $object Object
* @param string $action Action
* @param HookManager $hookmanager Hook manager
* @return int 0 = OK, >0 = number of errors
*/
public function addOpenElementsDashboardGroup($parameters, &$object, &$action, $hookmanager)
{
global $langs;
$langs->load('importzugferd@importzugferd');
$this->results['importzugferd_newproducts'] = array(
'groupName' => $langs->trans("NewProductsToReview"),
'stats' => array('importzugferd_newproducts'),
);
return 0;
}
}

View file

@ -0,0 +1,98 @@
<?php
include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php';
class box_new_products extends ModeleBoxes
{
public $boxcode = "newproductsreview";
public $boximg = "product";
public $boxlabel = "BoxNewProductsToReview";
public $depends = array("product");
public function __construct($db, $param = '')
{
$this->db = $db;
}
public function loadBox($max = 5)
{
global $langs;
$langs->load('importzugferd@importzugferd');
// Auf Produktseite alle Einträge zeigen
if (strpos($_SERVER['PHP_SELF'], '/product/index.php') !== false) {
$max = 0; // 0 = kein Limit
}
include_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$productstatic = new Product($this->db);
// Anzahl zählen
$sql = "SELECT COUNT(*) as total FROM ".MAIN_DB_PREFIX."product WHERE ref LIKE 'New%'";
$resql = $this->db->query($sql);
$total = 0;
if ($resql) {
$obj = $this->db->fetch_object($resql);
$total = $obj->total;
}
$this->info_box_head = array(
'text' => $langs->trans("BoxNewProductsToReview").' <span class="badge">'.$total.'</span>',
'sublink' => dol_buildpath('/importzugferd/new_products.php', 1),
'subtext' => $langs->trans("ShowAll"),
'subpicto' => 'object_product',
);
// Produkte laden
$sql = "SELECT rowid, ref, label, datec FROM ".MAIN_DB_PREFIX."product";
$sql .= " WHERE ref LIKE 'New%'";
$sql .= " ORDER BY datec DESC";
if ($max > 0) {
$sql .= " LIMIT ".((int) $max);
}
$result = $this->db->query($sql);
if ($result) {
$num = $this->db->num_rows($result);
$line = 0;
while ($line < $num) {
$objp = $this->db->fetch_object($result);
$productstatic->id = $objp->rowid;
$productstatic->ref = $objp->ref;
$productstatic->label = $objp->label;
$this->info_box_contents[$line][] = array(
'td' => 'class="tdoverflowmax150"',
'text' => $productstatic->getNomUrl(1),
'asis' => 1,
);
$this->info_box_contents[$line][] = array(
'td' => 'class="tdoverflowmax150"',
'text' => $objp->label,
);
$this->info_box_contents[$line][] = array(
'td' => 'class="right"',
'text' => dol_print_date($this->db->jdate($objp->datec), 'day'),
);
$line++;
}
if ($num == 0) {
$this->info_box_contents[0][0] = array(
'td' => 'class="center"',
'text' => $langs->trans("NoNewProductsToReview"),
);
}
}
}
public function showBox($head = null, $contents = null, $nooutput = 0)
{
return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput);
}
}

48
css/importzugferd.css.php Normal file
View file

@ -0,0 +1,48 @@
<?php
/* Copyright (C) 2024 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* \file css/importzugferd.css.php
* \ingroup importzugferd
* \brief CSS file for importzugferd module
*/
// Load Dolibarr environment
if (!defined('NOREQUIRESOC')) {
define('NOREQUIRESOC', '1');
}
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOLOGIN')) {
define('NOLOGIN', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '1');
}
session_cache_limiter('public');
require_once '../../../main.inc.php';
header('Content-Type: text/css');
?>
/* Icon for importzugferd new products dashboard box */
.fa-dol-importzugferd_newproducts:before {
content: "\f1b3"; /* FontAwesome cubes icon for products */
}
/* Background color for the dashboard box */
.bg-infobox-importzugferd_newproducts {
background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%) !important;
}

291
new_products.php Normal file
View file

@ -0,0 +1,291 @@
<?php
/* Copyright (C) 2024 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
/**
* \file new_products.php
* \ingroup importzugferd
* \brief List of products starting with "New" that need review after import
*/
// Load Dolibarr environment
require '../../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
// Load translation files
$langs->loadLangs(array('products', 'stocks', 'importzugferd@importzugferd'));
// Security check
if (!$user->hasRight('produit', 'lire')) {
accessforbidden();
}
// Get parameters
$action = GETPOST('action', 'aZ09');
$massaction = GETPOST('massaction', 'alpha');
$confirm = GETPOST('confirm', 'alpha');
$toselect = GETPOST('toselect', 'array');
$optioncss = GETPOST('optioncss', 'alpha');
// Search Criteria
$search_ref = GETPOST("search_ref", 'alpha');
$search_label = GETPOST("search_label", 'alpha');
$search_tosell = GETPOST("search_tosell");
$search_tobuy = GETPOST("search_tobuy");
// Load variable for pagination
$limit = GETPOSTINT('limit') ? GETPOSTINT('limit') : $conf->liste_limit;
$sortfield = GETPOST('sortfield', 'aZ09comma');
$sortorder = GETPOST('sortorder', 'aZ09comma');
$page = GETPOSTISSET('pageplusone') ? (GETPOSTINT('pageplusone') - 1) : GETPOSTINT("page");
if (empty($page) || $page < 0 || GETPOST('button_search', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
$page = 0;
}
$offset = $limit * $page;
$pageprev = $page - 1;
$pagenext = $page + 1;
if (!$sortfield) {
$sortfield = "p.datec";
}
if (!$sortorder) {
$sortorder = "DESC";
}
// Initialize objects
$object = new Product($db);
$form = new Form($db);
$arrayfields = array(
'p.ref' => array('label' => 'Ref', 'checked' => 1, 'position' => 10),
'p.label' => array('label' => 'Label', 'checked' => 1, 'position' => 20),
'p.fk_product_type' => array('label' => 'Type', 'checked' => 1, 'position' => 30),
'p.price' => array('label' => 'SellingPrice', 'checked' => 1, 'position' => 40),
'p.price_ttc' => array('label' => 'SellingPriceTTC', 'checked' => 0, 'position' => 41),
'p.tosell' => array('label' => 'OnSell', 'checked' => 1, 'position' => 50),
'p.tobuy' => array('label' => 'OnBuy', 'checked' => 1, 'position' => 60),
'p.datec' => array('label' => 'DateCreation', 'checked' => 1, 'position' => 70),
);
/*
* Actions
*/
if (GETPOST('button_removefilter_x', 'alpha') || GETPOST('button_removefilter.x', 'alpha') || GETPOST('button_removefilter', 'alpha')) {
$search_ref = '';
$search_label = '';
$search_tosell = '';
$search_tobuy = '';
$toselect = array();
$search_array_options = array();
}
/*
* View
*/
$title = $langs->trans('NewProductsToReview');
$help_url = '';
llxHeader('', $title, $help_url);
// Build SQL query
$sql = "SELECT p.rowid, p.ref, p.label, p.fk_product_type, p.entity,";
$sql .= " p.price, p.price_ttc, p.price_base_type, p.tva_tx,";
$sql .= " p.tosell, p.tobuy, p.datec, p.tms";
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
$sql .= " WHERE p.entity IN (".getEntity('product').")";
$sql .= " AND p.ref LIKE 'New%'";
// Add search filters
if ($search_ref) {
$sql .= natural_search('p.ref', $search_ref);
}
if ($search_label) {
$sql .= natural_search('p.label', $search_label);
}
if ($search_tosell != '' && $search_tosell >= 0) {
$sql .= " AND p.tosell = ".((int) $search_tosell);
}
if ($search_tobuy != '' && $search_tobuy >= 0) {
$sql .= " AND p.tobuy = ".((int) $search_tobuy);
}
// Count total
$sqlcount = preg_replace('/^SELECT[^F]*FROM/', 'SELECT COUNT(*) as nbtotalofrecords FROM', $sql);
$sqlcount = preg_replace('/ORDER BY.*$/', '', $sqlcount);
$resqlcount = $db->query($sqlcount);
$nbtotalofrecords = 0;
if ($resqlcount) {
$objcount = $db->fetch_object($resqlcount);
$nbtotalofrecords = $objcount->nbtotalofrecords;
}
// Add sorting
$sql .= $db->order($sortfield, $sortorder);
$sql .= $db->plimit($limit + 1, $offset);
$resql = $db->query($sql);
if (!$resql) {
dol_print_error($db);
exit;
}
$num = $db->num_rows($resql);
$param = '';
if ($search_ref) {
$param .= '&search_ref='.urlencode($search_ref);
}
if ($search_label) {
$param .= '&search_label='.urlencode($search_label);
}
if ($search_tosell != '') {
$param .= '&search_tosell='.urlencode($search_tosell);
}
if ($search_tobuy != '') {
$param .= '&search_tobuy='.urlencode($search_tobuy);
}
if ($limit > 0 && $limit != $conf->liste_limit) {
$param .= '&limit='.((int) $limit);
}
// List header
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="list">';
print '<input type="hidden" name="sortfield" value="'.$sortfield.'">';
print '<input type="hidden" name="sortorder" value="'.$sortorder.'">';
print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, 'product', 0, '', '', $limit, 0, 0, 1);
// Info box
print '<div class="info">';
print $langs->trans('NewProductsToReviewDesc', 'New');
print '</div><br>';
print '<div class="div-table-responsive">';
print '<table class="tagtable liste'.($optioncss ? ' '.$optioncss : '').'">';
// Header row with search fields
print '<tr class="liste_titre_filter">';
print '<td class="liste_titre"><input type="text" class="flat maxwidth100" name="search_ref" value="'.dol_escape_htmltag($search_ref).'"></td>';
print '<td class="liste_titre"><input type="text" class="flat maxwidth200" name="search_label" value="'.dol_escape_htmltag($search_label).'"></td>';
print '<td class="liste_titre"></td>';
print '<td class="liste_titre right"></td>';
print '<td class="liste_titre center">'.$form->selectyesno('search_tosell', $search_tosell, 1, false, 1, 1).'</td>';
print '<td class="liste_titre center">'.$form->selectyesno('search_tobuy', $search_tobuy, 1, false, 1, 1).'</td>';
print '<td class="liste_titre"></td>';
print '<td class="liste_titre center">';
print '<input type="image" class="liste_titre" name="button_search" src="'.img_picto($langs->trans("Search"), 'search.png', '', 0, 1).'" value="'.dol_escape_htmltag($langs->trans("Search")).'" title="'.dol_escape_htmltag($langs->trans("Search")).'">';
print '<input type="image" class="liste_titre" name="button_removefilter" src="'.img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', 0, 1).'" value="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'" title="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'">';
print '</td>';
print '</tr>';
// Column headers
print '<tr class="liste_titre">';
print_liste_field_titre('Ref', $_SERVER["PHP_SELF"], 'p.ref', '', $param, '', $sortfield, $sortorder);
print_liste_field_titre('Label', $_SERVER["PHP_SELF"], 'p.label', '', $param, '', $sortfield, $sortorder);
print_liste_field_titre('Type', $_SERVER["PHP_SELF"], 'p.fk_product_type', '', $param, '', $sortfield, $sortorder);
print_liste_field_titre('SellingPrice', $_SERVER["PHP_SELF"], 'p.price', '', $param, '', $sortfield, $sortorder, 'right ');
print_liste_field_titre('OnSell', $_SERVER["PHP_SELF"], 'p.tosell', '', $param, '', $sortfield, $sortorder, 'center ');
print_liste_field_titre('OnBuy', $_SERVER["PHP_SELF"], 'p.tobuy', '', $param, '', $sortfield, $sortorder, 'center ');
print_liste_field_titre('DateCreation', $_SERVER["PHP_SELF"], 'p.datec', '', $param, '', $sortfield, $sortorder, 'center ');
print_liste_field_titre('', $_SERVER["PHP_SELF"], '', '', $param, '', $sortfield, $sortorder, 'center ');
print '</tr>';
// Data rows
$i = 0;
while ($i < min($num, $limit)) {
$obj = $db->fetch_object($resql);
if (!$obj) {
break;
}
$product_static = new Product($db);
$product_static->id = $obj->rowid;
$product_static->ref = $obj->ref;
$product_static->label = $obj->label;
$product_static->type = $obj->fk_product_type;
$product_static->entity = $obj->entity;
$product_static->status = $obj->tosell;
$product_static->status_buy = $obj->tobuy;
print '<tr class="oddeven">';
// Ref
print '<td class="nowraponall">';
print $product_static->getNomUrl(1);
print '</td>';
// Label
print '<td class="tdoverflowmax200" title="'.dol_escape_htmltag($obj->label).'">'.dol_escape_htmltag($obj->label).'</td>';
// Type
print '<td>';
if ($obj->fk_product_type == 0) {
print $langs->trans('Product');
} else {
print $langs->trans('Service');
}
print '</td>';
// Price
print '<td class="right nowraponall">';
if ($obj->price_base_type == 'TTC') {
print '<span class="amount">'.price($obj->price_ttc).'</span>';
} else {
print '<span class="amount">'.price($obj->price).'</span>';
}
print '</td>';
// On sell
print '<td class="center">';
print $product_static->LibStatut($obj->tosell, 5, 0);
print '</td>';
// On buy
print '<td class="center">';
print $product_static->LibStatut($obj->tobuy, 5, 1);
print '</td>';
// Date creation
print '<td class="center nowraponall">';
print dol_print_date($db->jdate($obj->datec), 'dayhour');
print '</td>';
// Action column
print '<td class="center">';
print '<a class="editfielda" href="'.DOL_URL_ROOT.'/product/card.php?id='.$obj->rowid.'&action=edit&token='.newToken().'">';
print img_picto($langs->trans('Edit'), 'edit');
print '</a>';
print '</td>';
print '</tr>';
$i++;
}
if ($num == 0) {
print '<tr><td colspan="8" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
}
print '</table>';
print '</div>';
print '</form>';
// End of page
llxFooter();
$db->close();