accounting-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
package/.eslintignore ADDED
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ bin
package/.eslintrc.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "jest": true
5
+ },
6
+ "extends": ["airbnb-base", "plugin:jest/recommended", "plugin:security/recommended", "plugin:prettier/recommended"],
7
+ "plugins": ["jest", "security", "prettier"],
8
+ "parserOptions": {
9
+ "ecmaVersion": 2020
10
+ },
11
+ "rules": {
12
+ "no-console": "off",
13
+ "func-names": "off",
14
+ "no-underscore-dangle": "off",
15
+ "no-unused-private-class-members": "off",
16
+ "consistent-return": "off",
17
+ "jest/expect-expect": "off",
18
+ "security/detect-object-injection": "off"
19
+ }
20
+ }
package/.gitattributes ADDED
@@ -0,0 +1,3 @@
1
+ # Convert text file line endings to lf
2
+ * text eol=lf
3
+ *.js text
@@ -0,0 +1,3 @@
1
+ {
2
+ "*.js": "eslint"
3
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ coverage
3
+
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "printWidth": 125
4
+ }
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # accounting-engine
2
+
3
+ Bu paket, verilen faturanın, belirtilen kurallar çerçevesinde muhasebeleştirilmesini kolaylaştırmak adına oluşturulmuştur.
4
+
5
+ ## Kurulum
6
+
7
+ Paketi projenize eklemek için aşağıdaki komutu kullanabilirsiniz:
8
+
9
+ ```bash
10
+ npm install accounting-engine
11
+ ```
12
+
13
+ ## Kullanım
14
+
15
+ ```javascript
16
+ const fs = require('fs');
17
+ const engine = require('accounting-engine');
18
+
19
+ // Fatura XML belgesinin içeriğini oku
20
+ const xmlData = fs.readFileSync('path/to/your/xml/invoice.xml', 'utf-8');
21
+ const config = fs.readFileSync('path/to/your/rules/rules.json', 'utf-8');
22
+
23
+ // Fatura XML belgesini düzenlenmiş ve küçültülmüş JSON objesine çevirmek için
24
+ const accountedInvoice = await engine.normalizeXML(config, data, 'out');
25
+
26
+ console.log(accountedInvoice);
27
+ ```
28
+
29
+ ### Örnek Kural Dosyası
30
+
31
+ ```json
32
+ {
33
+ "variables": {
34
+ "kdv_20_tax": "%20.tax",
35
+ "kdv_20_taxable": "%20.taxable",
36
+ "kdv_10_tax": "%10.tax",
37
+ "kdv_10_taxable": "%10.taxable"
38
+ },
39
+ "rules": [
40
+ {
41
+ "condition": "invoice.direction === 'out'",
42
+ "rules": [
43
+ {
44
+ "condition": "line.type === 'payable'",
45
+ "return": [
46
+ {
47
+ "borc": "line.amount",
48
+ "alacak": 0,
49
+ "hesap_kodu": "{{musteri}}"
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ "condition": "line.code === '0015'",
55
+ "rules": [
56
+ {
57
+ "condition": "line.percent === 20",
58
+ "rules": [
59
+ {
60
+ "condition": "line.type === 'tax'",
61
+ "return": [
62
+ {
63
+ "alacak": "line.amount",
64
+ "borc": 0,
65
+ "hesap_kodu": "{{kdv_20_tax}}"
66
+ }
67
+ ]
68
+ },
69
+ {
70
+ "condition": "line.type === 'taxable'",
71
+ "return": [
72
+ {
73
+ "alacak": "line.amount",
74
+ "borc": 0,
75
+ "hesap_kodu": "{{kdv_20_taxable}}"
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "condition": "line.percent === 10",
83
+ "rules": [
84
+ {
85
+ "condition": "line.type === 'tax'",
86
+ "return": [
87
+ {
88
+ "alacak": "line.amount",
89
+ "borc": 0,
90
+ "hesap_kodu": "{{kdv_10_tax}}"
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ "condition": "line.type === 'taxable'",
96
+ "return": [
97
+ {
98
+ "alacak": "line.amount",
99
+ "borc": 0,
100
+ "hesap_kodu": "{{kdv_10_taxable}}"
101
+ }
102
+ ]
103
+ }
104
+ ]
105
+ },
106
+ {
107
+ "condition": "line.percent === 1",
108
+ "rules": [
109
+ {
110
+ "condition": "line.type === 'tax'",
111
+ "return": [
112
+ {
113
+ "alacak": "line.amount",
114
+ "borc": 0,
115
+ "hesap_kodu": "{{kdv_1_tax}}"
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "condition": "line.type === 'taxable'",
121
+ "return": [
122
+ {
123
+ "alacak": "line.amount",
124
+ "borc": 0,
125
+ "hesap_kodu": "{{kdv_1_taxable}}"
126
+ }
127
+ ]
128
+ }
129
+ ]
130
+ },
131
+ {
132
+ "condition": "line.percent === 0",
133
+ "return": [
134
+ {
135
+ "alacak": "line.amount",
136
+ "borc": 0,
137
+ "hesap_kodu": "{{kdv_0_taxable}}"
138
+ }
139
+ ]
140
+ }
141
+ ]
142
+ },
143
+ {
144
+ "condition": "line.code === '0003' && line.type === 'tax'",
145
+ "return": [
146
+ {
147
+ "borc": "line.amount",
148
+ "alacak": 0,
149
+ "hesap_kodu": "{{gelir_stopaj}}"
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "condition": "line.code === '0059' && line.type === 'tax'",
155
+ "return": [
156
+ {
157
+ "alacak": "line.amount",
158
+ "borc": 0,
159
+ "hesap_kodu": "{{konaklama}}"
160
+ }
161
+ ]
162
+ }
163
+ ]
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ ## Lisans
170
+
171
+ Bu paket [MIT lisansı](LICENSE) ile lisanslanmıştır. Detaylı bilgi için lisans dosyasını kontrol edebilirsiniz.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "accounting-engine",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest -i --colors --verbose --detectOpenHandles",
8
+ "test:watch": "jest -i --watchAll",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint . --fix",
11
+ "prettier": "prettier --check **/*.js",
12
+ "prettier:fix": "prettier --write **/*.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Rahat-Fatura/accounting-engine.git"
17
+ },
18
+ "author": "Berkay Gökçe <berkaygkc7@gmail.com>",
19
+ "license": "ISC",
20
+ "bugs": {
21
+ "url": "https://github.com/Rahat-Fatura/accounting-engine/issues"
22
+ },
23
+ "homepage": "https://github.com/Rahat-Fatura/accounting-engine#readme",
24
+ "devDependencies": {
25
+ "eslint": "^8.55.0",
26
+ "eslint-config-airbnb-base": "^15.0.0",
27
+ "eslint-config-prettier": "^9.1.0",
28
+ "eslint-plugin-import": "^2.29.0",
29
+ "eslint-plugin-jest": "^27.6.0",
30
+ "eslint-plugin-prettier": "^5.0.1",
31
+ "eslint-plugin-security": "^1.7.1",
32
+ "faker": "^5.1.0",
33
+ "jest": "^29.7.0",
34
+ "lint-staged": "^15.2.0",
35
+ "prettier": "^3.1.0",
36
+ "supertest": "^6.3.3"
37
+ },
38
+ "dependencies": {
39
+ "date-and-time": "^3.1.1",
40
+ "lodash": "^4.17.21",
41
+ "moment": "^2.30.1",
42
+ "ubl2json": "^2.0.2"
43
+ }
44
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ module.exports.normalizeXML = require('./services/engine.service').normalizeXML;
2
+ module.exports.normalizeJSON = require('./services/engine.service').normalizeJSON;
@@ -0,0 +1,214 @@
1
+ /* eslint-disable security/detect-non-literal-regexp */
2
+ /* eslint-disable no-eval */
3
+ /* eslint-disable security/detect-eval-with-expression */
4
+ /* eslint-disable no-restricted-syntax */
5
+ const _ = require('lodash');
6
+ const moment = require('moment');
7
+ const ubl2json = require('ubl2json');
8
+
9
+ const updateDataWithVariables = async (variables, data) => {
10
+ let updatedData = JSON.stringify(data);
11
+ const keys = Object.keys(variables);
12
+ for await (const key of keys) {
13
+ const value = variables[key];
14
+ updatedData = updatedData.replaceAll(new RegExp(`{{${key}}}`, 'g'), value);
15
+ }
16
+ return JSON.parse(updatedData);
17
+ };
18
+
19
+ const applyRules = async (rules, data, upperCondition = null) => {
20
+ const resultLines = [];
21
+ let conditions;
22
+ if (upperCondition) {
23
+ if (Array.isArray(upperCondition)) {
24
+ conditions = [rules.condition, ...upperCondition];
25
+ } else {
26
+ conditions = [rules.condition, upperCondition];
27
+ }
28
+ } else {
29
+ conditions = [rules.condition];
30
+ }
31
+ if (Object.keys(rules).includes('rules')) {
32
+ for await (const rule of rules.rules) {
33
+ const result = await applyRules(rule, data, conditions);
34
+ // eslint-disable-next-line no-param-reassign
35
+ data = result.data;
36
+ if (result.resultLines.length > 0) {
37
+ resultLines.push(...result.resultLines);
38
+ }
39
+ }
40
+ return { resultLines, data };
41
+ }
42
+ if (Object.keys(rules).includes('return')) {
43
+ const returnData = rules.return;
44
+ const evalCondition = conditions.join(' && ');
45
+ const invoice = data;
46
+ const willPull = [];
47
+ for await (const line of invoice.accounting_lines) {
48
+ if (eval(evalCondition)) {
49
+ willPull.push(_.findIndex(invoice.accounting_lines, line));
50
+ returnData.forEach((item) => {
51
+ resultLines.push({
52
+ borc: eval(item.borc),
53
+ alacak: eval(item.alacak),
54
+ hesap_kodu: item.hesap_kodu,
55
+ });
56
+ });
57
+ }
58
+ }
59
+ if (willPull.length > 0) {
60
+ _.pullAt(invoice.accounting_lines, willPull);
61
+ }
62
+ return { resultLines, data: invoice };
63
+ }
64
+ };
65
+
66
+ const normalizeInvoiceForAccounting = async (invoice, direction) => {
67
+ if (['in', 'out'].indexOf(direction) === -1) {
68
+ throw new Error('Direction must be in or out');
69
+ }
70
+ const taxes = [];
71
+ const taxables = [];
72
+ const payable = {
73
+ type: 'payable',
74
+ amount: invoice.payable_amount,
75
+ };
76
+ if (!_.find(invoice.lines, (line) => !line.tax_subtotals || !(line.tax_subtotals.length > 0))) {
77
+ for await (const line of invoice.lines) {
78
+ for await (const item of line.tax_subtotals) {
79
+ if (_.find(taxes, { id: line.id, type: 'tax', code: item.code, percent: item.percent })) {
80
+ const index = _.findIndex(taxes, { id: line.id, type: 'tax', code: item.code, percent: item.percent });
81
+ taxes[index].amount += item.amount;
82
+ } else {
83
+ taxes.push({
84
+ id: line.id,
85
+ name: line.name,
86
+ quantity: line.quantity,
87
+ unit: line.quantity_unit,
88
+ price: line.price,
89
+ extension_amount: line.extension_amount,
90
+ additional: line.additional,
91
+ type: 'tax',
92
+ code: item.code,
93
+ percent: item.percent,
94
+ amount: item.amount,
95
+ });
96
+ }
97
+
98
+ if (_.find(taxables, { id: line.id, type: 'taxable', code: item.code, percent: item.percent })) {
99
+ const index = _.findIndex(taxables, { id: line.id, type: 'taxable', code: item.code, percent: item.percent });
100
+ taxables[index].amount += item.taxable;
101
+ } else {
102
+ taxables.push({
103
+ id: line.id,
104
+ name: line.name,
105
+ quantity: line.quantity,
106
+ unit: line.quantity_unit,
107
+ price: line.price,
108
+ extension_amount: line.extension_amount,
109
+ additional: line.additional,
110
+ type: 'taxable',
111
+ code: item.code,
112
+ percent: item.percent,
113
+ amount: item.taxable,
114
+ });
115
+ }
116
+ }
117
+ }
118
+ } else {
119
+ for await (const item of invoice.tax_subtotals) {
120
+ if (_.find(taxes, { type: 'tax', code: item.code, percent: item.percent })) {
121
+ const index = _.findIndex(taxes, { type: 'tax', code: item.code, percent: item.percent });
122
+ taxes[index].amount += item.amount;
123
+ } else {
124
+ taxes.push({
125
+ type: 'tax',
126
+ code: item.code,
127
+ percent: item.percent,
128
+ amount: item.amount,
129
+ });
130
+ }
131
+
132
+ if (_.find(taxables, { type: 'taxable', code: item.code, percent: item.percent })) {
133
+ const index = _.findIndex(taxables, { type: 'taxable', code: item.code, percent: item.percent });
134
+ taxables[index].amount += item.taxable;
135
+ } else {
136
+ taxables.push({
137
+ type: 'taxable',
138
+ code: item.code,
139
+ percent: item.percent,
140
+ amount: item.taxable,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ const payableCounterLines = [...taxes, ...taxables];
146
+ const payableLines = [payable];
147
+ // const payableCounterTotalAmount = payableCounterLines.reduce((acc, item) => acc + item.amount, 0);
148
+ // if (payableCounterTotalAmount > payable.amount) {
149
+ // payableLines.push({
150
+ // type: 'payable_extra',
151
+ // amount: payableCounterTotalAmount - payable.amount,
152
+ // });
153
+ // } else if (payableCounterTotalAmount < payable.amount) {
154
+ // payableCounterLines.push({
155
+ // type: 'taxable_extra',
156
+ // amount: payable.amount - payableCounterTotalAmount,
157
+ // });
158
+ // }
159
+ const accountingLines = [...payableLines, ...payableCounterLines];
160
+ const data = {
161
+ uuid: invoice.uuid,
162
+ number: invoice.number,
163
+ direction,
164
+ profile_id: invoice.profile_id,
165
+ type_code: invoice.type_code,
166
+ date: moment(invoice.issue_datetime).format('YYYY-MM-DD'),
167
+ notes: invoice.notes,
168
+ currency_code: invoice.currency_code,
169
+ sender_object: invoice.sender_object,
170
+ receiver_object: invoice.receiver_object,
171
+ accounting_lines: accountingLines,
172
+ };
173
+ return data;
174
+ };
175
+
176
+ const normalizeJSON = async (opts, data, dir) => {
177
+ const variables = opts.variables || {};
178
+ const { rules } = opts;
179
+ const invoice = await normalizeInvoiceForAccounting(data, dir);
180
+ const results = [];
181
+ for await (const rule of rules) {
182
+ const result = await applyRules(rule, invoice);
183
+ if (result.resultLines.length > 0) {
184
+ results.push(...result.resultLines);
185
+ }
186
+ }
187
+ const setVariables = await updateDataWithVariables(variables, results);
188
+ const accountedInvoice = _.reduce(
189
+ setVariables,
190
+ (acc, item) => {
191
+ const index = _.findIndex(acc, (line) => line.hesap_kodu === item.hesap_kodu);
192
+ if (index !== -1) {
193
+ acc[index].borc += item.borc;
194
+ acc[index].alacak += item.alacak;
195
+ } else {
196
+ acc.push(item);
197
+ }
198
+ return acc;
199
+ },
200
+ [],
201
+ );
202
+ return accountedInvoice;
203
+ };
204
+
205
+ const normalizeXML = async (opts, data, dir) => {
206
+ const json = await ubl2json.invoice.convertedJson(data);
207
+ const normalized = await normalizeJSON(opts, json, dir);
208
+ return normalized;
209
+ };
210
+
211
+ module.exports = {
212
+ normalizeJSON,
213
+ normalizeXML,
214
+ };
@@ -0,0 +1 @@
1
+ module.exports.engineService = require('./engine.service');