1616 lines
68 KiB
PHP
1616 lines
68 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 ZUGFeRD Import Module
|
|
*
|
|
* 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 datanorm_update.php
|
|
* \ingroup importzugferd
|
|
* \brief Mass update products from Datanorm catalogs
|
|
*/
|
|
|
|
// Load Dolibarr environment
|
|
$res = 0;
|
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
|
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
|
}
|
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
|
$tmp2 = realpath(__FILE__);
|
|
$i = strlen($tmp) - 1;
|
|
$j = strlen($tmp2) - 1;
|
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
|
$i--;
|
|
$j--;
|
|
}
|
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
|
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
|
}
|
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
|
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../main.inc.php")) {
|
|
$res = @include "../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../main.inc.php")) {
|
|
$res = @include "../../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../../main.inc.php")) {
|
|
$res = @include "../../../main.inc.php";
|
|
}
|
|
if (!$res) {
|
|
die("Include of main fails");
|
|
}
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
dol_include_once('/importzugferd/class/datanorm.class.php');
|
|
dol_include_once('/importzugferd/lib/importzugferd.lib.php');
|
|
|
|
// Load translations
|
|
$langs->loadLangs(array("importzugferd@importzugferd", "products", "bills"));
|
|
|
|
// Security check
|
|
if (!$user->hasRight('produit', 'creer')) {
|
|
accessforbidden();
|
|
}
|
|
|
|
// Get parameters
|
|
$action = GETPOST('action', 'aZ09');
|
|
$fk_soc = GETPOSTINT('fk_soc');
|
|
$search_mode = GETPOST('search_mode', 'alpha') ?: 'supplier'; // supplier, manual
|
|
$search_term = GETPOST('search_term', 'alphanohtml');
|
|
$search_by_name = GETPOSTINT('search_by_name');
|
|
$search_by_ean = GETPOSTINT('search_by_ean');
|
|
$search_by_ref = GETPOSTINT('search_by_ref');
|
|
|
|
// Filters for what to update
|
|
// On first load (no action), default to price and description enabled
|
|
// On form submit, respect actual checkbox states
|
|
$isFormSubmitted = ($action == 'search' || GETPOSTISSET('fk_soc'));
|
|
if ($isFormSubmitted) {
|
|
$filter_price = GETPOSTINT('filter_price');
|
|
$filter_description = GETPOSTINT('filter_description');
|
|
$filter_label = GETPOSTINT('filter_label');
|
|
$only_differences = GETPOSTINT('only_differences');
|
|
} else {
|
|
// Defaults for first page load
|
|
$filter_price = 1;
|
|
$filter_description = 1;
|
|
$filter_label = 0;
|
|
$only_differences = 0;
|
|
}
|
|
|
|
// Initialize objects
|
|
$form = new Form($db);
|
|
$formcompany = new FormCompany($db);
|
|
$datanorm = new Datanorm($db);
|
|
|
|
// Store pending changes in session
|
|
if (!isset($_SESSION['datanorm_pending_changes'])) {
|
|
$_SESSION['datanorm_pending_changes'] = array();
|
|
}
|
|
|
|
/*
|
|
* Actions
|
|
*/
|
|
|
|
// Apply single row update
|
|
if ($action == 'apply_single' && GETPOSTINT('product_id') && GETPOST('datanorm_key', 'alphanohtml')) {
|
|
$product_id = GETPOSTINT('product_id');
|
|
$datanorm_key = GETPOST('datanorm_key', 'alphanohtml');
|
|
$apply_price = GETPOSTINT('apply_price');
|
|
$apply_description = GETPOSTINT('apply_description');
|
|
$apply_label = GETPOSTINT('apply_label');
|
|
|
|
$result = applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $apply_price, $apply_description, $apply_label);
|
|
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('ProductUpdated'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($langs->trans('ErrorUpdatingProduct'), null, 'errors');
|
|
}
|
|
|
|
// Redirect to same page with same parameters
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences);
|
|
exit;
|
|
}
|
|
|
|
// Add to pending changes
|
|
if ($action == 'add_pending') {
|
|
$product_id = GETPOSTINT('product_id');
|
|
$datanorm_key = GETPOST('datanorm_key', 'alphanohtml');
|
|
$apply_fields = GETPOST('apply_fields', 'array');
|
|
|
|
if ($product_id > 0 && !empty($datanorm_key)) {
|
|
$_SESSION['datanorm_pending_changes'][$product_id] = array(
|
|
'datanorm_key' => $datanorm_key,
|
|
'fk_soc' => $fk_soc,
|
|
'apply_fields' => $apply_fields
|
|
);
|
|
setEventMessages($langs->trans('AddedToPendingChanges'), null, 'mesgs');
|
|
}
|
|
|
|
// Redirect back with same parameters to preserve supplier selection
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&action=search');
|
|
exit;
|
|
}
|
|
|
|
// Remove from pending
|
|
if ($action == 'remove_pending') {
|
|
$product_id = GETPOSTINT('product_id');
|
|
unset($_SESSION['datanorm_pending_changes'][$product_id]);
|
|
|
|
// Redirect back with same parameters
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&action=search');
|
|
exit;
|
|
}
|
|
|
|
// Clear all pending
|
|
if ($action == 'clear_pending') {
|
|
$_SESSION['datanorm_pending_changes'] = array();
|
|
setEventMessages($langs->trans('PendingChangesCleared'), null, 'mesgs');
|
|
|
|
// Redirect back
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc);
|
|
exit;
|
|
}
|
|
|
|
// Add all items with differences to pending
|
|
if ($action == 'add_all_pending') {
|
|
$items_json = GETPOST('items_data', 'restricthtml');
|
|
if (!empty($items_json)) {
|
|
$items = json_decode($items_json, true);
|
|
$added_count = 0;
|
|
|
|
// Build apply_fields based on user's filter selection
|
|
$apply_fields = array();
|
|
if ($filter_price) $apply_fields[] = 'price';
|
|
if ($filter_description) $apply_fields[] = 'description';
|
|
if ($filter_label) $apply_fields[] = 'label';
|
|
|
|
if (is_array($items) && !empty($apply_fields)) {
|
|
foreach ($items as $item) {
|
|
if (!empty($item['product_id']) && $item['product_id'] > 0 && !empty($item['datanorm_key'])) {
|
|
$_SESSION['datanorm_pending_changes'][$item['product_id']] = array(
|
|
'datanorm_key' => $item['datanorm_key'],
|
|
'fk_soc' => $fk_soc,
|
|
'apply_fields' => $apply_fields
|
|
);
|
|
$added_count++;
|
|
}
|
|
}
|
|
}
|
|
if ($added_count > 0) {
|
|
setEventMessages($langs->trans('AddedAllToPendingChanges', $added_count), null, 'mesgs');
|
|
}
|
|
}
|
|
|
|
// Redirect back with same parameters to preserve supplier selection
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?fk_soc='.$fk_soc.'&search_mode='.$search_mode.'&search_term='.urlencode($search_term).'&filter_price='.$filter_price.'&filter_description='.$filter_description.'&filter_label='.$filter_label.'&only_differences='.$only_differences.'&action=search');
|
|
exit;
|
|
}
|
|
|
|
// Show confirmation dialog
|
|
if ($action == 'confirm_apply_all') {
|
|
// Will be handled in view section
|
|
}
|
|
|
|
// AJAX: Get raw Datanorm lines for debugging
|
|
if ($action == 'get_raw_lines' && GETPOST('article_number', 'alphanohtml')) {
|
|
header('Content-Type: application/json');
|
|
$article_number = GETPOST('article_number', 'alphanohtml');
|
|
$ajax_fk_soc = GETPOSTINT('fk_soc');
|
|
|
|
$result = array(
|
|
'datanorm_line' => '',
|
|
'datpreis_line' => '',
|
|
'article_number' => $article_number
|
|
);
|
|
|
|
// Get the upload directory for this supplier
|
|
$upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$ajax_fk_soc;
|
|
|
|
if (is_dir($upload_dir)) {
|
|
$allFiles = glob($upload_dir . '/*');
|
|
|
|
// Search in DATANORM files
|
|
foreach ($allFiles as $file) {
|
|
$basename = strtoupper(basename($file));
|
|
if (preg_match('/^DATANORM\.\d{3}$/', $basename)) {
|
|
$handle = fopen($file, 'r');
|
|
if ($handle) {
|
|
while (($line = fgets($handle)) !== false) {
|
|
// A-Satz starts with A; and contains the article number
|
|
if (preg_match('/^A;/', $line)) {
|
|
$parts = explode(';', $line);
|
|
if (isset($parts[2]) && trim($parts[2]) == $article_number) {
|
|
$result['datanorm_line'] = trim($line);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
fclose($handle);
|
|
}
|
|
if (!empty($result['datanorm_line'])) break;
|
|
}
|
|
}
|
|
|
|
// Search in DATPREIS files
|
|
foreach ($allFiles as $file) {
|
|
$basename = strtoupper(basename($file));
|
|
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
|
|
$handle = fopen($file, 'r');
|
|
if ($handle) {
|
|
while (($line = fgets($handle)) !== false) {
|
|
// P-Satz contains article numbers at various positions
|
|
if (preg_match('/^P;/', $line) && strpos($line, $article_number) !== false) {
|
|
$result['datpreis_line'] = trim($line);
|
|
break;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
}
|
|
if (!empty($result['datpreis_line'])) break;
|
|
}
|
|
}
|
|
|
|
$result['upload_dir'] = $upload_dir;
|
|
} else {
|
|
$result['error'] = 'Upload directory not found: ' . $upload_dir;
|
|
}
|
|
|
|
echo json_encode($result);
|
|
exit;
|
|
}
|
|
|
|
// Apply all pending changes
|
|
if ($action == 'apply_all_confirmed' && GETPOST('confirm', 'alpha') == 'yes') {
|
|
$success = 0;
|
|
$errors = 0;
|
|
|
|
// Generate batch ID for this mass update
|
|
$batch_id = 'batch_'.date('Ymd_His').'_'.$user->id;
|
|
|
|
foreach ($_SESSION['datanorm_pending_changes'] as $product_id => $change) {
|
|
$apply_price = in_array('price', $change['apply_fields']) ? 1 : 0;
|
|
$apply_description = in_array('description', $change['apply_fields']) ? 1 : 0;
|
|
$apply_label = in_array('label', $change['apply_fields']) ? 1 : 0;
|
|
|
|
$result = applyDatanormUpdate($db, $user, $product_id, $change['datanorm_key'], $change['fk_soc'], $apply_price, $apply_description, $apply_label, $batch_id);
|
|
|
|
if ($result > 0) {
|
|
$success++;
|
|
} else {
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
$_SESSION['datanorm_pending_changes'] = array();
|
|
setEventMessages($langs->trans('DatanormMassUpdateComplete', $success, $errors), null, 'mesgs');
|
|
|
|
// Redirect to change log with batch filter
|
|
header('Location: '.dol_buildpath('/importzugferd/datanorm_changelog.php', 1).'?batch_id='.urlencode($batch_id));
|
|
exit;
|
|
}
|
|
|
|
/*
|
|
* View
|
|
*/
|
|
|
|
$title = $langs->trans('DatanormMassUpdate');
|
|
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-datanorm-update');
|
|
|
|
print load_fiche_titre($title, '', 'fa-sync');
|
|
|
|
// Check if Datanorm data exists
|
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$resql = $db->query($sql);
|
|
$obj = $db->fetch_object($resql);
|
|
if ($obj->cnt == 0) {
|
|
print '<div class="warning">'.$langs->trans('NoDatanormData').'</div>';
|
|
print '<br><a href="'.dol_buildpath('/importzugferd/datanorm.php', 1).'" class="button">'.$langs->trans('UploadDatanorm').'</a>';
|
|
llxFooter();
|
|
$db->close();
|
|
exit;
|
|
}
|
|
|
|
// Search form
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" name="searchform">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="search">';
|
|
|
|
print '<div class="fichecenter">';
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
|
|
// Supplier selection
|
|
print '<tr class="liste_titre">';
|
|
print '<td colspan="4">'.$langs->trans('SelectSupplier').'</td>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<td class="titlefield">'.$langs->trans('Supplier').'</td>';
|
|
print '<td colspan="3">';
|
|
// Get suppliers with Datanorm data
|
|
$sql = "SELECT DISTINCT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s";
|
|
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."importzugferd_datanorm d ON d.fk_soc = s.rowid";
|
|
$sql .= " WHERE s.fournisseur = 1";
|
|
$sql .= " ORDER BY s.nom";
|
|
$resql = $db->query($sql);
|
|
print '<select name="fk_soc" class="flat minwidth300" onchange="this.form.submit()">';
|
|
print '<option value="">'.$langs->trans('SelectASupplier').'</option>';
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
$selected = ($obj->rowid == $fk_soc) ? 'selected' : '';
|
|
print '<option value="'.$obj->rowid.'" '.$selected.'>'.dol_escape_htmltag($obj->nom).'</option>';
|
|
}
|
|
print '</select>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Search mode
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$langs->trans('SearchMode').'</td>';
|
|
print '<td colspan="3">';
|
|
print '<input type="radio" name="search_mode" value="supplier" id="mode_supplier" '.($search_mode == 'supplier' ? 'checked' : '').'>';
|
|
print '<label for="mode_supplier"> '.$langs->trans('SearchBySupplierProducts').'</label>';
|
|
print ' ';
|
|
print '<input type="radio" name="search_mode" value="manual" id="mode_manual" '.($search_mode == 'manual' ? 'checked' : '').'>';
|
|
print '<label for="mode_manual"> '.$langs->trans('ManualSearch').'</label>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Manual search term
|
|
print '<tr class="oddeven" id="manual_search_row" style="'.($search_mode != 'manual' ? 'display:none;' : '').'">';
|
|
print '<td>'.$langs->trans('SearchTerm').'</td>';
|
|
print '<td colspan="3">';
|
|
print '<input type="text" name="search_term" value="'.dol_escape_htmltag($search_term).'" class="minwidth300" placeholder="'.$langs->trans('ArticleNumberOrName').'">';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Additional search options
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$langs->trans('AdditionalSearchOptions').'</td>';
|
|
print '<td colspan="3">';
|
|
print '<input type="checkbox" name="search_by_name" value="1" id="search_by_name" '.($search_by_name ? 'checked' : '').'>';
|
|
print '<label for="search_by_name"> '.$langs->trans('AlsoSearchByName').'</label>';
|
|
print ' ';
|
|
print '<input type="checkbox" name="search_by_ean" value="1" id="search_by_ean" '.($search_by_ean ? 'checked' : '').'>';
|
|
print '<label for="search_by_ean"> '.$langs->trans('AlsoSearchByEAN').'</label>';
|
|
print ' ';
|
|
print '<input type="checkbox" name="search_by_ref" value="1" id="search_by_ref" '.($search_by_ref ? 'checked' : '').'>';
|
|
print '<label for="search_by_ref"> '.$langs->trans('AlsoSearchByRef').'</label>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Filter: What to compare/update
|
|
print '<tr class="liste_titre">';
|
|
print '<td colspan="4">'.$langs->trans('FieldsToCompare').'</td>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$langs->trans('Fields').'</td>';
|
|
print '<td colspan="3">';
|
|
print '<input type="checkbox" name="filter_price" value="1" id="filter_price" '.($filter_price ? 'checked' : '').'>';
|
|
print '<label for="filter_price"> '.$langs->trans('Price').'</label>';
|
|
print ' ';
|
|
print '<input type="checkbox" name="filter_description" value="1" id="filter_description" '.($filter_description ? 'checked' : '').'>';
|
|
print '<label for="filter_description"> '.$langs->trans('Description').'</label>';
|
|
print ' ';
|
|
print '<input type="checkbox" name="filter_label" value="1" id="filter_label" '.($filter_label ? 'checked' : '').'>';
|
|
print '<label for="filter_label"> '.$langs->trans('Label').'</label>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Only show differences
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$langs->trans('Display').'</td>';
|
|
print '<td colspan="3">';
|
|
print '<input type="checkbox" name="only_differences" value="1" id="only_differences" '.($only_differences ? 'checked' : '').'>';
|
|
print '<label for="only_differences"> '.$langs->trans('OnlyShowDifferences').'</label>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
print '</div>';
|
|
|
|
print '<div class="center" style="margin: 10px;">';
|
|
print '<input type="submit" class="button button-primary" value="'.$langs->trans('Search').'">';
|
|
if (!empty($_SESSION['datanorm_pending_changes'])) {
|
|
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=clear_pending&token='.newToken().'" class="button">'.$langs->trans('ClearPendingChanges').' ('.count($_SESSION['datanorm_pending_changes']).')</a>';
|
|
}
|
|
print '</div>';
|
|
|
|
print '</form>';
|
|
|
|
// Show pending changes section (always visible when there are pending changes)
|
|
if (!empty($_SESSION['datanorm_pending_changes'])) {
|
|
$pendingCount = count($_SESSION['datanorm_pending_changes']);
|
|
print '<br>';
|
|
print '<div class="fichecenter">';
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th colspan="2">'.$langs->trans('PendingChanges').' ('.$pendingCount.')</th>';
|
|
print '</tr>';
|
|
print '<tr class="oddeven">';
|
|
print '<td class="center">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=confirm_apply_all&token='.newToken().'" class="button button-primary">';
|
|
print '<i class="fas fa-check-double paddingright"></i>'.$langs->trans('ApplyAllPendingChanges').' ('.$pendingCount.')';
|
|
print '</a>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
print '</table>';
|
|
print '</div>';
|
|
print '</div>';
|
|
}
|
|
|
|
// JavaScript for toggling manual search and initializing state
|
|
print '<script>
|
|
(function() {
|
|
// Toggle manual search row visibility
|
|
function updateManualSearchVisibility() {
|
|
var manualRow = document.getElementById("manual_search_row");
|
|
var selectedMode = document.querySelector("input[name=search_mode]:checked");
|
|
if (manualRow && selectedMode) {
|
|
manualRow.style.display = (selectedMode.value == "manual") ? "" : "none";
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
updateManualSearchVisibility();
|
|
|
|
// Add event listeners for radio buttons
|
|
document.querySelectorAll("input[name=search_mode]").forEach(function(radio) {
|
|
radio.addEventListener("change", updateManualSearchVisibility);
|
|
});
|
|
})();
|
|
</script>';
|
|
|
|
// Results
|
|
if ($fk_soc > 0 && ($action == 'search' || GETPOST('search_mode'))) {
|
|
$comparison_results = array();
|
|
|
|
if ($search_mode == 'supplier') {
|
|
// Find all products linked to this supplier
|
|
$comparison_results = findProductsForSupplier($db, $fk_soc, $search_by_name, $search_by_ean, $search_by_ref);
|
|
} elseif ($search_mode == 'manual' && !empty($search_term)) {
|
|
// Manual search in Datanorm
|
|
$comparison_results = searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name, $search_by_ean, $search_by_ref);
|
|
}
|
|
|
|
// Count differences before filtering
|
|
$total_results = count($comparison_results);
|
|
$diff_count = 0;
|
|
foreach ($comparison_results as $item) {
|
|
if (($filter_price && !empty($item['price_differs'])) ||
|
|
($filter_description && !empty($item['description_differs'])) ||
|
|
($filter_label && !empty($item['label_differs']))) {
|
|
$diff_count++;
|
|
}
|
|
}
|
|
|
|
// Filter results if needed
|
|
if ($only_differences) {
|
|
$comparison_results = array_filter($comparison_results, function($item) use ($filter_price, $filter_description, $filter_label) {
|
|
return ($filter_price && $item['price_differs']) ||
|
|
($filter_description && $item['description_differs']) ||
|
|
($filter_label && $item['label_differs']);
|
|
});
|
|
}
|
|
|
|
// Collect items with differences for "Add all" button
|
|
$items_with_diff = array();
|
|
foreach ($comparison_results as $item) {
|
|
$has_difference = ($filter_price && $item['price_differs']) ||
|
|
($filter_description && $item['description_differs']) ||
|
|
($filter_label && $item['label_differs']);
|
|
if ($has_difference && $item['product_id'] > 0) {
|
|
$items_with_diff[] = array(
|
|
'product_id' => $item['product_id'],
|
|
'datanorm_key' => $item['datanorm_key']
|
|
);
|
|
}
|
|
}
|
|
|
|
// Show summary
|
|
print '<br>';
|
|
print '<div class="info" style="display: flex; justify-content: space-between; align-items: center;">';
|
|
print '<div>';
|
|
print '<strong>'.$langs->trans('Results').':</strong> ';
|
|
print $total_results.' '.$langs->trans('Products');
|
|
if ($diff_count > 0) {
|
|
print ' | <span style="color: #d9534f;"><strong>'.$diff_count.'</strong> '.$langs->trans('WithDifferences').'</span>';
|
|
}
|
|
if ($only_differences) {
|
|
print ' | <em>'.$langs->trans('OnlyShowingDifferences').'</em>';
|
|
}
|
|
print '</div>';
|
|
|
|
// "Add all with differences" button
|
|
if (!empty($items_with_diff)) {
|
|
print '<div>';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" style="display:inline;" id="addAllForm">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_all_pending">';
|
|
print '<input type="hidden" name="fk_soc" value="'.$fk_soc.'">';
|
|
print '<input type="hidden" name="filter_price" value="'.$filter_price.'">';
|
|
print '<input type="hidden" name="filter_description" value="'.$filter_description.'">';
|
|
print '<input type="hidden" name="filter_label" value="'.$filter_label.'">';
|
|
print '<input type="hidden" name="search_mode" value="'.dol_escape_htmltag($search_mode).'">';
|
|
print '<input type="hidden" name="search_term" value="'.dol_escape_htmltag($search_term).'">';
|
|
print '<input type="hidden" name="only_differences" value="'.$only_differences.'">';
|
|
print '<input type="hidden" name="items_data" id="items_data" value="'.dol_escape_htmltag(json_encode($items_with_diff)).'">';
|
|
print '<button type="button" class="button" id="btnAddAllWithDiff">';
|
|
print '<i class="fas fa-check-double"></i> '.$langs->trans('AddAllWithDifferences').' ('.$diff_count.')';
|
|
print '</button>';
|
|
print '</form>';
|
|
print '</div>';
|
|
|
|
// Hidden dialog for confirmation
|
|
print '<div id="dialogConfirmAddAll" title="'.$langs->trans('Confirmation').'" style="display:none;">';
|
|
print '<p><span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span>';
|
|
print $langs->trans('ConfirmAddAllToPending');
|
|
print '</p>';
|
|
print '<p class="opacitymedium"><strong>'.$diff_count.'</strong> '.$langs->trans('Products').'</p>';
|
|
print '</div>';
|
|
}
|
|
print '</div>';
|
|
|
|
if (!empty($comparison_results)) {
|
|
print '<br>';
|
|
print '<div class="div-table-responsive">';
|
|
print '<table class="noborder centpercent">';
|
|
|
|
// Header
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans('Product').'</th>';
|
|
print '<th>'.$langs->trans('DatanormArticle').'</th>';
|
|
if ($filter_price) {
|
|
print '<th class="right">'.$langs->trans('CurrentPrice').'</th>';
|
|
print '<th class="right">'.$langs->trans('DatanormPrice').'</th>';
|
|
}
|
|
if ($filter_description) {
|
|
print '<th>'.$langs->trans('CurrentDescription').'</th>';
|
|
print '<th>'.$langs->trans('DatanormDescription').'</th>';
|
|
}
|
|
if ($filter_label) {
|
|
print '<th>'.$langs->trans('CurrentLabel').'</th>';
|
|
print '<th>'.$langs->trans('DatanormLabel').'</th>';
|
|
}
|
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
|
print '</tr>';
|
|
|
|
foreach ($comparison_results as $item) {
|
|
$has_difference = ($filter_price && $item['price_differs']) ||
|
|
($filter_description && $item['description_differs']) ||
|
|
($filter_label && $item['label_differs']);
|
|
|
|
$rowClass = $has_difference ? 'oddeven highlighted' : 'oddeven';
|
|
|
|
print '<tr class="'.$rowClass.'">';
|
|
|
|
// Product
|
|
print '<td>';
|
|
if ($item['product_id'] > 0) {
|
|
$product = new Product($db);
|
|
$product->fetch($item['product_id']);
|
|
print $product->getNomUrl(1, '', 0, 0, 0, 1, 1); // Open in new tab
|
|
print '<br><span class="opacitymedium">'.$product->ref.'</span>';
|
|
} else {
|
|
print '<span class="opacitymedium">'.$langs->trans('ProductNotInDatabase').'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Datanorm article
|
|
print '<td>';
|
|
print '<strong>'.dol_escape_htmltag($item['datanorm_ref']).'</strong>';
|
|
print '<br><span class="opacitymedium">'.dol_escape_htmltag(dol_trunc($item['datanorm_name'], 50)).'</span>';
|
|
// Show price_unit (PE = Preiseinheit)
|
|
$pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
|
|
print '<br><span class="opacitymedium small">PE='.$pe.'</span>';
|
|
print '</td>';
|
|
|
|
// Price comparison
|
|
if ($filter_price) {
|
|
$priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : '';
|
|
print '<td class="right nowraponall" style="'.$priceStyle.'">';
|
|
if ($item['product_id'] > 0) {
|
|
print price($item['current_price']);
|
|
// Show copper surcharge from invoice if available
|
|
if (!empty($item['current_kupferzuschlag']) && $item['current_kupferzuschlag'] > 0) {
|
|
print '<br><span class="opacitymedium small" title="Kupferzuschlag aus Rechnung">';
|
|
print '<i class="fas fa-plus-circle" style="color:#f0ad4e;"></i> '.price($item['current_kupferzuschlag']);
|
|
print '</span>';
|
|
// Show total price (material + surcharge)
|
|
$totalWithSurcharge = $item['current_price'] + $item['current_kupferzuschlag'];
|
|
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
|
print '<strong>='.price($totalWithSurcharge).'</strong>';
|
|
print '</span>';
|
|
}
|
|
} else {
|
|
print '-';
|
|
}
|
|
print '</td>';
|
|
print '<td class="right nowraponall" style="'.$priceStyle.'">';
|
|
print price($item['datanorm_price']);
|
|
// Show original price and unit if price_unit > 1
|
|
if (!empty($item['datanorm_price_unit']) && $item['datanorm_price_unit'] > 1) {
|
|
print '<br><span class="opacitymedium small">('.price($item['datanorm_price_raw']).'/'.$item['datanorm_price_unit'].')</span>';
|
|
}
|
|
// Show effective surcharge (from invoice/datanorm)
|
|
if (!empty($item['effective_surcharge']) && $item['effective_surcharge'] > 0) {
|
|
// Determine surcharge source
|
|
$surchargeSource = isset($item['surcharge_source']) ? $item['surcharge_source'] : 'datanorm';
|
|
$sourceLabels = array('invoice' => 'Rechnung', 'datanorm' => 'Datanorm');
|
|
$sourceColors = array('invoice' => '#f0ad4e', 'datanorm' => '#95a5a6');
|
|
$sourceLabel = isset($sourceLabels[$surchargeSource]) ? $sourceLabels[$surchargeSource] : $surchargeSource;
|
|
$sourceColor = isset($sourceColors[$surchargeSource]) ? $sourceColors[$surchargeSource] : '#f0ad4e';
|
|
|
|
print '<br><span class="opacitymedium small" title="Kupferzuschlag ('.$sourceLabel.')">';
|
|
print '<i class="fas fa-plus-circle" style="color:'.$sourceColor.';"></i> '.price($item['effective_surcharge']);
|
|
print '</span>';
|
|
// Show total price with surcharge
|
|
if (!empty($item['datanorm_price_with_surcharge'])) {
|
|
print '<br><span class="small" style="color:#337ab7;" title="Gesamt (Material+CU)">';
|
|
print '<strong>='.price($item['datanorm_price_with_surcharge']).'</strong>';
|
|
print '</span>';
|
|
}
|
|
}
|
|
if ($item['price_differs'] && $item['product_id'] > 0) {
|
|
$diff = $item['datanorm_price'] - $item['current_price'];
|
|
$diffPercent = ($item['current_price'] > 0) ? ($diff / $item['current_price'] * 100) : 0;
|
|
print '<br>';
|
|
if ($diff > 0) {
|
|
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($diffPercent, 1).'%</span>';
|
|
} else {
|
|
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format($diffPercent, 1).'%</span>';
|
|
}
|
|
}
|
|
print '</td>';
|
|
}
|
|
|
|
// Description comparison
|
|
if ($filter_description) {
|
|
$descStyle = $item['description_differs'] ? 'background-color: #fcf8e3;' : '';
|
|
print '<td style="'.$descStyle.'">';
|
|
print dol_escape_htmltag(dol_trunc($item['current_description'], 80));
|
|
print '</td>';
|
|
print '<td style="'.$descStyle.'">';
|
|
print dol_escape_htmltag(dol_trunc($item['datanorm_description'], 80));
|
|
print '</td>';
|
|
}
|
|
|
|
// Label comparison
|
|
if ($filter_label) {
|
|
$labelStyle = $item['label_differs'] ? 'background-color: #fcf8e3;' : '';
|
|
print '<td style="'.$labelStyle.'">';
|
|
print dol_escape_htmltag($item['current_label']);
|
|
print '</td>';
|
|
print '<td style="'.$labelStyle.'">';
|
|
print dol_escape_htmltag($item['datanorm_label']);
|
|
print '</td>';
|
|
}
|
|
|
|
// Actions
|
|
print '<td class="center nowraponall">';
|
|
if ($item['product_id'] > 0 && $has_difference) {
|
|
// Quick apply form
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" style="display:inline;">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="apply_single">';
|
|
print '<input type="hidden" name="product_id" value="'.$item['product_id'].'">';
|
|
print '<input type="hidden" name="datanorm_key" value="'.dol_escape_htmltag($item['datanorm_key']).'">';
|
|
print '<input type="hidden" name="fk_soc" value="'.$fk_soc.'">';
|
|
print '<input type="hidden" name="search_mode" value="'.$search_mode.'">';
|
|
print '<input type="hidden" name="search_term" value="'.dol_escape_htmltag($search_term).'">';
|
|
print '<input type="hidden" name="filter_price" value="'.$filter_price.'">';
|
|
print '<input type="hidden" name="filter_description" value="'.$filter_description.'">';
|
|
print '<input type="hidden" name="filter_label" value="'.$filter_label.'">';
|
|
print '<input type="hidden" name="only_differences" value="'.$only_differences.'">';
|
|
|
|
// Checkboxes for what to apply
|
|
if ($filter_price && $item['price_differs']) {
|
|
print '<input type="checkbox" name="apply_price" value="1" checked title="'.$langs->trans('Price').'">';
|
|
print '<span class="opacitymedium">P</span> ';
|
|
}
|
|
if ($filter_description && $item['description_differs']) {
|
|
print '<input type="checkbox" name="apply_description" value="1" checked title="'.$langs->trans('Description').'">';
|
|
print '<span class="opacitymedium">D</span> ';
|
|
}
|
|
if ($filter_label && $item['label_differs']) {
|
|
print '<input type="checkbox" name="apply_label" value="1" checked title="'.$langs->trans('Label').'">';
|
|
print '<span class="opacitymedium">L</span> ';
|
|
}
|
|
|
|
print '<button type="submit" class="button smallpaddingimp" title="'.$langs->trans('ApplyChanges').'">';
|
|
print '<i class="fas fa-check"></i>';
|
|
print '</button>';
|
|
print '</form>';
|
|
|
|
// Add to pending
|
|
$isPending = isset($_SESSION['datanorm_pending_changes'][$item['product_id']]);
|
|
if (!$isPending) {
|
|
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=add_pending&product_id='.$item['product_id'].'&datanorm_key='.urlencode($item['datanorm_key']).'&fk_soc='.$fk_soc.'&apply_fields[]=price&apply_fields[]=description&apply_fields[]=label&token='.newToken().'" class="button smallpaddingimp" title="'.$langs->trans('AddToPending').'">';
|
|
print '<i class="fas fa-plus"></i>';
|
|
print '</a>';
|
|
} else {
|
|
print ' <span class="badge badge-status4">'.$langs->trans('Pending').'</span>';
|
|
}
|
|
} elseif ($item['product_id'] == 0) {
|
|
// Create product link
|
|
print '<a href="'.dol_buildpath('/product/card.php', 1).'?action=create&type=0" class="button smallpaddingimp" title="'.$langs->trans('CreateProduct').'" target="_blank">';
|
|
print '<i class="fas fa-plus"></i>';
|
|
print '</a>';
|
|
} else {
|
|
print '<span class="opacitymedium">'.$langs->trans('NoChanges').'</span>';
|
|
}
|
|
|
|
// Raw data button (always show)
|
|
print ' <a href="#" class="button smallpaddingimp" onclick="showRawDatanorm(\''.dol_escape_js($item['datanorm_ref']).'\', '.$fk_soc.'); return false;" title="Rohdaten anzeigen">';
|
|
print '<i class="fas fa-file-code"></i>';
|
|
print '</a>';
|
|
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
// Summary and mass apply button
|
|
$pendingCount = count($_SESSION['datanorm_pending_changes']);
|
|
if ($pendingCount > 0) {
|
|
print '<br>';
|
|
print '<div class="center">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=confirm_apply_all&token='.newToken().'" class="button button-primary">';
|
|
print '<i class="fas fa-check-double paddingright"></i>'.$langs->trans('ApplyAllPendingChanges').' ('.$pendingCount.')';
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
|
|
} else {
|
|
print '<br><div class="opacitymedium center">'.$langs->trans('NoResultsFound').'</div>';
|
|
}
|
|
}
|
|
|
|
// Confirmation dialog for mass apply
|
|
if ($action == 'confirm_apply_all' && !empty($_SESSION['datanorm_pending_changes'])) {
|
|
print '<br><br>';
|
|
print '<div class="confirmmessage">';
|
|
print '<h3>'.$langs->trans('ConfirmMassUpdate').'</h3>';
|
|
print '<p>'.$langs->trans('FollowingProductsWillBeUpdated').':</p>';
|
|
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans('Product').'</th>';
|
|
print '<th>'.$langs->trans('Changes').'</th>';
|
|
print '</tr>';
|
|
|
|
foreach ($_SESSION['datanorm_pending_changes'] as $product_id => $change) {
|
|
$product = new Product($db);
|
|
$product->fetch($product_id);
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$product->getNomUrl(1).' - '.$product->label.'</td>';
|
|
print '<td>';
|
|
$changes = array();
|
|
if (in_array('price', $change['apply_fields'])) $changes[] = $langs->trans('Price');
|
|
if (in_array('description', $change['apply_fields'])) $changes[] = $langs->trans('Description');
|
|
if (in_array('label', $change['apply_fields'])) $changes[] = $langs->trans('Label');
|
|
print implode(', ', $changes);
|
|
print '</td>';
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
|
|
print '<br>';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="apply_all_confirmed">';
|
|
print '<input type="hidden" name="confirm" value="yes">';
|
|
print '<div class="center">';
|
|
print '<input type="submit" class="button button-primary" value="'.$langs->trans('Confirm').'">';
|
|
print ' ';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'" class="button">'.$langs->trans('Cancel').'</a>';
|
|
print '</div>';
|
|
print '</form>';
|
|
|
|
print '</div>';
|
|
}
|
|
|
|
print '<style>
|
|
.highlighted { background-color: #fff3cd !important; }
|
|
.confirmmessage { background: #f8f9fa; padding: 20px; border: 1px solid #dee2e6; border-radius: 5px; }
|
|
/* Modal styles */
|
|
.datanorm-modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.5);
|
|
z-index: 10000;
|
|
}
|
|
.datanorm-modal {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
max-width: 90%;
|
|
max-height: 80%;
|
|
overflow: auto;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
z-index: 10001;
|
|
}
|
|
.datanorm-modal h3 {
|
|
margin-top: 0;
|
|
border-bottom: 1px solid #ddd;
|
|
padding-bottom: 10px;
|
|
}
|
|
.datanorm-modal pre {
|
|
background: #f5f5f5;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
font-size: 12px;
|
|
font-family: monospace;
|
|
}
|
|
.datanorm-modal .close-btn {
|
|
float: right;
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
color: #999;
|
|
}
|
|
.datanorm-modal .close-btn:hover {
|
|
color: #333;
|
|
}
|
|
</style>';
|
|
|
|
// Modal for raw data
|
|
print '<div id="datanormRawModal" class="datanorm-modal-overlay" onclick="closeRawDatanormModal(event)">';
|
|
print '<div class="datanorm-modal" onclick="event.stopPropagation()">';
|
|
print '<span class="close-btn" onclick="closeRawDatanormModal()">×</span>';
|
|
print '<h3><i class="fas fa-file-code"></i> Rohdaten: <span id="modalArticleNumber"></span></h3>';
|
|
print '<div id="modalContent">';
|
|
print '<p><em>Laden...</em></p>';
|
|
print '</div>';
|
|
print '</div>';
|
|
print '</div>';
|
|
|
|
print '<script>
|
|
function showRawDatanorm(articleNumber, fkSoc) {
|
|
document.getElementById("datanormRawModal").style.display = "block";
|
|
document.getElementById("modalArticleNumber").textContent = articleNumber;
|
|
document.getElementById("modalContent").innerHTML = "<p><em>Laden...</em></p>";
|
|
|
|
// AJAX request
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("GET", "'.$_SERVER['PHP_SELF'].'?action=get_raw_lines&article_number=" + encodeURIComponent(articleNumber) + "&fk_soc=" + fkSoc + "&token='.newToken().'", true);
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var data = JSON.parse(xhr.responseText);
|
|
var html = "";
|
|
|
|
html += "<h4>DATANORM (A-Satz)</h4>";
|
|
if (data.datanorm_line) {
|
|
html += "<pre>" + escapeHtml(data.datanorm_line) + "</pre>";
|
|
// Parse and show fields
|
|
var parts = data.datanorm_line.split(";");
|
|
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
|
|
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(parts[0] || "") + "</td></tr>";
|
|
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(parts[1] || "") + "</td></tr>";
|
|
html += "<tr><td><b>2: Artikelnummer</b></td><td>" + escapeHtml(parts[2] || "") + "</td></tr>";
|
|
html += "<tr><td><b>3: Textkennzeichen</b></td><td>" + escapeHtml(parts[3] || "") + "</td></tr>";
|
|
html += "<tr><td><b>4: Kurztext 1</b></td><td>" + escapeHtml(parts[4] || "") + "</td></tr>";
|
|
html += "<tr><td><b>5: Kurztext 2</b></td><td>" + escapeHtml(parts[5] || "") + "</td></tr>";
|
|
html += "<tr><td><b>6: Preiskennzeichen</b></td><td>" + escapeHtml(parts[6] || "") + " (1=Brutto, 2=Netto)</td></tr>";
|
|
html += "<tr><td><b>7: PE-Code</b></td><td>" + escapeHtml(parts[7] || "") + " (0=1, 1=10, 2=100, 3=1000)</td></tr>";
|
|
html += "<tr><td><b>8: Mengeneinheit</b></td><td>" + escapeHtml(parts[8] || "") + "</td></tr>";
|
|
html += "<tr><td><b>9: Preis (Cent)</b></td><td>" + escapeHtml(parts[9] || "") + "</td></tr>";
|
|
html += "<tr><td><b>10: Rabattgruppe</b></td><td>" + escapeHtml(parts[10] || "") + "</td></tr>";
|
|
html += "<tr><td><b>11: Warengruppe</b></td><td>" + escapeHtml(parts[11] || "") + "</td></tr>";
|
|
html += "</table>";
|
|
} else {
|
|
html += "<p class=\"opacitymedium\">Keine DATANORM-Zeile gefunden</p>";
|
|
}
|
|
|
|
html += "<br><h4>DATPREIS (P-Satz)</h4>";
|
|
if (data.datpreis_line) {
|
|
html += "<pre>" + escapeHtml(data.datpreis_line) + "</pre>";
|
|
// Parse P-Satz
|
|
var pparts = data.datpreis_line.split(";");
|
|
html += "<table class=\"noborder\" style=\"margin-top:10px;font-size:12px;\">";
|
|
html += "<tr><td><b>0: Satzart</b></td><td>" + escapeHtml(pparts[0] || "") + "</td></tr>";
|
|
html += "<tr><td><b>1: Aktionscode</b></td><td>" + escapeHtml(pparts[1] || "") + "</td></tr>";
|
|
// Find the article in the P-Satz (multiple articles per line)
|
|
for (var i = 2; i < pparts.length; i += 9) {
|
|
if (pparts[i] && pparts[i].trim() === articleNumber) {
|
|
html += "<tr><td colspan=\"2\"><b>--- Artikel " + escapeHtml(pparts[i]) + " ---</b></td></tr>";
|
|
html += "<tr><td><b>" + i + ": Artikelnummer</b></td><td>" + escapeHtml(pparts[i] || "") + "</td></tr>";
|
|
html += "<tr><td><b>" + (i+1) + ": Preiskennzeichen</b></td><td>" + escapeHtml(pparts[i+1] || "") + " (2=Netto)</td></tr>";
|
|
html += "<tr><td><b>" + (i+2) + ": Preis (Cent)</b></td><td>" + escapeHtml(pparts[i+2] || "") + " = " + (parseFloat(pparts[i+2] || 0) / 100).toFixed(2) + " EUR</td></tr>";
|
|
html += "<tr><td><b>" + (i+3) + ": Unbekannt</b></td><td>" + escapeHtml(pparts[i+3] || "") + "</td></tr>";
|
|
html += "<tr><td><b>" + (i+4) + ": Metallzuschlag (Cent)</b></td><td>" + escapeHtml(pparts[i+4] || "") + " = " + (parseFloat(pparts[i+4] || 0) / 100).toFixed(2) + " EUR</td></tr>";
|
|
break;
|
|
}
|
|
}
|
|
html += "</table>";
|
|
} else {
|
|
html += "<p class=\"opacitymedium\">Keine DATPREIS-Zeile gefunden</p>";
|
|
}
|
|
|
|
if (data.error) {
|
|
html += "<br><p class=\"error\">" + escapeHtml(data.error) + "</p>";
|
|
}
|
|
|
|
document.getElementById("modalContent").innerHTML = html;
|
|
} catch(e) {
|
|
document.getElementById("modalContent").innerHTML = "<p class=\"error\">Fehler beim Parsen: " + e.message + "</p><pre>" + escapeHtml(xhr.responseText) + "</pre>";
|
|
}
|
|
} else {
|
|
document.getElementById("modalContent").innerHTML = "<p class=\"error\">HTTP Fehler: " + xhr.status + "</p>";
|
|
}
|
|
}
|
|
};
|
|
xhr.send();
|
|
}
|
|
|
|
function closeRawDatanormModal(event) {
|
|
if (!event || event.target.id === "datanormRawModal") {
|
|
document.getElementById("datanormRawModal").style.display = "none";
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
var div = document.createElement("div");
|
|
div.appendChild(document.createTextNode(text || ""));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Close modal with ESC key
|
|
document.addEventListener("keydown", function(e) {
|
|
if (e.key === "Escape") {
|
|
closeRawDatanormModal();
|
|
}
|
|
});
|
|
|
|
// Confirm dialog for adding all items using jQuery UI Dialog
|
|
$(document).ready(function() {
|
|
$("#btnAddAllWithDiff").on("click", function() {
|
|
$("#dialogConfirmAddAll").dialog({
|
|
resizable: false,
|
|
height: "auto",
|
|
width: 450,
|
|
modal: true,
|
|
buttons: {
|
|
"' . dol_escape_js($langs->trans('Validate')) . '": function() {
|
|
$(this).dialog("close");
|
|
document.getElementById("addAllForm").submit();
|
|
},
|
|
"' . dol_escape_js($langs->trans('Cancel')) . '": function() {
|
|
$(this).dialog("close");
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>';
|
|
|
|
llxFooter();
|
|
$db->close();
|
|
|
|
/*
|
|
* Helper functions
|
|
*/
|
|
|
|
/**
|
|
* Find products linked to a supplier and compare with Datanorm
|
|
*
|
|
* @param object $db Database handler
|
|
* @param int $fk_soc Supplier ID
|
|
* @param int $search_by_name Search by name
|
|
* @param int $search_by_ean Search by EAN
|
|
* @param int $search_by_ref Search by reference
|
|
* @return array Comparison results
|
|
*/
|
|
function findProductsForSupplier($db, $fk_soc, $search_by_name = 0, $search_by_ean = 0, $search_by_ref = 0)
|
|
{
|
|
global $conf;
|
|
|
|
$results = array();
|
|
|
|
// Get all supplier products
|
|
$sql = "SELECT DISTINCT pf.fk_product, pf.ref_fourn, pf.price as fourn_price, p.ref, p.label, p.description, p.barcode";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf";
|
|
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = pf.fk_product";
|
|
$sql .= " WHERE pf.fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND pf.entity IN (".getEntity('product').")";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
// Try to find matching Datanorm article
|
|
$datanorm = findDatanormMatch($db, $fk_soc, $obj->ref_fourn, $obj->label, $obj->barcode, $obj->ref, $search_by_name, $search_by_ean, $search_by_ref);
|
|
|
|
if ($datanorm) {
|
|
$results[] = buildComparisonResult($obj, $datanorm);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Search Datanorm products manually
|
|
*
|
|
* @param object $db Database handler
|
|
* @param int $fk_soc Supplier ID
|
|
* @param string $search_term Search term
|
|
* @param int $search_by_name Search by name
|
|
* @param int $search_by_ean Search by EAN
|
|
* @param int $search_by_ref Search by reference
|
|
* @return array Comparison results
|
|
*/
|
|
function searchDatanormProducts($db, $fk_soc, $search_term, $search_by_name = 0, $search_by_ean = 0, $search_by_ref = 0)
|
|
{
|
|
global $conf;
|
|
|
|
$results = array();
|
|
|
|
// Search in Datanorm
|
|
$sql = "SELECT d.* FROM ".MAIN_DB_PREFIX."importzugferd_datanorm d";
|
|
$sql .= " WHERE d.fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND (d.article_number LIKE '%".$db->escape($search_term)."%'";
|
|
$sql .= " OR d.short_text1 LIKE '%".$db->escape($search_term)."%'";
|
|
$sql .= " OR d.short_text2 LIKE '%".$db->escape($search_term)."%'";
|
|
if ($search_by_ean) {
|
|
$sql .= " OR d.ean LIKE '%".$db->escape($search_term)."%'";
|
|
}
|
|
$sql .= ")";
|
|
$sql .= " ORDER BY d.article_number";
|
|
$sql .= " LIMIT 100";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql) {
|
|
while ($datanorm = $db->fetch_object($resql)) {
|
|
// Try to find matching product in database
|
|
$product = findProductMatch($db, $fk_soc, $datanorm);
|
|
|
|
// Calculate unit price (Datanorm price may be per price_unit pieces)
|
|
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
|
|
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
|
|
|
// Get current price and copper surcharge from extrafield
|
|
$current_price = 0;
|
|
$current_kupferzuschlag = 0;
|
|
if ($product) {
|
|
$priceDetails = getSupplierPriceDetails($db, $product->rowid, $fk_soc);
|
|
$current_price = $priceDetails['unitprice'];
|
|
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
|
|
}
|
|
|
|
// Priority for surcharge: 1) Invoice extrafield, 2) Datanorm
|
|
if ($current_kupferzuschlag > 0) {
|
|
$effective_surcharge = $current_kupferzuschlag;
|
|
$surcharge_source = 'invoice';
|
|
} else {
|
|
$effective_surcharge = $datanorm_metal_surcharge;
|
|
$surcharge_source = 'datanorm';
|
|
}
|
|
|
|
// Calculate prices
|
|
$datanorm_material_unit_price = $datanorm->price / $price_unit;
|
|
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit);
|
|
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
|
|
|
|
$results[] = array(
|
|
'product_id' => $product ? $product->rowid : 0,
|
|
'current_price' => $current_price,
|
|
'current_kupferzuschlag' => $current_kupferzuschlag,
|
|
'current_description' => $product ? $product->description : '',
|
|
'current_label' => $product ? $product->label : '',
|
|
'datanorm_key' => $datanorm->article_number,
|
|
'datanorm_ref' => $datanorm->article_number,
|
|
'datanorm_name' => $datanorm->short_text1,
|
|
'datanorm_price' => $datanorm_material_unit_price,
|
|
'datanorm_price_with_surcharge' => $datanorm_total_unit_price,
|
|
'datanorm_price_raw' => $datanorm->price,
|
|
'datanorm_material_price' => $datanorm->price,
|
|
'datanorm_metal_surcharge' => $datanorm_metal_surcharge,
|
|
'effective_surcharge' => $effective_surcharge,
|
|
'surcharge_source' => $surcharge_source,
|
|
'datanorm_price_unit' => $price_unit,
|
|
'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
|
'datanorm_label' => $datanorm->short_text1,
|
|
'price_differs' => $product && abs($current_price - $datanorm_material_unit_price) > 0.01,
|
|
'description_differs' => $product && $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
|
'label_differs' => $product && $product->label != $datanorm->short_text1,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Find Datanorm match for a product
|
|
*/
|
|
function findDatanormMatch($db, $fk_soc, $ref_fourn, $label, $barcode, $ref, $search_by_name, $search_by_ean, $search_by_ref)
|
|
{
|
|
// First try by supplier reference (article number)
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$sql .= " WHERE fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND article_number = '".$db->escape($ref_fourn)."'";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
|
|
// Try by EAN if enabled
|
|
if ($search_by_ean && !empty($barcode)) {
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$sql .= " WHERE fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND ean = '".$db->escape($barcode)."'";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
}
|
|
|
|
// Try by product ref if enabled
|
|
if ($search_by_ref && !empty($ref)) {
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$sql .= " WHERE fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND article_number = '".$db->escape($ref)."'";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
}
|
|
|
|
// Try by product name/label if enabled
|
|
if ($search_by_name && !empty($label)) {
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$sql .= " WHERE fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND (short_text1 LIKE '%".$db->escape($label)."%'";
|
|
$sql .= " OR short_text2 LIKE '%".$db->escape($label)."%')";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Find product match for Datanorm article
|
|
*/
|
|
function findProductMatch($db, $fk_soc, $datanorm)
|
|
{
|
|
// Try by supplier reference
|
|
$sql = "SELECT p.* FROM ".MAIN_DB_PREFIX."product p";
|
|
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."product_fournisseur_price pf ON pf.fk_product = p.rowid";
|
|
$sql .= " WHERE pf.fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
|
|
// Try by EAN
|
|
if (!empty($datanorm->ean)) {
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."product";
|
|
$sql .= " WHERE barcode = '".$db->escape($datanorm->ean)."'";
|
|
$sql .= " LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
return $db->fetch_object($resql);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get supplier price for a product
|
|
*/
|
|
function getSupplierPrice($db, $product_id, $fk_soc)
|
|
{
|
|
// Use unitprice (price per 1 piece) for comparison, not price (which may be for a quantity)
|
|
$sql = "SELECT unitprice, price, quantity FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
|
$sql .= " WHERE fk_product = ".((int)$product_id);
|
|
$sql .= " AND fk_soc = ".((int)$fk_soc);
|
|
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
$obj = $db->fetch_object($resql);
|
|
// Return unitprice if available, otherwise calculate from price/quantity
|
|
if (!empty($obj->unitprice) && $obj->unitprice > 0) {
|
|
return $obj->unitprice;
|
|
}
|
|
// Fallback: calculate unit price from price and quantity
|
|
if (!empty($obj->quantity) && $obj->quantity > 0) {
|
|
return $obj->price / $obj->quantity;
|
|
}
|
|
return $obj->price;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get supplier price details including extrafields (Kupferzuschlag)
|
|
*/
|
|
function getSupplierPriceDetails($db, $product_id, $fk_soc)
|
|
{
|
|
$result = array(
|
|
'unitprice' => 0,
|
|
'kupferzuschlag' => 0,
|
|
'preiseinheit' => 1,
|
|
'price_id' => 0,
|
|
);
|
|
|
|
// Get base price
|
|
$sql = "SELECT pf.rowid, pf.unitprice, pf.price, pf.quantity";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf";
|
|
$sql .= " WHERE pf.fk_product = ".((int)$product_id);
|
|
$sql .= " AND pf.fk_soc = ".((int)$fk_soc);
|
|
$sql .= " ORDER BY pf.rowid DESC LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
$obj = $db->fetch_object($resql);
|
|
$result['price_id'] = $obj->rowid;
|
|
|
|
// Calculate unit price
|
|
if (!empty($obj->unitprice) && $obj->unitprice > 0) {
|
|
$result['unitprice'] = $obj->unitprice;
|
|
} elseif (!empty($obj->quantity) && $obj->quantity > 0) {
|
|
$result['unitprice'] = $obj->price / $obj->quantity;
|
|
} else {
|
|
$result['unitprice'] = $obj->price;
|
|
}
|
|
|
|
// Get extrafields (Kupferzuschlag, Preiseinheit)
|
|
$sql_extra = "SELECT kupferzuschlag, preiseinheit";
|
|
$sql_extra .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields";
|
|
$sql_extra .= " WHERE fk_object = ".((int)$obj->rowid);
|
|
|
|
$res_extra = $db->query($sql_extra);
|
|
if ($res_extra && $db->num_rows($res_extra) > 0) {
|
|
$extra = $db->fetch_object($res_extra);
|
|
$result['kupferzuschlag'] = !empty($extra->kupferzuschlag) ? (float)$extra->kupferzuschlag : 0;
|
|
$result['preiseinheit'] = !empty($extra->preiseinheit) ? (int)$extra->preiseinheit : 1;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Extract price unit from short text (e.g. "Ri100" = 100, "Tr.500" = 500)
|
|
* This is a fallback when price_unit field is not properly filled
|
|
*
|
|
* @param string $short_text1 Short text 1
|
|
* @param string $short_text2 Short text 2 (optional)
|
|
* @return int Extracted price unit or 1 if not found
|
|
*/
|
|
function extractPriceUnitFromText($short_text1, $short_text2 = '')
|
|
{
|
|
$text = $short_text1 . ' ' . $short_text2;
|
|
|
|
// Patterns to match:
|
|
// Ri100, Ri.100, Ri 100, Ri. 100 (Rolle = Roll)
|
|
// Tr100, Tr.100, Tr 100, Tr. 100 (Trommel = Drum)
|
|
// Ring 100, Ring100
|
|
// /100, /50 (per unit indicator)
|
|
// VPE100, VPE 100 (Verpackungseinheit)
|
|
// 100er, 50er (German quantity suffix)
|
|
|
|
$patterns = array(
|
|
'/\bRi\.?\s*(\d+)\b/i', // Ri100, Ri.100, Ri 100
|
|
'/\bTr\.?\s*(\d+)\b/i', // Tr100, Tr.100, Tr 500
|
|
'/\bRing\.?\s*(\d+)\b/i', // Ring 100
|
|
'/\bRolle\.?\s*(\d+)\b/i', // Rolle 100
|
|
'/\bTrommel\.?\s*(\d+)\b/i', // Trommel 500
|
|
'/\/(\d+)\s*(?:Stk?|m|M)?\b/', // /100, /100Stk, /100m
|
|
'/\bVPE\.?\s*(\d+)\b/i', // VPE100, VPE 100
|
|
'/\b(\d+)er\b/', // 100er
|
|
'/\bPE\s*(\d+)\b/i', // PE100
|
|
);
|
|
|
|
foreach ($patterns as $pattern) {
|
|
if (preg_match($pattern, $text, $matches)) {
|
|
$unit = (int)$matches[1];
|
|
if ($unit > 1 && $unit <= 10000) {
|
|
return $unit;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get effective price unit - uses database value if > 1, otherwise tries to extract from text
|
|
*
|
|
* @param object $datanorm Datanorm database object
|
|
* @return int Effective price unit
|
|
*/
|
|
function getEffectivePriceUnit($datanorm)
|
|
{
|
|
// If database has a valid price_unit > 1, use it
|
|
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
|
|
return (int)$datanorm->price_unit;
|
|
}
|
|
|
|
// Otherwise try to extract from text
|
|
$short_text2 = isset($datanorm->short_text2) ? $datanorm->short_text2 : '';
|
|
return extractPriceUnitFromText($datanorm->short_text1, $short_text2);
|
|
}
|
|
|
|
/**
|
|
* Build comparison result array
|
|
*
|
|
* @param object $product Product from supplier price
|
|
* @param object $datanorm Datanorm data
|
|
* @return array Comparison result
|
|
*/
|
|
function buildComparisonResult($product, $datanorm)
|
|
{
|
|
global $db;
|
|
|
|
$fk_soc = $datanorm->fk_soc;
|
|
// Get supplier price details including extrafields (Kupferzuschlag)
|
|
$priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc);
|
|
$current_price = $priceDetails['unitprice'];
|
|
$current_kupferzuschlag = $priceDetails['kupferzuschlag'];
|
|
|
|
// Calculate unit price (Datanorm price may be per price_unit pieces)
|
|
// Datanorm metal_surcharge is usually 0 for Sonepar - use extrafield from invoice instead
|
|
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
|
|
$datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
|
|
|
// Priority for surcharge: 1) Invoice extrafield, 2) Datanorm
|
|
if ($current_kupferzuschlag > 0) {
|
|
$effective_surcharge = $current_kupferzuschlag;
|
|
$surcharge_source = 'invoice';
|
|
} else {
|
|
$effective_surcharge = $datanorm_metal_surcharge;
|
|
$surcharge_source = 'datanorm';
|
|
}
|
|
|
|
// Calculate prices
|
|
$datanorm_material_unit_price = $datanorm->price / $price_unit;
|
|
$total_price_with_surcharge = $datanorm->price + ($effective_surcharge * $price_unit);
|
|
$datanorm_total_unit_price = $total_price_with_surcharge / $price_unit;
|
|
|
|
return array(
|
|
'product_id' => $product->fk_product,
|
|
'current_price' => $current_price,
|
|
'current_kupferzuschlag' => $current_kupferzuschlag,
|
|
'current_description' => $product->description,
|
|
'current_label' => $product->label,
|
|
'datanorm_key' => $datanorm->article_number,
|
|
'datanorm_ref' => $datanorm->article_number,
|
|
'datanorm_name' => $datanorm->short_text1,
|
|
'datanorm_price' => $datanorm_material_unit_price, // Material price per unit
|
|
'datanorm_price_with_surcharge' => $datanorm_total_unit_price, // Total price including surcharge
|
|
'datanorm_price_raw' => $datanorm->price, // Raw price from DATPREIS
|
|
'datanorm_material_price' => $datanorm->price,
|
|
'datanorm_metal_surcharge' => $datanorm_metal_surcharge, // From Datanorm (usually 0)
|
|
'effective_surcharge' => $effective_surcharge, // From invoice or Datanorm
|
|
'surcharge_source' => $surcharge_source, // Source of surcharge (invoice/datanorm)
|
|
'datanorm_price_unit' => $price_unit,
|
|
'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
|
'datanorm_label' => $datanorm->short_text1,
|
|
'price_differs' => abs($current_price - $datanorm_material_unit_price) > 0.01,
|
|
'description_differs' => $product->description != trim($datanorm->short_text1.' '.$datanorm->short_text2),
|
|
'label_differs' => $product->label != $datanorm->short_text1,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Apply Datanorm update to a product and log changes
|
|
*/
|
|
function applyDatanormUpdate($db, $user, $product_id, $datanorm_key, $fk_soc, $apply_price, $apply_description, $apply_label, $batch_id = '')
|
|
{
|
|
global $conf;
|
|
|
|
// Get Datanorm data
|
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."importzugferd_datanorm";
|
|
$sql .= " WHERE fk_soc = ".((int)$fk_soc);
|
|
$sql .= " AND article_number = '".$db->escape($datanorm_key)."'";
|
|
|
|
$resql = $db->query($sql);
|
|
if (!$resql || $db->num_rows($resql) == 0) {
|
|
return -1;
|
|
}
|
|
|
|
$datanorm = $db->fetch_object($resql);
|
|
|
|
// Calculate unit price (Datanorm price may be per price_unit pieces)
|
|
// Total price = material price + metal surcharge (for cables)
|
|
$price_unit = (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) ? $datanorm->price_unit : 1;
|
|
$metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0;
|
|
$total_price = $datanorm->price + $metal_surcharge;
|
|
$datanorm_unit_price = $total_price / $price_unit;
|
|
|
|
// Load product
|
|
$product = new Product($db);
|
|
$result = $product->fetch($product_id);
|
|
if ($result <= 0) {
|
|
return -2;
|
|
}
|
|
|
|
// Store original values for logging
|
|
$old_label = $product->label;
|
|
$old_description = $product->description;
|
|
$old_price = getSupplierPrice($db, $product_id, $fk_soc);
|
|
|
|
$updated = false;
|
|
$changes = array();
|
|
|
|
// Update label
|
|
if ($apply_label && $product->label != $datanorm->short_text1) {
|
|
$changes[] = array(
|
|
'field' => 'label',
|
|
'old' => $old_label,
|
|
'new' => $datanorm->short_text1
|
|
);
|
|
$product->label = $datanorm->short_text1;
|
|
$updated = true;
|
|
}
|
|
|
|
// Update description
|
|
if ($apply_description) {
|
|
$new_desc = trim($datanorm->short_text1.' '.$datanorm->short_text2);
|
|
if ($product->description != $new_desc) {
|
|
$changes[] = array(
|
|
'field' => 'description',
|
|
'old' => $old_description,
|
|
'new' => $new_desc
|
|
);
|
|
$product->description = $new_desc;
|
|
$updated = true;
|
|
}
|
|
}
|
|
|
|
// Save product changes
|
|
if ($updated) {
|
|
$result = $product->update($product->id, $user);
|
|
if ($result < 0) {
|
|
return -3;
|
|
}
|
|
}
|
|
|
|
// Update supplier price
|
|
if ($apply_price) {
|
|
$productFourn = new ProductFournisseur($db);
|
|
$productFourn->fetch($product_id);
|
|
|
|
// Load supplier object (required by update_buyprice - expects Societe object, not integer)
|
|
$supplier = new Societe($db);
|
|
$supplier->fetch($fk_soc);
|
|
|
|
// Find existing supplier price
|
|
$sql = "SELECT rowid, quantity, price, unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
|
|
$sql .= " WHERE fk_product = ".((int)$product_id);
|
|
$sql .= " AND fk_soc = ".((int)$fk_soc);
|
|
$sql .= " ORDER BY rowid DESC LIMIT 1";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql && $db->num_rows($resql) > 0) {
|
|
$priceObj = $db->fetch_object($resql);
|
|
|
|
// Get the actual unit price from Dolibarr (price per 1 piece)
|
|
$current_unit_price = (!empty($priceObj->unitprice) && $priceObj->unitprice > 0)
|
|
? $priceObj->unitprice
|
|
: ($priceObj->quantity > 0 ? $priceObj->price / $priceObj->quantity : $priceObj->price);
|
|
|
|
// Only update if unit price differs
|
|
if (abs($current_unit_price - $datanorm_unit_price) > 0.01) {
|
|
$changes[] = array(
|
|
'field' => 'price',
|
|
'old' => $current_unit_price,
|
|
'new' => $datanorm_unit_price
|
|
);
|
|
|
|
// Calculate total price for the quantity (Dolibarr expects total price, not unit price)
|
|
// Dolibarr will calculate: unitprice = price / quantity
|
|
$total_price_for_qty = $datanorm_unit_price * $priceObj->quantity;
|
|
|
|
// Update existing price - $supplier must be Societe object, not integer ID
|
|
$result = $productFourn->update_buyprice(
|
|
$priceObj->quantity,
|
|
$total_price_for_qty,
|
|
$user,
|
|
'HT',
|
|
$supplier, // Societe object, not integer
|
|
0, // availability
|
|
$datanorm->article_number, // ref_fourn
|
|
0, // tva_tx
|
|
0, // charges
|
|
0, // remise_percent
|
|
0, // remise
|
|
0, // newnpr
|
|
0, // delivery_time_days
|
|
'', // supplier_reputation
|
|
array(), // localtaxes
|
|
'', // newdefaultvatcode
|
|
0, // multicurrency_buyprice
|
|
'', // multicurrency_price_base_type
|
|
0, // multicurrency_tx
|
|
'', // multicurrency_code
|
|
'', // desc_fourn
|
|
'', // barcode
|
|
0, // fk_barcode_type
|
|
array() // options
|
|
);
|
|
|
|
if ($result < 0) {
|
|
return -4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log all changes
|
|
if (!empty($changes)) {
|
|
$now = dol_now();
|
|
$batch_id = $batch_id ?: uniqid('single_');
|
|
|
|
foreach ($changes as $change) {
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."importzugferd_datanorm_log";
|
|
$sql .= " (fk_product, fk_soc, fk_user, datanorm_ref, field_changed, old_value, new_value, date_change, batch_id, entity)";
|
|
$sql .= " VALUES (";
|
|
$sql .= ((int)$product_id).", ";
|
|
$sql .= ((int)$fk_soc).", ";
|
|
$sql .= ((int)$user->id).", ";
|
|
$sql .= "'".$db->escape($datanorm_key)."', ";
|
|
$sql .= "'".$db->escape($change['field'])."', ";
|
|
$sql .= "'".$db->escape($change['old'])."', ";
|
|
$sql .= "'".$db->escape($change['new'])."', ";
|
|
$sql .= "'".$db->idate($now)."', ";
|
|
$sql .= "'".$db->escape($batch_id)."', ";
|
|
$sql .= ((int)$conf->entity);
|
|
$sql .= ")";
|
|
|
|
$db->query($sql);
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|