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 '
'; print ''; print ''; print '
'; print '
'; print ''; // Supplier selection print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Search mode print ''; print ''; print ''; print ''; // Manual search term print ''; print ''; print ''; print ''; // Additional search options print ''; print ''; print ''; print ''; // Filter: What to compare/update print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Only show differences print ''; print ''; print ''; print ''; print '
'.$langs->trans('SelectSupplier').'
'.$langs->trans('Supplier').''; // 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 ''; print '
'.$langs->trans('SearchMode').''; print ''; print ''; print '     '; print ''; print ''; print '
'.$langs->trans('SearchTerm').''; print ''; print '
'.$langs->trans('AdditionalSearchOptions').''; print ''; print ''; print '   '; print ''; print ''; print '   '; print ''; print ''; print '
'.$langs->trans('FieldsToCompare').'
'.$langs->trans('Fields').''; print ''; print ''; print '   '; print ''; print ''; print '   '; print ''; print ''; print '
'.$langs->trans('Display').''; print ''; print ''; print '
'; print '
'; print '
'; print '
'; print ''; if (!empty($_SESSION['datanorm_pending_changes'])) { print '   '.$langs->trans('ClearPendingChanges').' ('.count($_SESSION['datanorm_pending_changes']).')'; } print '
'; 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 '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'.$langs->trans('PendingChanges').' ('.$pendingCount.')
'; print ''; print ''.$langs->trans('ApplyAllPendingChanges').' ('.$pendingCount.')'; print ''; print '
'; 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 ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'; print '
'; // Hidden dialog for confirmation print ''; } print '
'; if (!empty($comparison_results)) { print '
'; print '
'; print ''; // Header print ''; print ''; print ''; if ($filter_price) { print ''; print ''; } if ($filter_description) { print ''; print ''; } if ($filter_label) { print ''; print ''; } print ''; 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 ''; // Datanorm article print ''; // Price comparison if ($filter_price) { $priceStyle = $item['price_differs'] ? 'background-color: #fcf8e3;' : ''; print ''; print ''; } // Description comparison if ($filter_description) { $descStyle = $item['description_differs'] ? 'background-color: #fcf8e3;' : ''; print ''; print ''; } // Label comparison if ($filter_label) { $labelStyle = $item['label_differs'] ? 'background-color: #fcf8e3;' : ''; print ''; print ''; } // Actions print ''; print ''; } print '
'.$langs->trans('Product').''.$langs->trans('DatanormArticle').''.$langs->trans('CurrentPrice').''.$langs->trans('DatanormPrice').''.$langs->trans('CurrentDescription').''.$langs->trans('DatanormDescription').''.$langs->trans('CurrentLabel').''.$langs->trans('DatanormLabel').''.$langs->trans('Actions').'
'; 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 '
'; 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 '
'; 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 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 '
'; print dol_escape_htmltag(dol_trunc($item['current_description'], 80)); print ''; print dol_escape_htmltag(dol_trunc($item['datanorm_description'], 80)); print ''; print dol_escape_htmltag($item['current_label']); print ''; print dol_escape_htmltag($item['datanorm_label']); print ''; if ($item['product_id'] > 0 && $has_difference) { // Quick apply form print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Checkboxes for what to apply if ($filter_price && $item['price_differs']) { print ''; print 'P '; } if ($filter_description && $item['description_differs']) { print ''; print 'D '; } if ($filter_label && $item['label_differs']) { print ''; print 'L '; } print ''; 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 '
'; // Summary and mass apply button $pendingCount = count($_SESSION['datanorm_pending_changes']); if ($pendingCount > 0) { print '
'; print '
'; print ''; print ''.$langs->trans('ApplyAllPendingChanges').' ('.$pendingCount.')'; 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 ''; print ''; print ''; foreach ($_SESSION['datanorm_pending_changes'] as $product_id => $change) { $product = new Product($db); $product->fetch($product_id); print ''; print ''; print ''; print ''; } print '
'.$langs->trans('Product').''.$langs->trans('Changes').'
'.$product->getNomUrl(1).' - '.$product->label.''; $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 '
'; print ''; print '   '; print ''.$langs->trans('Cancel').''; 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; }