Erweitere VoTiAn NG um Fachfunktionen und verbessere die Listenoberfläche

Ergänze Auftragsbearbeitung, Disposition, Stammdaten, Kunden- und
Transporteurportale, Lagerverwaltung, Abrechnung, Exporte und Statistik.
Füge Kontoeinstellungen, Mitteilungen, Automatikdienste und Betriebsstatus
sowie die zugehörigen Tests und PHP-Abgleichsdokumentation hinzu.

Vergrößere die persönlichen Auftragslisten auf einheitliche Höhen und
richte Statusanzeigen sowie die Ranking-Vorschau-Steuerelemente bündig aus.

Validierung: 44 Frontendtests und Produktionsbuild erfolgreich.
Backend funktional unverändert gegenüber dem vollständigen Lauf mit 1.624
erfolgreichen und drei übersprungenen Tests. Layout im Browser und lokale
Bereitstellung geprüft.
This commit is contained in:
Sven committed 2026-10-20 11:19:43 +02:00
1 parent 363bc27c26
commit 781a2b82dc
1165 files changed
+74986 -1028

No files matched your search

@@ -0,0 +1,42 @@
package de.stadtbote.votian;
import de.stadtbote.votian.database.DatabaseService;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import static de.stadtbote.votian.AdminSupport.*;
import static de.stadtbote.votian.LegacyMapping.*;
import static de.stadtbote.votian.OrderEntryService.fields;
/** Customer detail articles and gross calculator lines from include/inc_job.inc.php. */
@RestController
@RequestMapping("/api/customer-portal/orders/{order}/items")
class CustomerOrderItems {
static final List<String> KEYS=List.of("at_id","trat_quantity","trat_sort","trat_packingpieces","trat_serialno","trat_state","trat_remark","trat_price","trat_name","trat_remark","trat_weight","trat_id","tr_id","trat_int_serialno");
static final List<String> LABELS=List.of("ID","Menge","Sortierung","Packstücke","Seriennummer","Status","Bemerkung","Preis","Nummer","Artikelbeschreibung","Gewicht","Positions-ID","Stations-ID","Interne Seriennummer");
final DatabaseService db;final CustomerPortal portal;
CustomerOrderItems(DatabaseService db,CustomerPortal portal){this.db=db;this.portal=portal;}
@GetMapping Object view(Authentication a,@PathVariable long order){return view(ApiController.user(a),order);}
String parameter(long hq,String key){String value=portal.global(hq,key);return value.isEmpty()?portal.global(0,key):value;}
Map<String,Object> view(LegacyUser u,long order){return db.readTransaction(()->{
portal.detail(u,new CustomerPortal.Detail(order));
if(!portal.enabled(u.headquartersId(),"MASK_JOBDETAILS_JBP_BOOKING_ENABLED"))return fields("enabled",false);
var job=one(db.queryForList("SELECT jb_sales_tax_rate,jb_payment,csc_id_payer FROM job WHERE jb_id=?",order));
if(id(job,"jb_payment")==1||id(job,"csc_id_payer")==0)return fields("enabled",true,"message","Artikel und Rechnungsposten eines Kostensplits benötigen eine eindeutige Kostenstellenzuordnung.","columns",List.of(),"articles",List.of(),"items",List.of());
String raw=parameter(u.headquartersId(),"MASK_JOBDETAILS_ARTICLES");if(raw.isBlank())raw="9,8,1,10,3,7";String titles=parameter(u.headquartersId(),"MASK_JOBDETAILS_ARTICLE_HEADLINE");var names=titles.isBlank()?LABELS:Arrays.asList(titles.split(",",-1));var indexes=new ArrayList<Integer>();
for(String part:raw.split(",",-1)){if(!part.strip().matches("[0-9]{1,2}"))throw bad("Die Artikelspalten sind ungültig.");int index=Integer.parseInt(part.strip());if(index==99)continue;if(index<0||index>=KEYS.size())throw bad("Eine Artikelspalte benötigt einen aktualisierten Feldabgleich.");indexes.add(index);if(indexes.size()>30)throw bad("Zu viele Artikelspalten.");}
boolean scans=parameter(u.headquartersId(),"MASK_JOBDETAILS_ARTICLE_SCAN_EVENTS").equals("1");if(scans&&!indexes.contains(4))indexes.add(4);
var articles=db.queryForList("SELECT trat_id,tr_id,at_id,trat_quantity,trat_price,trat_sort,trat_packingpieces,trat_weight,trat_int_serialno,trat_serialno,trat_state,trat_remark,trat_name FROM tourarticle WHERE jb_id=? AND tr_sort=2 ORDER BY trat_sort,trat_id LIMIT 1001",order);
if(articles.size()>1000)throw bad("Mehr als 1.000 Artikel im Kundenauftrag.");
var rows=new ArrayList<Map<String,Object>>();BigDecimal weight=BigDecimal.ZERO,price=BigDecimal.ZERO,pieces=BigDecimal.ZERO;
for(var article:articles){var cells=new ArrayList<String>();for(int index:indexes){String value=text(article,KEYS.get(index));if(index==9&&value.length()>50)value=value.substring(0,50);cells.add(value);}rows.add(fields("id",id(article,"trat_id"),"cells",cells));weight=weight.add(BillingService.nz(decimal(article,"trat_weight")));price=price.add(BillingService.nz(decimal(article,"trat_price")));pieces=pieces.add(BillingService.nz(decimal(article,"trat_packingpieces")));}
var columns=indexes.stream().map(i->fields("label",i<names.size()&&!names.get(i).isBlank()?names.get(i):LABELS.get(i),"type",i==7?"money":i==10?"decimal":"text")).toList();
// PHP treats the stored value as a tax-table ID here and falls back to 19 percent.
BigDecimal tax=new BigDecimal("19");BigDecimal storedTax=BillingService.nz(decimal(job,"jb_sales_tax_rate"));long taxId=storedTax.stripTrailingZeros().scale()<=0?storedTax.longValue():0;if(taxId>0){var values=db.queryForList("SELECT tx_value FROM tax WHERE tx_id=?",taxId);if(values.size()>1)throw bad("Der Umsatzsteuersatz ist mehrdeutig.");if(!values.isEmpty()&&values.getFirst().get("tx_value")!=null)tax=decimal(values.getFirst(),"tx_value");}if(tax.signum()<0||tax.compareTo(BigDecimal.valueOf(100))>0)throw bad("Ungültiger Umsatzsteuersatz.");BigDecimal factor=BigDecimal.ONE.add(tax.movePointLeft(2)),total=BigDecimal.ZERO;
var calc=db.queryForList("SELECT srv_name,srvt_name,jbc_amount,jbc_price,jbc_totalprice FROM jobcalculator WHERE jb_id=? AND tr_sort=2 ORDER BY srv_name,srv_id,srvt_id,tr_id LIMIT 1001",order);if(calc.size()>1000)throw bad("Mehr als 1.000 Rechnungsposten im Kundenauftrag.");var items=new ArrayList<Map<String,Object>>();
for(var item:calc){String name=text(item,"srv_name");if(name.length()>50)name=name.substring(0,50);BigDecimal value=BillingService.nz(decimal(item,"jbc_totalprice"));items.add(fields("name",name,"quantity",decimal(item,"jbc_amount"),"unitPrice",BillingService.money(BillingService.nz(decimal(item,"jbc_price")).multiply(factor)),"price",BillingService.money(value.multiply(factor))));total=total.add(value);}
var extra=db.queryForList("SELECT gdc_content FROM genericdatacontainer WHERE gdc_obj_type='jb' AND gdc_obj_id=? AND gdc_gen_fieldname='jb_addinfo'",order);if(extra.size()>1)throw bad("Die Artikel-Zusatzinformation ist mehrdeutig.");
return fields("enabled",true,"message","","columns",columns,"articles",rows,"items",items,"weight",weight,"pieces",pieces,"goodsValue",BillingService.money(price),"total",BillingService.money(total.multiply(factor)),"tax",tax,"additional",extra.isEmpty()?"":OrderDeliveryData.publicRemark(text(extra.getFirst(),"gdc_content")),"scanEvents",scans);
});}
}