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'); $hide_cables = GETPOSTINT('hide_cables'); $filter_price_up = GETPOSTINT('filter_price_up'); $filter_price_down = GETPOSTINT('filter_price_down'); } else { // Defaults for first page load $filter_price = 1; $filter_description = 1; $filter_label = 0; $only_differences = 0; $hide_cables = 0; $filter_price_up = 0; $filter_price_down = 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 (preserve all filters) 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.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&action=search'); 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 and filters 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.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&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 and filters 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.'&hide_cables='.$hide_cables.'&filter_price_up='.$filter_price_up.'&filter_price_down='.$filter_price_down.'&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 ''; // 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 ''; // Price direction filter 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 '
Preisfilter'; print ''; print ''; print '     '; 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) { // Filter cables if requested // Datanorm groups: 01-19, 101-119, 202, 205 are cables/wires if ($hide_cables && !empty($item['datanorm_product_group'])) { $pg = (int)$item['datanorm_product_group']; if (($pg >= 1 && $pg <= 19) || ($pg >= 101 && $pg <= 119) || $pg == 202 || $pg == 205) { continue; // Skip cables } } // Calculate price difference for filtering $price_diff = 0; if ($item['product_id'] > 0 && $item['price_differs']) { $datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1; $datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : 0; $current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0; $effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1; $current_total = isset($item['current_total_price']) ? $item['current_total_price'] : 0; // Scale Cu to Datanorm's price_unit $cu_per_unit = ($current_cu > 0 && $effective_qty > 0) ? $current_cu / $effective_qty : 0; $cu_for_pe = $cu_per_unit * $datanorm_pe; $datanorm_total = $datanorm_raw + $cu_for_pe; $current_unit = $effective_qty > 0 ? $current_total / $effective_qty : $current_total; $datanorm_unit = $datanorm_total / $datanorm_pe; $price_diff = $datanorm_unit - $current_unit; } // Filter by price direction if ($filter_price_up && !$filter_price_down && $price_diff <= 0) { continue; // Only show price increases } if ($filter_price_down && !$filter_price_up && $price_diff >= 0) { continue; // Only show price decreases } if ($filter_price_up && $filter_price_down && $price_diff == 0) { continue; // Both checked: show any change, skip unchanged } $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) { $dolibarr_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price']; $dolibarr_qty = isset($item['current_quantity']) ? $item['current_quantity'] : 1; $dolibarr_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0; // IMPORTANT: Dolibarr price already includes Cu! Show as info only if ($dolibarr_cu > 0) { print '(davon '.price($dolibarr_cu).' Cu)
'; } // Total price for minimum quantity (already includes Cu!) print ''.price($dolibarr_total); if ($dolibarr_qty > 1) { print '/'.$dolibarr_qty; } print ''; // Unit price as secondary info if ($dolibarr_qty > 1) { $dolibarr_unit = $dolibarr_total / $dolibarr_qty; print '
('.price($dolibarr_unit).'/Stk.)'; } } else { print '-'; } print '
'; $datanorm_pe = isset($item['datanorm_price_unit']) ? $item['datanorm_price_unit'] : 1; $datanorm_raw = isset($item['datanorm_price_raw']) ? $item['datanorm_price_raw'] : $item['datanorm_price']; $current_cu = isset($item['current_kupferzuschlag']) ? $item['current_kupferzuschlag'] : 0; $effective_qty = isset($item['current_effective_quantity']) ? $item['current_effective_quantity'] : 1; // Scale Cu from Dolibarr's quantity to Datanorm's price_unit // Example: Cu 254,55€ for 50m → for 100m = 509,10€ $cu_per_unit = ($current_cu > 0 && $effective_qty > 0) ? $current_cu / $effective_qty : 0; $cu_for_pe = $cu_per_unit * $datanorm_pe; // Show breakdown if copper exists if ($current_cu > 0) { print ''.price($datanorm_raw).' + '.price($cu_for_pe).' Cu
'; } // Total price for Datanorm price_unit (with scaled Cu) $datanorm_total = $datanorm_raw + $cu_for_pe; print ''.price($datanorm_total); if ($datanorm_pe > 1) { print '/'.$datanorm_pe; } print ''; // Unit price as secondary info if ($datanorm_pe > 1) { $datanorm_unit = $datanorm_total / $datanorm_pe; print '
('.price($datanorm_unit).'/Stk.)'; } if ($item['price_differs'] && $item['product_id'] > 0) { // Calculate percentage difference using UNIT PRICE basis $current_total = isset($item['current_total_price']) ? $item['current_total_price'] : $item['current_price']; // Dolibarr: unit price (already includes Cu) $current_compare = $effective_qty > 0 ? $current_total / $effective_qty : $current_total; // Datanorm: unit price (material + scaled Cu) $datanorm_compare = $datanorm_total / $datanorm_pe; $diff = $datanorm_compare - $current_compare; $diffPercent = ($current_compare > 0) ? ($diff / $current_compare * 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 ''; 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_total_price = 0; $current_quantity = 1; $current_kaufmenge = 0; $current_kupferzuschlag = 0; $datanorm_price_unit_code = isset($datanorm->price_unit_code) ? $datanorm->price_unit_code : -1; if ($product) { $priceDetails = getSupplierPriceDetails($db, $product->rowid, $fk_soc); $current_total_price = $priceDetails['price']; $current_quantity = $priceDetails['quantity']; $current_kaufmenge = $priceDetails['kaufmenge']; $current_kupferzuschlag = $priceDetails['kupferzuschlag']; } // Use kaufmenge if set, otherwise fall back to quantity $effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : $current_quantity; // 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 for comparison - UNIT PRICE basis // IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again! // Datanorm price is WITHOUT kupferzuschlag, so add SCALED Cu for comparison // // Example: Kabel NYM-J 5x10 // - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m // - Datanorm: 168,50€ for 100m (PE=100) // - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€ // - Datanorm total: 168,50€ + 509,10€ = 677,60€ → 6,78€/m // Calculate Cu per unit (from Dolibarr's quantity basis) $cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0) ? $current_kupferzuschlag / $effective_quantity : 0; // Scale Cu to Datanorm's price_unit basis $cu_for_price_unit = $cu_per_unit * $price_unit; // Dolibarr: unit price (already includes Cu) $current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price; // Datanorm: material price + scaled Cu, then to unit price $datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit; // For display: always show unit prices $datanorm_material_unit_price = $datanorm->price / $price_unit; $total_price_with_surcharge = $datanorm->price + $cu_for_price_unit; $datanorm_total_unit_price = $total_price_with_surcharge / $price_unit; $current_unit_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price; $results[] = array( 'product_id' => $product ? $product->rowid : 0, 'current_price' => $current_unit_price, 'current_total_price' => $current_total_price, 'current_quantity' => $current_quantity, 'current_kaufmenge' => $current_kaufmenge, 'current_effective_quantity' => $effective_quantity, '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, 'datanorm_price_unit_code' => $datanorm_price_unit_code, 'effective_surcharge' => $effective_surcharge, 'surcharge_source' => $surcharge_source, 'datanorm_price_unit' => $price_unit, 'datanorm_product_group' => isset($datanorm->product_group) ? $datanorm->product_group : '', 'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2), 'datanorm_label' => $datanorm->short_text1, 'price_differs' => $product && abs($current_compare_price - $datanorm_compare_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( 'price' => 0, 'quantity' => 1, 'kaufmenge' => 0, 'unitprice' => 0, 'kupferzuschlag' => 0, 'preiseinheit' => 1, 'price_id' => 0, ); // Get base price - ALWAYS load price + quantity, NOT unitprice alone! $sql = "SELECT pf.rowid, 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; $result['price'] = (float)$obj->price; $result['quantity'] = max(1, (int)$obj->quantity); // Calculate unit price from price / quantity $result['unitprice'] = $result['quantity'] > 0 ? $result['price'] / $result['quantity'] : $result['price']; // Get extrafields (Kupferzuschlag, Preiseinheit, Kaufmenge) $sql_extra = "SELECT kupferzuschlag, preiseinheit, kaufmenge"; $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; $result['kaufmenge'] = !empty($extra->kaufmenge) ? (int)$extra->kaufmenge : 0; } } 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, Kaufmenge) $priceDetails = getSupplierPriceDetails($db, $product->fk_product, $fk_soc); $current_total_price = $priceDetails['price']; $current_quantity = $priceDetails['quantity']; $current_kaufmenge = $priceDetails['kaufmenge']; // Actual purchase quantity (if set) $current_kupferzuschlag = $priceDetails['kupferzuschlag']; // Use kaufmenge if set, otherwise fall back to quantity $effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : $current_quantity; $current_unit_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price; // 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_price_unit_code = isset($datanorm->price_unit_code) ? $datanorm->price_unit_code : -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 for comparison // IMPORTANT: Dolibarr price already INCLUDES kupferzuschlag! Don't add it again! // Datanorm price is WITHOUT kupferzuschlag, so add it for comparison // Compare on UNIT PRICE basis (per 1 piece/meter) // // Example: Kabel NYM-J 5x10 // - Dolibarr: 331,27€ for 50m (includes 254,55€ Cu for 50m) → 6,63€/m // - Datanorm: 168,50€ for 100m (PE=100) + Cu must be scaled to 100m // - Cu per unit: 254,55€ / 50m = 5,09€/m → for 100m = 509,10€ // - Datanorm total for 100m: 168,50€ + 509,10€ = 677,60€ → 6,78€/m // Calculate Cu per unit (from Dolibarr's quantity basis) $cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0) ? $current_kupferzuschlag / $effective_quantity : 0; // Scale Cu to Datanorm's price_unit basis $cu_for_price_unit = $cu_per_unit * $price_unit; // Dolibarr: unit price (already includes Cu) $current_compare_price = $effective_quantity > 0 ? $current_total_price / $effective_quantity : $current_total_price; // Datanorm: material price + scaled Cu, then to unit price $datanorm_compare_price = ($datanorm->price + $cu_for_price_unit) / $price_unit; // For display: always show unit prices $datanorm_material_unit_price = $datanorm->price / $price_unit; $total_price_with_surcharge = $datanorm->price + $cu_for_price_unit; $datanorm_total_unit_price = $total_price_with_surcharge / $price_unit; return array( 'product_id' => $product->fk_product, 'current_price' => $current_unit_price, 'current_total_price' => $current_total_price, 'current_quantity' => $current_quantity, 'current_kaufmenge' => $current_kaufmenge, 'current_effective_quantity' => $effective_quantity, '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) 'datanorm_price_unit_code' => $datanorm_price_unit_code, 'effective_surcharge' => $effective_surcharge, // From invoice or Datanorm 'surcharge_source' => $surcharge_source, // Source of surcharge (invoice/datanorm) 'datanorm_price_unit' => $price_unit, 'datanorm_product_group' => isset($datanorm->product_group) ? $datanorm->product_group : '', 'datanorm_description' => trim($datanorm->short_text1.' '.$datanorm->short_text2), 'datanorm_label' => $datanorm->short_text1, 'price_differs' => abs($current_compare_price - $datanorm_compare_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; // Get existing supplier price details to get kupferzuschlag and quantity from extrafield $priceDetails = getSupplierPriceDetails($db, $product_id, $fk_soc); $current_kupferzuschlag = $priceDetails['kupferzuschlag']; $current_quantity = $priceDetails['quantity']; $current_kaufmenge = $priceDetails['kaufmenge']; $effective_quantity = ($current_kaufmenge > 0) ? $current_kaufmenge : max(1, $current_quantity); // Priority for surcharge: 1) Dolibarr extrafield (from invoice), 2) Datanorm metal_surcharge $datanorm_metal_surcharge = !empty($datanorm->metal_surcharge) ? (float)$datanorm->metal_surcharge : 0; // Scale Cu from Dolibarr's quantity to Datanorm's price_unit // Example: Cu 152,73€ for 50m → per meter = 3,05€ → for 100m = 305,46€ $cu_per_unit = ($current_kupferzuschlag > 0 && $effective_quantity > 0) ? $current_kupferzuschlag / $effective_quantity : 0; $cu_for_price_unit = $cu_per_unit * $price_unit; // Use scaled Cu, or fallback to Datanorm metal_surcharge $effective_surcharge = ($cu_for_price_unit > 0) ? $cu_for_price_unit : $datanorm_metal_surcharge; // Total price for price_unit includes scaled surcharge $total_price_for_pe = $datanorm->price + $effective_surcharge; // Unit price (per 1 piece/meter) $datanorm_unit_price = $total_price_for_pe / $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 label only (description goes to supplier price desc_fourn below) // Save product changes if ($updated) { $result = $product->update($product->id, $user); if ($result < 0) { return -3; } } // Update supplier price and/or description if ($apply_price || $apply_description) { $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, desc_fourn 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); $price_rowid = $priceObj->rowid; // Use effective_quantity (kaufmenge if set, otherwise quantity) for price comparison // This ensures consistent comparison with buildComparisonResult() $effective_qty = ($priceDetails['kaufmenge'] > 0) ? $priceDetails['kaufmenge'] : max(1, $priceObj->quantity); // Get the actual unit price from Dolibarr (price per 1 effective piece) $current_unit_price = $effective_qty > 0 ? $priceObj->price / $effective_qty : $priceObj->price; // Prepare new description if requested $new_desc_fourn = null; if ($apply_description) { $new_desc_fourn = trim($datanorm->short_text1.' '.$datanorm->short_text2); if ($priceObj->desc_fourn != $new_desc_fourn) { $changes[] = array( 'field' => 'desc_fourn', 'old' => $priceObj->desc_fourn, 'new' => $new_desc_fourn ); } else { $new_desc_fourn = null; // No change needed } } // Check if price needs update $price_changed = $apply_price && (abs($current_unit_price - $datanorm_unit_price) > 0.01); if ($price_changed) { $changes[] = array( 'field' => 'price', 'old' => $current_unit_price, 'new' => $datanorm_unit_price ); } // Update only the fields that need changing (preserves all other fields!) if ($price_changed || $new_desc_fourn !== null) { $update_fields = array(); if ($price_changed) { // Calculate total price for the quantity, round to 2 decimals $total_price_for_qty = round($datanorm_unit_price * $priceObj->quantity, 2); $rounded_unit_price = round($datanorm_unit_price, 2); $update_fields[] = "price = ".((float)$total_price_for_qty); $update_fields[] = "unitprice = ".((float)$rounded_unit_price); } if ($new_desc_fourn !== null) { $update_fields[] = "desc_fourn = '".$db->escape($new_desc_fourn)."'"; } if (!empty($update_fields)) { $sql_update = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price"; $sql_update .= " SET ".implode(", ", $update_fields); $sql_update .= " WHERE rowid = ".(int)$price_rowid; $result = $db->query($sql_update); if (!$result) { 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; }