Hallo,
da wir gerade mit Dolibarr (v23) angefangen haben, wollten wir unsere alten Lieferungen von zwei Artikeln mit Seriennummer importieren.
Herausgekommen ist ein Python-Script, das eine CSV-Datei mit folgenden Spalten entgegennimmt: Kunden-ID, Lieferscheinname, Seriennummer Artikel 1, Seriennummer Artikel 2.
#!/usr/bin/env python3
„“"
Dolibarr Historischer Lieferschein-Import
Liest die CSV und legt pro Zeile folgendes an:
- Seriennummern (llx_product_lot) für Produkt 1 (hw_id) und Produkt 2 (stick_id)
- Lieferschein-Kopf (llx_expedition) mit Kundennummer und Lieferscheinnummer
- Kundenauftrag (llx_commande) mit Auftragspositionen (llx_commandedet)
- Lieferschein-Positionen (llx_expeditiondet) je Produkt
- Lagerbewegungen (llx_product_stock_movement) als Ausgang
Konfiguration anpassen, dann ausführen:
python3 dolibarr_import.py --dry-run # Nur prüfen, nichts schreiben
python3 dolibarr_import.py # Echter Import
„“"
import csv
import argparse
import sys
from datetime import datetime
try:
import mysql.connector
except ImportError:
print(„Bitte mysql-connector-python installieren: pip install mysql-connector-python“)
sys.exit(1)
============================================================
KONFIGURATION – hier anpassen
============================================================
DB_CONFIG = {
„host“: „localhost“,
„port“: 3306,
„user“: „doliuser“,
„password“: „dolipass“,
„database“: „dolibarr“,
}
CSV_FILE = „doliimport-1.csv“
ENTITY = 1 # Dolibarr-Entity (fast immer 1)
FK_USER = 1 # Dolibarr-User-ID für den Import (rowid aus llx_user)
WAREHOUSE_ID = 1 # Lager-ID (rowid aus llx_entrepot)
PRODUCT_ID_HW = 24 # Produkt-ID für hw_id (rowid aus llx_product)
PRODUCT_ID_STICK= 28 # Produkt-ID für stick_id (rowid aus llx_product)
DELIVERY_DATE = „2024-01-01“ # Historisches Lieferdatum (YYYY-MM-DD)
============================================================
def get_or_create_lot(cursor, product_id, serial, entity, dry_run):
„“„Seriennummer in llx_product_lot anlegen, falls nicht vorhanden.“„“
cursor.execute(
„SELECT rowid FROM llx_product_lot WHERE fk_product = %s AND batch = %s AND entity = %s“,
(product_id, serial, entity)
)
row = cursor.fetchone()
if row:
return row[0]
if dry_run:
print(f" [DRY] Neue Seriennummer: Produkt {product_id} / {serial}")
return -1
cursor.execute(
"""INSERT INTO llx_product_lot
(fk_product, batch, entity, tms, datec)
VALUES (%s, %s, %s, NOW(), NOW())""",
(product_id, serial, entity)
)
return cursor.lastrowid
def create_expedition(cursor, fk_soc, ref, delivery_date, entity, fk_user, dry_run):
„“„Lieferschein-Kopf in llx_expedition anlegen.“„“
if dry_run:
print(f" [DRY] Neuer Lieferschein: {ref} für Kunde {fk_soc}")
return -1
cursor.execute(
"""INSERT INTO llx_expedition
(ref, fk_soc, date_delivery, fk_statut, entity, fk_user_author, date_creation, tms)
VALUES (%s, %s, %s, 2, %s, %s, NOW(), NOW())""",
(ref, fk_soc, delivery_date, entity, fk_user)
)
return cursor.lastrowid
def create_commande(cursor, fk_soc, ref, entity, fk_user, dry_run):
„“„Kundenauftrag in llx_commande anlegen.“„“
if dry_run:
print(f" [DRY] Neuer Auftrag: {ref} für Kunde {fk_soc}")
return -1
cursor.execute(
"""INSERT INTO llx_commande
(ref, ref_client, fk_soc, entity, fk_user_author, date_creation, tms)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())""",
(ref, ref, fk_soc, entity, fk_user)
)
return cursor.lastrowid
def create_element_element(cursor, cmd_id, exp_id, dry_run):
„“„Verknüpfung Auftrag ↔ Lieferschein in llx_element_element anlegen.“„“
if dry_run:
print(f" [DRY] Verknüpfung: Auftrag {cmd_id} → Lieferschein {exp_id}")
return
cursor.execute(
"""INSERT INTO llx_element_element
(fk_source, sourcetype, fk_target, targettype)
VALUES (%s, 'commande', %s, 'shipping')""",
(cmd_id, exp_id)
)
def create_commandedet(cursor, fk_commande, fk_product, rang, dry_run):
„“„Auftragsposition in llx_commandedet anlegen.“„“
if dry_run:
print(f" [DRY] Auftragsposition: Auftrag {fk_commande} / Produkt {fk_product} / Rang {rang}")
return -1
cursor.execute(
"SELECT description FROM llx_product WHERE rowid = %s",
(fk_product,)
)
row = cursor.fetchone()
description = row[0] if row and row[0] else ""
cursor.execute(
"""INSERT INTO llx_commandedet
(fk_commande, fk_product, rang, description)
VALUES (%s, %s, %s, %s)""",
(fk_commande, fk_product, rang, description)
)
return cursor.lastrowid
def create_expedition_detail(cursor, fk_expedition, fk_product, lot_id, serial, stock_id, commandedet_id, rang, dry_run):
„“„Lieferschein-Position in llx_expeditiondet anlegen.“„“
if dry_run:
print(f" [DRY] Position: Expedition {fk_expedition} / Produkt {fk_product} / {serial} / Rang {rang}")
return
cursor.execute(
"""INSERT INTO llx_expeditiondet
(fk_expedition, fk_product, fk_entrepot, qty, rang, fk_elementdet)
VALUES (%s, %s, %s, 1, %s, %s)""",
(fk_expedition, fk_product, WAREHOUSE_ID, rang, commandedet_id)
)
det_id = cursor.lastrowid
# Seriennummer zur Position verknüpfen
if lot_id and lot_id > 0:
cursor.execute(
"""INSERT INTO llx_expeditiondet_batch
(fk_expeditiondet, batch, qty, fk_origin_stock, fk_warehouse)
VALUES (%s, %s, 1, %s, %s)""",
(det_id, serial, stock_id, WAREHOUSE_ID)
)
def create_stock_mouvement(cursor, fk_product, serial, lot_id, warehouse_id, fk_user, exp_id, dry_run):
„“„Zuführung und Auslieferung in llx_stock_mouvement anlegen.“„“
if dry_run:
print(f" [DRY] Lagerbewegung: Produkt {fk_product} / {serial}")
return -1
# Zuführung
cursor.execute(
"""INSERT INTO llx_stock_mouvement
(fk_product, fk_entrepot, value, type_mouvement, fk_origin, batch, datem, fk_user_author)
VALUES (%s, %s, 1, 0, 0, %s, NOW(), %s)""",
(fk_product, warehouse_id, serial, fk_user)
)
# Auslieferung
cursor.execute(
"""INSERT INTO llx_stock_mouvement
(fk_product, fk_entrepot, value, type_mouvement, fk_origin, origintype, batch, datem, fk_user_author)
VALUES (%s, %s, -1, 2, %s, 'shipping', %s, NOW(), %s)""",
(fk_product, warehouse_id, exp_id, serial, fk_user)
)
return cursor.lastrowid
def main():
parser = argparse.ArgumentParser()
parser.add_argument(„–dry-run“, action=„store_true“, help=„Nur simulieren, nichts in DB schreiben“)
args = parser.parse_args()
dry_run = args.dry_run
if dry_run:
print("=== DRY-RUN MODUS – keine Datenbankänderungen ===\n")
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()
delivery_date = datetime.strptime(DELIVERY_DATE, "%Y-%m-%d").date()
errors = []
count = 0
with open(CSV_FILE, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for lineno, row in enumerate(reader, start=2):
fk_soc = row["location_name"].strip()
ref = row["internal_id"].strip()
hw_id = row["hw_id"].strip()
stick_id = row["stick_id"].strip()
print(f"Zeile {lineno}: {ref} (Kunde {fk_soc})")
try:
# Seriennummern anlegen
lot_hw = get_or_create_lot(cursor, PRODUCT_ID_HW, hw_id, ENTITY, dry_run)
lot_stick = get_or_create_lot(cursor, PRODUCT_ID_STICK, stick_id, ENTITY, dry_run)
# Auftrag anlegen
cmd_id = create_commande(cursor, fk_soc, ref, ENTITY, FK_USER, dry_run)
cmd_det_hw = create_commandedet(cursor, cmd_id, PRODUCT_ID_HW, 1, dry_run)
cmd_det_stick = create_commandedet(cursor, cmd_id, PRODUCT_ID_STICK, 2, dry_run)
# Lieferschein anlegen
exp_id = create_expedition(cursor, fk_soc, ref, delivery_date, ENTITY, FK_USER, dry_run)
create_element_element(cursor, cmd_id, exp_id, dry_run)
# Lagerbewegungen (zuerst, damit wir die rowid für fk_origin_stock haben)
stock_id_hw = create_stock_mouvement(cursor, PRODUCT_ID_HW, hw_id, lot_hw, WAREHOUSE_ID, FK_USER, exp_id, dry_run)
stock_id_stick = create_stock_mouvement(cursor, PRODUCT_ID_STICK, stick_id, lot_stick, WAREHOUSE_ID, FK_USER, exp_id, dry_run)
# Positionen anlegen (rang 1 = hw_id, rang 2 = stick_id)
create_expedition_detail(cursor, exp_id, PRODUCT_ID_HW, lot_hw, hw_id, stock_id_hw, cmd_det_hw, 1, dry_run)
create_expedition_detail(cursor, exp_id, PRODUCT_ID_STICK, lot_stick, stick_id, stock_id_stick, cmd_det_stick, 2, dry_run)
if not dry_run:
conn.commit()
count += 1
except Exception as e:
conn.rollback()
msg = f"Zeile {lineno} ({ref}): FEHLER – {e}"
print(f" ❌ {msg}")
errors.append(msg)
cursor.close()
conn.close()
print(f"\n{'='*50}")
print(f"Verarbeitet: {count} Datensätze")
if errors:
print(f"Fehler ({len(errors)}):")
for e in errors:
print(f" - {e}")
else:
print("Keine Fehler.")
if name == „main“:
main()




