feat: Enhance invoice management with PDF optimization and date fixes

Optimize PDF export layout with compact columns and more space for partner names.
Add accounting period display to invoices matching Trial Balance format. Fix date
filtering to use local timezone instead of UTC. Update invoice ordering to
chronological sequence (DATAACT, NRACT, NUME).

**Backend changes:**
- Add accounting period query from calendar table
- Add currency (valuta) and cont filter support
- Change invoice ordering to chronological (DATAACT ASC, NRACT ASC, NUME)
- Add accounting_period field to InvoiceListResponse model

**Frontend changes:**
- Optimize PDF column widths (37% for partner names, compact numeric columns)
- Add custom column width support in exportUtils
- Fix date conversion from UTC to local timezone (prevents day shift)
- Add accounting period display in PDF exports
- Enhance E2E test coverage

**Cleanup:**
- Remove obsolete Trial Balance feature documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 15:29:24 +02:00
parent a45dfa826d
commit 8eed1566a3
11 changed files with 750 additions and 1051 deletions

View File

@@ -36,12 +36,29 @@ test.describe('Invoices View', () => {
});
});
// Mock invoices endpoint
await page.route('**/api/invoices/COMP1', async route => {
// Mock invoices endpoint - FIX: Use query parameters instead of path parameter
await page.route('**/api/invoices**', async route => {
const url = route.request().url();
const urlParams = new URL(url).searchParams;
const partnerType = urlParams.get('partner_type') || 'CLIENTI';
// Return different data based on partner_type
const invoicesData = partnerType === 'CLIENTI'
? mockInvoices.filter(inv => inv.type === 'client')
: mockInvoices.filter(inv => inv.type === 'supplier');
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockInvoices),
body: JSON.stringify({
invoices: invoicesData,
total_count: invoicesData.length,
filtered_count: invoicesData.length,
total_amount: invoicesData.reduce((sum, inv) => sum + inv.totctva, 0),
page: parseInt(urlParams.get('page') || '1'),
page_size: parseInt(urlParams.get('page_size') || '50'),
has_more: false
}),
});
});
@@ -203,12 +220,225 @@ test.describe('Invoices View', () => {
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Click refresh button
await invoicesPage.clickRefreshButton();
await invoicesPage.waitForLoadingToFinish();
// Table should still be visible after refresh
expect(await invoicesPage.isInvoicesTableVisible()).toBe(true);
});
// NEW TESTS for fixed issues
test('should filter by invoice type (CLIENTI/FURNIZORI)', async ({ page }) => {
let capturedPartnerType = null;
// Intercept API requests to verify partner_type parameter
await page.route('**/api/invoices**', async route => {
const url = route.request().url();
const urlParams = new URL(url).searchParams;
capturedPartnerType = urlParams.get('partner_type');
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
invoices: [],
total_count: 0,
filtered_count: 0,
total_amount: 0,
page: 1,
page_size: 50,
has_more: false
}),
});
});
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Select FURNIZORI from dropdown
await page.locator('[placeholder="Tip factură"]').click();
await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click();
await page.waitForTimeout(1000); // Wait for API call
// Verify partner_type parameter was sent correctly
expect(capturedPartnerType).toBe('FURNIZORI');
});
test('should filter by cont (account number)', async ({ page }) => {
let capturedCont = null;
// Intercept API requests to verify cont parameter
await page.route('**/api/invoices**', async route => {
const url = route.request().url();
const urlParams = new URL(url).searchParams;
capturedCont = urlParams.get('cont');
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
invoices: [],
total_count: 0,
filtered_count: 0,
total_amount: 0,
page: 1,
page_size: 50,
has_more: false
}),
});
});
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Enter cont filter
await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111');
await page.waitForTimeout(1000); // Wait for debounced API call
// Verify cont parameter was sent correctly
expect(capturedCont).toBe('4111');
});
test('should use partner_name parameter for search', async ({ page }) => {
let capturedPartnerName = null;
let capturedSearchParam = null;
// Intercept API requests to verify correct parameter name
await page.route('**/api/invoices**', async route => {
const url = route.request().url();
const urlParams = new URL(url).searchParams;
capturedPartnerName = urlParams.get('partner_name');
capturedSearchParam = urlParams.get('search');
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
invoices: [],
total_count: 0,
filtered_count: 0,
total_amount: 0,
page: 1,
page_size: 50,
has_more: false
}),
});
});
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Search for partner name
await page.locator('[placeholder="Căutați după număr, partener..."]').fill('Test Partner');
await page.waitForTimeout(1000); // Wait for debounced API call
// Verify partner_name parameter was sent (not search)
expect(capturedPartnerName).toBe('Test Partner');
expect(capturedSearchParam).toBeNull();
});
test('should export XLSX with all filters applied', async ({ page }) => {
let exportRequestParams = null;
// Intercept export API request
await page.route('**/api/invoices**', async route => {
const url = route.request().url();
const urlParams = new URL(url).searchParams;
// Capture params if it's the export request (page_size = 999999)
if (urlParams.get('page_size') === '999999') {
exportRequestParams = {
partner_type: urlParams.get('partner_type'),
partner_name: urlParams.get('partner_name'),
cont: urlParams.get('cont'),
page_size: urlParams.get('page_size')
};
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
invoices: [
{
cont: '4111',
nract: 'INV001',
dataact: '2024-01-01',
datascad: '2024-02-01',
nume: 'Test Client',
totctva: 1000,
achitat: 500,
soldfinal: 500
}
],
total_count: 1,
filtered_count: 1,
total_amount: 1000,
page: 1,
page_size: 999999,
has_more: false
}),
});
});
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Apply filters before export
await page.locator('[placeholder="Tip factură"]').click();
await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click();
await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111');
await page.waitForTimeout(500);
// Click Excel export
const downloadPromise = page.waitForEvent('download', { timeout: 10000 }).catch(() => null);
await page.locator('button:has-text("Export Excel")').click();
await page.waitForTimeout(2000); // Wait for export to complete
// Verify export request included all filters
expect(exportRequestParams).toBeTruthy();
expect(exportRequestParams.partner_type).toBe('FURNIZORI');
expect(exportRequestParams.cont).toBe('4111');
expect(exportRequestParams.page_size).toBe('999999');
// Download may or may not occur due to mock, but we verified the API call
await downloadPromise;
});
test('should have hover effect on table rows', async ({ page }) => {
await invoicesPage.waitForPageLoad();
await invoicesPage.selectCompany('Compania Test 1');
await page.waitForSelector(invoicesPage.invoicesTable);
// Wait for table rows to load
const firstRow = page.locator('.p-datatable-tbody tr').first();
await firstRow.waitFor();
// Get initial background color
const initialBgColor = await firstRow.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Hover over the row
await firstRow.hover();
await page.waitForTimeout(300); // Wait for transition
// Get background color after hover
const hoverBgColor = await firstRow.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Background color should change on hover
expect(hoverBgColor).not.toBe(initialBgColor);
// Verify hover color is the expected blue (#e3f2fd = rgb(227, 242, 253))
expect(hoverBgColor).toBe('rgb(227, 242, 253)');
});
});