Compare commits

...

7 commits

Author SHA1 Message Date
Jan Böhmer
753ecee849 Merge remote-tracking branch 'origin/master'
Some checks failed
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/amd64, amd64, ubuntu-latest) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm/v7, armv7, ubuntu-24.04-arm) (push) Has been cancelled
Docker Image Build (FrankenPHP) / build (linux/arm64, arm64, ubuntu-24.04-arm) (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.5, sqlite) (push) Has been cancelled
Docker Image Build / merge (push) Has been cancelled
Docker Image Build (FrankenPHP) / merge (push) Has been cancelled
2026-03-15 22:09:22 +01:00
Jan Böhmer
8f6ed74d93 Bumped version to 2.9.1 2026-03-15 22:09:10 +01:00
Jan Böhmer
17f11c02f3
New Crowdin updates (#1301)
* New translations validators.en.xlf (Chinese Simplified)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (German)
2026-03-15 22:08:38 +01:00
Jan Böhmer
a070ebb2ce Fixed 500 error with displaying part prices, when a user has a currency preference different of base currency, and there is no conversion rate known for it
This fixes issue #1317
2026-03-15 22:02:10 +01:00
Jan Böhmer
44bb132de1 Merge remote-tracking branch 'origin/master' 2026-03-15 21:47:21 +01:00
Jan Böhmer
95f3fc66c2 Do not throw an 500 error, if mapping is not possible
This fixes issue #1298
2026-03-15 21:47:15 +01:00
Jan Böhmer
74e5102943 Automatically detect the delimiter of generic BOM imports
The detectFields does this anyway, so use that guessed value further on
2026-03-15 21:35:38 +01:00
8 changed files with 90 additions and 53 deletions

View file

@ -1 +1 @@
2.9.0 2.9.1

View file

@ -240,7 +240,8 @@ class ProjectController extends AbstractController
} }
// Detect fields and get suggestions // Detect fields and get suggestions
$detected_fields = $BOMImporter->detectFields($file_content); $detected_delimiter = $BOMImporter->detectDelimiter($file_content);
$detected_fields = $BOMImporter->detectFields($file_content, $detected_delimiter);
$suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields); $suggested_mapping = $BOMImporter->getSuggestedFieldMapping($detected_fields);
// Create mapping of original field names to sanitized field names for template // Create mapping of original field names to sanitized field names for template
@ -257,7 +258,7 @@ class ProjectController extends AbstractController
$builder->add('delimiter', ChoiceType::class, [ $builder->add('delimiter', ChoiceType::class, [
'label' => 'project.bom_import.delimiter', 'label' => 'project.bom_import.delimiter',
'required' => true, 'required' => true,
'data' => ',', 'data' => $detected_delimiter,
'choices' => [ 'choices' => [
'project.bom_import.delimiter.comma' => ',', 'project.bom_import.delimiter.comma' => ',',
'project.bom_import.delimiter.semicolon' => ';', 'project.bom_import.delimiter.semicolon' => ';',

View file

@ -721,26 +721,36 @@ class BOMImporter
return $mapped; return $mapped;
} }
/**
* Try to detect the separator used in the CSV data by analyzing the first line and counting occurrences of common delimiters.
* @param string $data
* @return string
*/
public function detectDelimiter(string $data): string
{
$delimiters = [',', ';', "\t"];
$lines = explode("\n", $data, 2);
$header_line = $lines[0] ?? '';
$delimiter_counts = [];
foreach ($delimiters as $delim) {
$delimiter_counts[$delim] = substr_count($header_line, $delim);
}
// Choose the delimiter with the highest count, default to comma if all are zero
$max_count = max($delimiter_counts);
$delimiter = array_search($max_count, $delimiter_counts, true);
if ($max_count === 0 || $delimiter === false) {
$delimiter = ',';
}
return $delimiter;
}
/** /**
* Detect available fields in CSV data for field mapping UI * Detect available fields in CSV data for field mapping UI
*/ */
public function detectFields(string $data, ?string $delimiter = null): array public function detectFields(string $data, ?string $delimiter = null): array
{ {
if ($delimiter === null) { if ($delimiter === null) {
// Detect delimiter by counting occurrences in the first row (header) $delimiter = $this->detectDelimiter($data);
$delimiters = [',', ';', "\t"];
$lines = explode("\n", $data, 2);
$header_line = $lines[0] ?? '';
$delimiter_counts = [];
foreach ($delimiters as $delim) {
$delimiter_counts[$delim] = substr_count($header_line, $delim);
}
// Choose the delimiter with the highest count, default to comma if all are zero
$max_count = max($delimiter_counts);
$delimiter = array_search($max_count, $delimiter_counts, true);
if ($max_count === 0 || $delimiter === false) {
$delimiter = ',';
}
} }
// Handle potential BOM (Byte Order Mark) at the beginning // Handle potential BOM (Byte Order Mark) at the beginning
$data = preg_replace('/^\xEF\xBB\xBF/', '', $data); $data = preg_replace('/^\xEF\xBB\xBF/', '', $data);

View file

@ -47,17 +47,17 @@
</td> </td>
<td> <td>
{{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }}
{% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %} {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) %}
{% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %}
<span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }})</span> <span class="text-muted">({{ tmp | format_money(app.user.currency ?? null) }})</span>
{% endif %} {% endif %}
<small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small> <small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small>
</td> </td>
<td> <td>
{{ detail.PricePerUnit | format_money(detail.currency) }} {{ detail.PricePerUnit | format_money(detail.currency) }}
{% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %} {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) %}
{% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %}
<span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }})</span> <span class="text-muted">({{ tmp | format_money(app.user.currency ?? null) }})</span>
{% endif %} {% endif %}
<small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small> <small class="text-muted">{{- helper.vat_text(detail.includesVAT) -}}</small>
</td> </td>

View file

@ -48,51 +48,59 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th>
<th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th> <th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for field in detected_fields %} {% for field in detected_fields %}
<tr> <tr>
<td> <td>
<code>{{ field }}</code> <code>{{ field }}</code>
</td> </td>
<td> <td>
{# TODO: This is more a workaround than a proper fix. Ideally the controller should be fixed in a way, that we get the correct fields here #}
{% if field_name_mapping[field] is defined %}
{% set field_name = field_name_mapping[field] %}
{{ form_widget(form['mapping_' ~ field_name_mapping[field]], { {{ form_widget(form['mapping_' ~ field_name_mapping[field]], {
'attr': { 'attr': {
'class': 'form-select field-mapping-select', 'class': 'form-select field-mapping-select',
'data-field': field 'data-field': field
} }
}) }} }) }}
</td> {% else %}
<td> <b class="text-danger">
{% if suggested_mapping[field] is defined %} {% trans %}project.bom_import.field_mapping.error.check_delimiter{% endtrans %}
<span class="badge bg-success"> </b>
{% endif %}
</td>
<td>
{% if suggested_mapping[field] is defined %}
<span class="badge bg-success">
<i class="fa-solid fa-magic fa-fw"></i> <i class="fa-solid fa-magic fa-fw"></i>
{{ suggested_mapping[field] }} {{ suggested_mapping[field] }}
</span> </span>
{% else %} {% else %}
<span class="text-muted"> <span class="text-muted">
<i class="fa-solid fa-question fa-fw"></i> <i class="fa-solid fa-question fa-fw"></i>
{% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %} {% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %}
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<input type="number" <input type="number"
class="form-control form-control-sm priority-input" class="form-control form-control-sm priority-input"
min="1" min="1"
value="10" value="10"
style="width: 80px;" style="width: 80px;"
data-field="{{ field }}" data-field="{{ field }}"
title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}"> title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}">
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -12945,5 +12945,11 @@ Buerklin-API-Authentication-Server:
<target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target> <target>[Part_lot] aus Barcode erstellt: Bitte überprüfen Sie, ob die Daten korrekt und gewünscht sind.</target>
</segment> </segment>
</unit> </unit>
<unit id="F8pQuL9" name="project.bom_import.field_mapping.error.check_delimiter">
<segment state="translated">
<source>project.bom_import.field_mapping.error.check_delimiter</source>
<target>Zuordnungsfehler: Bitte prüfen Sie, ob Sie das richtige Trennzeichen ausgewählt haben!</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12947,5 +12947,11 @@ Buerklin-API Authentication server:
<target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target> <target>[Part_lot] created from barcode: Please check if the data is correct and desired.</target>
</segment> </segment>
</unit> </unit>
<unit id="F8pQuL9" name="project.bom_import.field_mapping.error.check_delimiter">
<segment state="translated">
<source>project.bom_import.field_mapping.error.check_delimiter</source>
<target>Mapping error: Check if you have selected the right delimiter!</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -247,5 +247,11 @@
<target>该类型在此语言下已存在翻译定义!</target> <target>该类型在此语言下已存在翻译定义!</target>
</segment> </segment>
</unit> </unit>
<unit id="zT_j_oQ" name="validator.invalid_gtin">
<segment state="translated">
<source>validator.invalid_gtin</source>
<target>无效的GTIN / EAN 码。</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>