Assemblies einführen

This commit is contained in:
Marcel Diegelmann 2025-03-19 08:13:45 +01:00
parent e1418dfdc1
commit 6fa960df42
107 changed files with 14101 additions and 96 deletions

View file

@ -0,0 +1,62 @@
{% extends "admin/base_admin.html.twig" %}
{# @var entity App\Entity\AssemblySystem\Assembly #}
{% block card_title %}
<i class="fas fa-archive fa-fw"></i> {% trans %}assembly.caption{% endtrans %}
{% endblock %}
{% block edit_title %}
{% trans %}assembly.edit{% endtrans %}: {{ entity.name }}
{% endblock %}
{% block new_title %}
{% trans %}assembly.new{% endtrans %}
{% endblock %}
{% block additional_pills %}
<li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#bom">BOM</a></li>
{% endblock %}
{% block quick_links %}
<div class="btn-toolbar" style="display: inline-block;">
<div class="btn-group">
<a class="btn btn-outline-secondary" href="{{ entity_url(entity) }}"><i class="fas fa-eye fa-fw"></i></a>
</div>
</div>
{% endblock %}
{% block additional_controls %}
{{ form_row(form.description) }}
{{ form_row(form.status) }}
{% if entity.id %}
<div class="mb-2 row">
<label class="col-form-label col-sm-3">{% trans %}assembly.edit.associated_build_part{% endtrans %}</label>
<div class="col-sm-9">
{% if entity.buildPart %}
<span class="form-control-static"><a href="{{ entity_url(entity.buildPart) }}">{{ entity.buildPart.name }}</a></span>
{% else %}
<a href="{{ path('part_new_build_part_assembly', {"assembly_id": entity.id , "_redirect": uri_without_host(app.request)}) }}"
class="btn btn-outline-success">{% trans %}assembly.edit.associated_build_part.add{% endtrans %}</a>
{% endif %}
<p class="text-muted">{% trans %}assembly.edit.associated_build.hint{% endtrans %}</p>
</div>
</div>
{% endif %}
{% endblock %}
{% block additional_panes %}
<div class="tab-pane" id="bom">
{% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %}
{{ form_errors(form.bom_entries) }}
{{ form_widget(form.bom_entries) }}
{% if entity.id %}
<a href="{{ path('assembly_import_bom', {'id': entity.id}) }}" class="btn btn-secondary mb-2"
{% if not is_granted('edit', entity) %}disabled="disabled"{% endif %}>
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}assembly.edit.bom.import_bom{% endtrans %}
</a>
{% endif %}
</div>
{% endblock %}

View file

@ -36,7 +36,7 @@
{% if entity.buildPart %}
<span class="form-control-static"><a href="{{ entity_url(entity.buildPart) }}">{{ entity.buildPart.name }}</a></span>
{% else %}
<a href="{{ path('part_new_build_part', {"project_id": entity.id , "_redirect": uri_without_host(app.request)}) }}"
<a href="{{ path('part_new_build_part_project', {"project_id": entity.id , "_redirect": uri_without_host(app.request)}) }}"
class="btn btn-outline-success">{% trans %}project.edit.associated_build_part.add{% endtrans %}</a>
{% endif %}
<p class="text-muted">{% trans %}project.edit.associated_build.hint{% endtrans %}</p>

View file

@ -0,0 +1,22 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}assembly.add_parts_to_assembly{% endtrans %}{% endblock %}
{% block card_title %}
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
{% trans %}assembly.add_parts_to_assembly{% endtrans %}{% if assembly %}: <i>{{ assembly.name }}</i>{% endif %}
{% endblock %}
{% block card_content %}
{{ form_start(form) }}
{{ form_row(form.assembly) }}
{% form_theme form.bom_entries with ['form/collection_types_layout_assembly.html.twig'] %}
{{ form_widget(form.bom_entries) }}
{{ form_row(form.submit) }}
{{ form_end(form) }}
{% endblock %}

View file

@ -0,0 +1,88 @@
{% import "helper.twig" as helper %}
{{ form_start(form) }}
<table class="table table-sm table-responsive table-hover">
<thead>
<tr>
<th>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" data-multicheck-name="lots_check" {{ stimulus_controller('pages/checkbox_multicheck') }}>
</div>
</th>
<th>{% trans %}part.table.name{% endtrans %}</th>
<th>{% trans %}assembly.bom.mountnames{% endtrans %}</th>
<th class="text-end">{% trans %}assembly.build.required_qty{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for bom_entry in build_request.bomEntries %}
{# 1st row basic infos about the BOM entry #}
<tr class="{% if bom_entry.part is null or buildHelper.bOMEntryBuildable(bom_entry, number_of_builds) %}table-primary{% else %}table-danger{% endif %}">
<td style="width: 20px;">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" data-multicheck-name="lots_check" required>
{# <label class="form-check-label" for="checkbox_{{ loop.index }}"> #}
</div>
</td>
<td >
{% if bom_entry.part %}
<b><a target="_blank" href="{{ entity_url(bom_entry.part) }}">{{ bom_entry.part.name }}</a></b> {% if bom_entry.name %}({{ bom_entry.name }}){% endif %}
{% else %}
<b>{{ bom_entry.name }}</b>
{% endif %}
</td>
<td>
{% for tag in bom_entry.mountnames|split(',') %}
<span class="badge bg-secondary badge-secondary" >{{ tag | trim }}</span>
{% endfor %}
</td>
<td class="text-end">
<b>{{ build_request.neededAmountForBOMEntry(bom_entry) | format_amount(bom_entry.part.partUnit ?? null) }}</b> {% trans %}assembly.builds.needed{% endtrans %}
(= {{ number_of_builds }} x {{ bom_entry.quantity | format_amount(bom_entry.part.partUnit ?? null) }})
</td>
</tr>
<tr>
<td colspan="4">
{% set lots = build_request.partLotsForBOMEntry(bom_entry) %}
{% if lots is not null %}
{% for lot in lots %}
{# @var lot \App\Entity\Parts\PartLot #}
<div class="mb-2 row">
<label class="col-form-label col-sm-4" for="category_admin_form_parent">
{% if lot.storageLocation %}
<small>{{ helper.structural_entity_link(lot.storageLocation) }}</small>
{% endif %}
{% if lot.name is not empty %}
(<small>{{ lot.name }}</small>)
{% endif %}
</label>
<div class="col-sm-6">
{{ form_errors(form["lot_"~lot.id]) }}
{{ form_widget(form["lot_"~lot.id]) }}
</div>
<div class="col-sm-2 mt-1 text-end">
/ <b>{{ lot.amount | format_amount(lot.part.partUnit) }}</b> {% trans %}assembly.builds.stocked{% endtrans %}
</div>
</div>
{% endfor %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ form_row(form.comment) }}
<div {{ stimulus_controller('pages/dont_check_quantity_checkbox') }}>
{{ form_row(form.dontCheckQuantity) }}
</div>
{{ form_row(form.addBuildsToBuildsPart) }}
{% if form.buildsPartLot is defined %}
{{ form_row(form.buildsPartLot) }}
{% endif %}
{{ form_row(form.submit) }}
{{ form_end(form) }}

View file

@ -0,0 +1,40 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}assembly.info.builds.label{% endtrans %}: {{ number_of_builds }}x {{ assembly.name }}{% endblock %}
{% block card_title %}
<i class="fa-solid fa-bolt fa-fw"></i>
{% trans %}assembly.info.builds.label{% endtrans %}: <b>{{ number_of_builds }}x</b> <i>{{ assembly.name }}</i>
{% endblock %}
{% block card_content %}
{% set can_build = buildHelper.assemblyBuildable(assembly, number_of_builds) %}
{% import "components/assemblies.macro.html.twig" as assembly_macros %}
{% if assembly.status is not empty and assembly.status != "in_production" %}
<div class="alert alert-warning" role="alert">
<i class="fa-solid fa-triangle-exclamation fa-fw"></i> {% trans with {"%assembly_status%": ('assembly.status.'~assembly.status)|trans } %}assembly.builds.check_assembly_status{% endtrans %}
</div>
{% endif %}
<div class="alert {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}assembly.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans with {"%number_of_builds%": number_of_builds} %}assembly.builds.following_bom_entries_miss_instock_n{% endtrans %}</b>
<ul>
{% for bom_entry in buildHelper.nonBuildableAssemblyBomEntries(assembly, number_of_builds) %}
<li>{{ assembly_macros.assembly_bom_entry_with_missing_instock(bom_entry, number_of_builds) }}</li>
{% endfor %}
</ul>
{% else %}
<h5><i class="fa-solid fa-circle-check fa-fw"></i> {% trans %}assembly.builds.build_possible{% endtrans %}</h5>
<span>{% trans with {"%max_builds%": number_of_builds} %}assembly.builds.number_of_builds_possible{% endtrans %}</span>
{% endif %}
</div>
<p class="text-muted">{% trans %}assembly.build.help{% endtrans %}</p>
{% include 'assemblies/build/_form.html.twig' %}
{% endblock %}

View file

@ -0,0 +1,60 @@
{% extends "main_card.html.twig" %}
{% block title %}{% trans %}assembly.import_bom{% endtrans %}{% endblock %}
{% block before_card %}
{% if errors %}
<div class="alert alert-danger">
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}parts.import.errors.title{% endtrans %}</h4>
<ul>
{% for violation in errors %}
<li>
<b>{{ violation.propertyPath }}: </b>
{{ violation.message|trans(violation.parameters, 'validators') }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block card_title %}
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}assembly.import_bom{% endtrans %}{% if assembly %}: <i>{{ assembly.name }}</i>{% endif %}
{% endblock %}
{% block card_content %}
{{ form(form) }}
{% endblock %}
{% block additional_content %}
<div class="container-fluid row d-flex align-items-stretch pe-0 me-0">
<div class="col-md-12 col-lg-6 ps-0 mt-3 d-flex">
<div class="card border-secondary flex-grow-1 overflow-auto">
<div class="card-header bg-secondary text-white">
{% trans %}assembly.import_bom.template.header.json{% endtrans %}
</div>
<div class="card-body">
<pre>{{ jsonTemplate|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_UNICODE')) }}</pre>
{{ 'assembly.bom_import.template.json.table'|trans|raw }}
</div>
</div>
</div>
<div class="col-md-12 col-lg-6 ps-0 mt-3 d-flex overflow-auto">
<div class="card border-secondary flex-grow-1 overflow-auto">
<div class="card-header bg-secondary text-white">
{% trans %}assembly.import_bom.template.header.kicad_pcbnew{% endtrans %}
</div>
<div class="card-body">
{{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns'|trans }}
<pre>Id;Designator;Package;Quantity;Designation;Supplier and ref</pre>
{{ 'assembly.bom_import.template.kicad_pcbnew.exptected_columns.note'|trans|raw }}
{{ 'assembly.bom_import.template.json.table'|trans|raw }}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% import "components/datatables.macro.html.twig" as datatables %}
<div class="btn-group mb-2 mt-2">
<a class="btn btn-success" {% if not is_granted('@assemblies.edit') %}disabled{% endif %}
href="{{ path('assembly_add_parts', {"id": assembly.id, "_redirect": uri_without_host(app.request)}) }}">
<i class="fa-solid fa-square-plus fa-fw"></i>
{% trans %}assembly.info.bom_add_parts{% endtrans %}
</a>
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{{ path('assembly_import_bom', {'id': assembly.id}) }}" {% if not is_granted('edit', assembly) %}disabled="disabled"{% endif %}>
<i class="fa-solid fa-file-import fa-fw"></i>
{% trans %}assembly.edit.bom.import_bom{% endtrans %}
</a>
</li>
</ul>
</div>
{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'assemblies') }}

View file

@ -0,0 +1,40 @@
{% set can_build = buildHelper.assemblyBuildable(assembly) %}
{% import "components/assemblies.macro.html.twig" as assembly_macros %}
{% if assembly.status is not empty and assembly.status != "in_production" %}
<div class="alert mt-2 alert-warning" role="alert">
<i class="fa-solid fa-triangle-exclamation fa-fw"></i> {% trans with {"%assembly_status%": ('assembly.status.'~assembly.status)|trans } %}assembly.builds.check_assembly_status{% endtrans %}
</div>
{% endif %}
<div class="alert mt-2 {% if can_build %}alert-success{% else %}alert-danger{% endif %}" role="alert">
{% if not can_build %}
<h5><i class="fa-solid fa-circle-exclamation fa-fw"></i> {% trans %}assembly.builds.build_not_possible{% endtrans %}</h5>
<b>{% trans %}assembly.builds.following_bom_entries_miss_instock{% endtrans %}</b>
<ul>
{% for bom_entry in buildHelper.nonBuildableAssemblyBomEntries(assembly) %}
<li>{{ assembly_macros.assembly_bom_entry_with_missing_instock(bom_entry) }}</li>
{% endfor %}
</ul>
{% else %}
<h5><i class="fa-solid fa-circle-check fa-fw"></i> {% trans %}assembly.builds.build_possible{% endtrans %}</h5>
<span>{% trans with {"%max_builds%": buildHelper.maximumBuildableCount(assembly)} %}assembly.builds.number_of_builds_possible{% endtrans %}</span>
{% endif %}
</div>
<form method="get" action="{{ path('assembly_build', {"id": assembly.iD }) }}">
<div class="row mt-2">
<div class="col-4">
<div class="input-group mb-3">
<input type="number" min="1" class="form-control" placeholder="{% trans %}assembly.builds.number_of_builds{% endtrans %}" name="n" required>
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<button class="btn btn-outline-secondary" type="submit" id="button-addon2">{% trans %}assembly.build.btn_build{% endtrans %}</button>
</div>
</div>
</div>
</form>
{% if assembly.buildPart %}
<p><b>{% trans %}assembly.builds.no_stocked_builds{% endtrans %}:</b> <a href="{{ entity_url(assembly.buildPart) }}">{{ assembly.buildPart.amountSum }}</a></p>
{% endif %}

View file

@ -0,0 +1,77 @@
{% import "helper.twig" as helper %}
<div class="row mt-2">
<div class="col-md-8">
<div class="row">
<div class="col-md-3 col-lg-4 col-4 mt-auto mb-auto">
{% if assembly.masterPictureAttachment %}
<a href="{{ entity_url(assembly.masterPictureAttachment, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener">
<img class="d-block w-100 img-fluid img-thumbnail bg-body-tertiary part-info-image" src="{{ entity_url(assembly.masterPictureAttachment, 'file_view') }}" alt="">
</a>
{% else %}
<img src="{{ asset('img/part_placeholder.svg') }}" class="img-fluid img-thumbnail bg-body-tertiary mb-2 " alt="Part main image" height="300" width="300">
{% endif %}
</div>
<div class="col-md-9 col-lg-8 col-7">
<h3 class="w-fit" title="{% trans %}name.label{% endtrans %}">{{ assembly.name }}
{# You need edit permission to use the edit button #}
{% if is_granted('edit', assembly) %}
<a href="{{ entity_url(assembly, 'edit') }}"><i class="fas fa-fw fa-sm fa-edit"></i></a>
{% endif %}
</h3>
<h6 class="text-muted w-fit" title="{% trans %}description.label{% endtrans %}"><span>{{ assembly.description|format_markdown(true) }}</span></h6>
{% if assembly.buildPart %}
<h6>{% trans %}assembly.edit.associated_build_part{% endtrans %}:</h6>
<a href="{{ entity_url(assembly.buildPart) }}">{{ assembly.buildPart.name }}</a>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4"> {# Sidebar panel with infos about last creation date, etc. #}
<div class="mb-3">
<span class="text-muted" title="{% trans %}lastModified{% endtrans %}">
<i class="fas fa-history fa-fw"></i> {{ helper.date_user_combination(assembly, true) }}
</span>
<br>
<span class="text-muted mt-1" title="{% trans %}createdAt{% endtrans %}">
<i class="fas fa-calendar-plus fa-fw"></i> {{ helper.date_user_combination(assembly, false) }}
</span>
</div>
<div class="mt-1">
<h6>
{{ helper.assemblies_status_to_badge(assembly.status) }}
</h6>
</div>
<div class="mt-1">
<h6>
<span class="badge badge-primary bg-primary">
<i class="fa-solid fa-list-check fa-fw"></i>
{{ assembly.bomEntries | length }}
{% trans %}assembly.info.bom_entries_count{% endtrans %}
</span>
</h6>
</div>
{% if assembly.children is not empty %}
<div class="mt-1">
<h6>
<span class="badge badge-primary bg-secondary">
<i class="fa-solid fa-folder-tree fa-fw"></i>
{{ assembly.children | length }}
{% trans %}assembly.info.sub_assemblies_count{% endtrans %}
</span>
</h6>
</div>
{% endif %}
</div>
{% if assembly.comment is not empty %}
<p>
<h5>{% trans %}comment.label{% endtrans %}:</h5>
{{ assembly.comment|format_markdown }}
</p>
{% endif %}
</div>

View file

@ -0,0 +1,133 @@
{% import "helper.twig" as helper %}
{% import "label_system/dropdown_macro.html.twig" as dropdown %}
{{ helper.breadcrumb_entity_link(assembly) }}
<div class="accordion mb-4" id="listAccordion">
<div class="accordion-item">
<div class="accordion-header">
<button class="accordion-button collapsed py-2" data-bs-toggle="collapse" data-bs-target="#entityInfo" aria-expanded="true">
{% if assembly.masterPictureAttachment is not null and attachment_manager.isFileExisting(assembly.masterPictureAttachment) %}
<img class="hoverpic ms-0 me-1 d-inline" {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ entity_url(assembly.masterPictureAttachment, 'file_view') }}" src="{{ attachment_thumbnail(assembly.masterPictureAttachment, 'thumbnail_sm') }}">
{% else %}
{{ helper.entity_icon(assembly, "me-1") }}
{% endif %}
{% trans %}assembly.label{% endtrans %}:&nbsp;<b>{{ assembly.name }}</b>
</button>
</div>
<div id="entityInfo" class="accordion-collapse collapse show" data-bs-parent="#listAccordion">
<div class="accordion-body">
{% if assembly.description is not empty %}
{{ assembly.description|format_markdown }}
{% endif %}
<div class="row">
<div class="col-sm-2">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<a class="nav-link active" id="v-pills-home-tab" data-bs-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">
<i class="fas fa-info-circle fa-fw"></i>
{% trans %}entity.info.common.tab{% endtrans %}
</a>
<a class="nav-link" id="v-pills-statistics-tab" data-bs-toggle="pill" href="#v-pills-statistics" role="tab" aria-controls="v-pills-profile" aria-selected="false">
<i class="fas fa-chart-pie fa-fw"></i>
{% trans %}entity.info.statistics.tab{% endtrans %}
</a>
{% if assembly.attachments is not empty %}
<a class="nav-link" id="v-pills-attachments-tab" data-bs-toggle="pill" href="#v-pills-attachments" role="tab" aria-controls="v-pills-attachments" aria-selected="false">
<i class="fas fa-paperclip fa-fw"></i>
{% trans %}entity.info.attachments.tab{% endtrans %}
</a>
{% endif %}
{% if assembly.parameters is not empty %}
<a class="nav-link" id="v-pills-parameters-tab" data-bs-toggle="pill" href="#v-pills-parameters" role="tab" aria-controls="v-pills-parameters" aria-selected="false">
<i class="fas fa-atlas fa-fw"></i>
{% trans %}entity.info.parameters.tab{% endtrans %}
</a>
{% endif %}
{% if assembly.comment is not empty %}
<a class="nav-link" id="v-pills-comment-tab" data-bs-toggle="pill" href="#v-pills-comment" role="tab">
<i class="fas fa-comment-alt fa-fw"></i>
{% trans %}comment.label{% endtrans %}
</a>
{% endif %}
</div>
</div>
<div class="col-sm-10">
<div class="tab-content" id="v-pills-tabContent">
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
<div class="row">
<div class="col-sm-9 form-horizontal">
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.name{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ assembly.name }}</span>
</div>
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.parent{% endtrans %}:</label>
<span class="col-sm form-control-static">
{% if assembly.parent %}
{{ assembly.parent.fullPath }}
{% else %}
-
{% endif %}
</span>
</div>
</div>
<div class="col-sm-3">
{% block quick_links %}{% endblock %}
<a class="btn btn-secondary w-100 mb-2" href="{{ entity_url(assembly, 'edit') }}">
<i class="fas fa-edit"></i> {% trans %}entity.edit.btn{% endtrans %}
</a>
<div class="">
<span class="text-muted" title="{% trans %}lastModified{% endtrans %}">
<i class="fas fa-history fa-fw"></i> {{ assembly.lastModified | format_datetime("short") }}
</span>
<br>
<span class="text-muted mt-1" title="{% trans %}createdAt{% endtrans %}">
<i class="fas fa-calendar-plus fa-fw"></i> {{ assembly.addedDate | format_datetime("short") }}
</span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="v-pills-statistics" role="tabpanel" aria-labelledby="v-pills-statistics-tab">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.children_count{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ assembly.children | length }}</span>
</div>
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.parts_count{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ assembly.bomEntries | length }}</span>
</div>
</div>
</div>
{% if assembly.attachments is not empty %}
<div class="tab-pane fade" id="v-pills-attachments" role="tabpanel" aria-labelledby="v-pills-attachments-tab">
{% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %}
</div>
{% endif %}
{% if assembly.parameters is not empty %}
<div class="tab-pane fade" id="v-pills-parameters" role="tabpanel" aria-labelledby="v-pills-parameters-tab">
{% for name, parameters in assembly.groupedParameters %}
{% if name is not empty %}<h5 class="mt-1">{{ name }}</h5>{% endif %}
{{ helper.parameters_table(assembly) }}
{% endfor %}
</div>
{% endif %}
{% if assembly.comment is not empty %}
<div class="tab-pane fade" id="v-pills-comment" role="tabpanel" aria-labelledby="home-tab">
<div class="container-fluid mt-2 latex" data-controller="common--latex">
{{ assembly.comment|format_markdown }}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>{% trans %}name.label{% endtrans %}</th>
<th>{% trans %}description.label{% endtrans %}</th>
<th># {% trans %}assembly.info.bom_entries_count{% endtrans %}</th>
<th># {% trans %}assembly.info.sub_assemblies_count{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for subassembly in assembly.children %}
<tr>
<td> {# Name #}
<a href="{{ entity_url(subassembly, 'info') }}">{{ subassembly.name }}</a>
</td>
<td> {# Description #}
{{ subassembly.description | format_markdown }}
</td>
<td>
{{ subassembly.bomEntries | length }}
</td>
<td>
{{ subassembly.children | length }}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,105 @@
{% extends "main_card.html.twig" %}
{% import "helper.twig" as helper %}
{% block title %}
{% trans %}assembly.info.title{% endtrans %}: {{ assembly.name }}
{% endblock %}
{% block content %}
{{ helper.breadcrumb_entity_link(assembly) }}
{{ parent() }}
{% endblock %}
{% block card_title %}
{% if assembly.masterPictureAttachment is not null and attachment_manager.isFileExisting(assembly.masterPictureAttachment) %}
<img class="hoverpic ms-0 me-1 d-inline" {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ entity_url(assembly.masterPictureAttachment, 'file_view') }}" src="{{ attachment_thumbnail(assembly.masterPictureAttachment, 'thumbnail_sm') }}">
{% else %}
{{ helper.entity_icon(assembly, "me-1") }}
{% endif %}
{% trans %}assembly.info.title{% endtrans %}:&nbsp;<b>{{ assembly.name }}</b>
{% endblock %}
{% block card_content %}
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="info-tab" data-bs-toggle="tab" data-bs-target="#info-tab-pane"
type="button" role="tab" aria-controls="info-tab-pane" aria-selected="true">
<i class="fa-solid fa-circle-info fa-fw"></i>
{% trans %}assembly.info.info.label{% endtrans %}
</button>
</li>
{% if assembly.children is not empty %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="subassemblies-tab" data-bs-toggle="tab" data-bs-target="#subassemblies-tab-pane"
type="button" role="tab" aria-controls="subassemblies-tab-pane" aria-selected="false">
<i class="fa-solid fa-folder-tree fa-fw"></i>
{% trans %}assembly.info.sub_assemblies.label{% endtrans %}
<span class="badge bg-secondary">{{ assembly.children | length }}</span>
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="bom-tab" data-bs-toggle="tab" data-bs-target="#bom-tab-pane"
type="button" role="tab" aria-controls="bom-tab-pane" aria-selected="false">
<i class="fa-solid fa-list-check fa-fw"></i>
{% trans %}assembly_bom_entry.label{% endtrans %}
<span class="badge bg-secondary">{{ assembly.bomEntries | length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="builds-tab" data-bs-toggle="tab" data-bs-target="#builds-tab-pane"
type="button" role="tab" aria-controls="builds-tab-pane" aria-selected="false">
<i class="fa-solid fa-bolt fa-fw"></i>
{% trans %}assembly.info.builds.label{% endtrans %}
</button>
</li>
{% if assembly.attachments is not empty %}
<li class="nav-item">
<a class="nav-link" id="attachments-tab" data-bs-toggle="tab"
data-bs-target="#attachments-tab-pane" role="tab">
<i class="fas fa-paperclip fa-fw"></i>
{% trans %}attachment.labelp{% endtrans %}
<span class="badge bg-secondary">{{ assembly.attachments | length }}</span>
</a>
</li>
{% endif %}
{% if assembly.parameters is not empty %}
<li class="nav-item">
<a class="nav-link" id="parameters-tab" data-bs-toggle="tab"
data-bs-target="#parameters-tab-pane" role="tab">
<i class="fas fa-atlas fa-fw"></i>
{% trans %}entity.info.parameters.tab{% endtrans %}
<span class="badge bg-secondary">{{ assembly.parameters | length }}</span>
</a>
</li>
{% endif %}
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="info-tab-pane" role="tabpanel" aria-labelledby="info-tab" tabindex="0">
{% include "assemblies/info/_info.html.twig" %}
</div>
{% if assembly.children is not empty %}
<div class="tab-pane fade" id="subassemblies-tab-pane" role="tabpanel" aria-labelledby="bom-tab" tabindex="0">
{% include "assemblies/info/_subassemblies.html.twig" %}
</div>
{% endif %}
<div class="tab-pane fade" id="bom-tab-pane" role="tabpanel" aria-labelledby="bom-tab" tabindex="0">
{% include "assemblies/info/_bom.html.twig" %}
</div>
<div class="tab-pane fade" id="builds-tab-pane" role="tabpanel" aria-labelledby="builds-tab" tabindex="0">
{% include "assemblies/info/_builds.html.twig" %}
</div>
<div class="tab-pane fade" id="attachments-tab-pane" role="tabpanel" aria-labelledby="attachments-tab" tabindex="0">
{% include "parts/info/_attachments_info.html.twig" with {"part": assembly} %}
</div>
<div class="tab-pane fade" id="parameters-tab-pane" role="tabpanel" aria-labelledby="parameters-tab">
{% for name, parameters in assembly.groupedParameters %}
{% if name is not empty %}<h5 class="mt-1">{{ name }}</h5>{% endif %}
{{ helper.parameters_table(assembly.parameters) }}
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% macro assembly_bom_entry_with_missing_instock(assembly_bom_entry, number_of_builds = 1) %}
{# @var \App\Entity\AssemblySystem\AssemblyBOMEntry assembly_bom_entry #}
<b><a href="{{ entity_url(assembly_bom_entry.part) }}">{{ assembly_bom_entry.part.name }}</a></b>
{% if assembly_bom_entry.name %}&nbsp;({{ assembly_bom_entry.name }}){% endif %}:
<b>{{ assembly_bom_entry.part.amountSum | format_amount(assembly_bom_entry.part.partUnit) }}</b> {% trans %}assembly.builds.stocked{% endtrans %}
/
<b>{{ (assembly_bom_entry.quantity * number_of_builds) | format_amount(assembly_bom_entry.part.partUnit) }}</b> {% trans %}assembly.builds.needed{% endtrans %}
{% endmacro %}

View file

@ -7,6 +7,7 @@
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')],
['tools', path('tree_tools'), 'tools.label', true],
] %}

View file

@ -6,7 +6,7 @@
<tr>
<th></th> {# expand button #}
<th>{% trans %}project.bom.quantity{% endtrans %}</th>
<th>{% trans %}project.bom.part{% endtrans %}</th>
<th>{% trans %}project.bom.partOrAssembly{% endtrans %}</th>
<th>{% trans %}project.bom.name{% endtrans %}</th>
<th></th> {# Remove button #}
</tr>
@ -41,9 +41,11 @@
{{ form_widget(form.quantity) }}
{{ form_errors(form.quantity) }}
</td>
<td style="min-width: 250px;">
{{ form_widget(form.part) }}
<td style="min-width: 300px;">
{{ form_row(form.part) }}
{{ form_errors(form.part) }}
{{ form_widget(form.assembly) }}
{{ form_errors(form.assembly) }}
</td>
<td>
{{ form_widget(form.name) }}

View file

@ -0,0 +1,80 @@
{% block assembly_bom_entry_collection_widget %}
{% import 'components/collection_type.macro.html.twig' as collection %}
<div {{ collection.controller(form, 'assembly.bom.delete.confirm', 3) }}>
<table class="table table-striped table-bordered table-sm" {{ collection.target() }}>
<thead>
<tr>
<th></th> {# expand button #}
<th>{% trans %}assembly.bom.quantity{% endtrans %}</th>
<th>{% trans %}assembly.bom.part{% endtrans %}</th>
<th>{% trans %}assembly.bom.name{% endtrans %}</th>
<th></th> {# Remove button #}
</tr>
</thead>
<tbody>
{% for entry in form %}
{{ form_widget(entry) }}
{% endfor %}
</tbody>
</table>
<button type="button" class="btn btn-success mb-2" {{ collection.create_btn() }}>
<i class="fas fa-plus-square fa-fw"></i>
{% trans %}assembly.bom.add_entry{% endtrans %}
</button>
</div>
{% endblock %}
{% block assembly_bom_entry_widget %}
{% set target_id = 'expand_row-' ~ form.vars.name %}
{% import 'components/collection_type.macro.html.twig' as collection %}
<tr>
<td>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#{{ target_id }}">
<i class="fa-solid fa-eye"></i>
</button>
</td>
<td>
{{ form_widget(form.quantity) }}
{{ form_errors(form.quantity) }}
</td>
<td style="min-width: 250px;">
{{ form_widget(form.part) }}
{{ form_errors(form.part) }}
</td>
<td>
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
</td>
<td>
<button type="button" class="btn btn-danger lot_btn_delete position-relative" {{ collection.delete_btn() }}>
<i class="fas fa-trash-alt fa-fw"></i>
{{ collection.new_element_indicator(value) }}
</button>
{{ form_errors(form) }}
</td>
</tr>
<tr class="p-0 d-none"></tr>
<tr class="p-0">
<td colspan="5" class="accordion-body collapse" id="{{ target_id }}">
<div class="">
{{ form_row(form.mountnames) }}
<div class="row mb-2">
<label class="col-form-label col-sm-3">{% trans %}assembly.bom.price{% endtrans %}</label>
<div class="col-sm-9">
<div class="input-group">
{{ form_widget(form.price) }}
{{ form_widget(form.priceCurrency) }}
</div>
{{ form_errors(form.price) }}
{{ form_errors(form.priceCurrency) }}
</div>
</div>
{{ form_row(form.comment) }}
</div>
</td>
</tr>
{% endblock %}

View file

@ -76,6 +76,21 @@
{% endif %}
{% endmacro %}
{% macro assemblies_status_to_badge(status, class="badge") %}
{% if status is not empty %}
{% set color = " bg-secondary" %}
{% if status == "in_production" %}
{% set color = " bg-success" %}
{% endif %}
<span class="{{ class ~ color}}">
<i class="fa-fw fas fa-info-circle"></i>
{{ ("assembly.status." ~ status) | trans }}
</span>
{% endif %}
{% endmacro %}
{% macro structural_entity_link(entity, link_type = "list_parts") %}
{# @var entity \App\Entity\Base\StructuralDBElement #}
{% if entity %}
@ -101,6 +116,7 @@
"category": ["fa-solid fa-tags", "category.label"],
"currency": ["fa-solid fa-coins", "currency.label"],
"device": ["fa-solid fa-archive", "project.label"],
"assembly": ["fa-solid fa-list", "assembly.label"],
"footprint": ["fa-solid fa-microchip", "footprint.label"],
"group": ["fa-solid fa-users", "group.label"],
"label_profile": ["fa-solid fa-qrcode", "label_profile.label"],

View file

@ -27,7 +27,9 @@
</td>
<td >
{% if bom_entry.part %}
<b><a target="_blank" href="{{ entity_url(bom_entry.part) }}">{{ bom_entry.part.name }}</a></b> {% if bom_entry.name %}({{ bom_entry.name }}){% endif %}
<b><a target="_blank" href="{{ entity_url(bom_entry.part) }}">{{ 'projects.build.form.part'|trans({'%name%': bom_entry.part.name}) }}</a></b> {% if bom_entry.name %}({{ bom_entry.name }}){% endif %}
{% elseif bom_entry.assembly %}
<b><a target="_blank" href="{{ entity_url(bom_entry.assembly) }}">{{ 'projects.build.form.assembly'|trans({'%name%': bom_entry.assembly.name}) }}</a></b> {% if bom_entry.name %}({{ bom_entry.name }}){% endif %}
{% else %}
<b>{{ bom_entry.name }}</b>
{% endif %}
@ -45,9 +47,29 @@
<tr>
<td colspan="4">
{% set lots = build_request.partLotsForBOMEntry(bom_entry) %}
{% set assemblyBomEntriesWithoutPart = build_request.assemblyBomEntriesWithoutPart(bom_entry) %}
{% set assemblyBomEntriesWithPartNoStock = build_request.assemblyBomEntriesWithPartNoStock(bom_entry) %}
{% if lots is not null %}
{% set previousLabel = null %}
{% for lot in lots %}
{# @var lot \App\Entity\Parts\PartLot #}
{% set label = '' %}
{% if form["lot_"~lot.id].vars.label is defined and form["lot_"~lot.id].vars.label is not empty %}
{% set label = form["lot_"~lot.id].vars.label %}
{% endif %}
{% if label != '' and (previousLabel is null or label != previousLabel) %}
<div class="mb-2 row">
<label class="col-form-label col-lg-4">
<small>{{ label|raw }}</small>
</label>
</div>
{% endif %}
{% set previousLabel = label %}
<div class="mb-2 row">
<label class="col-form-label col-sm-4" for="category_admin_form_parent">
{% if lot.storageLocation %}
@ -61,12 +83,41 @@
{{ form_errors(form["lot_"~lot.id]) }}
{{ form_widget(form["lot_"~lot.id]) }}
</div>
<div class="col-sm-2 mt-1 text-end">
<div class="col-sm-2 mt-1 text-end">
/ <b>{{ lot.amount | format_amount(lot.part.partUnit) }}</b> {% trans %}project.builds.stocked{% endtrans %}
</div>
</div>
{% endfor %}
{% endif %}
{% if assemblyBomEntriesWithoutPart is not null %}
{% for bomEntryWithoutPart in assemblyBomEntriesWithoutPart %}
<div class="mb-2 row">
<label class="col-form-label col-sm-4" for="category_admin_form_parent">
<small>{{ 'projects.build.form.assembly.bom.entry'|trans({'%name%': bomEntryWithoutPart.name, '%quantity%': bomEntryWithoutPart.quantity * number_of_builds}) }}</small>
</label>
<div class="col-sm-6"></div>
<div class="col-sm-2 mt-1 text-end">
/ {% trans %}project.builds.no_stock{% endtrans %}
</div>
</div>
{% endfor %}
{% endif %}
{% if assemblyBomEntriesWithPartNoStock is not null %}
{% for bomEntryWithPartNoStock in assemblyBomEntriesWithPartNoStock %}
<div class="alert alert-danger">
<div class="mb-2 row">
<label class="col-form-label col-sm-4" for="category_admin_form_parent">
{% trans %}projects.build.form.assembly.bom.entry.no.stock{% endtrans %}<br/>
<small>{{ 'projects.build.form.assembly.bom.entry'|trans({'%name%': bomEntryWithPartNoStock.name ? bomEntryWithPartNoStock.name : bomEntryWithPartNoStock.part.name, '%quantity%': bomEntryWithPartNoStock.quantity * number_of_builds}) }}</small>
</label>
<div class="col-sm-6"></div>
<div class="col-sm-2 mt-1 text-end">
/ {% trans %}project.builds.no_stock{% endtrans %}
</div>
</div>
</div>
{% endfor %}
{% endif %}
</td>
</tr>
{% endfor %}