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 +9 -0
- package/.eslintignore +2 -0
- package/.eslintrc.json +20 -0
- package/.gitattributes +3 -0
- package/.lintstagedrc.json +3 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +4 -0
- package/README.md +171 -0
- package/package.json +44 -0
- package/src/index.js +2 -0
- package/src/services/engine.service.js +214 -0
- package/src/services/index.js +1 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
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
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
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,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');
|