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 '
'.$langs->trans('NoDatanormData').'
';
print '
'.$langs->trans('UploadDatanorm').'';
llxFooter();
$db->close();
exit;
}
// Search form
print '';
// Show pending changes section (always visible when there are pending changes)
if (!empty($_SESSION['datanorm_pending_changes'])) {
$pendingCount = count($_SESSION['datanorm_pending_changes']);
print '
';
print '';
}
// JavaScript for toggling manual search and initializing state
print '';
// 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 '
';
print '';
print '
';
print ''.$langs->trans('Results').': ';
print $total_results.' '.$langs->trans('Products');
if ($diff_count > 0) {
print ' | '.$diff_count.' '.$langs->trans('WithDifferences').'';
}
if ($only_differences) {
print ' | '.$langs->trans('OnlyShowingDifferences').'';
}
print '
';
// "Add all with differences" button
if (!empty($items_with_diff)) {
print '
';
print '';
print '
';
// Hidden dialog for confirmation
print '
';
print '
';
print $langs->trans('ConfirmAddAllToPending');
print '
';
print '
'.$diff_count.' '.$langs->trans('Products').'
';
print '
';
}
print '
';
if (!empty($comparison_results)) {
print '
';
print '';
print '
';
// Header
print '';
print '| '.$langs->trans('Product').' | ';
print ''.$langs->trans('DatanormArticle').' | ';
if ($filter_price) {
print ''.$langs->trans('CurrentPrice').' | ';
print ''.$langs->trans('DatanormPrice').' | ';
}
if ($filter_description) {
print ''.$langs->trans('CurrentDescription').' | ';
print ''.$langs->trans('DatanormDescription').' | ';
}
if ($filter_label) {
print ''.$langs->trans('CurrentLabel').' | ';
print ''.$langs->trans('DatanormLabel').' | ';
}
print ''.$langs->trans('Actions').' | ';
print '
';
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 '';
// Product
print '';
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 ' '.$product->ref.'';
} else {
print ''.$langs->trans('ProductNotInDatabase').'';
}
print ' | ';
// Datanorm article
print '';
print ''.dol_escape_htmltag($item['datanorm_ref']).'';
print ' '.dol_escape_htmltag(dol_trunc($item['datanorm_name'], 50)).'';
// Show price_unit (PE = Preiseinheit)
$pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1;
print ' PE='.$pe.'';
print ' | ';
// Price comparison
if ($filter_price) {
$priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : '';
print '';
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 ' ';
print ' '.price($item['current_kupferzuschlag']);
print '';
// Show total price (material + surcharge)
$totalWithSurcharge = $item['current_price'] + $item['current_kupferzuschlag'];
print ' ';
print '='.price($totalWithSurcharge).'';
print '';
}
} else {
print '-';
}
print ' | ';
print '';
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 ' ('.price($item['datanorm_price_raw']).'/'.$item['datanorm_price_unit'].')';
}
// 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 ' ';
print ' '.price($item['effective_surcharge']);
print '';
// Show total price with surcharge
if (!empty($item['datanorm_price_with_surcharge'])) {
print ' ';
print '='.price($item['datanorm_price_with_surcharge']).'';
print '';
}
}
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 ' ';
if ($diff > 0) {
print ' +'.number_format($diffPercent, 1).'%';
} else {
print ' '.number_format($diffPercent, 1).'%';
}
}
print ' | ';
}
// Description comparison
if ($filter_description) {
$descStyle = $item['description_differs'] ? 'background-color: #fcf8e3;' : '';
print '';
print dol_escape_htmltag(dol_trunc($item['current_description'], 80));
print ' | ';
print '';
print dol_escape_htmltag(dol_trunc($item['datanorm_description'], 80));
print ' | ';
}
// Label comparison
if ($filter_label) {
$labelStyle = $item['label_differs'] ? 'background-color: #fcf8e3;' : '';
print '';
print dol_escape_htmltag($item['current_label']);
print ' | ';
print '';
print dol_escape_htmltag($item['datanorm_label']);
print ' | ';
}
// Actions
print '';
if ($item['product_id'] > 0 && $has_difference) {
// Quick apply form
print '';
// Add to pending
$isPending = isset($_SESSION['datanorm_pending_changes'][$item['product_id']]);
if (!$isPending) {
print ' ';
print '';
print '';
} else {
print ' '.$langs->trans('Pending').'';
}
} elseif ($item['product_id'] == 0) {
// Create product link
print '';
print '';
print '';
} else {
print ''.$langs->trans('NoChanges').'';
}
// Raw data button (always show)
print ' ';
print '';
print '';
print ' | ';
print '
';
}
print '
';
print '
';
// Summary and mass apply button
$pendingCount = count($_SESSION['datanorm_pending_changes']);
if ($pendingCount > 0) {
print '
';
print '';
}
} else {
print '
'.$langs->trans('NoResultsFound').'
';
}
}
// Confirmation dialog for mass apply
if ($action == 'confirm_apply_all' && !empty($_SESSION['datanorm_pending_changes'])) {
print '
';
print '';
print '
'.$langs->trans('ConfirmMassUpdate').'
';
print '
'.$langs->trans('FollowingProductsWillBeUpdated').':
';
print '
';
print '';
print '| '.$langs->trans('Product').' | ';
print ''.$langs->trans('Changes').' | ';
print '
';
foreach ($_SESSION['datanorm_pending_changes'] as $product_id => $change) {
$product = new Product($db);
$product->fetch($product_id);
print '';
print '| '.$product->getNomUrl(1).' - '.$product->label.' | ';
print '';
$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 ' | ';
print '
';
}
print '
';
print '
';
print '
';
print '
';
}
print '';
// Modal for raw data
print '';
print '
';
print '
×';
print '
Rohdaten:
';
print '
';
print '
Laden...
';
print '
';
print '
';
print '
';
print '';
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;
}