* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ /** * \file class/datanorm.class.php * \ingroup importzugferd * \brief Class for Datanorm article database operations */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; /** * Class Datanorm * Manages Datanorm articles in database */ class Datanorm extends CommonObject { /** * @var string ID to identify managed object */ public $element = 'datanorm'; /** * @var string Name of table without prefix */ public $table_element = 'importzugferd_datanorm'; /** * @var int Does object support multicompany */ public $ismultientitymanaged = 1; /** * @var int Supplier ID */ public $fk_soc; /** * @var string Article number */ public $article_number; /** * @var string Short text 1 */ public $short_text1; /** * @var string Short text 2 */ public $short_text2; /** * @var string Long text */ public $long_text; /** * @var string EAN/GTIN */ public $ean; /** * @var string Manufacturer article number */ public $manufacturer_ref; /** * @var string Manufacturer name */ public $manufacturer_name; /** * @var string Unit code */ public $unit_code; /** * @var float Price */ public $price = 0; /** * @var int Price unit (pieces per price) */ public $price_unit = 1; /** * @var string Discount group */ public $discount_group; /** * @var string Product group */ public $product_group; /** * @var string Alternative unit */ public $alt_unit; /** * @var float Alternative unit factor */ public $alt_unit_factor = 1; /** * @var float Weight in kg */ public $weight; /** * @var string Matchcode */ public $matchcode; /** * @var string Datanorm version */ public $datanorm_version; /** * @var string Import date */ public $import_date; /** * @var int Active flag */ public $active = 1; /** * @var string Date creation */ public $date_creation; /** * @var int User creator */ public $fk_user_creat; /** * @var int User modifier */ public $fk_user_modif; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Create object into database * * @param User $user User that creates * @return int <0 if KO, Id of created object if OK */ public function create($user) { global $conf; $this->entity = $conf->entity; if (empty($this->date_creation)) { $this->date_creation = dol_now(); } if (empty($this->import_date)) { $this->import_date = dol_now(); } $this->fk_user_creat = $user->id; $sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " ("; $sql .= "fk_soc, article_number, short_text1, short_text2, long_text,"; $sql .= "ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= "price, price_unit, discount_group, product_group,"; $sql .= "alt_unit, alt_unit_factor, weight, matchcode,"; $sql .= "datanorm_version, import_date, active, date_creation, fk_user_creat, entity"; $sql .= ") VALUES ("; $sql .= (int) $this->fk_soc . ","; $sql .= "'" . $this->db->escape($this->article_number) . "',"; $sql .= "'" . $this->db->escape($this->short_text1) . "',"; $sql .= "'" . $this->db->escape($this->short_text2) . "',"; $sql .= "'" . $this->db->escape($this->long_text) . "',"; $sql .= "'" . $this->db->escape($this->ean) . "',"; $sql .= "'" . $this->db->escape($this->manufacturer_ref) . "',"; $sql .= "'" . $this->db->escape($this->manufacturer_name) . "',"; $sql .= "'" . $this->db->escape($this->unit_code) . "',"; $sql .= (float) $this->price . ","; $sql .= (int) $this->price_unit . ","; $sql .= "'" . $this->db->escape($this->discount_group) . "',"; $sql .= "'" . $this->db->escape($this->product_group) . "',"; $sql .= "'" . $this->db->escape($this->alt_unit) . "',"; $sql .= (float) $this->alt_unit_factor . ","; $sql .= ($this->weight !== null ? (float) $this->weight : 'NULL') . ","; $sql .= "'" . $this->db->escape($this->matchcode) . "',"; $sql .= "'" . $this->db->escape($this->datanorm_version) . "',"; $sql .= "'" . $this->db->escape($this->db->idate($this->import_date)) . "',"; $sql .= (int) $this->active . ","; $sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',"; $sql .= (int) $this->fk_user_creat . ","; $sql .= (int) $this->entity; $sql .= ")"; dol_syslog(get_class($this) . "::create", LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element); return $this->id; } /** * Create or update article (upsert) * * @param User $user User that creates/modifies * @return int <0 if KO, Id of object if OK */ public function createOrUpdate($user) { // Check if article exists $existing = $this->fetchByArticleNumber($this->fk_soc, $this->article_number); if ($existing > 0) { return $this->update($user); } else { return $this->create($user); } } /** * Load object in memory from database * * @param int $id Id object * @return int <0 if KO, 0 if not found, >0 if OK */ public function fetch($id) { $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,"; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= " price, price_unit, discount_group, product_group,"; $sql .= " alt_unit, alt_unit_factor, weight, matchcode,"; $sql .= " datanorm_version, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity"; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE rowid = " . (int) $id; dol_syslog(get_class($this) . "::fetch", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { if ($this->db->num_rows($resql)) { $obj = $this->db->fetch_object($resql); $this->setFromObject($obj); $this->db->free($resql); return 1; } else { $this->db->free($resql); return 0; } } else { $this->error = $this->db->lasterror(); return -1; } } /** * Fetch by supplier and article number * * @param int $fk_soc Supplier ID * @param string $article_number Article number * @return int <0 if KO, 0 if not found, >0 if OK */ public function fetchByArticleNumber($fk_soc, $article_number) { global $conf; $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,"; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= " price, price_unit, discount_group, product_group,"; $sql .= " alt_unit, alt_unit_factor, weight, matchcode,"; $sql .= " datanorm_version, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity"; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE fk_soc = " . (int) $fk_soc; $sql .= " AND article_number = '" . $this->db->escape($article_number) . "'"; $sql .= " AND entity = " . (int) $conf->entity; dol_syslog(get_class($this) . "::fetchByArticleNumber", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { if ($this->db->num_rows($resql)) { $obj = $this->db->fetch_object($resql); $this->setFromObject($obj); $this->db->free($resql); return 1; } else { $this->db->free($resql); return 0; } } else { $this->error = $this->db->lasterror(); return -1; } } /** * Set object properties from database object * * @param object $obj Database row object */ protected function setFromObject($obj) { $this->id = $obj->rowid; $this->fk_soc = $obj->fk_soc; $this->article_number = $obj->article_number; $this->short_text1 = $obj->short_text1; $this->short_text2 = $obj->short_text2; $this->long_text = $obj->long_text; $this->ean = $obj->ean; $this->manufacturer_ref = $obj->manufacturer_ref; $this->manufacturer_name = $obj->manufacturer_name; $this->unit_code = $obj->unit_code; $this->price = $obj->price; $this->price_unit = $obj->price_unit; $this->discount_group = $obj->discount_group; $this->product_group = $obj->product_group; $this->alt_unit = $obj->alt_unit; $this->alt_unit_factor = $obj->alt_unit_factor; $this->weight = $obj->weight; $this->matchcode = $obj->matchcode; $this->datanorm_version = $obj->datanorm_version; $this->import_date = $this->db->jdate($obj->import_date); $this->active = $obj->active; $this->date_creation = $this->db->jdate($obj->date_creation); $this->tms = $this->db->jdate($obj->tms); $this->fk_user_creat = $obj->fk_user_creat; $this->fk_user_modif = $obj->fk_user_modif; $this->entity = $obj->entity; } /** * Update object in database * * @param User $user User that modifies * @return int <0 if KO, >0 if OK */ public function update($user) { $this->fk_user_modif = $user->id; $this->import_date = dol_now(); $sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET"; $sql .= " short_text1 = '" . $this->db->escape($this->short_text1) . "',"; $sql .= " short_text2 = '" . $this->db->escape($this->short_text2) . "',"; $sql .= " long_text = '" . $this->db->escape($this->long_text) . "',"; $sql .= " ean = '" . $this->db->escape($this->ean) . "',"; $sql .= " manufacturer_ref = '" . $this->db->escape($this->manufacturer_ref) . "',"; $sql .= " manufacturer_name = '" . $this->db->escape($this->manufacturer_name) . "',"; $sql .= " unit_code = '" . $this->db->escape($this->unit_code) . "',"; $sql .= " price = " . (float) $this->price . ","; $sql .= " price_unit = " . (int) $this->price_unit . ","; $sql .= " discount_group = '" . $this->db->escape($this->discount_group) . "',"; $sql .= " product_group = '" . $this->db->escape($this->product_group) . "',"; $sql .= " alt_unit = '" . $this->db->escape($this->alt_unit) . "',"; $sql .= " alt_unit_factor = " . (float) $this->alt_unit_factor . ","; $sql .= " weight = " . ($this->weight !== null ? (float) $this->weight : 'NULL') . ","; $sql .= " matchcode = '" . $this->db->escape($this->matchcode) . "',"; $sql .= " datanorm_version = '" . $this->db->escape($this->datanorm_version) . "',"; $sql .= " import_date = '" . $this->db->escape($this->db->idate($this->import_date)) . "',"; $sql .= " active = " . (int) $this->active . ","; $sql .= " fk_user_modif = " . (int) $this->fk_user_modif; $sql .= " WHERE rowid = " . (int) $this->id; dol_syslog(get_class($this) . "::update", LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } return 1; } /** * Delete object from database * * @param User $user User that deletes * @return int <0 if KO, >0 if OK */ public function delete($user) { $sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE rowid = " . (int) $this->id; dol_syslog(get_class($this) . "::delete", LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } return 1; } /** * Delete all articles for a supplier * * @param User $user User that deletes * @param int $fk_soc Supplier ID * @return int <0 if KO, number of deleted rows if OK */ public function deleteAllBySupplier($user, $fk_soc) { global $conf; $sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE fk_soc = " . (int) $fk_soc; $sql .= " AND entity = " . (int) $conf->entity; dol_syslog(get_class($this) . "::deleteAllBySupplier", LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } return $this->db->affected_rows($resql); } /** * Search articles by article number (exact or partial) * * @param string $article_number Article number to search * @param int $fk_soc Supplier ID (0 = all suppliers) * @param bool $searchAll Search all suppliers if not found in specified * @param int $limit Maximum results * @return array Array of matching articles */ public function searchByArticleNumber($article_number, $fk_soc = 0, $searchAll = false, $limit = 50) { global $conf; $results = array(); // First try exact match with specified supplier if ($fk_soc > 0) { $result = $this->fetchByArticleNumber($fk_soc, $article_number); if ($result > 0) { $results[] = $this->toArray(); return $results; } } // Search partial match $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,"; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= " price, price_unit, discount_group, product_group, matchcode"; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'"; $sql .= " OR ean = '" . $this->db->escape($article_number) . "'"; $sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')"; if ($fk_soc > 0 && !$searchAll) { $sql .= " AND fk_soc = " . (int) $fk_soc; } elseif ($fk_soc > 0 && $searchAll) { // Order by matching supplier first $sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, article_number"; } $sql .= " AND active = 1"; $sql .= " AND entity = " . (int) $conf->entity; if ($fk_soc == 0 || !$searchAll) { $sql .= " ORDER BY article_number"; } $sql .= " LIMIT " . (int) $limit; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $results[] = array( 'id' => $obj->rowid, 'fk_soc' => $obj->fk_soc, 'article_number' => $obj->article_number, 'short_text1' => $obj->short_text1, 'short_text2' => $obj->short_text2, 'ean' => $obj->ean, 'manufacturer_ref' => $obj->manufacturer_ref, 'manufacturer_name' => $obj->manufacturer_name, 'unit_code' => $obj->unit_code, 'price' => $obj->price, 'price_unit' => $obj->price_unit, 'discount_group' => $obj->discount_group, 'product_group' => $obj->product_group, 'matchcode' => $obj->matchcode, ); } $this->db->free($resql); } return $results; } /** * Convert object to array * * @return array Object as array */ public function toArray() { return array( 'id' => $this->id, 'fk_soc' => $this->fk_soc, 'article_number' => $this->article_number, 'short_text1' => $this->short_text1, 'short_text2' => $this->short_text2, 'long_text' => $this->long_text, 'ean' => $this->ean, 'manufacturer_ref' => $this->manufacturer_ref, 'manufacturer_name' => $this->manufacturer_name, 'unit_code' => $this->unit_code, 'price' => $this->price, 'price_unit' => $this->price_unit, 'discount_group' => $this->discount_group, 'product_group' => $this->product_group, 'alt_unit' => $this->alt_unit, 'alt_unit_factor' => $this->alt_unit_factor, 'weight' => $this->weight, 'matchcode' => $this->matchcode, 'datanorm_version' => $this->datanorm_version, 'import_date' => $this->import_date, 'active' => $this->active, ); } /** * Count articles for a supplier * * @param int $fk_soc Supplier ID * @return int Count */ public function countBySupplier($fk_soc) { global $conf; $sql = "SELECT COUNT(*) as nb FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " WHERE fk_soc = " . (int) $fk_soc; $sql .= " AND entity = " . (int) $conf->entity; $resql = $this->db->query($sql); if ($resql) { $obj = $this->db->fetch_object($resql); return (int) $obj->nb; } return 0; } /** * Get all suppliers with Datanorm data * * @return array Array of suppliers with article counts */ public function getSuppliersWithData() { global $conf; $suppliers = array(); $sql = "SELECT d.fk_soc, s.nom as supplier_name, COUNT(*) as article_count,"; $sql .= " MAX(d.import_date) as last_import"; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element . " as d"; $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON s.rowid = d.fk_soc"; $sql .= " WHERE d.entity = " . (int) $conf->entity; $sql .= " GROUP BY d.fk_soc, s.nom"; $sql .= " ORDER BY s.nom"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $suppliers[] = array( 'fk_soc' => $obj->fk_soc, 'name' => $obj->supplier_name, 'article_count' => $obj->article_count, 'last_import' => $this->db->jdate($obj->last_import), ); } $this->db->free($resql); } return $suppliers; } /** * Import articles from parser * * @param User $user User that imports * @param int $fk_soc Supplier ID * @param DatanormParser $parser Parser with parsed articles * @param bool $deleteExisting Delete existing articles before import * @return int Number of imported articles, <0 on error */ public function importFromParser($user, $fk_soc, $parser, $deleteExisting = false) { $this->db->begin(); // Delete existing if requested if ($deleteExisting) { $result = $this->deleteAllBySupplier($user, $fk_soc); if ($result < 0) { $this->db->rollback(); return -1; } } $count = 0; $errors = 0; foreach ($parser->getArticles() as $articleData) { $article = new Datanorm($this->db); $article->fk_soc = $fk_soc; $article->article_number = $articleData['article_number']; $article->short_text1 = $articleData['short_text1'] ?? ''; $article->short_text2 = $articleData['short_text2'] ?? ''; $article->long_text = $articleData['long_text'] ?? ''; $article->ean = $articleData['ean'] ?? ''; $article->manufacturer_ref = $articleData['manufacturer_ref'] ?? ''; $article->manufacturer_name = $articleData['manufacturer_name'] ?? ''; $article->unit_code = $articleData['unit_code'] ?? ''; $article->price = $articleData['price'] ?? 0; $article->price_unit = $articleData['price_unit'] ?? 1; $article->discount_group = $articleData['discount_group'] ?? ''; $article->product_group = $articleData['product_group'] ?? ''; $article->matchcode = $articleData['matchcode'] ?? ''; $article->datanorm_version = $parser->version; $result = $article->createOrUpdate($user); if ($result > 0) { $count++; } else { $errors++; $this->errors[] = 'Error importing ' . $articleData['article_number'] . ': ' . $article->error; } } if ($errors > 0 && $count == 0) { $this->db->rollback(); $this->error = 'All imports failed'; return -1; } $this->db->commit(); return $count; } /** * Import articles from directory using streaming (for large files) * Uses batch inserts to minimize memory usage * * @param User $user User that imports * @param int $fk_soc Supplier ID * @param string $directory Directory with Datanorm files * @param bool $deleteExisting Delete existing articles before import * @return int Number of imported articles, <0 on error */ public function importFromDirectoryStreaming($user, $fk_soc, $directory, $deleteExisting = false) { global $conf; require_once __DIR__ . '/datanormparser.class.php'; // Delete existing if requested if ($deleteExisting) { $result = $this->deleteAllBySupplier($user, $fk_soc); if ($result < 0) { return -1; } } $db = $this->db; $importCount = 0; $version = ''; // Create batch callback that inserts articles directly to database $batchCallback = function ($articles) use ($db, $user, $fk_soc, &$importCount, &$version, $conf) { if (empty($articles)) { return; } // Use multi-row INSERT for better performance $values = array(); $now = $db->idate(dol_now()); foreach ($articles as $articleData) { $values[] = sprintf( "(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', %d)", (int) $fk_soc, $db->escape($articleData['article_number'] ?? ''), $db->escape($articleData['short_text1'] ?? ''), $db->escape($articleData['short_text2'] ?? ''), $db->escape($articleData['long_text'] ?? ''), $db->escape($articleData['ean'] ?? ''), $db->escape($articleData['manufacturer_ref'] ?? ''), $db->escape($articleData['manufacturer_name'] ?? ''), $db->escape($articleData['unit_code'] ?? ''), (float) ($articleData['price'] ?? 0), (int) ($articleData['price_unit'] ?? 1), $db->escape($articleData['discount_group'] ?? ''), $db->escape($articleData['product_group'] ?? ''), $db->escape($articleData['matchcode'] ?? ''), $db->escape($version), $now, (int) $user->id, $now, (int) $conf->entity ); } if (!empty($values)) { // Use INSERT IGNORE to skip duplicates (for the same supplier + article_number) $sql = "INSERT INTO " . MAIN_DB_PREFIX . "importzugferd_datanorm "; $sql .= "(fk_soc, article_number, short_text1, short_text2, long_text, "; $sql .= "ean, manufacturer_ref, manufacturer_name, unit_code, "; $sql .= "price, price_unit, discount_group, product_group, matchcode, "; $sql .= "datanorm_version, import_date, fk_user_creat, date_creation, entity) VALUES "; $sql .= implode(", ", $values); // For updates of existing articles, use ON DUPLICATE KEY UPDATE $sql .= " ON DUPLICATE KEY UPDATE "; $sql .= "short_text1 = VALUES(short_text1), "; $sql .= "short_text2 = VALUES(short_text2), "; $sql .= "long_text = VALUES(long_text), "; $sql .= "ean = VALUES(ean), "; $sql .= "manufacturer_ref = VALUES(manufacturer_ref), "; $sql .= "manufacturer_name = VALUES(manufacturer_name), "; $sql .= "unit_code = VALUES(unit_code), "; $sql .= "price = VALUES(price), "; $sql .= "price_unit = VALUES(price_unit), "; $sql .= "discount_group = VALUES(discount_group), "; $sql .= "product_group = VALUES(product_group), "; $sql .= "matchcode = VALUES(matchcode), "; $sql .= "datanorm_version = VALUES(datanorm_version), "; $sql .= "import_date = VALUES(import_date), "; $sql .= "fk_user_modif = " . (int) $user->id; $resql = $db->query($sql); if ($resql) { $importCount += count($values); } } }; // Parse with streaming enabled // The parser now loads prices first, then articles $parser = new DatanormParser(); $parser->enableStreaming($batchCallback, 500); // Parse directory - prices are loaded first, then articles with streaming $count = $parser->parseDirectory($directory); $version = $parser->version; if ($count < 0) { $this->error = $parser->error; return -1; } // Second pass: Update prices from DATPREIS files $priceFiles = glob($directory . '/DATPREIS.*'); if (!empty($priceFiles)) { foreach ($priceFiles as $file) { $ext = strtoupper(pathinfo($file, PATHINFO_EXTENSION)); if (preg_match('/^\d{3}$/', $ext)) { $this->updatePricesFromFile($fk_soc, $file); } } } return $importCount; } /** * Update prices from DATPREIS file (streaming) * Processes file line by line and updates database directly * * @param int $fk_soc Supplier ID * @param string $file Path to DATPREIS file * @return int Number of prices updated */ protected function updatePricesFromFile($fk_soc, $file) { global $conf; $handle = fopen($file, 'r'); if ($handle === false) { return 0; } $updated = 0; $batch = array(); $batchSize = 500; while (($line = fgets($handle)) !== false) { $line = rtrim($line, "\r\n"); // Convert encoding if needed if (!mb_check_encoding($line, 'UTF-8')) { $line = mb_convert_encoding($line, 'UTF-8', 'ISO-8859-1'); } if (strlen($line) < 10 || strpos($line, ';') === false) { continue; } $parts = explode(';', $line); $recordType = trim($parts[0] ?? ''); // P;A format - multiple articles per line if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') { $i = 2; while ($i < count($parts) - 2) { $articleNumber = trim($parts[$i] ?? ''); $priceRaw = trim($parts[$i + 2] ?? '0'); $price = (float)$priceRaw / 100; // Convert cents to euros if (!empty($articleNumber) && $price > 0) { $batch[$articleNumber] = $price; } $i += 9; // 9 fields per article } } elseif ($recordType === 'P' || $recordType === '0') { $articleNumber = trim($parts[1] ?? ''); $priceRaw = trim($parts[3] ?? '0'); if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) { $price = (float)$priceRaw / 100; } else { $priceRaw = str_replace(',', '.', $priceRaw); $price = (float)$priceRaw; } if (!empty($articleNumber) && $price > 0) { $batch[$articleNumber] = $price; } } // Flush batch when it reaches the limit if (count($batch) >= $batchSize) { $updated += $this->flushPriceBatch($fk_soc, $batch); $batch = array(); } } // Flush remaining if (!empty($batch)) { $updated += $this->flushPriceBatch($fk_soc, $batch); } fclose($handle); return $updated; } /** * Flush price batch to database * * @param int $fk_soc Supplier ID * @param array $prices Array of article_number => price * @return int Number of rows updated */ protected function flushPriceBatch($fk_soc, $prices) { global $conf; if (empty($prices)) { return 0; } $updated = 0; // Build CASE statement for batch update $cases = array(); $articleNumbers = array(); foreach ($prices as $artNum => $price) { $cases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$price; $articleNumbers[] = "'" . $this->db->escape($artNum) . "'"; } if (!empty($cases)) { $sql = "UPDATE " . MAIN_DB_PREFIX . "importzugferd_datanorm SET "; $sql .= "price = CASE article_number "; $sql .= implode(" ", $cases); $sql .= " END "; $sql .= "WHERE fk_soc = " . (int)$fk_soc; $sql .= " AND entity = " . (int)$conf->entity; $sql .= " AND article_number IN (" . implode(",", $articleNumbers) . ")"; $resql = $this->db->query($sql); if ($resql) { $updated = $this->db->affected_rows($resql); } } return $updated; } /** * Get full description for product creation * * @return string Full description */ public function getFullDescription() { $desc = ''; if (!empty($this->short_text1)) { $desc .= $this->short_text1; } if (!empty($this->short_text2)) { $desc .= ($desc ? "\n" : '') . $this->short_text2; } if (!empty($this->long_text)) { $desc .= ($desc ? "\n\n" : '') . $this->long_text; } // Add metadata $meta = array(); if (!empty($this->manufacturer_name)) { $meta[] = 'Hersteller: ' . $this->manufacturer_name; } if (!empty($this->manufacturer_ref)) { $meta[] = 'Hersteller-Nr: ' . $this->manufacturer_ref; } if (!empty($this->ean)) { $meta[] = 'EAN: ' . $this->ean; } if (!empty($this->product_group)) { $meta[] = 'Warengruppe: ' . $this->product_group; } if (!empty($meta)) { $desc .= ($desc ? "\n\n" : '') . implode("\n", $meta); } return $desc; } /** * Calculate selling price with markup * * @param float $markupPercent Markup percentage * @return float Selling price */ public function getSellingPrice($markupPercent = 0) { $basePrice = $this->price; // Adjust for price unit if ($this->price_unit > 1) { $basePrice = $basePrice / $this->price_unit; } if ($markupPercent > 0) { return $basePrice * (1 + $markupPercent / 100); } return $basePrice; } }