Compare commits
2 commits
59ce17b7b5
...
745fc68fc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 745fc68fc9 | |||
| 8b0d1830a3 |
10 changed files with 938 additions and 256 deletions
32
CHANGELOG.md
Normal file → Executable file
32
CHANGELOG.md
Normal file → Executable file
|
|
@ -2,6 +2,38 @@
|
|||
|
||||
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [3.8] - 2026-02-25
|
||||
|
||||
### Hinzugefügt
|
||||
- **Kabel-Preisberechnung**: Zentrale Funktion `calculateCablePricing()` für einheitliche Preislogik
|
||||
- **Kupfergehalt-Berechnung**: Automatische Berechnung aus Aderanzahl × Querschnitt × 8.9
|
||||
- **Ringgröße-Erkennung**: Unterstützt Ri100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m"
|
||||
- **Extrafield "produktpreis"**: Speichert reinen Materialpreis ohne Kupferzuschlag (nur Kabel)
|
||||
- **EAN-Auto-Update**: Barcodes aus ZUGFeRD-Rechnungen werden automatisch in Lieferantenpreise übernommen
|
||||
|
||||
### Verbessert
|
||||
- **Lieferanten-Formate**: Korrekte Unterscheidung zwischen Sonepar (price_unit=1, Ring im Namen) und Kluxen/Witte (price_unit=100)
|
||||
- **Cross-Catalog-Suche**: Nur noch über EAN, nicht mehr über Artikelnummern (verhindert Fehlzuordnungen)
|
||||
- **EAN-Barcode-Typ**: Automatische Erkennung (EAN8, EAN13, UPC-A) statt hardcoded EAN13
|
||||
- **Error-Handling**: Besseres Logging bei Extrafield-Fehlern
|
||||
|
||||
### Behoben
|
||||
- Division durch Null bei Preisberechnung abgesichert
|
||||
- Mindestbestellmenge und Verpackungseinheit werden von existierenden Lieferantenpreisen übernommen
|
||||
|
||||
## [3.7] - 2026-02-23
|
||||
|
||||
### Hinzugefügt
|
||||
- **GlobalNotify Integration**: Benachrichtigungen über das zentrale GlobalNotify-Modul
|
||||
- Import-Fehler: Warnung bei fehlgeschlagenen Importen
|
||||
- Rechnungen zur Prüfung: Aktion wenn neue Rechnungen warten
|
||||
- IMAP-Fehler: Warnung wenn E-Mail Postfach nicht erreichbar
|
||||
- Exception/Fatal: Sofortige Benachrichtigung bei Abstürzen
|
||||
- **Helper-Funktion**: `notify()` für sichere GlobalNotify-Nutzung mit Fallback
|
||||
|
||||
### Hinweis
|
||||
GlobalNotify ist optional. Ohne das Modul werden Benachrichtigungen ins Dolibarr-Log geschrieben.
|
||||
|
||||
## [3.6] - 2026-02-23
|
||||
|
||||
### Behoben
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -102,23 +102,29 @@ Available in:
|
|||
|
||||
## Version History
|
||||
|
||||
### 1.1
|
||||
- New persistent import workflow with database storage
|
||||
- Manual product assignment via dropdown
|
||||
- Product removal/reassignment
|
||||
- Status "Pending" for imports requiring manual intervention
|
||||
- Pending imports overview on upload page
|
||||
- UN/ECE unit code translation (C62 → Stk., MTR → m, etc.)
|
||||
- Batch import from folder or IMAP mailbox
|
||||
- IMAP connection test with folder selection
|
||||
- Product template feature (duplicate existing product)
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
||||
|
||||
### 3.8 (Current)
|
||||
- Improved cable pricing for different supplier formats (Sonepar vs Kluxen/Witte)
|
||||
- Automatic ring size detection from product names (Ri100, Tr500, etc.)
|
||||
- EAN auto-update from ZUGFeRD invoices with automatic barcode type detection
|
||||
- New extrafield "produktpreis" for material price without copper surcharge
|
||||
- Cross-catalog search now EAN-only (prevents mismatches)
|
||||
|
||||
### 3.7
|
||||
- GlobalNotify integration for import notifications
|
||||
|
||||
### 3.6
|
||||
- Cron job stability fixes and dedicated logging
|
||||
|
||||
### 3.5
|
||||
- Automatic cron import from watch folder and IMAP
|
||||
|
||||
### 3.0
|
||||
- Datanorm integration for article prices
|
||||
|
||||
### 1.0
|
||||
- Initial release
|
||||
- Basic ZUGFeRD/Factur-X import
|
||||
- Automatic product matching
|
||||
- Supplier detection
|
||||
- Duplicate detection
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -316,6 +316,32 @@ class ActionsImportZugferd
|
|||
$processed_line['product_ref'] = $product->ref;
|
||||
$processed_line['product_label'] = $product->label;
|
||||
}
|
||||
|
||||
// Update supplier price with EAN from invoice if empty
|
||||
$invoiceEan = !empty($line['product']['global_id']) ? trim($line['product']['global_id']) : '';
|
||||
$supplierRef = !empty($line['product']['seller_id']) ? $line['product']['seller_id'] : '';
|
||||
if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
|
||||
// Barcode-Typ basierend auf Länge bestimmen
|
||||
$eanLen = strlen($invoiceEan);
|
||||
if ($eanLen == 13) {
|
||||
$barcodeType = 2; // EAN13
|
||||
} elseif ($eanLen == 8) {
|
||||
$barcodeType = 1; // EAN8
|
||||
} elseif ($eanLen == 12) {
|
||||
$barcodeType = 3; // UPC-A
|
||||
} else {
|
||||
$barcodeType = 0; // Unbekannt
|
||||
}
|
||||
|
||||
$sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
|
||||
$sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
|
||||
$sqlEan .= " WHERE fk_product = " . (int)$match['fk_product'];
|
||||
$sqlEan .= " AND fk_soc = " . (int)$supplier_id;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
|
||||
$sqlEan .= " AND (barcode IS NULL OR barcode = '')";
|
||||
$this->db->query($sqlEan);
|
||||
}
|
||||
} else {
|
||||
$processed_line['needs_creation'] = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,57 @@ class CronImportZugferd
|
|||
*/
|
||||
private $startTime = 0;
|
||||
|
||||
/**
|
||||
* Send notification via GlobalNotify (if available)
|
||||
*
|
||||
* @param string $type 'error', 'warning', 'info', 'action'
|
||||
* @param string $title Title
|
||||
* @param string $message Message
|
||||
* @param string $actionUrl URL for action button
|
||||
* @param string $actionLabel Label for action button
|
||||
* @return bool True if sent via GlobalNotify
|
||||
*/
|
||||
protected function notify($type, $title, $message, $actionUrl = '', $actionLabel = '')
|
||||
{
|
||||
if (!isModEnabled('globalnotify')) {
|
||||
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
$classFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0);
|
||||
if (!file_exists($classFile)) {
|
||||
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
require_once $classFile;
|
||||
|
||||
if (!class_exists('GlobalNotify')) {
|
||||
dol_syslog("ImportZugferd [{$type}]: {$title} - {$message}", LOG_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($type) {
|
||||
case 'error':
|
||||
GlobalNotify::error('importzugferd', $title, $message, $actionUrl, $actionLabel);
|
||||
break;
|
||||
case 'warning':
|
||||
GlobalNotify::warning('importzugferd', $title, $message, $actionUrl, $actionLabel);
|
||||
break;
|
||||
case 'action':
|
||||
GlobalNotify::actionRequired('importzugferd', $title, $message, $actionUrl, $actionLabel ?: 'Aktion erforderlich');
|
||||
break;
|
||||
default:
|
||||
GlobalNotify::info('importzugferd', $title, $message, $actionUrl, $actionLabel);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
dol_syslog("GlobalNotify error: ".$e->getMessage(), LOG_ERR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
|
|
@ -226,17 +277,34 @@ class CronImportZugferd
|
|||
$this->cronLog("Completed: imported={$this->imported_count}, skipped={$this->skipped_count}, errors={$this->error_count}, duration={$duration}s");
|
||||
$this->cronLog("========== CRON END (success) ==========");
|
||||
|
||||
// Send GlobalNotify notifications
|
||||
$this->sendImportNotifications();
|
||||
|
||||
return ($this->error_count > 0) ? -1 : 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error = 'Exception: '.$e->getMessage();
|
||||
$this->cronLog("EXCEPTION: ".$e->getMessage()."\n".$e->getTraceAsString(), 'ERROR');
|
||||
$this->cronLog("========== CRON END (exception) ==========");
|
||||
$this->notify(
|
||||
'error',
|
||||
'ZUGFeRD Import fehlgeschlagen',
|
||||
'Exception: '.$e->getMessage(),
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'Einstellungen prüfen'
|
||||
);
|
||||
return -1;
|
||||
} catch (Throwable $t) {
|
||||
$this->error = 'Fatal: '.$t->getMessage();
|
||||
$this->cronLog("FATAL: ".$t->getMessage()."\n".$t->getTraceAsString(), 'ERROR');
|
||||
$this->cronLog("========== CRON END (fatal) ==========");
|
||||
$this->notify(
|
||||
'error',
|
||||
'ZUGFeRD Import Absturz',
|
||||
'Fatal: '.$t->getMessage(),
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'Einstellungen prüfen'
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -645,4 +713,79 @@ class CronImportZugferd
|
|||
dol_syslog("CronImportZugferd: Keeping error file in watch folder: " . $file, LOG_INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications based on import results
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function sendImportNotifications()
|
||||
{
|
||||
// Check for errors
|
||||
if ($this->error_count > 0) {
|
||||
$errorSummary = count($this->errors) > 0 ? implode(', ', array_slice($this->errors, 0, 3)) : 'Siehe Log';
|
||||
$this->notify(
|
||||
'warning',
|
||||
$this->error_count.' ZUGFeRD Import-Fehler',
|
||||
$errorSummary,
|
||||
dol_buildpath('/importzugferd/list.php?status=error', 1),
|
||||
'Fehler anzeigen'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for imported invoices that need review
|
||||
if ($this->imported_count > 0) {
|
||||
// Count pending invoices (drafts needing approval)
|
||||
$pendingCount = $this->countPendingInvoices();
|
||||
|
||||
if ($pendingCount > 0) {
|
||||
$this->notify(
|
||||
'action',
|
||||
$this->imported_count.' ZUGFeRD Rechnungen importiert',
|
||||
"{$pendingCount} Lieferantenrechnungen warten auf Prüfung und Freigabe",
|
||||
dol_buildpath('/fourn/facture/list.php?search_status=0', 1),
|
||||
'Rechnungen prüfen'
|
||||
);
|
||||
} else {
|
||||
// All auto-created and validated
|
||||
$this->notify(
|
||||
'info',
|
||||
$this->imported_count.' ZUGFeRD Rechnungen importiert',
|
||||
'Alle Rechnungen wurden erfolgreich verarbeitet',
|
||||
dol_buildpath('/fourn/facture/list.php', 1),
|
||||
'Anzeigen'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// IMAP connection issues
|
||||
if (strpos($this->error, 'IMAP connection failed') !== false) {
|
||||
$this->notify(
|
||||
'error',
|
||||
'IMAP Verbindung fehlgeschlagen',
|
||||
'E-Mail Postfach für ZUGFeRD-Import nicht erreichbar',
|
||||
dol_buildpath('/importzugferd/admin/setup.php', 1),
|
||||
'IMAP prüfen'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count pending (draft) supplier invoices
|
||||
*
|
||||
* @return int Number of draft supplier invoices
|
||||
*/
|
||||
protected function countPendingInvoices()
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_fourn";
|
||||
$sql .= " WHERE fk_statut = 0"; // Draft status
|
||||
$sql .= " AND entity IN (".getEntity('facture_fourn').")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
return (int) $obj->cnt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,9 +520,22 @@ class Datanorm extends CommonObject
|
|||
if ($result > 0) {
|
||||
$results[] = $this->toArray();
|
||||
$foundIds[$this->id] = true;
|
||||
// Store EAN and manufacturer_ref for cross-catalog search
|
||||
// Store EAN from Datanorm
|
||||
$foundEan = $this->ean;
|
||||
$foundManufacturerRef = $this->manufacturer_ref;
|
||||
|
||||
// If Datanorm has no EAN, try to get it from supplier price (barcode field)
|
||||
if (empty($foundEan)) {
|
||||
$sqlEan = "SELECT barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " WHERE fk_soc = " . (int)$fk_soc;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($article_number) . "'";
|
||||
$sqlEan .= " AND barcode IS NOT NULL AND barcode != ''";
|
||||
$sqlEan .= " LIMIT 1";
|
||||
$resEan = $this->db->query($sqlEan);
|
||||
if ($resEan && $this->db->num_rows($resEan) > 0) {
|
||||
$objEan = $this->db->fetch_object($resEan);
|
||||
$foundEan = $objEan->barcode;
|
||||
}
|
||||
}
|
||||
|
||||
// If not searching all catalogs, return immediately
|
||||
if (!$searchAll) {
|
||||
|
|
@ -531,24 +544,15 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
}
|
||||
|
||||
// If searchAll is enabled and we found article with EAN/manufacturer_ref,
|
||||
// search other catalogs using these identifiers (cross-catalog search)
|
||||
if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) {
|
||||
// If searchAll is enabled and we found article with EAN,
|
||||
// search other catalogs using EAN ONLY (cross-catalog search)
|
||||
// Note: Artikelnummern-Vergleich macht keinen Sinn über Kataloge hinweg
|
||||
if ($searchAll && $fk_soc > 0 && !empty($foundEan)) {
|
||||
$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 (";
|
||||
|
||||
$conditions = array();
|
||||
if (!empty($foundEan)) {
|
||||
$conditions[] = "ean = '" . $this->db->escape($foundEan) . "'";
|
||||
}
|
||||
if (!empty($foundManufacturerRef)) {
|
||||
$conditions[] = "manufacturer_ref = '" . $this->db->escape($foundManufacturerRef) . "'";
|
||||
}
|
||||
$sql .= implode(' OR ', $conditions) . ")";
|
||||
|
||||
$sql .= " WHERE ean = '" . $this->db->escape($foundEan) . "'";
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
$sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier
|
||||
|
|
@ -588,30 +592,18 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback: Search by partial match on article_number, ean, or manufacturer_ref
|
||||
// Fallback: Search by EXACT article number match for the specified supplier only
|
||||
// No LIKE search - cross-catalog comparisons only work via EAN
|
||||
if ($fk_soc > 0 && empty($results)) {
|
||||
$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) . "%')";
|
||||
$sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
|
||||
$sql .= " AND fk_soc = " . (int) $fk_soc;
|
||||
$sql .= " AND active = 1";
|
||||
$sql .= " AND entity = " . (int) $conf->entity;
|
||||
|
||||
if ($fk_soc > 0 && !$searchAll) {
|
||||
$sql .= " AND fk_soc = " . (int) $fk_soc;
|
||||
}
|
||||
|
||||
// ORDER BY clause
|
||||
if ($fk_soc > 0 && $searchAll) {
|
||||
// Order by matching supplier first, then by price
|
||||
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC";
|
||||
} else {
|
||||
$sql .= " ORDER BY article_number";
|
||||
}
|
||||
|
||||
$sql .= " LIMIT " . (int) $limit;
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
|
|
@ -638,6 +630,7 @@ class Datanorm extends CommonObject
|
|||
}
|
||||
$this->db->free($resql);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,6 +624,32 @@ class ZugferdImport extends CommonObject
|
|||
if (!empty($match) && $match['fk_product'] > 0) {
|
||||
$fk_product = $match['fk_product'];
|
||||
$match_method = $match['method'];
|
||||
|
||||
// Update supplier price with EAN from invoice if empty
|
||||
$invoiceEan = !empty($line_data['product']['global_id']) ? trim($line_data['product']['global_id']) : '';
|
||||
$supplierRef = !empty($line_data['product']['seller_id']) ? $line_data['product']['seller_id'] : '';
|
||||
if (!empty($invoiceEan) && !empty($supplierRef) && ctype_digit($invoiceEan)) {
|
||||
// Barcode-Typ basierend auf Länge bestimmen
|
||||
$eanLen = strlen($invoiceEan);
|
||||
if ($eanLen == 13) {
|
||||
$barcodeType = 2; // EAN13
|
||||
} elseif ($eanLen == 8) {
|
||||
$barcodeType = 1; // EAN8
|
||||
} elseif ($eanLen == 12) {
|
||||
$barcodeType = 3; // UPC-A
|
||||
} else {
|
||||
$barcodeType = 0; // Unbekannt
|
||||
}
|
||||
|
||||
$sqlEan = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price";
|
||||
$sqlEan .= " SET barcode = '" . $this->db->escape($invoiceEan) . "'";
|
||||
$sqlEan .= ", fk_barcode_type = " . (int)$barcodeType;
|
||||
$sqlEan .= " WHERE fk_product = " . (int)$fk_product;
|
||||
$sqlEan .= " AND fk_soc = " . (int)$supplier_id;
|
||||
$sqlEan .= " AND ref_fourn = '" . $this->db->escape($supplierRef) . "'";
|
||||
$sqlEan .= " AND (barcode IS NULL OR barcode = '')";
|
||||
$this->db->query($sqlEan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class modImportZugferd extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@importzugferd'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '3.6';
|
||||
$this->version = '3.8';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -616,6 +616,28 @@ class modImportZugferd extends DolibarrModules
|
|||
'isModEnabled("importzugferd")' // enabled condition
|
||||
);
|
||||
|
||||
// Add extrafield for product price without copper surcharge (only for cables)
|
||||
$extrafields->addExtraField(
|
||||
'produktpreis', // attribute code
|
||||
'Produktpreis', // label (translation key)
|
||||
'price', // type (price field)
|
||||
115, // position
|
||||
'24,8', // size
|
||||
'product_fournisseur_price', // element type
|
||||
0, // unique
|
||||
0, // required
|
||||
'', // default value
|
||||
'', // param
|
||||
1, // always editable
|
||||
'', // permission
|
||||
1, // list (show in list)
|
||||
0, // printable
|
||||
'', // totalizable
|
||||
'', // langfile
|
||||
'importzugferd@importzugferd', // module
|
||||
'isModEnabled("importzugferd")' // enabled condition
|
||||
);
|
||||
|
||||
// Add extrafield for price unit (Preiseinheit) on supplier prices
|
||||
$extrafields->addExtraField(
|
||||
'preiseinheit', // attribute code
|
||||
|
|
|
|||
780
import.php
780
import.php
File diff suppressed because it is too large
Load diff
|
|
@ -451,6 +451,8 @@ LabelChange = Namensänderung
|
|||
#
|
||||
Kupferzuschlag = Kupferzuschlag
|
||||
KupferzuschlagHelp = Metallzuschlag pro Einheit (wird aus Rechnungen extrahiert)
|
||||
Produktpreis = Produktpreis
|
||||
ProduktpreisHelp = Reiner Materialpreis ohne Kupferzuschlag (nur bei Kabeln)
|
||||
Preiseinheit = Preiseinheit
|
||||
PreiseinheitHelp = Anzahl Einheiten pro Preis (z.B. 100 = Preis pro 100 Stück)
|
||||
Warengruppe = Warengruppe
|
||||
|
|
@ -489,3 +491,7 @@ MoreExpensiveBy = %s%% teurer
|
|||
RefreshProductListHelp = Produktlisten neu laden (nach Anlage neuer Produkte)
|
||||
SelectAll = Alle auswählen
|
||||
DeselectAll = Keine auswählen
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Alle aufklappen
|
||||
CollapseAll = Alle zuklappen
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@ NotifyEmail = Recipient email
|
|||
#
|
||||
Kupferzuschlag = Copper Surcharge
|
||||
KupferzuschlagHelp = Metal surcharge per unit (extracted from invoices)
|
||||
Produktpreis = Material Price
|
||||
ProduktpreisHelp = Material price without copper surcharge (cables only)
|
||||
Preiseinheit = Price Unit
|
||||
PreiseinheitHelp = Number of units per price (e.g. 100 = price per 100 pieces)
|
||||
Warengruppe = Product Group
|
||||
|
|
@ -420,3 +422,7 @@ MoreExpensiveBy = %s%% more expensive
|
|||
RefreshProductListHelp = Refresh product lists (after creating new products)
|
||||
SelectAll = Select all
|
||||
DeselectAll = Deselect all
|
||||
|
||||
# UI Buttons
|
||||
ExpandAll = Expand all
|
||||
CollapseAll = Collapse all
|
||||
|
|
|
|||
Loading…
Reference in a new issue