1086 lines
37 KiB
PHP
1086 lines
37 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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 (actual quantity: 1, 10, 100, or 1000)
|
|
*/
|
|
public $price_unit = 1;
|
|
|
|
/**
|
|
* @var int Price unit code (original Datanorm PE code: 0, 1, 2, or 3)
|
|
*/
|
|
public $price_unit_code = 0;
|
|
|
|
/**
|
|
* @var int Price type (1=gross/Brutto, 2=net/Netto)
|
|
*/
|
|
public $price_type = 1;
|
|
|
|
/**
|
|
* @var int VPE - Verpackungseinheit (packaging quantity from B-record)
|
|
*/
|
|
public $vpe;
|
|
|
|
/**
|
|
* @var float Metal surcharge (Metallzuschlag/Kupferzuschlag) for cables
|
|
*/
|
|
public $metal_surcharge = 0;
|
|
|
|
/**
|
|
* @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 Action code (N=New, A=Update, L=Delete)
|
|
*/
|
|
public $action_code = 'N';
|
|
|
|
/**
|
|
* @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;
|
|
|
|
// Set active=0 if action_code is L (deleted article)
|
|
if ($this->action_code === 'L') {
|
|
$this->active = 0;
|
|
}
|
|
|
|
$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, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
|
$sql .= "alt_unit, alt_unit_factor, weight, matchcode,";
|
|
$sql .= "datanorm_version, action_code, 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 .= (int) $this->price_unit_code . ",";
|
|
$sql .= (int) $this->price_type . ",";
|
|
$sql .= (float) $this->metal_surcharge . ",";
|
|
$sql .= ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
|
|
$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->action_code) . "',";
|
|
$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, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
|
$sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
|
|
$sql .= " datanorm_version, action_code, 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, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
|
|
$sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
|
|
$sql .= " datanorm_version, action_code, 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->price_unit_code = $obj->price_unit_code ?? 0;
|
|
$this->price_type = $obj->price_type ?? 1;
|
|
$this->metal_surcharge = $obj->metal_surcharge ?? 0;
|
|
$this->vpe = $obj->vpe;
|
|
$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->action_code = $obj->action_code ?? 'N';
|
|
$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();
|
|
|
|
// Set active=0 if action_code is L (deleted article)
|
|
if ($this->action_code === 'L') {
|
|
$this->active = 0;
|
|
}
|
|
|
|
$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 .= " price_unit_code = " . (int) $this->price_unit_code . ",";
|
|
$sql .= " price_type = " . (int) $this->price_type . ",";
|
|
$sql .= " metal_surcharge = " . (float) $this->metal_surcharge . ",";
|
|
$sql .= " vpe = " . ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
|
|
$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 .= " action_code = '" . $this->db->escape($this->action_code) . "',";
|
|
$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->price_unit_code = $articleData['price_unit_code'] ?? 0;
|
|
$article->price_type = $articleData['price_type'] ?? 1;
|
|
$article->metal_surcharge = $articleData['metal_surcharge'] ?? 0;
|
|
$article->vpe = $articleData['vpe'] ?? null;
|
|
$article->discount_group = $articleData['discount_group'] ?? '';
|
|
$article->product_group = $articleData['product_group'] ?? '';
|
|
$article->matchcode = $articleData['matchcode'] ?? '';
|
|
$article->datanorm_version = $parser->version;
|
|
$article->action_code = $articleData['action_code'] ?? 'N';
|
|
|
|
$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) {
|
|
$vpe = isset($articleData['vpe']) ? (int)$articleData['vpe'] : 'NULL';
|
|
$actionCode = $articleData['action_code'] ?? 'N';
|
|
$active = ($actionCode === 'L') ? 0 : 1; // Set active=0 for deleted articles
|
|
$values[] = sprintf(
|
|
"(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, %d, %d, %d, %f, %s, '%s', '%s', '%s', '%s', '%s', %d, '%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),
|
|
(int) ($articleData['price_unit_code'] ?? 0),
|
|
(int) ($articleData['price_type'] ?? 1),
|
|
(float) ($articleData['metal_surcharge'] ?? 0),
|
|
$vpe,
|
|
$db->escape($articleData['discount_group'] ?? ''),
|
|
$db->escape($articleData['product_group'] ?? ''),
|
|
$db->escape($articleData['matchcode'] ?? ''),
|
|
$db->escape($version),
|
|
$db->escape($actionCode),
|
|
$active,
|
|
$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, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group, matchcode, ";
|
|
$sql .= "datanorm_version, action_code, active, 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 .= "price_unit_code = VALUES(price_unit_code), ";
|
|
$sql .= "price_type = VALUES(price_type), ";
|
|
$sql .= "metal_surcharge = VALUES(metal_surcharge), ";
|
|
$sql .= "vpe = VALUES(vpe), ";
|
|
$sql .= "discount_group = VALUES(discount_group), ";
|
|
$sql .= "product_group = VALUES(product_group), ";
|
|
$sql .= "matchcode = VALUES(matchcode), ";
|
|
$sql .= "datanorm_version = VALUES(datanorm_version), ";
|
|
$sql .= "action_code = VALUES(action_code), ";
|
|
$sql .= "active = VALUES(active), ";
|
|
$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
|
|
// Use case-insensitive search for Linux compatibility
|
|
$priceFiles = array();
|
|
$allFiles = glob($directory . '/*');
|
|
foreach ($allFiles as $file) {
|
|
$basename = strtoupper(basename($file));
|
|
if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
|
|
$priceFiles[] = $file;
|
|
}
|
|
}
|
|
if (!empty($priceFiles)) {
|
|
foreach ($priceFiles as $file) {
|
|
$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
|
|
// Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;...
|
|
// PE is the price unit code from DATPREIS (may differ from A-record!)
|
|
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');
|
|
$datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // PE code from DATPREIS
|
|
$metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
|
|
$price = (float)$priceRaw / 100; // Convert cents to euros
|
|
$metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros
|
|
|
|
if (!empty($articleNumber) && $price > 0) {
|
|
$batch[$articleNumber] = array(
|
|
'price' => $price,
|
|
'metal_surcharge' => $metalSurcharge,
|
|
'datpreis_pe_code' => $datpreisPeCode
|
|
);
|
|
}
|
|
|
|
$i += 9; // 9 fields per article
|
|
}
|
|
} elseif ($recordType === 'P' || $recordType === '0') {
|
|
// Simple format: P;ArtNr;PreisKz;Preis;PE;...
|
|
$articleNumber = trim($parts[1] ?? '');
|
|
$priceRaw = trim($parts[3] ?? '0');
|
|
$datpreisPeCode = (int)trim($parts[4] ?? '0'); // PE code if available
|
|
|
|
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] = array(
|
|
'price' => $price,
|
|
'metal_surcharge' => 0,
|
|
'datpreis_pe_code' => $datpreisPeCode
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
* DATPREIS prices are already given for the A-Satz PE unit - no normalization needed!
|
|
*
|
|
* @param int $fk_soc Supplier ID
|
|
* @param array $prices Array of article_number => array('price' => ..., 'metal_surcharge' => ...)
|
|
* @return int Number of rows updated
|
|
*/
|
|
protected function flushPriceBatch($fk_soc, $prices)
|
|
{
|
|
global $conf;
|
|
|
|
if (empty($prices)) {
|
|
return 0;
|
|
}
|
|
|
|
$updated = 0;
|
|
|
|
// Build CASE statements for batch update
|
|
// Note: DATPREIS prices are already for the A-Satz PE unit, no normalization needed
|
|
$priceCases = array();
|
|
$metalCases = array();
|
|
$articleNumbers = array();
|
|
|
|
foreach ($prices as $artNum => $priceData) {
|
|
$priceCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['price'];
|
|
$metalCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['metal_surcharge'];
|
|
$articleNumbers[] = "'" . $this->db->escape($artNum) . "'";
|
|
}
|
|
|
|
if (!empty($priceCases)) {
|
|
$sql = "UPDATE " . MAIN_DB_PREFIX . "importzugferd_datanorm SET ";
|
|
$sql .= "price = CASE article_number ";
|
|
$sql .= implode(" ", $priceCases);
|
|
$sql .= " END, ";
|
|
$sql .= "metal_surcharge = CASE article_number ";
|
|
$sql .= implode(" ", $metalCases);
|
|
$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;
|
|
}
|
|
}
|