3239 lines
131 KiB
JavaScript
3239 lines
131 KiB
JavaScript
import { InvoiceFormatter } from './formatter.js';
|
|
|
|
// Constants
|
|
const XML_NAMESPACES = {
|
|
ubl: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
|
|
cbc: "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
|
|
cac: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
};
|
|
|
|
const VAT_TYPES = {
|
|
"S": "Cotă Standard",
|
|
"AE": "Taxare Inversă",
|
|
"O": "Neplătitor TVA",
|
|
"Z": "Cotă 0% TVA",
|
|
"E": "Neimpozabil"
|
|
};
|
|
|
|
const VAT_EXEMPTION_CODES = {
|
|
'AE': {
|
|
code: 'VATEX-EU-AE',
|
|
reason: 'Taxare inversa'
|
|
},
|
|
'K': {
|
|
code: 'VATEX-EU-IC',
|
|
reason: 'Livrare intracomunitara'
|
|
},
|
|
'O': {
|
|
code: 'VATEX-EU-O',
|
|
reason: 'Neplatitor TVA'
|
|
},
|
|
'E': [
|
|
{
|
|
code: '',
|
|
reason: 'Scutit'
|
|
},
|
|
{
|
|
code: 'VATEX-EU-F',
|
|
reason: 'Bunuri second hand'
|
|
},
|
|
{
|
|
code: 'VATEX-EU-D',
|
|
reason: 'Regim special agentii de turism'
|
|
}
|
|
]
|
|
};
|
|
|
|
const UNIT_CODES = new Map([
|
|
['EA', 'Bucată (EA)'],
|
|
['XPP', 'Bucată (XPP)'],
|
|
['KGM', 'Kilogram (KGM)'],
|
|
['MTR', 'Metri (MTR)'],
|
|
['LTR', 'Litru (LTR)'],
|
|
['H87', 'Bucată (H87)'],
|
|
['MTQ', 'Metri cubi (MTQ)']
|
|
]);
|
|
|
|
const ISO_3166_1_CODES = new Set([
|
|
'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX',
|
|
'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ',
|
|
'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK',
|
|
'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM',
|
|
'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR',
|
|
'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS',
|
|
'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN',
|
|
'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN',
|
|
'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV',
|
|
'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ',
|
|
'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI',
|
|
'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM',
|
|
'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC',
|
|
'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV',
|
|
'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR',
|
|
'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI',
|
|
'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW'
|
|
]);
|
|
|
|
const ROMANIAN_COUNTY_CODES = new Set([
|
|
'RO-AB', 'RO-AG', 'RO-AR', 'RO-B', 'RO-BC', 'RO-BH', 'RO-BN', 'RO-BR', 'RO-BT', 'RO-BV',
|
|
'RO-BZ', 'RO-CJ', 'RO-CL', 'RO-CS', 'RO-CT', 'RO-CV', 'RO-DB', 'RO-DJ', 'RO-GJ', 'RO-GL',
|
|
'RO-GR', 'RO-HD', 'RO-HR', 'RO-IF', 'RO-IL', 'RO-IS', 'RO-MH', 'RO-MM', 'RO-MS', 'RO-NT',
|
|
'RO-OT', 'RO-PH', 'RO-SB', 'RO-SJ', 'RO-SM', 'RO-SV', 'RO-TL', 'RO-TM', 'RO-TR', 'RO-VL',
|
|
'RO-VN', 'RO-VS'
|
|
]);
|
|
|
|
const CHARGE_REASON_CODES = {
|
|
'TV': 'Cheltuieli de transport',
|
|
'FC': 'Taxe transport',
|
|
'ZZZ': 'Definite reciproc'
|
|
};
|
|
|
|
const ALLOWANCE_REASON_CODES = {
|
|
'95': 'Reducere',
|
|
'41': 'Bonus lucrări în avans',
|
|
'42': 'Alt bonus',
|
|
'60': 'Reducere volum',
|
|
'62': 'Alte reduceri',
|
|
'63': 'Reducere producător',
|
|
'64': 'Din cauza războiului',
|
|
'65': 'Reducere outlet nou',
|
|
'66': 'Reducere mostre',
|
|
'67': 'Reducere end-of-range',
|
|
'68': 'Cost ambalaj returnabil',
|
|
'70': 'Reducere Incoterm',
|
|
'71': 'Prag vânzări',
|
|
'88': 'Suprataxă/deducere materiale',
|
|
'100': 'Reducere specială',
|
|
'102': 'Termen lung fix',
|
|
'103': 'Temporar',
|
|
'104': 'Standard',
|
|
'105': 'Cifră de afaceri anuală'
|
|
};
|
|
|
|
// Structure for item identifications
|
|
const IDENTIFICATION_TYPES = {
|
|
SELLERS: {
|
|
type: 'sellers',
|
|
label: 'Cod Furnizor',
|
|
xmlTag: 'SellersItemIdentification'
|
|
},
|
|
BUYERS: {
|
|
type: 'buyers',
|
|
label: 'Cod Client',
|
|
xmlTag: 'BuyersItemIdentification'
|
|
},
|
|
STANDARD: {
|
|
type: 'standard',
|
|
label: 'Cod Bare',
|
|
xmlTag: 'StandardItemIdentification',
|
|
schemeID: '0160'
|
|
},
|
|
COMMODITY: {
|
|
type: 'commodity',
|
|
label: 'Cod Clasificare',
|
|
xmlTag: 'CommodityClassification',
|
|
schemes: [
|
|
{ id: 'CV', name: 'Cod Vamal' },
|
|
{ id: 'TSP', name: 'Cod CPV' },
|
|
{ id: 'STI', name: 'Cod NC8' }
|
|
]
|
|
}
|
|
};
|
|
|
|
const formatter = new InvoiceFormatter()
|
|
|
|
const resolver = {
|
|
lookupNamespaceURI: prefix => {
|
|
const ns = {
|
|
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
|
|
'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
|
|
'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
|
};
|
|
return ns[prefix] || null;
|
|
}
|
|
};
|
|
|
|
// Global variables
|
|
let currentInvoice = null;
|
|
let originalTotals = null;
|
|
let vatRates = new Map();
|
|
let manuallyEditedVatRows = new Set();
|
|
|
|
// Initialize event listeners
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
|
initializeUI();
|
|
|
|
if (!currentInvoice) {
|
|
currentInvoice = createEmptyInvoice();
|
|
}
|
|
|
|
const totalElements = [
|
|
'subtotal', 'totalAllowances', 'totalCharges',
|
|
'netAmount', 'vat', 'total'
|
|
];
|
|
|
|
totalElements.forEach(elementId => {
|
|
const element = document.getElementById(elementId);
|
|
setupInlineEditing(element);
|
|
});
|
|
|
|
// Add currency code validation
|
|
const currencyInputs = document.querySelectorAll('[name="documentCurrencyCode"], [name="taxCurrencyCode"]');
|
|
currencyInputs.forEach(input => {
|
|
input.addEventListener('input', function(e) {
|
|
// Convert to uppercase
|
|
this.value = this.value.toUpperCase();
|
|
// Remove any non-letter characters
|
|
this.value = this.value.replace(/[^A-Z]/g, '');
|
|
// Limit to 3 characters
|
|
if (this.value.length > 3) {
|
|
this.value = this.value.slice(0, 3);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Make document currency code required
|
|
const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]');
|
|
if (documentCurrencyInput) {
|
|
documentCurrencyInput.required = true;
|
|
}
|
|
|
|
const supplierVATInput = document.querySelector('[name="supplierVAT"]');
|
|
if (supplierVATInput) {
|
|
supplierVATInput.addEventListener('change', function() {
|
|
updateAllVATTypes();
|
|
});
|
|
}
|
|
|
|
addExchangeRateField();
|
|
|
|
initializeLocationSelectors();
|
|
|
|
// Verifică dacă avem parametru XML în URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const xmlFileName = urlParams.get('xml');
|
|
|
|
if (xmlFileName) {
|
|
try {
|
|
// Încarcă XML-ul din fișierul temporar
|
|
const response = await fetch('temp/' + xmlFileName);
|
|
if (response.ok) {
|
|
const xmlContent = await response.text();
|
|
parseXML(xmlContent);
|
|
|
|
// Curăță fișierul temporar
|
|
fetch('receiver.php?cleanup=' + xmlFileName)
|
|
.catch(error => console.error('Eroare la ștergerea fișierului temporar:', error));
|
|
}
|
|
} catch (error) {
|
|
console.error('Eroare la încărcarea XML:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initialize event listeners for existing line items
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
handleLineItemChange(index);
|
|
});
|
|
|
|
// Inline editing setup function
|
|
function setupInlineEditing(element) {
|
|
let originalValue;
|
|
|
|
element.addEventListener('click', function() {
|
|
this.setAttribute('contenteditable', 'true');
|
|
// Store exact displayed value
|
|
originalValue = this.textContent;
|
|
this.focus();
|
|
});
|
|
|
|
element.addEventListener('blur', function() {
|
|
this.setAttribute('contenteditable', 'false');
|
|
if (this.textContent !== originalValue) {
|
|
updateTotals();
|
|
}
|
|
});
|
|
|
|
element.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
this.blur();
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateTotalDisplay(elementId, value) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
// Convertim la număr folosind parseFloat pentru a evita probleme de truncare
|
|
const numValue = parseFloat(value);
|
|
element.textContent = formatter.formatCurrency(numValue);
|
|
}
|
|
}
|
|
|
|
function displayTotals(totals) {
|
|
// Actualizează toate totalurile cu formatare
|
|
updateTotalDisplay('subtotal', totals.subtotal);
|
|
updateTotalDisplay('totalAllowances', totals.allowances);
|
|
updateTotalDisplay('totalCharges', totals.charges);
|
|
updateTotalDisplay('netAmount', totals.netAmount);
|
|
updateTotalDisplay('vat', totals.totalVat);
|
|
updateTotalDisplay('total', totals.total);
|
|
|
|
// Actualizează defalcarea TVA dacă există
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
if (totals.vatBreakdown) {
|
|
totals.vatBreakdown.forEach((data, key) => {
|
|
const [rate, type] = key.split('-');
|
|
addVATBreakdownRow(
|
|
parseFloat(rate),
|
|
data.baseAmount,
|
|
data.vatAmount,
|
|
type
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateVATDisplay(row, amount, type = 'amount') {
|
|
const input = row.querySelector(`.vat-${type}`);
|
|
if (input) {
|
|
input.value = formatter.formatCurrency(amount);
|
|
}
|
|
}
|
|
|
|
function validateVATExemption() {
|
|
const vatRows = document.querySelectorAll('.vat-row');
|
|
let isValid = true;
|
|
|
|
vatRows.forEach(row => {
|
|
const vatType = row.querySelector('.vat-type').value;
|
|
if (['E', 'K', 'AE', 'O'].includes(vatType)) {
|
|
const exemptionCodeInput = row.querySelector('.vat-exemption-code');
|
|
const exemptionReasonInput = row.querySelector('.vat-exemption-reason');
|
|
const exemptionCode = exemptionCodeInput?.value;
|
|
const exemptionReason = exemptionReasonInput?.value;
|
|
|
|
// Pentru neplătitori de TVA (tip O), verificăm să aibă valorile corecte
|
|
if (vatType === 'O') {
|
|
if (exemptionCode !== 'VATEX-EU-O') {
|
|
isValid = false;
|
|
exemptionCodeInput?.classList.add('invalid');
|
|
}
|
|
}
|
|
// Pentru celelalte tipuri de scutire, trebuie să aibă cel puțin unul dintre câmpuri completat
|
|
else if (!exemptionCode && !exemptionReason) {
|
|
isValid = false;
|
|
exemptionCodeInput?.classList.add('invalid');
|
|
exemptionReasonInput?.classList.add('invalid');
|
|
} else {
|
|
exemptionCodeInput?.classList.remove('invalid');
|
|
exemptionReasonInput?.classList.remove('invalid');
|
|
}
|
|
}
|
|
});
|
|
|
|
return isValid;
|
|
}
|
|
|
|
function addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason) {
|
|
if (!vatType || !exemptionCode) return;
|
|
|
|
if (vatType === 'E') {
|
|
// Verifică dacă codul există deja
|
|
const exists = VAT_EXEMPTION_CODES.E.some(e => e.code === exemptionCode);
|
|
if (!exists) {
|
|
VAT_EXEMPTION_CODES.E.push({
|
|
code: exemptionCode,
|
|
reason: exemptionReason || exemptionCode
|
|
});
|
|
}
|
|
} else if (!VAT_EXEMPTION_CODES[vatType]) {
|
|
// Adaugă un nou tip de TVA dacă nu există
|
|
VAT_EXEMPTION_CODES[vatType] = {
|
|
code: exemptionCode,
|
|
reason: exemptionReason || exemptionCode
|
|
};
|
|
}
|
|
}
|
|
|
|
function getDisplayValue(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
return element ? formatter.parseCurrency(element.textContent) : 0;
|
|
}
|
|
|
|
// Event delegation for dynamic elements
|
|
document.addEventListener('click', function(event) {
|
|
const target = event.target;
|
|
|
|
if (target.matches('.delete-line-item')) {
|
|
const lineItem = target.closest('.line-item');
|
|
if (lineItem) {
|
|
removeLineItem(parseInt(lineItem.dataset.index));
|
|
}
|
|
}
|
|
|
|
if (target.matches('.delete-allowance-charge')) {
|
|
const charge = target.closest('.allowance-charge');
|
|
if (charge) {
|
|
removeAllowanceCharge(parseInt(charge.dataset.index));
|
|
}
|
|
}
|
|
});
|
|
|
|
// File handling functions
|
|
function handleFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const xmlContent = e.target.result;
|
|
parseXML(xmlContent);
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
}
|
|
|
|
function parseXML(xmlContent) {
|
|
try {
|
|
const parser = new DOMParser();
|
|
const xmlDoc = parser.parseFromString(xmlContent, "text/xml");
|
|
|
|
const parserError = xmlDoc.querySelector('parsererror');
|
|
if (parserError) {
|
|
throw new Error('Eroare la parsarea XML: ' + parserError.textContent);
|
|
}
|
|
|
|
// Extrage și adaugă codurile de scutire din TaxTotal
|
|
const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal');
|
|
taxSubtotals.forEach(subtotal => {
|
|
const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory');
|
|
if (taxCategory) {
|
|
const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID');
|
|
const exemptionCode = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode');
|
|
const exemptionReason = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason');
|
|
|
|
if (vatType && exemptionCode) {
|
|
addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason);
|
|
}
|
|
}
|
|
});
|
|
|
|
currentInvoice = xmlDoc;
|
|
manuallyEditedVatRows.clear();
|
|
|
|
populateBasicDetails(xmlDoc);
|
|
populatePartyDetails(xmlDoc);
|
|
populateAllowanceCharges(xmlDoc);
|
|
populateLineItems(xmlDoc);
|
|
storeOriginalTotals(xmlDoc);
|
|
restoreOriginalTotals();
|
|
displayVATBreakdown(xmlDoc);
|
|
|
|
} catch (error) {
|
|
handleError(error, 'Eroare la parsarea fișierului XML');
|
|
}
|
|
}
|
|
|
|
// Create allowance charge HTML
|
|
function createAllowanceChargeHTML(index, charge) {
|
|
return `
|
|
<div class="allowance-charge" data-index="${index}">
|
|
<div class="grid">
|
|
<div class="form-group">
|
|
<label class="form-label">Tip</label>
|
|
<select class="form-input" name="chargeType${index}" onchange="updateReasonCodeOptions(${index})">
|
|
<option value="true" ${charge.isCharge ? 'selected' : ''}>Taxă</option>
|
|
<option value="false" ${!charge.isCharge ? 'selected' : ''}>Reducere</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Cod Motiv</label>
|
|
<select class="form-input" name="chargeReasonCode${index}">
|
|
${createReasonCodeOptions(charge.isCharge, charge.reasonCode)}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Motiv</label>
|
|
<input type="text" class="form-input" name="chargeReason${index}"
|
|
value="${charge.reason || ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Valoare</label>
|
|
<input type="number" step="0.01" class="form-input" name="chargeAmount${index}"
|
|
value="${charge.amount}" onchange="updateTotals()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Bază de calcul</label>
|
|
<input type="number" step="0.01" class="form-input" name="chargeBaseAmount${index}"
|
|
value="${charge.baseAmount}" onchange="updateTotals()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tip TVA</label>
|
|
<select class="form-input" name="chargeVatType${index}">
|
|
${Object.entries(VAT_TYPES).map(([key, value]) =>
|
|
`<option value="${key}" ${key === charge.vatTypeId ? 'selected' : ''}>${value}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Cotă TVA (%)</label>
|
|
<input type="number" step="0.1" class="form-input" name="chargeVatRate${index}"
|
|
value="${charge.vatRate}" ${charge.vatTypeId !== 'S' ? 'disabled' : ''}>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="button button-small button-danger remove-line-item" onclick="removeAllowanceCharge(${index})">
|
|
✕
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function createReasonCodeOptions(isCharge, selectedCode = '') {
|
|
const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES;
|
|
return Object.entries(codes)
|
|
.map(([code, description]) =>
|
|
`<option value="${code}" ${code === selectedCode ? 'selected' : ''}>${description} (${code})</option>`
|
|
).join('');
|
|
}
|
|
|
|
window.updateReasonCodeOptions = function(index) {
|
|
const chargeTypeSelect = document.querySelector(`[name="chargeType${index}"]`);
|
|
const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`);
|
|
const reasonInput = document.querySelector(`[name="chargeReason${index}"]`);
|
|
|
|
const isCharge = chargeTypeSelect.value === 'true';
|
|
reasonCodeSelect.innerHTML = createReasonCodeOptions(isCharge);
|
|
|
|
// Update reason text based on selected code
|
|
const selectedCode = reasonCodeSelect.value;
|
|
const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES;
|
|
reasonInput.value = codes[selectedCode] || '';
|
|
}
|
|
|
|
// Create line item HTML
|
|
function createLineItemHTML(index, description = '', quantity = '1', price = '0', vatRate = '19',
|
|
unitCode = 'EA', vatTypeId = 'S', itemDescription = '', lineDiscount = '0', discountReasonCode = '') {
|
|
|
|
return `
|
|
<div class="line-item" data-index="${index}">
|
|
<div class="grid">
|
|
<div class="form-group">
|
|
<label class="form-label">Denumire</label>
|
|
<input type="text" class="form-input" name="description${index}" value="${description}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Cantitate</label>
|
|
<input type="number" step="0.001" class="form-input" name="quantity${index}"
|
|
value="${quantity}" onchange="updateTotals()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">UM</label>
|
|
<select class="form-input" name="unit${index}">
|
|
${createUnitCodeOptionsHTML(unitCode)}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Preț</label>
|
|
<input type="number" step="0.01" class="form-input" name="price${index}"
|
|
value="${price}" onchange="updateTotals()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Discount</label>
|
|
<input type="number" step="0.01" class="form-input" name="lineDiscount${index}"
|
|
value="${lineDiscount}" onchange="updateTotals()">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Cod Motiv Reducere</label>
|
|
<select class="form-input" name="discountReasonCode${index}" ${lineDiscount == 0 ? 'disabled' : ''}>
|
|
${createLineDiscountReasonOptions(discountReasonCode)}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tip TVA</label>
|
|
<select class="form-input" name="vatType${index}" onchange="handleVatTypeChange(${index})">
|
|
${Object.entries(VAT_TYPES).map(([key, value]) =>
|
|
`<option value="${key}" ${key === vatTypeId ? 'selected' : ''}>${value}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Cotă TVA (%)</label>
|
|
<input type="number" step="1" class="form-input" name="vatRate${index}"
|
|
value="${vatRate}" onchange="updateTotals()">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="optional-details-toggle">
|
|
<button type="button" class="button button-secondary"
|
|
onclick="toggleOptionalDetails(${index})">
|
|
▼ Detalii Suplimentare
|
|
</button>
|
|
</div>
|
|
|
|
<div class="optional-details" id="optionalDetails${index}" style="display: none;">
|
|
<div class="optional-details-content">
|
|
<div class="form-group description-group">
|
|
<label class="form-label">Descriere</label>
|
|
<textarea class="form-input" name="itemDescription${index}" rows="2">${itemDescription}</textarea>
|
|
</div>
|
|
<div class="identifications-container" id="identifications${index}">
|
|
<div class="identifications-header">
|
|
<h4>Coduri de Identificare</h4>
|
|
<div class="identification-buttons">
|
|
${Object.entries(IDENTIFICATION_TYPES).map(([type, info]) => `
|
|
<button type="button" class="button button-small"
|
|
onclick="window.addIdentification(${index}, '${type}')">
|
|
+ ${info.label}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="identifications-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="button button-small button-danger remove-line-item" onclick="removeLineItem(${index})">
|
|
✕
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function createLineDiscountReasonOptions(selectedCode = '') {
|
|
return Object.entries(ALLOWANCE_REASON_CODES)
|
|
.map(([code, description]) =>
|
|
`<option value="${code}" ${code === selectedCode ? 'selected' : ''}>${description} (${code})</option>`
|
|
).join('');
|
|
}
|
|
|
|
// Add VAT breakdown row
|
|
function addVATBreakdownRow(rate, baseAmount, vatAmount, vatType = 'S', existingRowId = null, exemptionCode = '', exemptionReason = '') {
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
const rowId = existingRowId || `vat-row-${Date.now()}`;
|
|
|
|
const rowHtml = `
|
|
<div class="vat-row" id="${rowId}">
|
|
<div class="total-row">
|
|
<div class="vat-inputs">
|
|
<label>Tip:</label>
|
|
<select class="form-input vat-type" onchange="window.updateVATRow('${rowId}', 'manual')">
|
|
${Object.entries(VAT_TYPES).map(([key, value]) =>
|
|
`<option value="${key}" ${key === vatType ? 'selected' : ''}>${value}</option>`
|
|
).join('')}
|
|
</select>
|
|
<label>Cotă:</label>
|
|
<input type="text" class="form-input vat-rate" value="${rate}"
|
|
onchange="window.updateVATRow('${rowId}', 'manual')">%
|
|
<label>Bază Impozabilă:</label>
|
|
<input type="text" class="form-input vat-base" value="${baseAmount}"
|
|
onchange="window.updateVATRow('${rowId}', 'manual')">
|
|
<label>Valoare TVA:</label>
|
|
<input type="text" class="form-input vat-amount" value="${vatAmount}"
|
|
onchange="window.updateVATRowFromAmount('${rowId}')">
|
|
</div>
|
|
<div class="vat-exemption ${['E', 'K', 'O', 'AE'].includes(vatType) ? '' : 'hidden'}">
|
|
<div class="form-group">
|
|
<label>Cod Scutire:</label>
|
|
<select class="form-input vat-exemption-code" onchange="window.updateExemptionReason('${rowId}')">
|
|
${generateExemptionCodeOptions(vatType, exemptionCode)}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Motiv Scutire:</label>
|
|
<input type="text" class="form-input vat-exemption-reason" value="${exemptionReason}"
|
|
placeholder="Motiv scutire TVA">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="button button-small button-danger delete-identification"
|
|
onclick="window.removeVATRow('${rowId}')">✕</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.insertAdjacentHTML('beforeend', rowHtml);
|
|
|
|
// Add event listener for VAT type changes
|
|
const vatTypeSelect = document.querySelector(`#${rowId} .vat-type`);
|
|
vatTypeSelect.addEventListener('change', () => {
|
|
const exemptionContainer = document.querySelector(`#${rowId} .vat-exemption`);
|
|
const newVatType = vatTypeSelect.value;
|
|
|
|
if (['E', 'K', 'O', 'AE'].includes(newVatType)) {
|
|
exemptionContainer.classList.remove('hidden');
|
|
|
|
// Update exemption code options
|
|
const codeSelect = document.querySelector(`#${rowId} .vat-exemption-code`);
|
|
codeSelect.innerHTML = generateExemptionCodeOptions(newVatType);
|
|
|
|
// Set default values
|
|
const defaultExemption = getDefaultExemption(newVatType);
|
|
if (defaultExemption) {
|
|
codeSelect.value = defaultExemption.code;
|
|
document.querySelector(`#${rowId} .vat-exemption-reason`).value = defaultExemption.reason;
|
|
}
|
|
} else {
|
|
exemptionContainer.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
function generateExemptionCodeOptions(vatType, selectedCode = '') {
|
|
if (vatType === 'E') {
|
|
return VAT_EXEMPTION_CODES.E.map(exemption =>
|
|
`<option value="${exemption.code}" ${exemption.code === selectedCode ? 'selected' : ''}>
|
|
${exemption.code || 'Scutit'}
|
|
</option>`
|
|
).join('');
|
|
} else if (vatType === 'K' || vatType === 'AE' || vatType === 'O') {
|
|
const exemption = VAT_EXEMPTION_CODES[vatType];
|
|
return `<option value="${exemption.code}" selected>${exemption.code}</option>`;
|
|
}
|
|
return '<option value="">-</option>';
|
|
}
|
|
|
|
function getDefaultExemption(vatType) {
|
|
if (vatType === 'E') {
|
|
return VAT_EXEMPTION_CODES.E[0];
|
|
}
|
|
return VAT_EXEMPTION_CODES[vatType];
|
|
}
|
|
|
|
// Add this to window object
|
|
window.updateExemptionReason = function(rowId) {
|
|
const row = document.getElementById(rowId);
|
|
if (!row) return;
|
|
|
|
const vatType = row.querySelector('.vat-type').value;
|
|
const codeSelect = row.querySelector('.vat-exemption-code');
|
|
const reasonInput = row.querySelector('.vat-exemption-reason');
|
|
|
|
if (vatType === 'E') {
|
|
const selectedExemption = VAT_EXEMPTION_CODES.E.find(e => e.code === codeSelect.value);
|
|
if (selectedExemption) {
|
|
reasonInput.value = selectedExemption.reason;
|
|
}
|
|
} else if (vatType === 'K' || vatType === 'AE') {
|
|
reasonInput.value = VAT_EXEMPTION_CODES[vatType].reason;
|
|
}
|
|
};
|
|
|
|
// Toggle optional details
|
|
window.toggleOptionalDetails = function(index) {
|
|
const optionalDetails = document.getElementById(`optionalDetails${index}`);
|
|
const button = optionalDetails.previousElementSibling.querySelector('button');
|
|
|
|
if (optionalDetails.style.display === 'none') {
|
|
optionalDetails.style.display = 'block';
|
|
button.innerHTML = '▲ Detalii Suplimentare';
|
|
} else {
|
|
optionalDetails.style.display = 'none';
|
|
button.innerHTML = '▼ Detalii Suplimentare';
|
|
}
|
|
}
|
|
|
|
// Form validation
|
|
function validateForm(silent = false) {
|
|
let isValid = true;
|
|
let firstInvalidField = null;
|
|
|
|
// Validare câmpuri existente...
|
|
const requiredFields = [
|
|
'invoiceNumber',
|
|
'issueDate',
|
|
'dueDate',
|
|
'supplierName',
|
|
'supplierVAT',
|
|
'customerName',
|
|
];
|
|
|
|
requiredFields.forEach(fieldName => {
|
|
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
if (!field || !field.value.trim()) {
|
|
field.classList.add('invalid');
|
|
isValid = false;
|
|
if (!firstInvalidField)
|
|
firstInvalidField = field;
|
|
} else {
|
|
field.classList.remove('invalid');
|
|
}
|
|
});
|
|
|
|
// Adaugă validarea TVA
|
|
const vatExemptionValid = validateVATExemption();
|
|
if (!vatExemptionValid) {
|
|
isValid = false;
|
|
if (!silent) {
|
|
alert('Vă rugăm să completați codul și/sau motivul scutirii de TVA pentru toate categoriile care necesită această informație.');
|
|
}
|
|
}
|
|
|
|
// Restul validărilor existente...
|
|
const lineItems = document.querySelectorAll('.line-item');
|
|
if (lineItems.length === 0) {
|
|
isValid = false;
|
|
if (!silent) {
|
|
alert('Este necesară cel puțin o linie în factură');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
lineItems.forEach((item, index) => {
|
|
const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value);
|
|
const price = parseFloat(document.querySelector(`[name="price${index}"]`).value);
|
|
const description = document.querySelector(`[name="description${index}"]`).value;
|
|
|
|
if (!description.trim()) {
|
|
document.querySelector(`[name="description${index}"]`).classList.add('invalid');
|
|
isValid = false;
|
|
}
|
|
if (isNaN(quantity)) {
|
|
document.querySelector(`[name="quantity${index}"]`).classList.add('invalid');
|
|
isValid = false;
|
|
}
|
|
if (isNaN(price)) {
|
|
document.querySelector(`[name="price${index}"]`).classList.add('invalid');
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
const dateInputs = document.querySelectorAll('.date-input');
|
|
dateInputs.forEach(input => {
|
|
if (!validateDateInput(input)) {
|
|
isValid = false;
|
|
if (!firstInvalidField) firstInvalidField = input;
|
|
}
|
|
});
|
|
|
|
// Validare curs valutar dacă este cazul
|
|
const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.trim();
|
|
const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.trim();
|
|
|
|
if (taxCurrencyCode && taxCurrencyCode !== documentCurrencyCode) {
|
|
const exchangeRate = document.querySelector('[name="exchangeRate"]');
|
|
if (!exchangeRate || !exchangeRate.value || parseFloat(exchangeRate.value) <= 0) {
|
|
exchangeRate.classList.add('invalid');
|
|
isValid = false;
|
|
if (!firstInvalidField) {
|
|
firstInvalidField = exchangeRate;
|
|
}
|
|
} else {
|
|
exchangeRate.classList.remove('invalid');
|
|
}
|
|
}
|
|
|
|
if (!isValid && !silent) {
|
|
if (firstInvalidField) {
|
|
firstInvalidField.focus();
|
|
}
|
|
alert('Vă rugăm să completați toate câmpurile obligatorii');
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
function handleError(error, message) {
|
|
console.error(message, error);
|
|
alert(`${message}\nVă rugăm să verificați consola pentru detalii.`);
|
|
}
|
|
|
|
function formatDateToRomanian(date) {
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const year = date.getFullYear();
|
|
return `${day}.${month}.${year}`;
|
|
}
|
|
|
|
function parseRomanianDate(dateStr) {
|
|
const [day, month, year] = dateStr.split('.');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function createDatePicker(input, button) {
|
|
const picker = new Pikaday({
|
|
field: input,
|
|
trigger: button,
|
|
format: 'DD.MM.YYYY',
|
|
i18n: {
|
|
previousMonth: 'Luna anterioară',
|
|
nextMonth: 'Luna următoare',
|
|
months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'],
|
|
weekdays: ['Duminică', 'Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'],
|
|
weekdaysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm']
|
|
},
|
|
firstDay: 1,
|
|
onSelect: function(date) {
|
|
input.value = formatDateToRomanian(date);
|
|
validateDateInput(input);
|
|
}
|
|
});
|
|
return picker;
|
|
}
|
|
|
|
function validateDateInput(input) {
|
|
const value = input.value;
|
|
const regex = /^(\d{2})\.(\d{2})\.(\d{4})$/;
|
|
const match = value.match(regex);
|
|
|
|
if (match) {
|
|
const day = parseInt(match[1]);
|
|
const month = parseInt(match[2]);
|
|
const year = parseInt(match[3]);
|
|
|
|
// Create date object and verify if it's valid
|
|
const date = new Date(year, month - 1, day);
|
|
if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
|
|
input.classList.remove('invalid');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (value !== '') {
|
|
input.classList.add('invalid');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function restrictDateInput(input) {
|
|
input.addEventListener('input', function(e) {
|
|
let value = e.target.value;
|
|
|
|
// Remove any non-digit characters except dots
|
|
value = value.replace(/[^\d.]/g, '');
|
|
|
|
// Auto-add dots after day and month
|
|
if (value.length >= 2 && value.charAt(2) !== '.') {
|
|
value = value.slice(0, 2) + '.' + value.slice(2);
|
|
}
|
|
if (value.length >= 5 && value.charAt(5) !== '.') {
|
|
value = value.slice(0, 5) + '.' + value.slice(5);
|
|
}
|
|
|
|
// Restrict to exactly 10 characters (dd.mm.yyyy)
|
|
value = value.slice(0, 10);
|
|
|
|
e.target.value = value;
|
|
});
|
|
|
|
input.addEventListener('blur', function() {
|
|
validateDateInput(input);
|
|
});
|
|
}
|
|
|
|
function createCountryOptions() {
|
|
return Array.from(ISO_3166_1_CODES).map(code =>
|
|
`<option value="${code}" ${code === 'RO' ? 'selected' : ''}>${code}</option>`
|
|
).join('');
|
|
}
|
|
|
|
function createCountyOptions() {
|
|
return Array.from(ROMANIAN_COUNTY_CODES).map(code => {
|
|
const label = code.replace('RO-', '');
|
|
const counties = {
|
|
'AB': 'Alba', 'AR': 'Arad', 'AG': 'Argeș', 'BC': 'Bacău', 'BH': 'Bihor',
|
|
'BN': 'Bistrița-Năsăud', 'BT': 'Botoșani', 'BV': 'Brașov', 'BR': 'Brăila',
|
|
'B': 'București', 'BZ': 'Buzău', 'CS': 'Caraș-Severin', 'CL': 'Călărași',
|
|
'CJ': 'Cluj', 'CT': 'Constanța', 'CV': 'Covasna', 'DB': 'Dâmbovița',
|
|
'DJ': 'Dolj', 'GL': 'Galați', 'GR': 'Giurgiu', 'GJ': 'Gorj', 'HR': 'Harghita',
|
|
'HD': 'Hunedoara', 'IL': 'Ialomița', 'IS': 'Iași', 'IF': 'Ilfov',
|
|
'MM': 'Maramureș', 'MH': 'Mehedinți', 'MS': 'Mureș', 'NT': 'Neamț',
|
|
'OT': 'Olt', 'PH': 'Prahova', 'SM': 'Satu Mare', 'SJ': 'Sălaj',
|
|
'SB': 'Sibiu', 'SV': 'Suceava', 'TR': 'Teleorman', 'TM': 'Timiș',
|
|
'TL': 'Tulcea', 'VS': 'Vaslui', 'VL': 'Vâlcea', 'VN': 'Vrancea'
|
|
};
|
|
return `<option value="${code}">${counties[label] || label} (${label})</option>`;
|
|
}).join('');
|
|
}
|
|
|
|
function initializeLocationSelectors() {
|
|
// Replace country inputs with selects for both supplier and customer
|
|
['supplier', 'customer'].forEach(party => {
|
|
const countryInput = document.querySelector(`[name="${party}Country"]`);
|
|
if (countryInput) {
|
|
const select = document.createElement('select');
|
|
select.className = 'form-input';
|
|
select.name = countryInput.name;
|
|
select.innerHTML = createCountryOptions();
|
|
select.value = countryInput.dataset.xmlValue || countryInput.value || 'RO';
|
|
countryInput.parentNode?.replaceChild(select, countryInput);
|
|
}
|
|
|
|
const countyInput = document.querySelector(`[name="${party}CountrySubentity"]`);
|
|
if (countyInput) {
|
|
const select = document.createElement('select');
|
|
select.className = 'form-input';
|
|
select.name = countyInput.name;
|
|
select.innerHTML = createCountyOptions();
|
|
select.value = countyInput.dataset.xmlValue || countyInput.value || '';
|
|
countyInput.parentNode?.replaceChild(select, countyInput);
|
|
}
|
|
});
|
|
|
|
// Initialize location handlers for both parties
|
|
setupPartyLocationHandlers('supplier');
|
|
setupPartyLocationHandlers('customer');
|
|
}
|
|
|
|
function updateCountyVisibility(countrySelect, countySelect) {
|
|
if (!countrySelect || !countySelect) return;
|
|
const showCounty = countrySelect.value === 'RO';
|
|
countySelect.style.display = showCounty ? 'block' : 'none';
|
|
countySelect.required = showCounty;
|
|
}
|
|
|
|
function setupPartyLocationHandlers(party) {
|
|
const countrySelect = document.querySelector(`[name="${party}Country"]`);
|
|
const countySelect = document.querySelector(`[name="${party}CountrySubentity"]`);
|
|
const cityContainer = document.querySelector(`[name="${party}City"]`)?.parentNode;
|
|
|
|
if (!countrySelect || !countySelect || !cityContainer) return;
|
|
|
|
const handleLocationChange = () => {
|
|
const isBucharest = countrySelect.value === 'RO' && countySelect.value === 'RO-B';
|
|
const currentElement = cityContainer.querySelector('input, select');
|
|
const isCurrentlySector = currentElement.tagName.toLowerCase() === 'select';
|
|
|
|
if (isBucharest && !isCurrentlySector) {
|
|
const sectorSelect = document.createElement('select');
|
|
sectorSelect.className = 'form-input';
|
|
sectorSelect.name = `${party}City`;
|
|
sectorSelect.innerHTML = `
|
|
<option value="">Selectați sectorul</option>
|
|
<option value="SECTOR1">Sectorul 1</option>
|
|
<option value="SECTOR2">Sectorul 2</option>
|
|
<option value="SECTOR3">Sectorul 3</option>
|
|
<option value="SECTOR4">Sectorul 4</option>
|
|
<option value="SECTOR5">Sectorul 5</option>
|
|
<option value="SECTOR6">Sectorul 6</option>
|
|
`;
|
|
|
|
// Try to preserve any existing sector value
|
|
const currentValue = currentElement.value || '';
|
|
if (currentValue.toUpperCase().includes('SECTOR')) {
|
|
sectorSelect.value = currentValue.toUpperCase().replace(/\s+/g, '');
|
|
}
|
|
|
|
cityContainer.replaceChild(sectorSelect, currentElement);
|
|
} else if (!isBucharest && isCurrentlySector) {
|
|
const cityInput = document.createElement('input');
|
|
cityInput.type = 'text';
|
|
cityInput.className = 'form-input';
|
|
cityInput.name = `${party}City`;
|
|
cityInput.value = '';
|
|
cityContainer.replaceChild(cityInput, currentElement);
|
|
}
|
|
};
|
|
|
|
// Set up event listeners
|
|
countrySelect.addEventListener('change', () => {
|
|
updateCountyVisibility(countrySelect, countySelect);
|
|
handleLocationChange();
|
|
});
|
|
|
|
countySelect.addEventListener('change', handleLocationChange);
|
|
|
|
// Initial setup
|
|
updateCountyVisibility(countrySelect, countySelect);
|
|
handleLocationChange();
|
|
}
|
|
|
|
function initializeUI() {
|
|
document.querySelectorAll('.form-input').forEach(input => {
|
|
input.addEventListener('input', function() {
|
|
this.classList.remove('invalid');
|
|
updateTotals();
|
|
});
|
|
});
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.ctrlKey || event.metaKey) {
|
|
switch (event.key.toLowerCase()) {
|
|
case 's':
|
|
event.preventDefault();
|
|
saveXML();
|
|
break;
|
|
case 'o':
|
|
event.preventDefault();
|
|
document.getElementById('fileInput').click();
|
|
break;
|
|
case 'n':
|
|
event.preventDefault();
|
|
addLineItem();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initialize date pickers
|
|
const dateInputs = document.querySelectorAll('.date-input');
|
|
dateInputs.forEach(input => {
|
|
const button = input.parentElement.querySelector('.calendar-button');
|
|
createDatePicker(input, button);
|
|
restrictDateInput(input);
|
|
});
|
|
|
|
if (!currentInvoice) {
|
|
const today = new Date();
|
|
const dueDate = new Date();
|
|
dueDate.setDate(today.getDate() + 30);
|
|
|
|
document.querySelector('[name="issueDate"]').value = formatDateToRomanian(today);
|
|
document.querySelector('[name="dueDate"]').value = formatDateToRomanian(dueDate);
|
|
}
|
|
|
|
// Add note counter event listener
|
|
const noteInput = document.querySelector('[name="invoiceNote"]');
|
|
if (noteInput) {
|
|
noteInput.addEventListener('input', updateNoteCounter);
|
|
updateNoteCounter(); // Initial count
|
|
}
|
|
|
|
// Initialize location selectors
|
|
initializeLocationSelectors();
|
|
|
|
window.addLineItem = addLineItem;
|
|
window.removeLineItem = removeLineItem;
|
|
window.addAllowanceCharge = addAllowanceCharge;
|
|
window.removeAllowanceCharge = removeAllowanceCharge;
|
|
window.handleStorno = handleStorno;
|
|
window.updateTotals = updateTotals;
|
|
window.saveXML = saveXML;
|
|
window.refreshTotals = refreshTotals;
|
|
window.displayVATBreakdown = displayVATBreakdown;
|
|
}
|
|
|
|
// Handling VAT type changes
|
|
window.handleVatTypeChange = function(index) {
|
|
const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`);
|
|
const vatRateInput = document.querySelector(`[name="vatRate${index}"]`);
|
|
|
|
// Verifică dacă furnizorul este neplătitor TVA
|
|
if (!isVATRegistered()) {
|
|
vatTypeSelect.value = 'O';
|
|
vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
|
|
// Setează codul și motivul scutirii pentru neplătitori
|
|
const row = document.querySelector(`.vat-row`);
|
|
if (row) {
|
|
const exemptionCode = row.querySelector('.vat-exemption-code');
|
|
const exemptionReason = row.querySelector('.vat-exemption-reason');
|
|
if (exemptionCode) exemptionCode.value = 'VATEX-EU-O';
|
|
if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA';
|
|
}
|
|
} else {
|
|
vatTypeSelect.disabled = false; // Activează selectul pentru plătitori
|
|
|
|
switch(vatTypeSelect.value) {
|
|
case 'O': // Pentru neplătitori
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
break;
|
|
case 'AE': // Taxare inversă
|
|
case 'Z': // Cotă 0%
|
|
case 'E': // Scutit
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
break;
|
|
case 'S': // Standard
|
|
vatRateInput.value = '19';
|
|
vatRateInput.disabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateTotals();
|
|
}
|
|
|
|
function isVATRegistered() {
|
|
const supplierVAT = document.querySelector('[name="supplierVAT"]').value.trim().toUpperCase();
|
|
return supplierVAT.startsWith('RO');
|
|
}
|
|
|
|
function handleChargeVatTypeChange(index) {
|
|
const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`);
|
|
const vatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`);
|
|
|
|
if (!vatTypeSelect || !vatRateInput) return;
|
|
|
|
// Verifică dacă furnizorul este neplătitor TVA
|
|
if (!isVATRegistered()) {
|
|
vatTypeSelect.value = 'O';
|
|
vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
|
|
// Setează codul și motivul scutirii pentru neplătitori
|
|
const row = document.querySelector(`.vat-row`);
|
|
if (row) {
|
|
const exemptionCode = row.querySelector('.vat-exemption-code');
|
|
const exemptionReason = row.querySelector('.vat-exemption-reason');
|
|
if (exemptionCode) exemptionCode.value = 'VATEX-EU-O';
|
|
if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA';
|
|
}
|
|
} else {
|
|
vatTypeSelect.disabled = false; // Activează selectul pentru plătitori
|
|
|
|
switch(vatTypeSelect.value) {
|
|
case 'O': // Pentru neplătitori
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
break;
|
|
case 'AE': // Taxare inversă
|
|
case 'Z': // Cotă 0%
|
|
case 'E': // Scutit
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
break;
|
|
case 'S': // Standard
|
|
vatRateInput.value = '19';
|
|
vatRateInput.disabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clear manual edits and refresh
|
|
manuallyEditedVatRows.clear();
|
|
refreshTotals();
|
|
}
|
|
|
|
// Funcție pentru actualizarea tuturor categoriilor TVA când se modifică codul fiscal
|
|
function updateAllVATTypes() {
|
|
const isNotVATRegistered = !isVATRegistered();
|
|
|
|
// Actualizează toate liniile de articole
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const vatTypeSelect = item.querySelector(`[name="vatType${index}"]`);
|
|
const vatRateInput = item.querySelector(`[name="vatRate${index}"]`);
|
|
|
|
if (isNotVATRegistered) {
|
|
vatTypeSelect.value = 'O';
|
|
vatTypeSelect.disabled = true;
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
} else {
|
|
vatTypeSelect.disabled = false;
|
|
// Restabilește valorile implicite pentru plătitori
|
|
if (vatTypeSelect.value === 'O') {
|
|
vatTypeSelect.value = 'S';
|
|
vatRateInput.value = '19';
|
|
vatRateInput.disabled = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Actualizează breakdown-ul TVA
|
|
updateVATBreakdown();
|
|
refreshTotals();
|
|
}
|
|
|
|
// XML modifications
|
|
function addUnitCode(code) {
|
|
if (!UNIT_CODES.has(code)) {
|
|
UNIT_CODES.set(code, `${code} (${code})`);
|
|
}
|
|
}
|
|
|
|
function createUnitCodeOptionsHTML(selectedCode = 'EA') {
|
|
return Array.from(UNIT_CODES.entries())
|
|
.map(([code, description]) =>
|
|
`<option value="${code}" ${code === selectedCode ? 'selected' : ''}>${description}</option>`
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
function storeOriginalTotals(xmlDoc) {
|
|
const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal');
|
|
const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal');
|
|
|
|
originalTotals = {
|
|
subtotal: getXMLValue(monetaryTotal, 'cbc\\:LineExtensionAmount, LineExtensionAmount'),
|
|
allowances: getXMLValue(monetaryTotal, 'cbc\\:AllowanceTotalAmount, AllowanceTotalAmount', '0'),
|
|
charges: getXMLValue(monetaryTotal, 'cbc\\:ChargeTotalAmount, ChargeTotalAmount', '0'),
|
|
netAmount: getXMLValue(monetaryTotal, 'cbc\\:TaxExclusiveAmount, TaxExclusiveAmount'),
|
|
totalVat: getXMLValue(taxTotal, 'cbc\\:TaxAmount, TaxAmount'),
|
|
total: getXMLValue(monetaryTotal, 'cbc\\:TaxInclusiveAmount, TaxInclusiveAmount')
|
|
};
|
|
|
|
// console.log('Original totals from XML:', originalTotals);
|
|
|
|
const vatBreakdown = [];
|
|
const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal');
|
|
taxSubtotals.forEach(subtotal => {
|
|
const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory');
|
|
vatBreakdown.push({
|
|
taxableAmount: getXMLValue(subtotal, 'cbc\\:TaxableAmount, TaxableAmount'),
|
|
taxAmount: getXMLValue(subtotal, 'cbc\\:TaxAmount, TaxAmount'),
|
|
percent: getXMLValue(taxCategory, 'cbc\\:Percent, Percent'),
|
|
type: getXMLValue(taxCategory, 'cbc\\:ID, ID', 'S'),
|
|
exemptionCode: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'),
|
|
exemptionReason: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason')
|
|
});
|
|
});
|
|
originalTotals.vatBreakdown = vatBreakdown;
|
|
}
|
|
|
|
function restoreOriginalTotals() {
|
|
if (!originalTotals) return;
|
|
|
|
// Display exact values from XML with formatting
|
|
document.getElementById('subtotal').textContent = formatter.formatCurrency(originalTotals.subtotal);
|
|
document.getElementById('totalAllowances').textContent = formatter.formatCurrency(originalTotals.allowances);
|
|
document.getElementById('totalCharges').textContent = formatter.formatCurrency(originalTotals.charges);
|
|
document.getElementById('netAmount').textContent = formatter.formatCurrency(originalTotals.netAmount);
|
|
document.getElementById('vat').textContent = formatter.formatCurrency(originalTotals.totalVat);
|
|
|
|
// Bypass formatting for total
|
|
document.getElementById('total').textContent = originalTotals.total;
|
|
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
|
|
if (originalTotals.vatBreakdown && originalTotals.vatBreakdown.length > 0) {
|
|
originalTotals.vatBreakdown.forEach(vat => {
|
|
addVATBreakdownRow(
|
|
vat.percent,
|
|
vat.taxableAmount,
|
|
vat.taxAmount,
|
|
vat.type,
|
|
null,
|
|
vat.exemptionCode,
|
|
vat.exemptionReason
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateNoteCounter() {
|
|
const noteInput = document.querySelector('[name="invoiceNote"]');
|
|
const counter = document.querySelector('.note-counter');
|
|
if (noteInput && counter) {
|
|
const length = noteInput.value.length;
|
|
counter.textContent = `${length}/900 caractere`;
|
|
}
|
|
}
|
|
|
|
function splitNoteIntoChunks(text, maxLength) {
|
|
if (!text) return [];
|
|
const chunks = [];
|
|
let remainingText = text;
|
|
|
|
while (remainingText.length > 0) {
|
|
if (remainingText.length <= maxLength) {
|
|
chunks.push(remainingText);
|
|
break;
|
|
}
|
|
|
|
let splitPoint = remainingText.substr(0, maxLength).lastIndexOf('\n');
|
|
if (splitPoint === -1) {
|
|
splitPoint = remainingText.substr(0, maxLength).lastIndexOf(' ');
|
|
}
|
|
if (splitPoint === -1) splitPoint = maxLength;
|
|
|
|
chunks.push(remainingText.substr(0, splitPoint));
|
|
remainingText = remainingText.substr(splitPoint + 1);
|
|
}
|
|
|
|
return chunks.filter(chunk => chunk.trim().length > 0);
|
|
}
|
|
|
|
function populateBasicDetails(xmlDoc) {
|
|
document.querySelector('[name="invoiceNumber"]').value = getXMLValue(xmlDoc, 'cbc\\:ID, ID');
|
|
|
|
// Get and combine all Note elements
|
|
const notes = xmlDoc.querySelectorAll('cbc\\:Note, Note');
|
|
const combinedNotes = Array.from(notes).map(note => note.textContent).join('\n');
|
|
document.querySelector('[name="invoiceNote"]').value = combinedNotes;
|
|
updateNoteCounter();
|
|
|
|
const issueDate = getXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate');
|
|
const dueDate = getXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate');
|
|
|
|
if (issueDate) {
|
|
const [year, month, day] = issueDate.split('-');
|
|
document.querySelector('[name="issueDate"]').value = `${day}.${month}.${year}`;
|
|
}
|
|
|
|
if (dueDate) {
|
|
const [year, month, day] = dueDate.split('-');
|
|
document.querySelector('[name="dueDate"]').value = `${day}.${month}.${year}`;
|
|
}
|
|
|
|
// Add currency code handling
|
|
const documentCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', 'RON');
|
|
const taxCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:TaxCurrencyCode, TaxCurrencyCode', '');
|
|
|
|
document.querySelector('[name="documentCurrencyCode"]').value = documentCurrencyCode;
|
|
document.querySelector('[name="taxCurrencyCode"]').value = taxCurrencyCode;
|
|
|
|
// Store original totals and display them
|
|
storeOriginalTotals(xmlDoc);
|
|
restoreOriginalTotals();
|
|
}
|
|
|
|
function populatePartyDetails(xmlDoc) {
|
|
function extractPartyDetails(party, prefix) {
|
|
// Extract contact information
|
|
const contact = party.querySelector('cac\\:Contact, Contact');
|
|
const phone = contact?.querySelector('cbc\\:Telephone, Telephone')?.textContent || '';
|
|
const contactName = contact?.querySelector('cbc\\:Name, Name')?.textContent || '';
|
|
const email = contact?.querySelector('cbc\\:ElectronicMail, ElectronicMail')?.textContent || '';
|
|
|
|
// Country Code Extraction
|
|
const countryCodeElement = party.querySelector('cac\\:Country cbc\\:IdentificationCode, Country IdentificationCode');
|
|
const countryCode = countryCodeElement ? countryCodeElement.textContent.trim() : 'RO';
|
|
|
|
// Postal Address Details
|
|
const postalAddress = party.querySelector('cac\\:PostalAddress, PostalAddress');
|
|
const streetName = postalAddress ?
|
|
getXMLValue(postalAddress, 'cbc\\:StreetName, StreetName') : '';
|
|
const cityName = postalAddress ?
|
|
getXMLValue(postalAddress, 'cbc\\:CityName, CityName') : '';
|
|
const countyCode = postalAddress ?
|
|
getXMLValue(postalAddress, 'cbc\\:CountrySubentity, CountrySubentity') : '';
|
|
|
|
// Set inputs
|
|
document.querySelector(`[name="${prefix}Name"]`).value =
|
|
getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:RegistrationName, PartyLegalEntity RegistrationName');
|
|
document.querySelector(`[name="${prefix}VAT"]`).value =
|
|
getXMLValue(party, 'cac\\:PartyTaxScheme cbc\\:CompanyID, PartyTaxScheme CompanyID');
|
|
document.querySelector(`[name="${prefix}CompanyId"]`).value =
|
|
getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:CompanyID, PartyLegalEntity CompanyID');
|
|
document.querySelector(`[name="${prefix}Address"]`).value = streetName;
|
|
document.querySelector(`[name="${prefix}City"]`).value = cityName;
|
|
document.querySelector(`[name="${prefix}Phone"]`).value = phone;
|
|
document.querySelector(`[name="${prefix}ContactName"]`).value = contactName;
|
|
document.querySelector(`[name="${prefix}Email"]`).value = email;
|
|
|
|
// Country Select
|
|
const countrySelect = document.querySelector(`[name="${prefix}Country"]`);
|
|
if (countrySelect) {
|
|
countrySelect.value = countryCode;
|
|
countrySelect.dataset.xmlValue = countryCode;
|
|
}
|
|
|
|
// County Select
|
|
const countySelect = document.querySelector(`[name="${prefix}CountrySubentity"]`);
|
|
if (countySelect) {
|
|
countySelect.value = countyCode;
|
|
countySelect.dataset.xmlValue = countyCode;
|
|
}
|
|
}
|
|
|
|
const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty');
|
|
if (supplierParty) {
|
|
const supplierPartyDetails = supplierParty.querySelector('cac\\:Party, Party');
|
|
if (supplierPartyDetails) {
|
|
extractPartyDetails(supplierPartyDetails, 'supplier');
|
|
}
|
|
}
|
|
|
|
const customerParty = xmlDoc.querySelector('cac\\:AccountingCustomerParty, AccountingCustomerParty');
|
|
if (customerParty) {
|
|
const customerPartyDetails = customerParty.querySelector('cac\\:Party, Party');
|
|
if (customerPartyDetails) {
|
|
extractPartyDetails(customerPartyDetails, 'customer');
|
|
}
|
|
}
|
|
|
|
initializeLocationSelectors();
|
|
}
|
|
|
|
function populateAllowanceCharges(xmlDoc) {
|
|
const charges = parseAllowanceCharges(xmlDoc);
|
|
displayAllowanceCharges(charges);
|
|
|
|
charges.forEach((_, index) => {
|
|
setupAllowanceChargeListeners(index);
|
|
addChargeVatTypeChangeListener(index);
|
|
});
|
|
}
|
|
|
|
function populateLineItems(xmlDoc) {
|
|
const lineItems = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine');
|
|
const lineItemsContainer = document.getElementById('lineItems');
|
|
lineItemsContainer.innerHTML = '<h2 class="section-title">Articole Factură <button type="button" class="button button-small" onclick="addLineItem()">Adaugă Articol</button></h2>';
|
|
|
|
lineItems.forEach((item, index) => {
|
|
const quantity = getXMLValue(item, 'cbc\\:InvoicedQuantity, InvoicedQuantity', '0');
|
|
const unitCode = item.querySelector('cbc\\:InvoicedQuantity, InvoicedQuantity')?.getAttribute('unitCode') || 'EA';
|
|
const price = getXMLValue(item, 'cac\\:Price cbc\\:PriceAmount, Price PriceAmount', '0');
|
|
const itemElement = item.querySelector('cac\\:Item, Item');
|
|
const description = getXMLValue(itemElement, 'cbc\\:Name, Name', '');
|
|
const itemDescription = getXMLValue(itemElement, 'cbc\\:Description, Description', '');
|
|
|
|
const taxCategory = itemElement.querySelector('cac\\:ClassifiedTaxCategory, ClassifiedTaxCategory');
|
|
const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID') || 'S';
|
|
const vatRate = getXMLValue(taxCategory, 'cbc\\:Percent, Percent') || '19';
|
|
|
|
// Extragem discountul și codul de motiv de pe linie dacă există
|
|
let lineDiscount = 0;
|
|
let discountReasonCode = '';
|
|
const allowanceCharge = item.querySelector('cac\\:AllowanceCharge, AllowanceCharge');
|
|
if (allowanceCharge) {
|
|
const chargeIndicator = getXMLValue(allowanceCharge, 'cbc\\:ChargeIndicator, ChargeIndicator');
|
|
if (chargeIndicator === 'false') {
|
|
lineDiscount = parseFloat(getXMLValue(allowanceCharge, 'cbc\\:Amount, Amount', '0'));
|
|
discountReasonCode = getXMLValue(allowanceCharge, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode', '');
|
|
}
|
|
}
|
|
|
|
addUnitCode(unitCode);
|
|
const lineItemHtml = createLineItemHTML(
|
|
index, description, quantity, price, vatRate, unitCode, vatType,
|
|
itemDescription, lineDiscount, discountReasonCode
|
|
);
|
|
lineItemsContainer.insertAdjacentHTML('beforeend', lineItemHtml);
|
|
|
|
// Parse identifications after adding the line item HTML
|
|
if (itemElement) {
|
|
parseIdentifications(itemElement, index);
|
|
}
|
|
|
|
if (!isVATRegistered()) {
|
|
const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`);
|
|
const vatRateInput = document.querySelector(`[name="vatRate${index}"]`);
|
|
if (vatTypeSelect && vatRateInput) {
|
|
vatTypeSelect.value = 'O';
|
|
vatTypeSelect.disabled = true;
|
|
vatRateInput.value = '0';
|
|
vatRateInput.disabled = true;
|
|
}
|
|
}
|
|
|
|
// Enable/disable discount reason code field based on discount value
|
|
const discountInput = document.querySelector(`[name="lineDiscount${index}"]`);
|
|
const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`);
|
|
if (discountInput && reasonCodeSelect) {
|
|
reasonCodeSelect.disabled = lineDiscount <= 0;
|
|
}
|
|
|
|
handleLineItemChange(index);
|
|
});
|
|
|
|
}
|
|
|
|
function setupAllowanceChargeListeners(index) {
|
|
const chargeAmountInput = document.querySelector(`[name="chargeAmount${index}"]`);
|
|
const chargeTypeInput = document.querySelector(`[name="chargeType${index}"]`);
|
|
const chargeVatTypeInput = document.querySelector(`[name="chargeVatType${index}"]`);
|
|
const chargeVatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`);
|
|
const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`);
|
|
const reasonInput = document.querySelector(`[name="chargeReason${index}"]`);
|
|
|
|
// Add change listeners to all inputs
|
|
[chargeAmountInput, chargeTypeInput, chargeVatTypeInput, chargeVatRateInput].forEach(input => {
|
|
if (input) {
|
|
input.addEventListener('change', () => {
|
|
manuallyEditedVatRows.clear();
|
|
refreshTotals();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add reason code change listener
|
|
if (reasonCodeSelect) {
|
|
reasonCodeSelect.addEventListener('change', () => {
|
|
const isCharge = chargeTypeInput.value === 'true';
|
|
const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES;
|
|
reasonInput.value = codes[reasonCodeSelect.value] || '';
|
|
});
|
|
}
|
|
|
|
// Special handling for VAT type changes
|
|
if (chargeVatTypeInput) {
|
|
chargeVatTypeInput.addEventListener('change', () => handleChargeVatTypeChange(index));
|
|
}
|
|
}
|
|
|
|
function addChargeVatTypeChangeListener(index) {
|
|
const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`);
|
|
if (vatTypeSelect) {
|
|
vatTypeSelect.addEventListener('change', function() {
|
|
handleChargeVatTypeChange(index);
|
|
// Force refresh of VAT breakdown
|
|
displayVATBreakdown();
|
|
updateTotals();
|
|
});
|
|
}
|
|
}
|
|
|
|
function parseAllowanceCharges(xmlDoc) {
|
|
const charges = [];
|
|
// Folosim Set pentru a preveni dublarea elementelor din cauza namespace-urilor
|
|
const processedIds = new Set();
|
|
|
|
// Selectăm toate elementele AllowanceCharge care sunt copii direcți ai Invoice
|
|
const allowanceCharges = xmlDoc.querySelectorAll('cac\\:AllowanceCharge, AllowanceCharge');
|
|
|
|
allowanceCharges.forEach(ac => {
|
|
// Verificăm dacă elementul este copil direct al Invoice și nu a fost deja procesat
|
|
if (ac.parentElement === xmlDoc.documentElement) {
|
|
const amount = getXMLValue(ac, 'cbc\\:Amount, Amount');
|
|
const reasonCode = getXMLValue(ac, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode');
|
|
|
|
// Creăm un ID unic bazat pe cod și valoare pentru a evita duplicatele
|
|
const uniqueId = `${reasonCode}-${amount}`;
|
|
|
|
if (!processedIds.has(uniqueId)) {
|
|
processedIds.add(uniqueId);
|
|
|
|
const charge = {
|
|
isCharge: getXMLValue(ac, 'cbc\\:ChargeIndicator, ChargeIndicator') === 'true',
|
|
reasonCode: reasonCode,
|
|
reason: getXMLValue(ac, 'cbc\\:AllowanceChargeReason, AllowanceChargeReason'),
|
|
amount: parseFloat(amount) || 0,
|
|
baseAmount: parseFloat(getXMLValue(ac, 'cbc\\:BaseAmount, BaseAmount')) || 0,
|
|
vatRate: parseFloat(getXMLValue(ac, 'cac\\:TaxCategory cbc\\:Percent, TaxCategory Percent')) || 0,
|
|
vatTypeId: getXMLValue(ac, 'cac\\:TaxCategory cbc\\:ID, TaxCategory ID', 'S'),
|
|
multiplierFactorNumeric: parseFloat(getXMLValue(ac, 'cbc\\:MultiplierFactorNumeric, MultiplierFactorNumeric')) || 0
|
|
};
|
|
charges.push(charge);
|
|
}
|
|
}
|
|
});
|
|
|
|
return charges;
|
|
}
|
|
|
|
function displayAllowanceCharges(charges) {
|
|
const container = document.getElementById('allowanceCharges');
|
|
container.innerHTML = '<h2 class="section-title">Reduceri și Taxe Suplimentare <button type="button" class="button button-small" onclick="addAllowanceCharge()">Adaugă Reducere/Taxă</button></h2>';
|
|
|
|
charges.forEach((charge, index) => {
|
|
const html = createAllowanceChargeHTML(index, charge);
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
}
|
|
|
|
function addAllowanceCharge() {
|
|
const container = document.getElementById('allowanceCharges');
|
|
const index = document.querySelectorAll('.allowance-charge').length;
|
|
|
|
// Create new charge with default values
|
|
const newCharge = {
|
|
isCharge: true,
|
|
reasonCode: 'TV',
|
|
reason: 'Cheltuieli transport',
|
|
amount: 0,
|
|
vatRate: 19.0,
|
|
vatTypeId: 'S'
|
|
};
|
|
|
|
// Add the HTML
|
|
const html = createAllowanceChargeHTML(index, newCharge);
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
|
|
// Setup event listeners
|
|
setupAllowanceChargeListeners(index);
|
|
|
|
// Force refresh of totals and VAT
|
|
refreshTotals();
|
|
}
|
|
|
|
function removeAllowanceCharge(index) {
|
|
const charge = document.querySelector(`.allowance-charge[data-index="${index}"]`);
|
|
if (charge) {
|
|
// Remove the element
|
|
charge.remove();
|
|
|
|
// Renumber remaining charges
|
|
renumberAllowanceCharges();
|
|
|
|
// Clear manual edits and refresh totals
|
|
manuallyEditedVatRows.clear();
|
|
refreshTotals();
|
|
}
|
|
}
|
|
|
|
function renumberAllowanceCharges() {
|
|
document.querySelectorAll('.allowance-charge').forEach((charge, newIndex) => {
|
|
// Update data-index
|
|
charge.dataset.index = newIndex;
|
|
|
|
// Update all input names
|
|
charge.querySelectorAll('input, select').forEach(input => {
|
|
const name = input.getAttribute('name');
|
|
if (name) {
|
|
const baseName = name.replace(/\d+$/, '');
|
|
input.setAttribute('name', baseName + newIndex);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
function addLineItem() {
|
|
const container = document.getElementById('lineItems');
|
|
const index = document.querySelectorAll('.line-item').length;
|
|
|
|
// Determină tipul implicit de TVA bazat pe statusul furnizorului
|
|
const defaultVatType = isVATRegistered() ? 'S' : 'O';
|
|
const defaultVatRate = isVATRegistered() ? '19' : '0';
|
|
|
|
const lineItemHtml = createLineItemHTML(index, '', '1', '0', defaultVatRate, 'EA', defaultVatType);
|
|
container.insertAdjacentHTML('beforeend', lineItemHtml);
|
|
|
|
const newItem = container.lastElementChild;
|
|
const vatTypeSelect = newItem.querySelector(`[name="vatType${index}"]`);
|
|
|
|
if (!isVATRegistered()) {
|
|
vatTypeSelect.disabled = true;
|
|
newItem.querySelector(`[name="vatRate${index}"]`).disabled = true;
|
|
}
|
|
|
|
// Adăugăm event handlers pentru noua linie
|
|
handleLineItemChange(index);
|
|
|
|
manuallyEditedVatRows.clear();
|
|
refreshTotals();
|
|
}
|
|
|
|
function removeLineItem(index) {
|
|
const lineItem = document.querySelector(`.line-item[data-index="${index}"]`);
|
|
if (lineItem) {
|
|
lineItem.remove();
|
|
renumberLineItems();
|
|
manuallyEditedVatRows.clear(); // Clear manual edits
|
|
refreshTotals(); // Recalculate all totals
|
|
}
|
|
}
|
|
|
|
function handleLineItemChange(index) {
|
|
const quantityInput = document.querySelector(`[name="quantity${index}"]`);
|
|
const priceInput = document.querySelector(`[name="price${index}"]`);
|
|
const discountInput = document.querySelector(`[name="lineDiscount${index}"]`);
|
|
const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`);
|
|
|
|
// Handler pentru schimbarea discount-ului
|
|
discountInput?.addEventListener('input', function() {
|
|
const discountValue = parseFloat(this.value) || 0;
|
|
if (reasonCodeSelect) {
|
|
reasonCodeSelect.disabled = discountValue == 0; // activat pentru orice valoare nenulă
|
|
// Dacă discount != 0 și nu e selectat niciun cod, selectăm codul implicit
|
|
if (discountValue != 0 && !reasonCodeSelect.value) {
|
|
reasonCodeSelect.value = '95'; // Cod implicit pentru reducere
|
|
}
|
|
}
|
|
updateTotals();
|
|
});
|
|
|
|
// Handlers pentru cantitate și preț
|
|
quantityInput?.addEventListener('change', updateTotals);
|
|
priceInput?.addEventListener('change', updateTotals);
|
|
|
|
// Force refresh
|
|
updateTotals();
|
|
}
|
|
|
|
function handleStorno() {
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const quantityInput = document.querySelector(`[name="quantity${index}"]`);
|
|
const currentValue = parseFloat(quantityInput.value);
|
|
quantityInput.value = -currentValue;
|
|
});
|
|
|
|
document.querySelectorAll('.allowance-charge').forEach((item, index) => {
|
|
const amountInput = document.querySelector(`[name="chargeAmount${index}"]`);
|
|
const currentAmount = parseFloat(amountInput.value);
|
|
amountInput.value = -currentAmount;
|
|
});
|
|
|
|
document.querySelectorAll('.vat-row').forEach(row => {
|
|
const baseInput = row.querySelector('.vat-base');
|
|
const amountInput = row.querySelector('.vat-amount');
|
|
|
|
if (baseInput) {
|
|
const currentBase = parseFloat(baseInput.value) || 0;
|
|
baseInput.value = (-currentBase).toFixed(2);
|
|
}
|
|
|
|
if (amountInput) {
|
|
const currentAmount = parseFloat(amountInput.value) || 0;
|
|
amountInput.value = (-currentAmount).toFixed(2);
|
|
}
|
|
});
|
|
|
|
manuallyEditedVatRows.clear();
|
|
refreshTotals();
|
|
|
|
if (currentInvoice) {
|
|
updateTaxTotals(currentInvoice);
|
|
}
|
|
}
|
|
|
|
function updateTotals() {
|
|
// Calculăm totalurile liniilor (deja nete, după discount)
|
|
const lineItemTotals = calculateLineItemTotals();
|
|
const chargeTotals = calculateChargeTotals();
|
|
|
|
const subtotal = lineItemTotals.subtotal;
|
|
const allowances = chargeTotals.allowances;
|
|
const charges = chargeTotals.charges;
|
|
const netAmount = subtotal - allowances + charges;
|
|
|
|
// Calculate VAT breakdown
|
|
const { vatBreakdown } = calculateVATBreakdown();
|
|
let totalVat = 0;
|
|
|
|
vatBreakdown.forEach((entry) => {
|
|
if (entry.type === 'S') {
|
|
totalVat += entry.vatAmount;
|
|
}
|
|
});
|
|
|
|
// Display totals with 2 decimal places
|
|
displayTotals({
|
|
subtotal: roundNumber(subtotal, 2),
|
|
allowances: roundNumber(allowances, 2),
|
|
charges: roundNumber(charges, 2),
|
|
netAmount: roundNumber(netAmount, 2),
|
|
totalVat: roundNumber(totalVat, 2),
|
|
total: roundNumber(netAmount + totalVat, 2),
|
|
vatBreakdown
|
|
});
|
|
}
|
|
|
|
function refreshTotals() {
|
|
// Calculate line items first
|
|
const lineItemTotals = calculateLineItemTotals();
|
|
const chargeTotals = calculateChargeTotals();
|
|
|
|
const subtotal = lineItemTotals.subtotal;
|
|
const allowances = chargeTotals.allowances;
|
|
const charges = chargeTotals.charges;
|
|
const netAmount = subtotal - allowances + charges;
|
|
|
|
// Calculate VAT breakdown
|
|
const { vatBreakdown } = calculateVATBreakdown();
|
|
let totalVat = 0;
|
|
if (vatBreakdown) {
|
|
vatBreakdown.forEach(entry => {
|
|
totalVat += entry.vatAmount;
|
|
});
|
|
}
|
|
|
|
// Display totals
|
|
displayTotals({
|
|
subtotal,
|
|
allowances,
|
|
charges,
|
|
netAmount,
|
|
totalVat,
|
|
total: netAmount + totalVat,
|
|
vatBreakdown
|
|
});
|
|
}
|
|
|
|
function calculateLineItemTotals() {
|
|
let subtotal = 0;
|
|
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0;
|
|
const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0;
|
|
const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0;
|
|
|
|
// LineExtensionAmount = cantitate * preț - discount
|
|
const lineAmount = (quantity * price) - lineDiscount;
|
|
subtotal += lineAmount;
|
|
});
|
|
|
|
return {
|
|
subtotal: roundNumber(subtotal, 2)
|
|
};
|
|
}
|
|
|
|
function calculateChargeTotals() {
|
|
let allowances = 0;
|
|
let charges = 0;
|
|
|
|
document.querySelectorAll('.allowance-charge').forEach((item, index) => {
|
|
const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true';
|
|
const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0;
|
|
if (isCharge) {
|
|
charges += amount;
|
|
} else {
|
|
allowances += amount;
|
|
}
|
|
});
|
|
|
|
return {
|
|
allowances: roundNumber(allowances, 2),
|
|
charges: roundNumber(charges, 2)
|
|
};
|
|
}
|
|
|
|
function calculateTotals() {
|
|
let subtotal = 0;
|
|
let vatBreakdown = new Map();
|
|
|
|
// Calculăm totalurile liniilor
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0;
|
|
const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0;
|
|
const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0;
|
|
const vatType = document.querySelector(`[name="vatType${index}"]`).value;
|
|
const vatRate = parseFloat(document.querySelector(`[name="vatRate${index}"]`).value) || 0;
|
|
|
|
// LineExtensionAmount pentru linie = (cantitate * preț) - discount linie
|
|
const lineTotal = (quantity * price) - lineDiscount;
|
|
subtotal += lineTotal;
|
|
|
|
// Actualizăm baza de TVA pentru această linie
|
|
const key = `${vatRate}-${vatType}`;
|
|
if (!vatBreakdown.has(key)) {
|
|
vatBreakdown.set(key, {
|
|
baseAmount: 0,
|
|
vatAmount: 0,
|
|
rate: vatRate,
|
|
type: vatType
|
|
});
|
|
}
|
|
|
|
const entry = vatBreakdown.get(key);
|
|
// Adăugăm la baza de TVA valoarea netă a liniei (după discount)
|
|
entry.baseAmount += lineTotal;
|
|
});
|
|
|
|
// Calculăm discounturile și taxele globale
|
|
const { allowances, charges } = calculateChargeTotals();
|
|
|
|
// Calculăm suma netă după aplicarea discount-urilor și taxelor globale
|
|
const netAmount = subtotal - allowances + charges;
|
|
|
|
// Pentru fiecare discount/taxă globală, ajustăm baza de TVA corespunzătoare
|
|
document.querySelectorAll('.allowance-charge').forEach((item, index) => {
|
|
const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0;
|
|
const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value;
|
|
const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0;
|
|
const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true';
|
|
|
|
const key = `${vatRate}-${vatType}`;
|
|
if (!vatBreakdown.has(key)) {
|
|
vatBreakdown.set(key, {
|
|
baseAmount: 0,
|
|
vatAmount: 0,
|
|
rate: vatRate,
|
|
type: vatType
|
|
});
|
|
}
|
|
|
|
const entry = vatBreakdown.get(key);
|
|
// Ajustăm baza de TVA în funcție de tipul operațiunii (discount/taxă)
|
|
const adjustmentAmount = isCharge ? amount : -amount;
|
|
entry.baseAmount += adjustmentAmount;
|
|
});
|
|
|
|
// Calculăm TVA-ul pentru fiecare rată
|
|
let totalVat = 0;
|
|
vatBreakdown.forEach((entry, key) => {
|
|
if (entry.type === 'S') {
|
|
entry.vatAmount = roundNumber(entry.baseAmount * entry.rate / 100, 2);
|
|
totalVat += entry.vatAmount;
|
|
}
|
|
});
|
|
|
|
return {
|
|
subtotal: roundNumber(subtotal),
|
|
allowances: roundNumber(allowances),
|
|
charges: roundNumber(charges),
|
|
netAmount: roundNumber(netAmount),
|
|
totalVat: roundNumber(totalVat),
|
|
total: roundNumber(netAmount + totalVat),
|
|
vatBreakdown
|
|
};
|
|
}
|
|
|
|
function calculateVATBases(lines, globalAllowances, globalCharges) {
|
|
// Initialize VAT bases
|
|
const vatBases = {};
|
|
|
|
// First pass: calculate base amounts per VAT rate from lines
|
|
lines.forEach(line => {
|
|
const vatRate = line.vatRate;
|
|
if (!vatBases[vatRate]) {
|
|
vatBases[vatRate] = {
|
|
base: 0,
|
|
allowances: 0,
|
|
charges: 0
|
|
};
|
|
}
|
|
|
|
const lineTotal = line.quantity * line.price;
|
|
const lineAfterDiscount = lineTotal - line.lineDiscount;
|
|
vatBases[vatRate].base += lineAfterDiscount;
|
|
});
|
|
|
|
// Second pass: apply global allowances and charges per VAT rate
|
|
globalAllowances.forEach(allowance => {
|
|
const vatRate = allowance.vatRate;
|
|
if (vatBases[vatRate]) {
|
|
vatBases[vatRate].allowances += allowance.amount;
|
|
}
|
|
});
|
|
|
|
globalCharges.forEach(charge => {
|
|
const vatRate = charge.vatRate;
|
|
if (vatBases[vatRate]) {
|
|
vatBases[vatRate].charges += charge.amount;
|
|
}
|
|
});
|
|
|
|
// Calculate final VAT amounts
|
|
Object.keys(vatBases).forEach(rate => {
|
|
const rateData = vatBases[rate];
|
|
const netBase = rateData.base - rateData.allowances + rateData.charges;
|
|
rateData.vatAmount = netBase * (parseFloat(rate) / 100);
|
|
});
|
|
|
|
return vatBases;
|
|
}
|
|
|
|
function calculateTotalVAT() {
|
|
let totalVat = Array.from(document.querySelectorAll('.vat-amount'))
|
|
.reduce((sum, input) => sum + formatter.parseCurrency(input.value), 0);
|
|
return roundNumber(totalVat, 2);
|
|
}
|
|
|
|
function calculateVATBreakdown() {
|
|
let vatBreakdown = new Map();
|
|
let totalVat = 0;
|
|
|
|
// Process line items
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0;
|
|
const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0;
|
|
const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0;
|
|
const vatType = document.querySelector(`[name="vatType${index}"]`).value;
|
|
const vatRate = parseFloat(document.querySelector(`[name="vatRate${index}"]`).value) || 0;
|
|
|
|
const lineAmount = (quantity * price) - lineDiscount;
|
|
const key = `${vatRate}-${vatType}`;
|
|
|
|
if (!vatBreakdown.has(key)) {
|
|
vatBreakdown.set(key, {
|
|
baseAmount: 0,
|
|
vatAmount: 0,
|
|
rate: vatRate,
|
|
type: vatType
|
|
});
|
|
}
|
|
|
|
const entry = vatBreakdown.get(key);
|
|
entry.baseAmount += lineAmount;
|
|
});
|
|
|
|
// Process allowances and charges
|
|
document.querySelectorAll('.allowance-charge').forEach((charge, index) => {
|
|
const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0;
|
|
const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value;
|
|
const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0;
|
|
const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true';
|
|
|
|
if (amount === 0) return;
|
|
|
|
const key = `${vatRate}-${vatType}`;
|
|
|
|
if (!vatBreakdown.has(key)) {
|
|
vatBreakdown.set(key, {
|
|
baseAmount: 0,
|
|
vatAmount: 0,
|
|
rate: vatRate,
|
|
type: vatType
|
|
});
|
|
}
|
|
|
|
const entry = vatBreakdown.get(key);
|
|
const adjustedAmount = isCharge ? amount : -amount;
|
|
entry.baseAmount += adjustedAmount;
|
|
});
|
|
|
|
// Calculate VAT for each rate
|
|
vatBreakdown.forEach((entry) => {
|
|
if (entry.type === 'S') {
|
|
entry.vatAmount = roundNumber(entry.baseAmount * entry.rate / 100, 2);
|
|
totalVat += entry.vatAmount;
|
|
}
|
|
});
|
|
|
|
return { vatBreakdown, totalVat: roundNumber(totalVat, 2) };
|
|
}
|
|
|
|
window.updateVATRow = function(rowId, source) {
|
|
const row = document.getElementById(rowId);
|
|
if (!row) return;
|
|
|
|
const typeSelect = row.querySelector('.vat-type');
|
|
const rateInput = row.querySelector('.vat-rate');
|
|
const baseInput = row.querySelector('.vat-base');
|
|
const amountInput = row.querySelector('.vat-amount');
|
|
|
|
if (source === 'manual') {
|
|
manuallyEditedVatRows.add(rowId);
|
|
// Keep existing values, just update totals
|
|
updateTotalVAT();
|
|
refreshTotals();
|
|
return;
|
|
}
|
|
|
|
// Only calculate VAT amount for non-manual updates
|
|
if (!manuallyEditedVatRows.has(rowId)) {
|
|
const type = typeSelect.value;
|
|
const rate = parseFloat(rateInput.value) || 0;
|
|
const base = formatter.parseCurrency(baseInput.value) || 0;
|
|
|
|
const calculatedAmount = type === 'S' ? roundNumber(base * rate / 100, 2) : 0;
|
|
amountInput.value = formatter.formatCurrency(calculatedAmount);
|
|
|
|
updateTotalVAT();
|
|
refreshTotals();
|
|
}
|
|
};
|
|
|
|
window.updateVATRowFromAmount = function(rowId) {
|
|
const row = document.getElementById(rowId);
|
|
if (!row) return;
|
|
|
|
// Just mark as manually edited and update the totals
|
|
// Do not recalculate base amount
|
|
manuallyEditedVatRows.add(rowId);
|
|
|
|
const amountInput = row.querySelector('.vat-amount');
|
|
if (amountInput) {
|
|
const value = formatter.parseCurrency(amountInput.value);
|
|
amountInput.value = formatter.formatCurrency(value);
|
|
}
|
|
|
|
let totalVat = 0;
|
|
document.querySelectorAll('.vat-row').forEach(vatRow => {
|
|
const vatAmount = formatter.parseCurrency(vatRow.querySelector('.vat-amount').value) || 0;
|
|
totalVat += vatAmount;
|
|
});
|
|
|
|
// Update just total VAT and final total
|
|
const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent);
|
|
document.getElementById('vat').textContent = formatter.formatCurrency(totalVat);
|
|
document.getElementById('total').textContent = formatter.formatCurrency(netAmount + totalVat);
|
|
};
|
|
|
|
window.removeVATRow = function(rowId) {
|
|
const row = document.getElementById(rowId);
|
|
if (row) {
|
|
manuallyEditedVatRows.delete(rowId);
|
|
row.remove();
|
|
updateTotalVAT();
|
|
refreshTotals();
|
|
}
|
|
};
|
|
|
|
window.addVATRate = function() {
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
addVATBreakdownRow(19, 0, 0);
|
|
refreshTotals();
|
|
};
|
|
|
|
function updateTotalVAT() {
|
|
const totalVat = Array.from(document.querySelectorAll('.vat-amount'))
|
|
.reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
|
|
|
|
document.getElementById('vat').textContent = totalVat.toFixed(2);
|
|
|
|
const netAmount = parseFloat(document.getElementById('netAmount').textContent) || 0;
|
|
const total = netAmount + totalVat;
|
|
document.getElementById('total').textContent = total.toFixed(2);
|
|
}
|
|
|
|
function updateVATBreakdown() {
|
|
// Șterge și reconstruiește rândurile TVA
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
const { vatBreakdown } = calculateVATBreakdown();
|
|
|
|
vatBreakdown.forEach((data, key) => {
|
|
const [rate, type] = key.split('-');
|
|
addVATBreakdownRow(
|
|
parseFloat(rate),
|
|
data.baseAmount,
|
|
data.vatAmount,
|
|
type
|
|
);
|
|
});
|
|
}
|
|
|
|
function displayVATBreakdown(xmlDoc = null) {
|
|
const container = document.getElementById('vatBreakdownRows');
|
|
if (!container) return;
|
|
|
|
// Clear container
|
|
container.innerHTML = '';
|
|
|
|
// If XML is provided, use its VAT breakdown
|
|
if (xmlDoc && originalTotals && originalTotals.vatBreakdown) {
|
|
originalTotals.vatBreakdown.forEach((vat, index) => {
|
|
addVATBreakdownRow(
|
|
vat.percent,
|
|
vat.taxableAmount,
|
|
vat.taxAmount,
|
|
vat.type,
|
|
`vat-row-${index}`,
|
|
vat.exemptionCode,
|
|
vat.exemptionReason
|
|
);
|
|
});
|
|
} else {
|
|
// Calculate current VAT breakdown
|
|
const { vatBreakdown } = calculateVATBreakdown();
|
|
vatBreakdown.forEach((data, key) => {
|
|
const [rate, type] = key.split('-');
|
|
addVATBreakdownRow(
|
|
parseFloat(rate),
|
|
data.baseAmount,
|
|
data.vatAmount,
|
|
type
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
function createEmptyInvoice() {
|
|
const parser = new DOMParser();
|
|
const xmlString = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
|
|
<cbc:ID></cbc:ID>
|
|
<cbc:IssueDate></cbc:IssueDate>
|
|
<cbc:DueDate></cbc:DueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name></cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName></cbc:StreetName>
|
|
<cbc:CityName></cbc:CityName>
|
|
<cbc:CountrySubentity></cbc:CountrySubentity>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>RO</cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName></cbc:RegistrationName>
|
|
<cbc:CompanyID></cbc:CompanyID>
|
|
</cac:PartyLegalEntity>
|
|
<cac:Contact>
|
|
<cbc:Telephone></cbc:Telephone>
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name></cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName></cbc:StreetName>
|
|
<cbc:CityName></cbc:CityName>
|
|
<cbc:CountrySubentity></cbc:CountrySubentity>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID></cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName></cbc:RegistrationName>
|
|
<cbc:CompanyID></cbc:CompanyID>
|
|
</cac:PartyLegalEntity>
|
|
<cac:Contact>
|
|
<cbc:Telephone></cbc:Telephone>
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
</Invoice>`;
|
|
return parser.parseFromString(xmlString, "text/xml");
|
|
}
|
|
|
|
function saveXML() {
|
|
if (!validateForm()) return;
|
|
|
|
try {
|
|
if (!currentInvoice) {
|
|
currentInvoice = createEmptyInvoice();
|
|
}
|
|
|
|
const xmlDoc = currentInvoice;
|
|
|
|
// Update all the data
|
|
updateBasicDetails(xmlDoc);
|
|
updatePartyDetails(xmlDoc);
|
|
updateAllowanceCharges(xmlDoc);
|
|
|
|
// Remove existing TaxTotal and LegalMonetaryTotal elements
|
|
const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal');
|
|
existingTaxTotals.forEach(el => el.remove());
|
|
|
|
const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal');
|
|
if (existingMonetaryTotal) {
|
|
existingMonetaryTotal.remove();
|
|
}
|
|
|
|
// Remove existing InvoiceLine elements
|
|
const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine');
|
|
existingLines.forEach(el => el.remove());
|
|
|
|
// Add elements in the correct order
|
|
updateTaxTotals(xmlDoc);
|
|
updateMonetaryTotals(xmlDoc);
|
|
updateLineItems(xmlDoc);
|
|
|
|
downloadXML(xmlDoc);
|
|
} catch (error) {
|
|
handleError(error, 'Eroare la salvarea fișierului XML');
|
|
}
|
|
}
|
|
|
|
function updateBasicDetails(xmlDoc) {
|
|
setXMLValue(xmlDoc, 'cbc\\:ID, ID', document.querySelector('[name="invoiceNumber"]').value);
|
|
|
|
// Remove existing Note elements
|
|
const existingNotes = xmlDoc.querySelectorAll('cbc\\:Note, Note');
|
|
existingNotes.forEach(note => note.remove());
|
|
|
|
// Split note text and create new Note elements
|
|
const noteText = document.querySelector('[name="invoiceNote"]').value;
|
|
if (noteText) {
|
|
const insertAfter = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode');
|
|
const chunks = splitNoteIntoChunks(noteText, 300);
|
|
chunks.forEach(chunk => {
|
|
const noteElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Note", chunk);
|
|
if (insertAfter && insertAfter.parentNode) {
|
|
insertAfter.parentNode.insertBefore(noteElement, insertAfter.nextSibling);
|
|
}
|
|
});
|
|
}
|
|
|
|
const issueDateValue = document.querySelector('[name="issueDate"]').value;
|
|
const dueDateValue = document.querySelector('[name="dueDate"]').value;
|
|
|
|
setXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate', parseRomanianDate(issueDateValue));
|
|
setXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate', parseRomanianDate(dueDateValue));
|
|
|
|
// Update currency codes
|
|
const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON';
|
|
setXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', documentCurrencyCode);
|
|
|
|
const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase();
|
|
if (taxCurrencyCode) {
|
|
let taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode');
|
|
if (!taxCurrencyElement) {
|
|
taxCurrencyElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxCurrencyCode");
|
|
const insertAfter = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode');
|
|
if (insertAfter && insertAfter.parentNode) {
|
|
insertAfter.parentNode.insertBefore(taxCurrencyElement, insertAfter.nextSibling);
|
|
}
|
|
}
|
|
taxCurrencyElement.textContent = taxCurrencyCode;
|
|
} else {
|
|
// Remove TaxCurrencyCode if it exists and is empty
|
|
const taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode');
|
|
if (taxCurrencyElement) {
|
|
taxCurrencyElement.parentNode.removeChild(taxCurrencyElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
function createPartyElement(xmlDoc, isSupplier, partyData) {
|
|
const party = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Party");
|
|
const hasVatPrefix = /^[A-Z]{2}/.test(partyData.vat?.trim() || '');
|
|
const IsNeplatitor = vatHasO();
|
|
|
|
// Add PartyIdentification
|
|
if (partyData.companyId) {
|
|
const partyIdentification = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyIdentification");
|
|
partyIdentification.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", partyData.companyId));
|
|
party.appendChild(partyIdentification);
|
|
}
|
|
|
|
function validateCountryCode(countryCode) {
|
|
const code = countryCode?.trim().toUpperCase() || 'RO';
|
|
return ISO_3166_1_CODES.has(code) ? code : 'RO';
|
|
}
|
|
|
|
function validateCountyCode(countryCode, countyCode) {
|
|
if (countryCode === 'RO') {
|
|
return ROMANIAN_COUNTY_CODES.has(countyCode) ? countyCode : 'RO-B';
|
|
}
|
|
return countyCode;
|
|
}
|
|
|
|
const postalAddress = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PostalAddress");
|
|
postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:StreetName", partyData.address));
|
|
postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CityName", partyData.city));
|
|
postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CountrySubentity", partyData.county));
|
|
|
|
const country = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Country");
|
|
// Ensure country code is valid ISO 3166-1 format (2 uppercase letters)
|
|
const countryCode = partyData.country?.trim().toUpperCase() || 'RO';
|
|
country.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:IdentificationCode", countryCode));
|
|
postalAddress.appendChild(country);
|
|
party.appendChild(postalAddress);
|
|
|
|
const vatTypesInUse = new Set();
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const vatType = document.querySelector(`[name="vatType${index}"]`).value;
|
|
vatTypesInUse.add(vatType);
|
|
});
|
|
|
|
// Add PartyTaxScheme for all suppliers VAT types except 'O' or all customers with VAT prefix
|
|
if (hasVatPrefix ) {
|
|
const partyTaxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyTaxScheme");
|
|
partyTaxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat.toUpperCase()));
|
|
const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme");
|
|
if (!IsNeplatitor) {
|
|
taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT"));
|
|
}
|
|
else {
|
|
// console.log('not adding VAT ID');
|
|
}
|
|
partyTaxScheme.appendChild(taxScheme);
|
|
|
|
party.appendChild(partyTaxScheme);
|
|
}
|
|
|
|
// Funcție pentru detectarea dacă există un VAT cu tipul 'O' Neplatitor de TVA
|
|
// În cazul în care există, nu se va adăuga un element TaxScheme cu ID-ul 'VAT'
|
|
function vatHasO() {
|
|
const vatTypesInUse = new Set();
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const vatType = document.querySelector(`[name="vatType${index}"]`).value;
|
|
vatTypesInUse.add(vatType);
|
|
});
|
|
return vatTypesInUse.size > 0 && vatTypesInUse.has('O');
|
|
}
|
|
|
|
// Add PartyLegalEntity
|
|
const partyLegalEntity = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyLegalEntity");
|
|
partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:RegistrationName", partyData.name));
|
|
|
|
// Add CompanyID
|
|
if (!hasVatPrefix) {
|
|
// For non-VAT registered supplier, use VAT number in CompanyID
|
|
partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat));
|
|
} else {
|
|
// For others, use companyId
|
|
partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.companyId));
|
|
}
|
|
|
|
party.appendChild(partyLegalEntity);
|
|
|
|
// Add Contact if phone, email or contactName exists
|
|
if (partyData.phone || partyData.email || partyData.contactName) {
|
|
const contact = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Contact");
|
|
|
|
if (partyData.contactName) {
|
|
contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", partyData.contactName));
|
|
}
|
|
if (partyData.phone) {
|
|
contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Telephone", partyData.phone));
|
|
}
|
|
if (partyData.email) {
|
|
contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ElectronicMail", partyData.email));
|
|
}
|
|
party.appendChild(contact);
|
|
}
|
|
return party;
|
|
}
|
|
|
|
function updatePartyDetails(xmlDoc) {
|
|
// Update supplier details
|
|
const supplierData = {
|
|
name: document.querySelector('[name="supplierName"]').value,
|
|
vat: document.querySelector('[name="supplierVAT"]').value,
|
|
companyId: document.querySelector('[name="supplierCompanyId"]').value,
|
|
address: document.querySelector('[name="supplierAddress"]').value,
|
|
city: document.querySelector('[name="supplierCity"]').value,
|
|
county: document.querySelector('[name="supplierCountrySubentity"]').value,
|
|
country: document.querySelector('[name="supplierCountry"]').value,
|
|
phone: document.querySelector('[name="supplierPhone"]').value,
|
|
contactName: document.querySelector('[name="supplierContactName"]').value,
|
|
email: document.querySelector('[name="supplierEmail"]').value
|
|
};
|
|
|
|
updatePartyXML(xmlDoc, true, supplierData);
|
|
|
|
// Update customer details
|
|
const customerData = {
|
|
name: document.querySelector('[name="customerName"]').value,
|
|
vat: document.querySelector('[name="customerVAT"]').value,
|
|
companyId: document.querySelector('[name="customerCompanyId"]').value,
|
|
address: document.querySelector('[name="customerAddress"]').value,
|
|
city: document.querySelector('[name="customerCity"]').value,
|
|
county: document.querySelector('[name="customerCountrySubentity"]').value,
|
|
country: document.querySelector('[name="customerCountry"]').value,
|
|
phone: document.querySelector('[name="customerPhone"]').value,
|
|
contactName: document.querySelector('[name="customerContactName"]').value,
|
|
email: document.querySelector('[name="customerEmail"]').value
|
|
};
|
|
|
|
updatePartyXML(xmlDoc, false, customerData);
|
|
}
|
|
|
|
function updatePartyXML(xmlDoc, isSupplier, partyData) {
|
|
const partyElement = createPartyElement(xmlDoc, isSupplier, partyData);
|
|
const parentTag = isSupplier ? 'AccountingSupplierParty' : 'AccountingCustomerParty';
|
|
let parentElement = xmlDoc.querySelector(`cac\\:${parentTag}, ${parentTag}`);
|
|
|
|
if (!parentElement) {
|
|
parentElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${parentTag}`);
|
|
const insertPoint = isSupplier ?
|
|
xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode') :
|
|
xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty');
|
|
|
|
if (insertPoint && insertPoint.parentNode) {
|
|
insertPoint.parentNode.insertBefore(parentElement, insertPoint.nextSibling);
|
|
} else {
|
|
xmlDoc.documentElement.appendChild(parentElement);
|
|
}
|
|
}
|
|
|
|
// Remove existing Party element if it exists
|
|
const existingParty = parentElement.querySelector('cac\\:Party, Party');
|
|
if (existingParty) {
|
|
existingParty.remove();
|
|
}
|
|
|
|
parentElement.appendChild(partyElement);
|
|
}
|
|
|
|
function getAllowanceCharges() {
|
|
const charges = [];
|
|
document.querySelectorAll('.allowance-charge').forEach((item, index) => {
|
|
charges.push({
|
|
isCharge: document.querySelector(`[name="chargeType${index}"]`).value === 'true',
|
|
reasonCode: document.querySelector(`[name="chargeReasonCode${index}"]`).value,
|
|
reason: document.querySelector(`[name="chargeReason${index}"]`).value,
|
|
amount: parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0,
|
|
vatRate: parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 19.0,
|
|
vatTypeId: document.querySelector(`[name="chargeVatType${index}"]`).value || 'S'
|
|
});
|
|
});
|
|
return charges;
|
|
}
|
|
|
|
function updateAllowanceCharges(xmlDoc) {
|
|
const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON';
|
|
|
|
// Remove existing global allowances/charges
|
|
const existingCharges = xmlDoc.querySelectorAll('Invoice > cac\\:AllowanceCharge, Invoice > AllowanceCharge');
|
|
existingCharges.forEach(charge => charge.remove());
|
|
|
|
// Add global allowances
|
|
document.querySelectorAll('.allowance-charge').forEach((item, index) => {
|
|
const allowanceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge");
|
|
|
|
const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true';
|
|
const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0;
|
|
const baseAmount = parseFloat(document.querySelector(`[name="chargeBaseAmount${index}"]`).value) || 0;
|
|
const reasonCode = document.querySelector(`[name="chargeReasonCode${index}"]`).value;
|
|
const reason = document.querySelector(`[name="chargeReason${index}"]`).value;
|
|
const vatTypeId = document.querySelector(`[name="chargeVatType${index}"]`).value;
|
|
const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0;
|
|
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator",
|
|
isCharge.toString()));
|
|
|
|
if (reasonCode) {
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:AllowanceChargeReasonCode", reasonCode));
|
|
}
|
|
|
|
if (reason) {
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:AllowanceChargeReason", reason));
|
|
}
|
|
|
|
// Add multiplier if exists
|
|
if (baseAmount > 0) {
|
|
const multiplier = (amount / baseAmount) * 100;
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:MultiplierFactorNumeric", multiplier.toFixed(2)));
|
|
}
|
|
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount",
|
|
amount.toFixed(2), { currencyID }));
|
|
|
|
if (baseAmount > 0) {
|
|
allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount",
|
|
baseAmount.toFixed(2), { currencyID }));
|
|
}
|
|
|
|
// Add tax category
|
|
const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory");
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatTypeId));
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent",
|
|
vatRate.toString()));
|
|
|
|
const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme");
|
|
taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT"));
|
|
taxCategory.appendChild(taxScheme);
|
|
|
|
allowanceElement.appendChild(taxCategory);
|
|
|
|
// Insert before TaxTotal
|
|
const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal');
|
|
if (taxTotal) {
|
|
xmlDoc.documentElement.insertBefore(allowanceElement, taxTotal);
|
|
} else {
|
|
xmlDoc.documentElement.appendChild(allowanceElement);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateLineItems(xmlDoc) {
|
|
const currencyID = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode').textContent;
|
|
|
|
// Șterge liniile existente
|
|
const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine');
|
|
existingLines.forEach(line => line.remove());
|
|
|
|
document.querySelectorAll('.line-item').forEach((item, index) => {
|
|
const invoiceLine = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:InvoiceLine");
|
|
|
|
// Obține valorile liniei
|
|
const quantity = document.querySelector(`[name="quantity${index}"]`)?.value || '0';
|
|
const unitCode = document.querySelector(`[name="unit${index}"]`)?.value || 'EA';
|
|
const price = document.querySelector(`[name="price${index}"]`)?.value || '0';
|
|
const description = document.querySelector(`[name="description${index}"]`)?.value || '';
|
|
const itemDescription = document.querySelector(`[name="itemDescription${index}"]`)?.value || '';
|
|
const vatType = document.querySelector(`[name="vatType${index}"]`)?.value || 'S';
|
|
const vatRate = vatType === 'AE' ? '0.00' :
|
|
(document.querySelector(`[name="vatRate${index}"]`)?.value || '0');
|
|
const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`)?.value) || 0;
|
|
|
|
// Calculează valorile liniei
|
|
const baseLineAmount = quantity * price;
|
|
const lineAmount = roundNumber(baseLineAmount - lineDiscount);
|
|
|
|
// Adaugă elementele liniei
|
|
invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", (index + 1).toString()));
|
|
|
|
const quantityElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:InvoicedQuantity", quantity);
|
|
quantityElement.setAttribute('unitCode', unitCode);
|
|
invoiceLine.appendChild(quantityElement);
|
|
|
|
// Adaugă LineExtensionAmount (valoarea după discount)
|
|
invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:LineExtensionAmount",
|
|
lineAmount.toFixed(2), { currencyID }));
|
|
|
|
// Adaugă discount pe linie dacă există
|
|
if (lineDiscount != 0) {
|
|
const allowanceCharge = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge");
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator",
|
|
lineDiscount < 0 ? "true" : "false"));
|
|
|
|
const reasonCode = document.querySelector(`[name="discountReasonCode${index}"]`)?.value || '95';
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:AllowanceChargeReasonCode", reasonCode));
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:AllowanceChargeReason", ALLOWANCE_REASON_CODES[reasonCode] || 'Reducere'));
|
|
|
|
// BaseAmount trebuie să fie valoarea netă înainte de discount
|
|
const baseAmount = Math.abs(lineAmount);
|
|
const discountAmount = Math.abs(lineDiscount);
|
|
const multiplierFactor = (discountAmount / baseAmount * 100).toFixed(2);
|
|
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:MultiplierFactorNumeric", multiplierFactor));
|
|
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount",
|
|
discountAmount.toFixed(2), { currencyID }));
|
|
|
|
allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount",
|
|
baseAmount.toFixed(2), { currencyID }));
|
|
|
|
invoiceLine.appendChild(allowanceCharge);
|
|
}
|
|
|
|
// Adaugă detaliile articolului
|
|
const itemElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Item");
|
|
|
|
if (itemDescription) {
|
|
itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Description",
|
|
itemDescription));
|
|
}
|
|
|
|
itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", description));
|
|
|
|
// Adaugă identificările
|
|
const identificationsContainer = document.querySelector(`#identifications${index}`);
|
|
if (identificationsContainer) {
|
|
saveIdentificationsToXML(xmlDoc, itemElement, index);
|
|
}
|
|
|
|
// Adaugă categoria de taxă
|
|
const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:ClassifiedTaxCategory");
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType));
|
|
if (vatType !== 'O') {
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", vatRate));
|
|
}
|
|
|
|
const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme");
|
|
taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT"));
|
|
taxCategory.appendChild(taxScheme);
|
|
itemElement.appendChild(taxCategory);
|
|
|
|
invoiceLine.appendChild(itemElement);
|
|
|
|
// Adaugă prețul
|
|
const priceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Price");
|
|
priceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:PriceAmount",
|
|
price, { currencyID }));
|
|
invoiceLine.appendChild(priceElement);
|
|
|
|
xmlDoc.documentElement.appendChild(invoiceLine);
|
|
});
|
|
}
|
|
|
|
function updateTaxTotals(xmlDoc) {
|
|
const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON';
|
|
const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase();
|
|
|
|
// Remove existing TaxTotal elements
|
|
const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal');
|
|
existingTaxTotals.forEach(element => element.parentNode.removeChild(element));
|
|
|
|
// Create main TaxTotal for document currency
|
|
const taxTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal");
|
|
|
|
// Use precise currency parsing for total VAT
|
|
const uiTotalVat = formatter.parseCurrency(document.getElementById('vat').textContent);
|
|
const taxAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount",
|
|
uiTotalVat.toFixed(2), { currencyID });
|
|
taxTotal.appendChild(taxAmountElement);
|
|
|
|
// Add TaxSubtotal elements to the main TaxTotal
|
|
const vatRows = document.querySelectorAll('.vat-row');
|
|
vatRows.forEach(row => {
|
|
const taxSubtotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxSubtotal");
|
|
|
|
const baseAmount = formatter.parseCurrency(row.querySelector('.vat-base').value) || 0;
|
|
const vatAmount = formatter.parseCurrency(row.querySelector('.vat-amount').value) || 0;
|
|
const vatType = row.querySelector('.vat-type').value;
|
|
const vatRate = parseFloat(row.querySelector('.vat-rate').value) || 0;
|
|
|
|
taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxableAmount",
|
|
baseAmount.toFixed(2), { currencyID }));
|
|
taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount",
|
|
vatAmount.toFixed(2), { currencyID }));
|
|
|
|
const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory");
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType));
|
|
|
|
const percent = vatType === 'AE' ? '0.00' : vatRate.toFixed(2);
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", percent));
|
|
|
|
// Add exemption code and reason for special VAT types
|
|
if (['E', 'K', 'O', 'AE'].includes(vatType)) {
|
|
const exemptionCode = row.querySelector('.vat-exemption-code')?.value;
|
|
const exemptionReason = row.querySelector('.vat-exemption-reason')?.value;
|
|
|
|
if (exemptionCode) {
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:TaxExemptionReasonCode", exemptionCode));
|
|
}
|
|
if (exemptionReason) {
|
|
taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
"cbc:TaxExemptionReason", exemptionReason));
|
|
}
|
|
}
|
|
|
|
const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme");
|
|
taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT"));
|
|
taxCategory.appendChild(taxScheme);
|
|
|
|
taxSubtotal.appendChild(taxCategory);
|
|
taxTotal.appendChild(taxSubtotal);
|
|
});
|
|
|
|
// Insert TaxTotal in the correct position
|
|
let insertionPoint = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge');
|
|
if (insertionPoint) {
|
|
while (insertionPoint.nextElementSibling &&
|
|
(insertionPoint.nextElementSibling.localName === 'AllowanceCharge' ||
|
|
insertionPoint.nextElementSibling.localName === 'TaxTotal')) {
|
|
insertionPoint = insertionPoint.nextElementSibling;
|
|
}
|
|
insertionPoint.parentNode.insertBefore(taxTotal, insertionPoint.nextSibling);
|
|
} else {
|
|
const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal');
|
|
if (monetaryTotal) {
|
|
monetaryTotal.parentNode.insertBefore(taxTotal, monetaryTotal);
|
|
} else {
|
|
xmlDoc.documentElement.appendChild(taxTotal);
|
|
}
|
|
}
|
|
|
|
// If tax currency is specified, add another TaxTotal element
|
|
if (taxCurrencyCode && taxCurrencyCode !== currencyID) {
|
|
const taxCurrencyTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal");
|
|
|
|
const exchangeRateInput = document.querySelector('[name="exchangeRate"]');
|
|
const exchangeRate = exchangeRateInput ? parseFloat(exchangeRateInput.value) || 1 : 1;
|
|
const taxCurrencyVAT = uiTotalVat * exchangeRate;
|
|
|
|
const taxCurrencyAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount",
|
|
taxCurrencyVAT.toFixed(2), { currencyID: taxCurrencyCode });
|
|
taxCurrencyTotal.appendChild(taxCurrencyAmountElement);
|
|
|
|
// Insert after the main TaxTotal
|
|
taxTotal.parentNode.insertBefore(taxCurrencyTotal, taxTotal.nextSibling);
|
|
}
|
|
}
|
|
|
|
function updateMonetaryTotals(xmlDoc) {
|
|
const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON';
|
|
|
|
// Use direct parsing to preserve full precision and manual edits
|
|
const subtotal = formatter.parseCurrency(document.getElementById('subtotal').textContent);
|
|
const allowances = formatter.parseCurrency(document.getElementById('totalAllowances').textContent);
|
|
const charges = formatter.parseCurrency(document.getElementById('totalCharges').textContent);
|
|
const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent);
|
|
const totalVat = formatter.parseCurrency(document.getElementById('vat').textContent);
|
|
const total = formatter.parseCurrency(document.getElementById('total').textContent);
|
|
|
|
// Rest of the function remains the same...
|
|
const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal');
|
|
if (existingMonetaryTotal) {
|
|
existingMonetaryTotal.parentNode.removeChild(existingMonetaryTotal);
|
|
}
|
|
|
|
const monetaryTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:LegalMonetaryTotal");
|
|
|
|
const amounts = {
|
|
"LineExtensionAmount": subtotal,
|
|
"TaxExclusiveAmount": netAmount,
|
|
"TaxInclusiveAmount": total,
|
|
"AllowanceTotalAmount": allowances,
|
|
"ChargeTotalAmount": charges,
|
|
"PayableAmount": total
|
|
};
|
|
|
|
Object.entries(amounts).forEach(([elementName, value]) => {
|
|
monetaryTotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc,
|
|
`cbc:${elementName}`, value.toFixed(2), { currencyID }));
|
|
});
|
|
|
|
// Insertion logic remains the same
|
|
let insertionPoint = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal');
|
|
if (insertionPoint) {
|
|
while (insertionPoint.nextElementSibling &&
|
|
insertionPoint.nextElementSibling.localName === 'TaxTotal') {
|
|
insertionPoint = insertionPoint.nextElementSibling;
|
|
}
|
|
insertionPoint.parentNode.insertBefore(monetaryTotal, insertionPoint.nextSibling);
|
|
} else {
|
|
const firstInvoiceLine = xmlDoc.querySelector('cac\\:InvoiceLine, InvoiceLine');
|
|
if (firstInvoiceLine) {
|
|
firstInvoiceLine.parentNode.insertBefore(monetaryTotal, firstInvoiceLine);
|
|
} else {
|
|
xmlDoc.documentElement.appendChild(monetaryTotal);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getExchangeRate(fromCurrency, toCurrency) {
|
|
// For now, return 1 as default exchange rate
|
|
// TODO: Implement proper exchange rate handling
|
|
return 1;
|
|
}
|
|
|
|
// Update the invoice form to include exchange rate when tax currency is different
|
|
function addExchangeRateField() {
|
|
const taxCurrencyInput = document.querySelector('[name="taxCurrencyCode"]');
|
|
const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]');
|
|
|
|
function updateExchangeRateVisibility() {
|
|
const taxCurrency = taxCurrencyInput.value.toUpperCase();
|
|
const documentCurrency = documentCurrencyInput.value.toUpperCase();
|
|
|
|
let exchangeRateContainer = document.getElementById('exchangeRateContainer');
|
|
if (taxCurrency && taxCurrency !== documentCurrency) {
|
|
if (!exchangeRateContainer) {
|
|
const container = document.createElement('div');
|
|
container.id = 'exchangeRateContainer';
|
|
container.className = 'form-group';
|
|
container.innerHTML = `
|
|
<label class="form-label">Curs Valutar ${documentCurrency}/${taxCurrency}</label>
|
|
<input type="number" class="form-input" name="exchangeRate"
|
|
step="0.0001" min="0" value="1"
|
|
onchange="refreshTotals()">
|
|
`;
|
|
taxCurrencyInput.parentNode.after(container);
|
|
} else {
|
|
// Update label if currencies changed
|
|
const label = exchangeRateContainer.querySelector('label');
|
|
label.textContent = `Curs Valutar ${documentCurrency}/${taxCurrency}`;
|
|
}
|
|
} else if (exchangeRateContainer) {
|
|
exchangeRateContainer.remove();
|
|
}
|
|
}
|
|
|
|
taxCurrencyInput.addEventListener('input', updateExchangeRateVisibility);
|
|
documentCurrencyInput.addEventListener('input', updateExchangeRateVisibility);
|
|
|
|
// Initial check
|
|
updateExchangeRateVisibility();
|
|
}
|
|
|
|
|
|
// Utility functions
|
|
function getXMLValue(xmlDoc, selector, defaultValue = '') {
|
|
if (!xmlDoc) return defaultValue;
|
|
try {
|
|
const element = xmlDoc.querySelector(selector);
|
|
const value = element ? element.textContent : defaultValue;
|
|
// console.log(`getXMLValue: Selector: ${selector}, Value: ${value}`);
|
|
return value;
|
|
} catch (error) {
|
|
console.warn(`Eroare la obținerea valorii pentru selectorul ${selector}:`, error);
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
function setXMLValue(xmlDoc, selector, value) {
|
|
try {
|
|
const element = xmlDoc.querySelector(selector);
|
|
if (element) {
|
|
element.textContent = value;
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.warn(`Eroare la setarea valorii pentru selectorul ${selector}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createXMLElement(xmlDoc, namespace, elementName, value = '', attributes = {}) {
|
|
const element = xmlDoc.createElementNS(namespace, elementName);
|
|
if (value) {
|
|
element.textContent = value;
|
|
}
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
element.setAttribute(key, value);
|
|
});
|
|
return element;
|
|
}
|
|
|
|
function formatXML(xmlString) {
|
|
let formatted = '';
|
|
let indent = '';
|
|
const tab = ' ';
|
|
|
|
xmlString.split(/>\s*</).forEach(node => {
|
|
if (node.match(/^\/\w/)) {
|
|
indent = indent.substring(tab.length);
|
|
}
|
|
formatted += indent + '<' + node + '>\r\n';
|
|
if (node.match(/^<?\w[^>]*[^\/]$/) && !node.startsWith("?")) {
|
|
indent += tab;
|
|
}
|
|
});
|
|
|
|
return formatted.substring(1, formatted.length - 3);
|
|
}
|
|
|
|
function downloadXML(xmlDoc) {
|
|
const serializer = new XMLSerializer();
|
|
let xmlString = serializer.serializeToString(xmlDoc);
|
|
|
|
if (!xmlString.startsWith('<?xml')) {
|
|
xmlString = '<?xml version="1.0" encoding="UTF-8"?>\n' + xmlString;
|
|
}
|
|
|
|
xmlString = formatXML(xmlString);
|
|
|
|
const blob = new Blob([xmlString], { type: 'application/xml' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'factura_' + document.querySelector('[name="invoiceNumber"]').value + '.xml';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function roundNumber(number, decimals = 2) {
|
|
return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
}
|
|
|
|
function renumberLineItems() {
|
|
document.querySelectorAll('.line-item').forEach((item, newIndex) => {
|
|
item.dataset.index = newIndex;
|
|
|
|
item.querySelectorAll('input, select').forEach(input => {
|
|
const name = input.getAttribute('name');
|
|
if (name) {
|
|
const baseName = name.replace(/\d+$/, '');
|
|
input.setAttribute('name', baseName + newIndex);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
function createIdentificationHTML(index, type, value = '', schemeId = '') {
|
|
const typeInfo = IDENTIFICATION_TYPES[type];
|
|
const id = `${type.toLowerCase()}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
if (type === 'COMMODITY') {
|
|
return `
|
|
<div class="identification-row" data-type="${type}" data-id="${id}">
|
|
<div class="identification-content">
|
|
<select class="form-input scheme-select" name="scheme_${id}">
|
|
${typeInfo.schemes.map(scheme =>
|
|
`<option value="${scheme.id}" ${scheme.id === schemeId ? 'selected' : ''}>
|
|
${scheme.name}
|
|
</option>`
|
|
).join('')}
|
|
</select>
|
|
<input type="text" class="form-input" name="value_${id}"
|
|
value="${value}" placeholder="${typeInfo.label}">
|
|
<button type="button" class="button button-small button-danger delete-identification"
|
|
onclick="removeIdentification('${id}')">✕</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="identification-row" data-type="${type}" data-id="${id}">
|
|
<div class="identification-content">
|
|
${type === 'STANDARD' ? `<input type="hidden" name="scheme_${id}" value="0160">` : ''}
|
|
<input type="text" class="form-input" name="value_${id}"
|
|
value="${value}" placeholder="${typeInfo.label}">
|
|
<button type="button" class="button button-small button-danger delete-identification"
|
|
onclick="removeIdentification('${id}')">✕</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
window.addIdentification = function(lineItemIndex, type) {
|
|
const container = document.querySelector(`#identifications${lineItemIndex}`);
|
|
if (container) {
|
|
container.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, type));
|
|
validateIdentifications(lineItemIndex);
|
|
}
|
|
}
|
|
|
|
window.removeIdentification = function(id) {
|
|
const element = document.querySelector(`[data-id="${id}"]`);
|
|
if (element) {
|
|
const lineItem = element.closest('.line-item');
|
|
element.remove();
|
|
if (lineItem) {
|
|
validateIdentifications(parseInt(lineItem.dataset.index));
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// Update XML parsing
|
|
function parseIdentifications(itemElement, lineItemIndex) {
|
|
// console.log("Parsing identifications for line", lineItemIndex);
|
|
|
|
const container = document.querySelector(`#identifications${lineItemIndex}`);
|
|
if (!container) return;
|
|
|
|
const listContainer = container.querySelector('.identifications-list');
|
|
listContainer.innerHTML = '';
|
|
|
|
// Parse BuyersItemIdentification
|
|
const buyersId = itemElement.querySelector('cac\\:BuyersItemIdentification cbc\\:ID, BuyersItemIdentification ID');
|
|
if (buyersId) {
|
|
listContainer.insertAdjacentHTML('beforeend',
|
|
createIdentificationHTML(lineItemIndex, 'BUYERS', buyersId.textContent)
|
|
);
|
|
}
|
|
|
|
// Parse SellersItemIdentification
|
|
const sellersId = itemElement.querySelector('cac\\:SellersItemIdentification cbc\\:ID, SellersItemIdentification ID');
|
|
if (sellersId) {
|
|
listContainer.insertAdjacentHTML('beforeend',
|
|
createIdentificationHTML(lineItemIndex, 'SELLERS', sellersId.textContent)
|
|
);
|
|
}
|
|
|
|
// Parse StandardItemIdentification
|
|
const standardId = itemElement.querySelector('cac\\:StandardItemIdentification cbc\\:ID, StandardItemIdentification ID');
|
|
if (standardId) {
|
|
listContainer.insertAdjacentHTML('beforeend',
|
|
createIdentificationHTML(lineItemIndex, 'STANDARD', standardId.textContent)
|
|
);
|
|
}
|
|
|
|
// Parse CommodityClassifications
|
|
const commodityClassifications = itemElement.querySelectorAll('cac\\:CommodityClassification cbc\\:ItemClassificationCode, CommodityClassification ItemClassificationCode');
|
|
commodityClassifications.forEach(classification => {
|
|
const listId = classification.getAttribute('listID') || 'CV';
|
|
const code = classification.textContent;
|
|
if (code && listId) {
|
|
listContainer.insertAdjacentHTML('beforeend',
|
|
createIdentificationHTML(lineItemIndex, 'COMMODITY', code, listId)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update XML saving
|
|
function saveIdentificationsToXML(xmlDoc, itemElement, lineItemIndex) {
|
|
const container = document.querySelector(`#identifications${lineItemIndex}`);
|
|
if (!container) return;
|
|
|
|
container.querySelectorAll('.identification-row').forEach(row => {
|
|
const type = row.dataset.type;
|
|
const id = row.dataset.id;
|
|
const schemeInput = document.querySelector(`[name="scheme_${id}"]`);
|
|
const valueInput = document.querySelector(`[name="value_${id}"]`);
|
|
|
|
if (!valueInput?.value) return;
|
|
|
|
if (type === 'COMMODITY') {
|
|
const schemeSelect = document.querySelector(`[name="scheme_${id}"]`);
|
|
if (!schemeSelect) return;
|
|
|
|
const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:CommodityClassification');
|
|
const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ItemClassificationCode', valueInput.value);
|
|
idElement.setAttribute('listID', schemeSelect.value);
|
|
identificationElement.appendChild(idElement);
|
|
itemElement.appendChild(identificationElement);
|
|
} else {
|
|
const typeInfo = IDENTIFICATION_TYPES[type];
|
|
const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${typeInfo.xmlTag}`);
|
|
const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', valueInput.value);
|
|
|
|
if (type === 'STANDARD') {
|
|
idElement.setAttribute('schemeID', '0160');
|
|
}
|
|
|
|
identificationElement.appendChild(idElement);
|
|
itemElement.appendChild(identificationElement);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// Add validation
|
|
function validateIdentifications(lineItemIndex) {
|
|
const container = document.querySelector(`#identifications${lineItemIndex}`);
|
|
if (!container) return true;
|
|
|
|
let isValid = true;
|
|
container.querySelectorAll('.identification-row').forEach(row => {
|
|
const type = row.dataset.type;
|
|
const id = row.dataset.id;
|
|
const input = row.querySelector(`[name="value_${id}"]`);
|
|
|
|
if (type === 'COMMODITY') {
|
|
const scheme = row.querySelector('.scheme-select').value;
|
|
if (scheme === 'TSP' && !/^\d{8}-\d$/.test(input.value)) {
|
|
input.classList.add('invalid');
|
|
isValid = false;
|
|
} else if (scheme === 'STI' && !/^\d{8}$/.test(input.value)) {
|
|
input.classList.add('invalid');
|
|
isValid = false;
|
|
} else {
|
|
input.classList.remove('invalid');
|
|
}
|
|
}
|
|
});
|
|
|
|
return isValid;
|
|
}
|
|
|
|
// Export for testing if needed
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
calculateTotals,
|
|
validateForm,
|
|
roundNumber,
|
|
formatXML,
|
|
createXMLElement,
|
|
getXMLValue,
|
|
setXMLValue,
|
|
formatter,
|
|
setupInlineEditing,
|
|
updateTotalDisplay,
|
|
displayTotals,
|
|
updateVATDisplay,
|
|
getDisplayValue
|
|
};
|
|
}
|
|
|
|
import { InvoicePrintHandler } from './print.js';
|
|
|
|
// Create print handler instance
|
|
const printHandler = new InvoicePrintHandler();
|
|
|
|
// Initialize when the document is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Add print controls to the UI
|
|
const headerButtonGroup = document.querySelector('.button-group');
|
|
if (headerButtonGroup) {
|
|
const printControls = document.createElement('div');
|
|
printControls.className = 'print-controls';
|
|
printControls.style.display = 'flex';
|
|
printControls.style.gap = '8px';
|
|
printControls.style.alignItems = 'center';
|
|
|
|
// Create template selector
|
|
const templateSelect = document.createElement('select');
|
|
templateSelect.className = 'form-input';
|
|
templateSelect.style.width = 'auto';
|
|
templateSelect.innerHTML = `
|
|
<option value="standard">Standard</option>
|
|
<option value="compact">Compact</option>
|
|
`;
|
|
templateSelect.addEventListener('change', (e) => {
|
|
printHandler.setTemplate(e.target.value);
|
|
});
|
|
|
|
// Create print button
|
|
const printButton = document.createElement('button');
|
|
printButton.className = 'button';
|
|
printButton.onclick = () => printHandler.print();
|
|
printButton.innerHTML = 'Printează';
|
|
|
|
// Add elements to controls
|
|
printControls.appendChild(templateSelect);
|
|
printControls.appendChild(printButton);
|
|
|
|
// Add controls to header
|
|
headerButtonGroup.appendChild(printControls);
|
|
}
|
|
}); |