Add shipping note import to manual invoice
This commit is contained in:
@@ -34,6 +34,14 @@ class WarehouseShippingNoteController extends TTCrud {
|
||||
'delete' => 'Lieferschein wurde gelöscht',
|
||||
'noChanges' => 'Keine Änderungen vorgenommen'];
|
||||
protected array $permissionCheck = ['WarehouseUser'];
|
||||
protected array $additionalActions = [
|
||||
[
|
||||
'key' => 'createManualInvoice',
|
||||
'title' => 'Rechnung erstellen',
|
||||
'class' => 'fas fa-file-invoice text-primary',
|
||||
'condition' => ['status' => 'accepted']
|
||||
]
|
||||
];
|
||||
//@formatter:on
|
||||
|
||||
protected function prepareCrudConfig() {
|
||||
@@ -109,6 +117,177 @@ class WarehouseShippingNoteController extends TTCrud {
|
||||
));
|
||||
}
|
||||
|
||||
protected function getShippingNoteForInvoiceAction() {
|
||||
$id = $this->request->id;
|
||||
|
||||
// Get shipping note
|
||||
$shippingNote = WarehouseShippingNoteModel::get($id);
|
||||
if (!$shippingNote) {
|
||||
self::returnJson(['success' => false, 'message' => 'Lieferschein nicht gefunden']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get billing address info
|
||||
$billingAddress = null;
|
||||
if ($shippingNote->billingAddressId) {
|
||||
$billingAddress = Address::getOne($shippingNote->billingAddressId);
|
||||
}
|
||||
|
||||
// Determine price type ONCE (not in loop for performance)
|
||||
$priceType = 'Verkauf';
|
||||
if ($shippingNote->billingAddressId) {
|
||||
$addressPriceType = AddressPriceTypeModel::getFirst(['address_id' => $shippingNote->billingAddressId]);
|
||||
if ($addressPriceType) {
|
||||
$warehousePriceType = WarehouseArticlePriceTypeModel::get($addressPriceType->priceType_id);
|
||||
if ($warehousePriceType) {
|
||||
$priceType = $warehousePriceType->title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode and enrich positions
|
||||
$positions = json_decode($shippingNote->positions, true);
|
||||
if (!is_array($positions)) {
|
||||
$positions = [];
|
||||
}
|
||||
|
||||
$enrichedPositions = [];
|
||||
|
||||
foreach ($positions as $position) {
|
||||
if (isset($position['article'])) {
|
||||
// Fetch article details
|
||||
$article = WarehouseArticleModel::get($position['article']);
|
||||
if (!$article) continue;
|
||||
|
||||
// Get price for determined price type
|
||||
$prices = json_decode($article->cheapestSellPrice, true) ?: [];
|
||||
$price = 0;
|
||||
foreach ($prices as $p) {
|
||||
if ($p['title'] === $priceType) {
|
||||
$price = $p['price'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'type' => 'article',
|
||||
'articleId' => $article->id,
|
||||
'product_name' => $article->articleNumber . " | " . $article->title,
|
||||
'product_info' => $article->description,
|
||||
'amount' => $position['amount'],
|
||||
'unit' => $article->unit,
|
||||
'price' => $price,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
|
||||
} elseif (isset($position['articlePacket'])) {
|
||||
// Handle article packets
|
||||
$packet = WarehouseArticlePacketModel::get($position['articlePacket']);
|
||||
if (!$packet) continue;
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'type' => 'packet',
|
||||
'packetId' => $packet->id,
|
||||
'product_name' => $packet->title,
|
||||
'product_info' => $packet->description ?? '',
|
||||
'amount' => $position['amount'],
|
||||
'unit' => 'Pau.',
|
||||
'price' => 0,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
|
||||
} elseif (isset($position['articleText'])) {
|
||||
// Handle custom text entries
|
||||
$enrichedPositions[] = [
|
||||
'type' => 'text',
|
||||
'product_name' => $position['articleText'],
|
||||
'product_info' => '',
|
||||
'amount' => $position['amount'] ?? 1,
|
||||
'unit' => 'Stk.',
|
||||
'price' => 0,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add hours entries as positions
|
||||
$hoursEntries = json_decode($shippingNote->hoursEntries, true);
|
||||
if (!is_array($hoursEntries)) {
|
||||
$hoursEntries = [];
|
||||
}
|
||||
|
||||
foreach ($hoursEntries as $hoursEntry) {
|
||||
if (empty($hoursEntry['hourCount']) || floatval(str_replace(",", ".", $hoursEntry['hourCount'])) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userName = 'Unbekannt';
|
||||
if (!empty($hoursEntry['userId']) && is_numeric($hoursEntry['userId'])) {
|
||||
try {
|
||||
$user = UserModel::getOne($hoursEntry['userId']);
|
||||
$userName = $user ? $user->name : 'Unbekannt';
|
||||
} catch (Exception $e) {
|
||||
$userName = 'Unbekannt';
|
||||
}
|
||||
} elseif (!empty($hoursEntry['userId_text'])) {
|
||||
$userName = $hoursEntry['userId_text'];
|
||||
}
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'type' => 'hours',
|
||||
'product_name' => 'Arbeitsstunden - ' . $userName,
|
||||
'product_info' => 'Datum: ' . (isset($hoursEntry['date']) ? date('d.m.Y', strtotime($hoursEntry['date'])) : ''),
|
||||
'amount' => str_replace(",", ".", $hoursEntry['hourCount']),
|
||||
'unit' => 'h',
|
||||
'price' => 60,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
}
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'shippingNoteId' => $shippingNote->id,
|
||||
'billingAddress' => $billingAddress ? [
|
||||
'id' => $billingAddress->id,
|
||||
'customer_number' => $billingAddress->customer_number,
|
||||
'company' => $billingAddress->company,
|
||||
'firstname' => $billingAddress->firstname,
|
||||
'lastname' => $billingAddress->lastname,
|
||||
'street' => $billingAddress->street,
|
||||
'zip' => $billingAddress->zip,
|
||||
'city' => $billingAddress->city,
|
||||
'email' => $billingAddress->email,
|
||||
'uid' => $billingAddress->uid,
|
||||
'fibu_account_number' => $billingAddress->fibu_account_number,
|
||||
'billing_type' => $billingAddress->billing_type,
|
||||
'billing_delivery' => $billingAddress->billing_delivery,
|
||||
'bank_account_bank' => $billingAddress->bank_account_bank,
|
||||
'bank_account_owner' => $billingAddress->bank_account_owner,
|
||||
'bank_account_iban' => $billingAddress->bank_account_iban,
|
||||
'bank_account_bic' => $billingAddress->bank_account_bic,
|
||||
'sepa_date' => $billingAddress->sepa_date,
|
||||
'fibu_payment_due' => $billingAddress->fibu_payment_due,
|
||||
'fibu_payment_skonto' => $billingAddress->fibu_payment_skonto,
|
||||
'fibu_payment_skonto_rate' => $billingAddress->fibu_payment_skonto_rate
|
||||
] : null,
|
||||
'deliveryAddress' => [
|
||||
'name' => $shippingNote->deliveryAddressName,
|
||||
'line' => $shippingNote->deliveryAddressLine,
|
||||
'plz' => $shippingNote->deliveryAddressPLZ,
|
||||
'city' => $shippingNote->deliveryAddressCity,
|
||||
'email' => $shippingNote->deliveryAddressEMail
|
||||
],
|
||||
'note' => $shippingNote->note,
|
||||
'positions' => $enrichedPositions
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getArticleAddressPriceAction() {
|
||||
empty($this->request->articleId) && $this->sendError('Keine Artikel ID gefunden');
|
||||
empty($this->request->addressId) && $this->sendError('Keine Adress ID gefunden');
|
||||
|
||||
@@ -16,12 +16,29 @@ Vue.component('manual-invoice', {
|
||||
<button class="btn btn-sm btn-primary" @click="downloadPdf(row.id)" title="PDF herunterladen"><i class="fas fa-file-pdf"></i></button>
|
||||
</template>
|
||||
</tt-table-crud>
|
||||
<manual-invoice-modal v-if="isModalOpen" :initial-data="editingInvoiceData" @close="closeModal" @save="handleSave"/>
|
||||
<manual-invoice-modal v-if="isModalOpen" :initial-data="editingInvoiceData" :shipping-note-import="shippingNoteImportData" @close="closeModal" @save="handleSave"/>
|
||||
<gutschrift-modal v-if="isGutschriftModalOpen" :invoice-id="gutschriftInvoiceId" @close="closeGutschriftModal" @created="handleGutschriftCreated"/>
|
||||
<send-invoice-modal v-if="isSendModalOpen" :invoice-id="sendInvoiceId" @close="closeSendModal" @sent="handleInvoiceSent"/>
|
||||
</tt-card>
|
||||
`,
|
||||
data: () => ({ isModalOpen: false, editingInvoiceData: null, isGutschriftModalOpen: false, gutschriftInvoiceId: null, isSendModalOpen: false, sendInvoiceId: null }),
|
||||
data: () => ({ isModalOpen: false, editingInvoiceData: null, shippingNoteImportData: null, isGutschriftModalOpen: false, gutschriftInvoiceId: null, isSendModalOpen: false, sendInvoiceId: null }),
|
||||
mounted() {
|
||||
// Check for shipping note import data
|
||||
const shippingNoteData = localStorage.getItem('ManualInvoice_create');
|
||||
if (shippingNoteData) {
|
||||
try {
|
||||
// Parse and store the data
|
||||
this.shippingNoteImportData = JSON.parse(shippingNoteData);
|
||||
// Delete from localStorage immediately so it doesn't auto-open again on reload
|
||||
localStorage.removeItem('ManualInvoice_create');
|
||||
// Auto-open modal for import
|
||||
this.openModal();
|
||||
} catch (e) {
|
||||
console.error('Error parsing shipping note data:', e);
|
||||
localStorage.removeItem('ManualInvoice_create');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal(invoice = null) {
|
||||
this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null;
|
||||
@@ -30,6 +47,7 @@ Vue.component('manual-invoice', {
|
||||
closeModal() {
|
||||
this.isModalOpen = false;
|
||||
this.editingInvoiceData = null;
|
||||
this.shippingNoteImportData = null;
|
||||
this.$refs.table.$refs.table.refreshTable();
|
||||
},
|
||||
async handleSave(invoiceData) {
|
||||
@@ -126,7 +144,7 @@ Vue.component('manual-invoice', {
|
||||
});
|
||||
|
||||
Vue.component('manual-invoice-modal', {
|
||||
props: ['initialData'],
|
||||
props: ['initialData', 'shippingNoteImport'],
|
||||
template: `
|
||||
<div class="manual-invoice-overlay" :class="overlayClasses" tabindex="-1" ref="overlay">
|
||||
<div class="info-bar" v-if="!isLargeScreen"><i class="fas fa-info-circle mr-2"></i> Drücke <strong>STRG + Q</strong> um die Vorschau umzuschalten.</div>
|
||||
@@ -278,6 +296,16 @@ Vue.component('manual-invoice-modal', {
|
||||
}
|
||||
if (!Array.isArray(this.invoiceData.positions)) this.invoiceData.positions = [];
|
||||
}
|
||||
|
||||
// Check for shipping note import data from prop
|
||||
if (this.shippingNoteImport && Array.isArray(this.shippingNoteImport) && this.shippingNoteImport.length > 0) {
|
||||
try {
|
||||
this.processShippingNoteImport(this.shippingNoteImport);
|
||||
} catch (e) {
|
||||
console.error('Error processing shipping note import:', e);
|
||||
window.notify('error', 'Fehler beim Importieren des Lieferscheins');
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
@@ -334,6 +362,87 @@ Vue.component('manual-invoice-modal', {
|
||||
} finally {
|
||||
this.pdfLoading = false;
|
||||
}
|
||||
},
|
||||
processShippingNoteImport(shippingNoteDataArray) {
|
||||
// Temporarily disable the preview update during import to prevent memory leak
|
||||
clearTimeout(this.previewDebounceTimer);
|
||||
const originalWatcher = this.$options.watch['invoiceData'];
|
||||
delete this.$options.watch['invoiceData'];
|
||||
|
||||
try {
|
||||
for (const shippingNoteData of shippingNoteDataArray) {
|
||||
// Pre-fill billing address fields
|
||||
if (shippingNoteData.billingAddress) {
|
||||
const addr = shippingNoteData.billingAddress;
|
||||
|
||||
Object.assign(this.invoiceData, {
|
||||
billingaddress_id: addr.id,
|
||||
customer_number: addr.customer_number || 0,
|
||||
company: addr.company || '',
|
||||
firstname: addr.firstname || '',
|
||||
lastname: addr.lastname || '',
|
||||
street: addr.street || '',
|
||||
zip: addr.zip || '',
|
||||
city: addr.city || '',
|
||||
email: addr.email || '',
|
||||
uid: addr.uid || '',
|
||||
fibu_account_number: addr.fibu_account_number || 0,
|
||||
fibu_payment_due: addr.fibu_payment_due || 14,
|
||||
fibu_payment_skonto: addr.fibu_payment_skonto || 0,
|
||||
fibu_payment_skonto_rate: addr.fibu_payment_skonto_rate || 0,
|
||||
billing_type: addr.billing_type || 'invoice',
|
||||
owner_id: addr.id
|
||||
});
|
||||
|
||||
// Banking info (if SEPA)
|
||||
if (addr.billing_type === 'sepa') {
|
||||
Object.assign(this.invoiceData, {
|
||||
bank_account_bank: addr.bank_account_bank || '',
|
||||
bank_account_owner: addr.bank_account_owner || '',
|
||||
bank_account_iban: addr.bank_account_iban || '',
|
||||
bank_account_bic: addr.bank_account_bic || '',
|
||||
sepa_date: addr.sepa_date || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-fill external reference with shipping note reference
|
||||
this.invoiceData.externe_referenz = `Lieferschein #${shippingNoteData.shippingNoteId}`;
|
||||
|
||||
// Add introductory text if shipping note has notes
|
||||
if (shippingNoteData.note) {
|
||||
this.invoiceData.einleitender_text = shippingNoteData.note;
|
||||
}
|
||||
|
||||
// Add all positions (batch operation to avoid triggering watcher for each item)
|
||||
if (shippingNoteData.positions && Array.isArray(shippingNoteData.positions)) {
|
||||
const newPositions = shippingNoteData.positions.map(position => ({
|
||||
product_name: position.product_name || '',
|
||||
product_info: position.product_info || '',
|
||||
amount: parseFloat(position.amount) || 0,
|
||||
unit: position.unit || 'Stk.',
|
||||
price: parseFloat(position.price) || 0,
|
||||
discount: parseFloat(position.discount) || 0,
|
||||
vatrate: parseFloat(position.vatrate) || 20
|
||||
}));
|
||||
|
||||
// Add all positions at once instead of one by one
|
||||
this.invoiceData.positions.push(...newPositions);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify user
|
||||
const positionCount = shippingNoteDataArray.reduce((sum, sn) => sum + (sn.positions?.length || 0), 0);
|
||||
window.notify('success', `Lieferschein erfolgreich importiert (${positionCount} Position(en))`);
|
||||
} finally {
|
||||
// Re-enable the watcher
|
||||
this.$options.watch['invoiceData'] = originalWatcher;
|
||||
|
||||
// Trigger one preview update after import is complete
|
||||
this.$nextTick(() => {
|
||||
this.debouncedPreviewUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,6 +44,12 @@ window.TT_CONFIG["CRUD_CONFIG"]["additionalActions"] = [
|
||||
"key": "print",
|
||||
"title": "Drucken",
|
||||
"class": "fas fa-print text-primary",
|
||||
},
|
||||
{
|
||||
"key": "createManualInvoice",
|
||||
"title": "Rechnung erstellen",
|
||||
"class": "fas fa-file-invoice text-primary",
|
||||
"condition": (row) => row.status === 'accepted',
|
||||
}
|
||||
]
|
||||
|
||||
@@ -547,6 +553,7 @@ Vue.component('warehouse-shipping-note', {
|
||||
@status_to_cancelled="changeStatus($event.id, 'cancelled')"
|
||||
@status_to_new="changeStatus($event.id, 'new')"
|
||||
@add_log="addLogModalId = $event.id"
|
||||
@createManualInvoice="createManualInvoice($event)"
|
||||
@edit="shippingNoteModalId = $event.id"
|
||||
ref="table">
|
||||
|
||||
@@ -678,6 +685,30 @@ Vue.component('warehouse-shipping-note', {
|
||||
this.window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten');
|
||||
}
|
||||
},
|
||||
async createManualInvoice(row) {
|
||||
try {
|
||||
// Fetch shipping note with enriched article data
|
||||
const res = await axios.get(
|
||||
`${window.TT_CONFIG.BASE_PATH}/WarehouseShippingNote/getShippingNoteForInvoice`,
|
||||
{ params: { id: row.id } }
|
||||
);
|
||||
|
||||
if (!res.data.success) {
|
||||
window.notify('error', res.data.message || 'Fehler beim Laden der Lieferscheindaten');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store in localStorage as array (to match WarehouseOrder pattern)
|
||||
localStorage.setItem('ManualInvoice_create', JSON.stringify([res.data.data]));
|
||||
|
||||
// Navigate to ManualInvoice module
|
||||
window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating manual invoice:', error);
|
||||
window.notify('error', 'Fehler beim Erstellen der Rechnung');
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user