diff --git a/.bruno/BRF/api-entries--id.bru b/.bruno/BRF/api-entries--id.bru
new file mode 100644
index 0000000..89aa559
--- /dev/null
+++ b/.bruno/BRF/api-entries--id.bru
@@ -0,0 +1,20 @@
+meta {
+ name: /api/entries/:id
+ type: http
+ seq: 4
+}
+
+get {
+ url: {{base_url}}/api/entries/:id
+ body: none
+ auth: inherit
+}
+
+params:path {
+ id:
+}
+
+settings {
+ encodeUrl: true
+ timeout: 0
+}
diff --git a/.bruno/BRF/api-financial-years.bru b/.bruno/BRF/api-financial-years.bru
index 991dd5b..116a858 100644
--- a/.bruno/BRF/api-financial-years.bru
+++ b/.bruno/BRF/api-financial-years.bru
@@ -1,7 +1,7 @@
meta {
name: /api/financial-years
type: http
- seq: 4
+ seq: 5
}
get {
diff --git a/.bruno/BRF/api-invoices--id.bru b/.bruno/BRF/api-invoices--id.bru
index 4890c9c..e17cad0 100644
--- a/.bruno/BRF/api-invoices--id.bru
+++ b/.bruno/BRF/api-invoices--id.bru
@@ -5,11 +5,15 @@ meta {
}
get {
- url: {{base_url}}/api/invoices/2631
+ url: {{base_url}}/api/invoices/:id
body: none
auth: inherit
}
+params:path {
+ id: 1000
+}
+
settings {
encodeUrl: true
timeout: 0
diff --git a/.bruno/BRF/api-invoices-total-amount.bru b/.bruno/BRF/api-invoices-total-amount.bru
new file mode 100644
index 0000000..c6dd80b
--- /dev/null
+++ b/.bruno/BRF/api-invoices-total-amount.bru
@@ -0,0 +1,15 @@
+meta {
+ name: /api/invoices/total-amount
+ type: http
+ seq: 12
+}
+
+get {
+ url: {{base_url}}/api/invoices/total-amount
+ body: none
+ auth: inherit
+}
+
+settings {
+ encodeUrl: true
+}
diff --git a/.bruno/BRF/api-invoices.bru b/.bruno/BRF/api-invoices.bru
index 5d020b2..6e8aa2b 100644
--- a/.bruno/BRF/api-invoices.bru
+++ b/.bruno/BRF/api-invoices.bru
@@ -1,7 +1,7 @@
meta {
name: /api/invoices
type: http
- seq: 7
+ seq: 8
}
get {
diff --git a/.bruno/BRF/api-objects--id.bru b/.bruno/BRF/api-objects--id.bru
index 8bbd0eb..d37c795 100644
--- a/.bruno/BRF/api-objects--id.bru
+++ b/.bruno/BRF/api-objects--id.bru
@@ -1,7 +1,7 @@
meta {
name: /api/objects/:id
type: http
- seq: 5
+ seq: 6
}
get {
diff --git a/.bruno/BRF/api-objects.bru b/.bruno/BRF/api-objects.bru
index 5e1900b..0d055d2 100644
--- a/.bruno/BRF/api-objects.bru
+++ b/.bruno/BRF/api-objects.bru
@@ -1,7 +1,7 @@
meta {
name: /api/objects
type: http
- seq: 6
+ seq: 7
}
get {
diff --git a/.bruno/BRF/api-results--year.bru b/.bruno/BRF/api-results--year.bru
index 5561b81..3d6d0fa 100644
--- a/.bruno/BRF/api-results--year.bru
+++ b/.bruno/BRF/api-results--year.bru
@@ -1,7 +1,7 @@
meta {
name: /api/results/:year
type: http
- seq: 11
+ seq: 12
}
get {
diff --git a/.bruno/BRF/api-results.bru b/.bruno/BRF/api-results.bru
index 652bc37..8aeb3f9 100644
--- a/.bruno/BRF/api-results.bru
+++ b/.bruno/BRF/api-results.bru
@@ -1,7 +1,7 @@
meta {
name: /api/results
type: http
- seq: 10
+ seq: 11
}
get {
diff --git a/.bruno/BRF/api-suppliers-merge.bru b/.bruno/BRF/api-suppliers-merge.bru
new file mode 100644
index 0000000..d27c960
--- /dev/null
+++ b/.bruno/BRF/api-suppliers-merge.bru
@@ -0,0 +1,15 @@
+meta {
+ name: /api/suppliers/merge
+ type: http
+ seq: 13
+}
+
+post {
+ url: {{base_url}}/api/suppliers/merge
+ body: none
+ auth: inherit
+}
+
+settings {
+ encodeUrl: true
+}
diff --git a/.bruno/BRF/api-suppliers.bru b/.bruno/BRF/api-suppliers.bru
index 222eb98..b637fb1 100644
--- a/.bruno/BRF/api-suppliers.bru
+++ b/.bruno/BRF/api-suppliers.bru
@@ -1,7 +1,7 @@
meta {
name: /api/suppliers
type: http
- seq: 9
+ seq: 10
}
get {
diff --git a/client/public/components/entries_page.tsx b/client/public/components/entries_page.tsx
new file mode 100644
index 0000000..f980a1e
--- /dev/null
+++ b/client/public/components/entries_page.tsx
@@ -0,0 +1,101 @@
+import { h } from 'preact'
+import { useCallback, useEffect, useState } from 'preact/hooks'
+import { useLocation } from 'preact-iso'
+import { isEmpty } from 'lowline'
+import qs from 'mini-qs'
+import rek from 'rek'
+
+import Head from './head.ts'
+import serializeForm from '../../shared/utils/serialize_form.ts'
+
+const dateYear = new Date().getFullYear()
+
+const EntriesPage = () => {
+ const [journals, setJournals] = useState([])
+ const [financialYears, setFinancialYears] = useState([])
+ const [entries, setEntries] = useState([])
+
+ const location = useLocation()
+
+ const { journal: selectedJournal = 'A', year: selectedYear = dateYear } = location.query
+
+ const onSubmit = useCallback((e: SubmitEvent) => {
+ e.preventDefault()
+
+ const values = serializeForm(e.target as HTMLFormElement)
+
+ const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
+
+ location.route(`/entries${search}`)
+ }, [])
+
+ useEffect(() => {
+ rek('/api/journals').then((journals) => {
+ setJournals(journals)
+ })
+ rek('/api/financial-years').then((financialYears) => {
+ setFinancialYears(financialYears.toReversed())
+ })
+ }, [])
+
+ useEffect(() => {
+ rek(`/api/entries?journal=${selectedJournal}&year=${selectedYear}`).then((entries) => setEntries(entries))
+ }, [selectedJournal, selectedYear])
+
+ return financialYears.length && journals.length ? (
+
+
+ : Entries
+
+ Entries
+
+
+
+
+ {selectedJournal} : {selectedYear}
+
+
+
+
+ | ID |
+ Number |
+ Entry Date |
+ Transaction Date |
+ Amount |
+ Description |
+
+
+
+ {entries?.map((entry) => (
+
+ |
+ {entry.id}
+ |
+ {entry.number} |
+ {entry.entryDate?.slice(0, 10)} |
+ {entry.transactionDate?.slice(0, 10)} |
+ {entry.amount} |
+ {entry.description} |
+
+ ))}
+
+
+
+ ) : null
+}
+
+export default EntriesPage
diff --git a/client/public/components/entry_page.tsx b/client/public/components/entry_page.tsx
new file mode 100644
index 0000000..b41e83c
--- /dev/null
+++ b/client/public/components/entry_page.tsx
@@ -0,0 +1,92 @@
+import { h } from 'preact'
+import { useEffect, useState } from 'preact/hooks'
+import { useRoute, useLocation } from 'preact-iso'
+import { formatNumber } from '../utils/format_number.ts'
+import rek from 'rek'
+
+import Head from './head.ts'
+
+const EntriesPage = () => {
+ const [entry, setEntry] = useState([])
+ const location = useLocation()
+ const route = useRoute()
+
+ // console.dir(route)
+ // console.dir(location)
+ console.log(entry)
+
+ useEffect(() => {
+ rek(`/api/entries/${route.params.id}`).then((entry) => {
+ setEntry(entry)
+ })
+ }, [])
+
+ if (!entry) return
+
+ console.log(entry)
+
+ return (
+
+
+
+ {' '}
+ : Entry {entry.journal} {entry.number}{' '}
+
+
+
+ Entry {entry.journal} {entry.number}
+
+
+
+
+
+ | ID |
+ Journal |
+ Number |
+ Entry Date |
+ Transaction Date |
+ Amount |
+ Description |
+
+
+
+
+ |
+ {entry.id}
+ |
+ {entry.journal} |
+ {entry.number} |
+ {entry.entryDate?.slice(0, 10)} |
+ {entry.transactionDate?.slice(0, 10)} |
+ {entry.amount} |
+ {entry.description} |
+
+
+
+
+ Transactions
+
+
+
+ | Account |
+ Debit |
+ Credit |
+ Description |
+
+
+
+ {entry?.transactions?.map((transaction) => (
+
+ | {transaction.account_number} |
+ {transaction.amount >= 0 ? formatNumber(transaction.amount) : null} |
+ {transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null} |
+ {transaction.description} |
+
+ ))}
+
+
+
+ )
+}
+
+export default EntriesPage
diff --git a/client/public/components/invoice_page.tsx b/client/public/components/invoice_page.tsx
index 04eb88c..7c373ad 100644
--- a/client/public/components/invoice_page.tsx
+++ b/client/public/components/invoice_page.tsx
@@ -78,6 +78,7 @@ const InvoicePage = () => {
+ | Entry |
Account |
Debit |
Credit |
@@ -87,6 +88,9 @@ const InvoicePage = () => {
{invoice?.transactions?.map((transaction) => (
+ |
+ {transaction.entry_id}
+ |
{transaction.account_number} |
{transaction.amount >= 0 ? formatNumber(transaction.amount) : null} |
{transaction.amount < 0 ? formatNumber(Math.abs(transaction.amount)) : null} |
diff --git a/client/public/components/invoices_by_supplier_page.tsx b/client/public/components/invoices_by_supplier_page.tsx
index a3e413a..2486971 100644
--- a/client/public/components/invoices_by_supplier_page.tsx
+++ b/client/public/components/invoices_by_supplier_page.tsx
@@ -10,12 +10,16 @@ const format = Format.bind(null, null, 'YYYY.MM.DD')
const InvoicesPage = () => {
const [supplier, setSupplier] = useState(null)
const [invoices, setInvoices] = useState([])
+ const [totalAmount, setTotalAmount] = useState(null)
const route = useRoute()
useEffect(() => {
rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier))
rek(`/api/invoices?supplier=${route.params.supplier}`).then((invoices) => setInvoices(invoices))
+ rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then((totalAmount) =>
+ setTotalAmount(totalAmount.amount),
+ )
}, [route.params.supplier])
return (
@@ -26,6 +30,10 @@ const InvoicesPage = () => {
Invoices for {supplier?.name}
+
+ Total: {totalAmount}
+
+
diff --git a/client/public/components/invoices_page.tsx b/client/public/components/invoices_page.tsx
index 2b4f64f..4f26071 100644
--- a/client/public/components/invoices_page.tsx
+++ b/client/public/components/invoices_page.tsx
@@ -28,7 +28,9 @@ const InvoicesPage = () => {
diff --git a/client/public/components/result.tsx b/client/public/components/result.tsx
deleted file mode 100644
index 4cad2eb..0000000
--- a/client/public/components/result.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { h, type FunctionalComponent } from 'preact'
-import { useEffect, useState } from 'preact/hooks'
-import rek from 'rek'
-
-interface Props {
- year: number
-}
-
-const Result: FunctionalComponent = ({ year }) => {
- const [result, setResults] = useState([])
-
- useEffect(() => {
- rek(`/api/results/${year}`).then(setResults)
- }, [year])
-
- return (
-
-
- {result.map((result) => (
-
- | {result.accountNumber} |
- {result.description} |
- {result.amount} |
-
- ))}
-
-
- )
-}
-
-export default Result
diff --git a/client/public/components/results_page.module.scss b/client/public/components/results_page.module.scss
index f7918e1..014d7b6 100644
--- a/client/public/components/results_page.module.scss
+++ b/client/public/components/results_page.module.scss
@@ -1,5 +1,16 @@
-.years {
- display: flex;
- flex-direction: row-reverse;
- justify-content: start;
+.table {
+ td:nth-child(3),
+ td:nth-child(3) ~ td {
+ text-align: right;
+ }
+
+ tr:hover {
+ background: #eee;
+ }
+
+ td,
+ th {
+ border: 1px solid #ccc;
+ padding: 3px 5px;
+ }
}
diff --git a/client/public/components/results_page.tsx b/client/public/components/results_page.tsx
index 088ab2d..5e61506 100644
--- a/client/public/components/results_page.tsx
+++ b/client/public/components/results_page.tsx
@@ -3,17 +3,17 @@ import { useEffect, useState } from 'preact/hooks'
import rek from 'rek'
import Head from './head.ts'
import Result from './result.tsx'
+import Results from './results.tsx'
+import { formatNumber } from '../utils/format_number.ts'
import s from './results_page.module.scss'
const ResultsPage = () => {
- const [financialYears, setFinancialYears] = useState([])
- const [currentYear, setCurrentYear] = useState(null)
+ const [results, setResults] = useState([])
+ const [years, setYears] = useState([])
useEffect(() => {
- rek('/api/financial-years').then((financialYears) => {
- setFinancialYears(financialYears)
- setCurrentYear(financialYears[financialYears.length - 1].year)
- })
+ rek(`/api/results`).then(setResults)
+ rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year).toReversed()))
}, [])
return (
@@ -23,17 +23,30 @@ const ResultsPage = () => {
Results
-
- {financialYears.map((financialYear) => (
-
- ))}
-
- {currentYear ? (
-
-
{currentYear}
-
-
- ) : null}
+ {years.length && results.length && (
+
+
+
+ | Account |
+ Description |
+ {years.map((year) => (
+ {year} |
+ ))}
+
+
+
+ {results.map((result) => (
+
+ | {result.accountNumber} |
+ {result.description} |
+ {years.map((year) => (
+ {formatNumber(result[year])} |
+ ))}
+
+ ))}
+
+
+ )}
)
}
diff --git a/client/public/components/select.tsx b/client/public/components/select.tsx
new file mode 100644
index 0000000..9124a6b
--- /dev/null
+++ b/client/public/components/select.tsx
@@ -0,0 +1,3 @@
+import selectFactory from '../../shared/components/select_factory.tsx'
+
+export default selectFactory({ styles: {} })
diff --git a/client/public/routes.ts b/client/public/routes.ts
index 66a5510..e96d0ef 100644
--- a/client/public/routes.ts
+++ b/client/public/routes.ts
@@ -1,4 +1,6 @@
import Accounts from './components/accounts_page.tsx'
+import Entries from './components/entries_page.tsx'
+import Entry from './components/entry_page.tsx'
import Invoice from './components/invoice_page.tsx'
import Invoices from './components/invoices_page.tsx'
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
@@ -19,6 +21,19 @@ export default [
title: 'Accounts',
component: Accounts,
},
+ {
+ path: '/entries',
+ name: 'entries',
+ title: 'Entries',
+ component: Entries,
+ },
+ {
+ path: '/entries/:id',
+ name: 'entry',
+ title: 'Entry',
+ component: Entry,
+ nav: false,
+ },
{
path: '/objects',
name: 'objects',
diff --git a/client/public/styles/main.scss b/client/public/styles/main.scss
index 7be123f..628963c 100644
--- a/client/public/styles/main.scss
+++ b/client/public/styles/main.scss
@@ -3,3 +3,8 @@
*:after {
box-sizing: border-box;
}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/client/public/utils/format_number.ts b/client/public/utils/format_number.ts
index f95cdb4..8351b79 100644
--- a/client/public/utils/format_number.ts
+++ b/client/public/utils/format_number.ts
@@ -9,8 +9,8 @@ export const formatPrice = (price) => priceFormatter.format(price)
const numberFormatter = new Intl.NumberFormat('sv-SE', {
style: 'decimal',
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
})
export const formatNumber = (nbr) => numberFormatter.format(nbr)
diff --git a/client/shared/components/select_factory.tsx b/client/shared/components/select_factory.tsx
index a03996b..cff22cc 100644
--- a/client/shared/components/select_factory.tsx
+++ b/client/shared/components/select_factory.tsx
@@ -46,6 +46,8 @@ export default function selectFactory({ styles }): FunctionComponent<{
const [touched, setTouched] = useState(false)
const selectRef = useRef()
+ console.log(options)
+
const onBlur = useCallback(() => setTouched(true), [])
useEffect(() => {
diff --git a/docker/postgres/01-schema.sql b/docker/postgres/01-schema.sql
index 4b49f72..828a889 100644
--- a/docker/postgres/01-schema.sql
+++ b/docker/postgres/01-schema.sql
@@ -2,7 +2,7 @@
-- PostgreSQL database dump
--
-\restrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg
+\restrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd
-- Dumped from database version 18.1
-- Dumped by pg_dump version 18.1
@@ -89,6 +89,37 @@ CREATE SEQUENCE public.account_id_seq
ALTER SEQUENCE public.account_id_seq OWNED BY public.account.id;
+--
+-- Name: aliases_to_supplier; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.aliases_to_supplier (
+ id integer NOT NULL,
+ supplier_id integer NOT NULL,
+ alias text NOT NULL
+);
+
+
+--
+-- Name: aliases_to_supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.aliases_to_supplier_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: aliases_to_supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.aliases_to_supplier_id_seq OWNED BY public.aliases_to_supplier.id;
+
+
--
-- Name: dimension; Type: TABLE; Schema: public; Owner: -
--
@@ -336,7 +367,8 @@ ALTER SEQUENCE public.object_id_seq OWNED BY public.object.id;
CREATE TABLE public.supplier (
id integer NOT NULL,
name text,
- supplier_type_id integer NOT NULL
+ supplier_type_id integer NOT NULL,
+ tax_id text
);
@@ -445,6 +477,13 @@ CREATE TABLE public.transactions_to_objects (
ALTER TABLE ONLY public.account ALTER COLUMN id SET DEFAULT nextval('public.account_id_seq'::regclass);
+--
+-- Name: aliases_to_supplier id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.aliases_to_supplier ALTER COLUMN id SET DEFAULT nextval('public.aliases_to_supplier_id_seq'::regclass);
+
+
--
-- Name: dimension id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -539,6 +578,22 @@ ALTER TABLE ONLY public.account
ADD CONSTRAINT account_pkey PRIMARY KEY (id);
+--
+-- Name: aliases_to_supplier aliases_to_supplier_alias_key; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.aliases_to_supplier
+ ADD CONSTRAINT aliases_to_supplier_alias_key UNIQUE (alias);
+
+
+--
+-- Name: aliases_to_supplier aliases_to_supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.aliases_to_supplier
+ ADD CONSTRAINT aliases_to_supplier_pkey PRIMARY KEY (id);
+
+
--
-- Name: dimension dimension_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -635,6 +690,14 @@ ALTER TABLE ONLY public.supplier
ADD CONSTRAINT supplier_pkey PRIMARY KEY (id);
+--
+-- Name: supplier supplier_tax_id_key; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.supplier
+ ADD CONSTRAINT supplier_tax_id_key UNIQUE (tax_id);
+
+
--
-- Name: supplier_type supplier_type_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -659,6 +722,14 @@ ALTER TABLE ONLY public.transactions_to_objects
ADD CONSTRAINT transactions_to_objects_transaction_id_object_id_key UNIQUE (transaction_id, object_id);
+--
+-- Name: aliases_to_supplier aliases_to_supplier_supplier_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.aliases_to_supplier
+ ADD CONSTRAINT aliases_to_supplier_supplier_id_fkey FOREIGN KEY (supplier_id) REFERENCES public.supplier(id);
+
+
--
-- Name: files_to_invoice files_to_invoice_file_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -751,5 +822,5 @@ ALTER TABLE ONLY public.transactions_to_objects
-- PostgreSQL database dump complete
--
-\unrestrict LKaLyA1IGSZwb31KR2e5GrFZPB7KuPkTsiIxHVwV0aqqukXTaBdKHj8rPqgoMsg
+\unrestrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd
diff --git a/package.json b/package.json
index b922284..088d93a 100644
--- a/package.json
+++ b/package.json
@@ -29,12 +29,14 @@
"@fastify/static": "^8.3.0",
"@fastify/type-provider-typebox": "^6.1.0",
"chalk": "^5.6.2",
+ "classnames": "^2.5.1",
"easy-tz": "^0.2.0",
"fastify": "^5.6.2",
"fastify-plugin": "^5.1.0",
"knex": "^3.1.0",
"lodash": "^4.17.21",
"lowline": "^0.4.2",
+ "mini-qs": "^0.2.0",
"pg": "^8.16.3",
"pg-protocol": "^1.10.3",
"pino-abstract-transport": "^3.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8331666..a06a87b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
chalk:
specifier: ^5.6.2
version: 5.6.2
+ classnames:
+ specifier: ^2.5.1
+ version: 2.5.1
easy-tz:
specifier: ^0.2.0
version: 0.2.0
@@ -44,6 +47,9 @@ importers:
lowline:
specifier: ^0.4.2
version: 0.4.2
+ mini-qs:
+ specifier: ^0.2.0
+ version: 0.2.0
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -1085,6 +1091,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
+ classnames@2.5.1:
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
+
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@@ -1687,6 +1696,9 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
+ mini-qs@0.2.0:
+ resolution: {integrity: sha512-4hc/KDREjho6ApJC139+NS6YSiSMxz1D6nYTSdDzgSIRrJvIR2+o8qgp9ABMaO0VJgeImgAsUpc+vQArWdFiFQ==}
+
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
@@ -3091,6 +3103,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
+ classnames@2.5.1: {}
+
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@@ -3751,6 +3765,8 @@ snapshots:
mimic-function@5.0.1: {}
+ mini-qs@0.2.0: {}
+
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
diff --git a/server/lib/parse_stream.ts b/server/lib/parse_stream.ts
index df41ac7..8317d6f 100644
--- a/server/lib/parse_stream.ts
+++ b/server/lib/parse_stream.ts
@@ -40,7 +40,7 @@ const defaultDecoder = {
export default async function parseStream(stream: ReadableStream, decoder: Decoder = defaultDecoder) {
const journals = new Map()
- let currentEntryId: number
+ let currentEntry: { id: number; description: string }
let currentInvoiceId: number
let currentYear = null
const details: Record = {}
@@ -217,6 +217,10 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
await trx('invoice').insert({ financialYearId: currentYear.id, fiskenNumber, supplierId }).returning('id')
)[0]?.id
}
+
+ if (transaction.accountNumber === 2441 && currentEntry.description?.includes('Fakturajournal')) {
+ await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId)
+ }
}
if (invoiceId && currentInvoiceId) {
@@ -226,7 +230,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
const transactionId = (
await trx('transaction')
.insert({
- entryId: currentEntryId,
+ entryId: currentEntry.id,
...transaction,
invoiceId: invoiceId || currentInvoiceId,
})
@@ -313,15 +317,16 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
currentInvoiceId = null
}
- currentEntryId = (
+ currentEntry = (
await trx('entry')
.insert({
journalId,
financialYearId: currentYear!.id,
...rest,
})
- .returning('id')
- )[0].id
+ .returning(['id', 'description'])
+ )[0]
+ console.log(currentEntry)
break
}
diff --git a/server/routes/api.ts b/server/routes/api.ts
index e904576..b4579e5 100644
--- a/server/routes/api.ts
+++ b/server/routes/api.ts
@@ -9,6 +9,7 @@ import invoices from './api/invoices.ts'
import journals from './api/journals.ts'
import objects from './api/objects.ts'
import results from './api/results.ts'
+import suppliers from './api/suppliers.ts'
export const FinancialYear = Type.Object({
year: Type.Number(),
@@ -26,6 +27,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
fastify.register(journals, { prefix: '/journals' })
fastify.register(objects, { prefix: '/objects' })
fastify.register(results, { prefix: '/results' })
+ fastify.register(suppliers, { prefix: '/suppliers' })
done()
}
diff --git a/server/routes/api/entries.ts b/server/routes/api/entries.ts
index 78f802a..84a895e 100644
--- a/server/routes/api/entries.ts
+++ b/server/routes/api/entries.ts
@@ -20,7 +20,38 @@ const entryRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
return null
}
- return knex('entry').select('*').orderBy('entryDate').where({ financialYearId, journalId })
+ return knex('entry AS e')
+ .select('e.*')
+ .sum('t.amount AS amount')
+ .innerJoin('transaction AS t', 'e.id', 't.entry_id')
+ .orderBy('e.id')
+ .where({ financialYearId, journalId })
+ .andWhere('t.amount', '>', 0)
+ .groupBy('e.id')
+ },
+ })
+
+ fastify.route({
+ url: '/:id',
+ method: 'GET',
+ schema: {
+ params: Type.Object({
+ id: Type.Number(),
+ }),
+ },
+ async handler(req) {
+ return knex('entry AS e')
+ .first('e.id', 'j.identifier AS journal', 'e.number', 'e.entryDate', 'e.transactionDate', 'e.description', {
+ transactions: knex
+ .select(knex.raw('json_agg(transactions)'))
+ .from(knex('transaction').select('*').where('transaction.entryId', knex.ref('e.id')).as('transactions')),
+ })
+ .sum('t.amount AS amount')
+ .innerJoin('journal AS j', 'e.journalId', 'j.id')
+ .innerJoin('transaction AS t', 'e.id', 't.entry_id')
+ .where('e.id', req.params.id)
+ .andWhere('t.amount', '>', 0)
+ .groupBy('e.id', 'j.identifier')
},
})
diff --git a/server/routes/api/invoices.ts b/server/routes/api/invoices.ts
index 95f7c8f..a07e10f 100644
--- a/server/routes/api/invoices.ts
+++ b/server/routes/api/invoices.ts
@@ -44,10 +44,38 @@ const invoiceRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
.from(knex.select('*').from('transaction AS t').where('t.invoice_id', knex.ref('i.id')).as('transactions')),
})
.leftOuterJoin('financialYear AS fy', 'i.financialYearId', 'fy.id')
+ .orderBy('i.invoiceDate')
.where(query)
},
})
+ fastify.route({
+ url: '/total-amount',
+ method: 'GET',
+ schema: {
+ querystring: Type.Object({
+ year: Type.Optional(Type.Number()),
+ supplier: Type.Optional(Type.Number()),
+ }),
+ },
+ async handler(req) {
+ let query: { financialYearId?: number; supplierId?: number } = {}
+
+ if (req.query.year) {
+ const year = await knex('financialYear').first('*').where('year', req.query.year)
+
+ if (!year) throw new StatusError(404, `Year ${req.query.year} not found.`)
+ query.financialYearId = year.id
+ }
+
+ if (req.query.supplier) {
+ query.supplierId = req.query.supplier
+ }
+
+ return knex('invoice AS i').first().sum('i.amount AS amount').where(query)
+ },
+ })
+
fastify.route({
url: '/:id',
method: 'GET',
diff --git a/server/routes/api/results.ts b/server/routes/api/results.ts
index cc26626..49f61ed 100644
--- a/server/routes/api/results.ts
+++ b/server/routes/api/results.ts
@@ -7,36 +7,62 @@ const resultRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
url: '/',
method: 'GET',
async handler() {
- const years = await knex('financialYear').select('*')
+ const financialYears = await knex('financialYear').select('*').orderBy('year', 'asc')
- const accounts = await knex('account').select('*')
-
- return Promise.all(
- years.map((year) =>
- knex('account AS a')
- .select('a.number', 'a.description')
- .sum('t.amount as amount')
- .innerJoin('transaction AS t', function () {
- this.on('t.accountNumber', '=', 'a.number')
- })
- .innerJoin('entry AS e', function () {
- this.on('t.entryId', '=', 'e.id')
- })
- .groupBy('a.number', 'a.description')
- .where('a.number', '>=', 3000)
- .where('e.financialYearId', year.id)
- .orderBy('a.number')
- .then((result) => ({
- startDate: year.startDate,
- endDate: year.endDate,
- result,
- })),
- ),
- ).then((years) => ({
- accounts,
- years,
- }))
+ return knex('transaction AS t')
+ .select(
+ 't.accountNumber',
+ 'a.description',
+ Object.fromEntries(
+ financialYears.map((fy) => [
+ fy.year,
+ knex.raw(`SUM(CASE WHEN fy.year = ${fy.year} THEN t.amount ELSE 0 END)`),
+ ]),
+ ),
+ )
+ .sum('t.amount AS amount')
+ .innerJoin('entry AS e', function () {
+ this.on('t.entryId', '=', 'e.id')
+ })
+ .innerJoin('financialYear AS fy', 'fy.id', 'e.financialYearId')
+ .innerJoin('account AS a', function () {
+ this.on('t.accountNumber', '=', 'a.number')
+ })
+ .groupBy('t.accountNumber', 'a.description')
+ .where('t.accountNumber', '>=', 3000)
+ .orderBy('t.accountNumber')
},
+ // async handler() {
+ // const years = await knex('financialYear').select('*')
+
+ // const accounts = await knex('account').select('*')
+
+ // return Promise.all(
+ // years.map((year) =>
+ // knex('account AS a')
+ // .select('a.number', 'a.description')
+ // .sum('t.amount as amount')
+ // .innerJoin('transaction AS t', function () {
+ // this.on('t.accountNumber', '=', 'a.number')
+ // })
+ // .innerJoin('entry AS e', function () {
+ // this.on('t.entryId', '=', 'e.id')
+ // })
+ // .groupBy('a.number', 'a.description')
+ // .where('a.number', '>=', 3000)
+ // .where('e.financialYearId', year.id)
+ // .orderBy('a.number')
+ // .then((result) => ({
+ // startDate: year.startDate,
+ // endDate: year.endDate,
+ // result,
+ // })),
+ // ),
+ // ).then((years) => ({
+ // accounts,
+ // years,
+ // }))
+ // },
})
fastify.route({
diff --git a/server/routes/api/suppliers.ts b/server/routes/api/suppliers.ts
index 4ce45ba..fa9f1fa 100644
--- a/server/routes/api/suppliers.ts
+++ b/server/routes/api/suppliers.ts
@@ -24,6 +24,31 @@ const journalRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => {
},
})
+ fastify.route({
+ url: '/merge',
+ method: 'POST',
+ schema: {
+ body: Type.Object({
+ ids: Type.Array(Type.Number()),
+ }),
+ },
+ async handler(req) {
+ console.dir(req.body)
+
+ const suppliers = await knex('supplier').select('*').whereIn('id', req.body.ids)
+
+ const trx = await knex.transaction()
+
+ await trx('invoice').update('supplier_id', req.body.ids[0]).whereIn('supplierId', req.body.ids.slice(1))
+ await trx('supplier').delete().whereIn('id', req.body.ids.slice(1))
+ // 556744-4301
+
+ trx.commit()
+
+ return suppliers
+ },
+ })
+
done()
}