flexbiz-server 12.5.49 → 12.5.50
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/package.json +1 -1
- package/server/libs/tinhgiatb.js +561 -30
- package/server/libs/tinhgiatb1vt.js +153 -5
package/package.json
CHANGED
package/server/libs/tinhgiatb.js
CHANGED
|
@@ -1,30 +1,561 @@
|
|
|
1
|
-
const ckvt=require(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
const ckvt = require('./ckvt');
|
|
2
|
+
const sokho = global.getModel('sokho');
|
|
3
|
+
const socai = global.getModel('socai');
|
|
4
|
+
const dmvt = global.getModel('dmvt');
|
|
5
|
+
const giatb = global.getModel('giatb');
|
|
6
|
+
const dmqddvt = global.getModel('dmqddvt');
|
|
7
|
+
const tinhgiatb1vt = require('./tinhgiatb1vt');
|
|
8
|
+
const _ = require("lodash");
|
|
9
|
+
const Controller = require('../controllers/controller');
|
|
10
|
+
const moment = require('moment');
|
|
11
|
+
const { getCurrentSession } = require("./sessionContext");
|
|
12
|
+
//const async = require("async");
|
|
13
|
+
|
|
14
|
+
const getRawData = (doc) => {
|
|
15
|
+
return (typeof doc.toObject === 'function') ? doc.toObject() : doc;
|
|
16
|
+
};
|
|
17
|
+
// Helper: Wrap callback function to Promise
|
|
18
|
+
const tinhGiaTbPromise = (query) => {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
tinhgiatb1vt(query, (err, data) => {
|
|
21
|
+
if (err) return reject(err);
|
|
22
|
+
resolve(data);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Helper: Wrap postData to Promise
|
|
28
|
+
const postDataPromise = (data, ctrl) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
Controller.postData(data, ctrl, (err, rs) => {
|
|
31
|
+
if (err) return reject(err);
|
|
32
|
+
resolve(rs);
|
|
33
|
+
}, { kiem_tra_han_muc_cong_no: false });
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = async function(condition, fn) {
|
|
38
|
+
try {
|
|
39
|
+
// 1. Validate inputs
|
|
40
|
+
if (!condition || !condition.tu_thang || !condition.den_thang || !condition.nam || !condition.id_app) {
|
|
41
|
+
throw new Error('Lỗi: Tính năng này yêu cầu các tham số: tu_thang, den_thang, nam, id_app');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tu_thang = Number(condition.tu_thang);
|
|
45
|
+
const den_thang = Number(condition.den_thang);
|
|
46
|
+
const ma_kho = condition.ma_kho;
|
|
47
|
+
const id_app = condition.id_app;
|
|
48
|
+
const tu_ngay = moment(new Date(condition.nam, tu_thang - 1, 15)).startOf("month").toDate();
|
|
49
|
+
const den_ngay = moment(new Date(condition.nam, den_thang - 1, 15)).endOf("month").toDate();
|
|
50
|
+
const session = getCurrentSession();
|
|
51
|
+
|
|
52
|
+
// 2. Load Config & Master Data
|
|
53
|
+
const app = await global.getModel("app").findOne({ _id: id_app }, { options: 1 }).lean();
|
|
54
|
+
if (!app) throw new Error("Công ty này không tồn tại");
|
|
55
|
+
const f_tien = (app.options || {}).f_tien || 0;
|
|
56
|
+
|
|
57
|
+
let query_dmvt = { id_app: id_app, gia_xuat: '1' };
|
|
58
|
+
if (condition.ma_nvt) query_dmvt.ma_nvt = condition.ma_nvt;
|
|
59
|
+
if (condition.ma_ncc) query_dmvt.ma_ncc = condition.ma_ncc;
|
|
60
|
+
if (condition.ma_vt) query_dmvt.ma_vt = condition.ma_vt;
|
|
61
|
+
|
|
62
|
+
Logger.info(`🔥[tinhgiatb] Start | Kho: ${ma_kho} | Session: ${session?._debugId}`);
|
|
63
|
+
|
|
64
|
+
// [OPTIMIZATION] Load trước bảng quy đổi đơn vị tính vào RAM để tra cứu O(1)
|
|
65
|
+
// Key: "MA_VT-MA_DVT" -> Value: he_so
|
|
66
|
+
const dmqdList = await dmqddvt.find({ id_app: id_app }).lean();
|
|
67
|
+
const mapQuyDoi = new Map();
|
|
68
|
+
dmqdList.forEach(qd => {
|
|
69
|
+
const he_so = (qd.mau ? (qd.tu / qd.mau) : qd.ty_le_qd) || 1;
|
|
70
|
+
mapQuyDoi.set(`${qd.ma_vt}-${qd.ma_dvt}`, he_so);
|
|
71
|
+
});
|
|
72
|
+
const getHeSoQuyDoi = (ma_vt, ma_dvt) => {
|
|
73
|
+
if (!ma_dvt) return 1;
|
|
74
|
+
return mapQuyDoi.get(`${ma_vt}-${ma_dvt}`) || 1;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// 3. Tính giá trung bình cho từng vật tư
|
|
78
|
+
const listVt = await dmvt.find(query_dmvt).lean();
|
|
79
|
+
Logger.info(`🚀 [tinhgiatb] Bắt đầu tính giá cho ${listVt.length} vật tư (Chạy song song)...`);
|
|
80
|
+
|
|
81
|
+
/*// Hàm xử lý từng vật tư
|
|
82
|
+
const processOneVt = async (ma_vt) => {
|
|
83
|
+
let query = {
|
|
84
|
+
id_app: id_app,
|
|
85
|
+
tu_ngay: tu_ngay,
|
|
86
|
+
den_ngay: den_ngay,
|
|
87
|
+
ma_vt,
|
|
88
|
+
ma_kho: ma_kho
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
// Lưu ý: Đảm bảo tinhGiaTbPromise KHÔNG được dùng session của Transaction này
|
|
92
|
+
const giaData = await tinhGiaTbPromise(query);
|
|
93
|
+
return {
|
|
94
|
+
...giaData,
|
|
95
|
+
id_app,
|
|
96
|
+
ma_kho,
|
|
97
|
+
status: true
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
Logger.error("[tinhgiatb] Lỗi tính giá VT:", query.ma_vt, err.message);
|
|
101
|
+
throw err; // Ném lỗi để dừng quy trình nếu cần
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Sử dụng async.mapLimit để chạy song song có kiểm soát
|
|
106
|
+
// mapLimit(mảng_đầu_vào, số_luồng_tối_đa, hàm_xử_lý)
|
|
107
|
+
// GIỚI HẠN: Chạy cùng lúc 100 vật tư (tùy vào độ mạnh server DB của bạn mà chỉnh số này)
|
|
108
|
+
const CONCURRENCY_LIMIT = 100;
|
|
109
|
+
|
|
110
|
+
const bang_gia = await new Promise((resolve, reject) => {
|
|
111
|
+
async.mapLimit(listVt, CONCURRENCY_LIMIT, async (vt) => {
|
|
112
|
+
return await processOneVt(vt);
|
|
113
|
+
}, (err, results) => {
|
|
114
|
+
if (err) return reject(err);
|
|
115
|
+
resolve(results);
|
|
116
|
+
});
|
|
117
|
+
});*/
|
|
118
|
+
const processGiaBan = async (ma_vt) => {
|
|
119
|
+
let query = {
|
|
120
|
+
id_app: id_app,
|
|
121
|
+
tu_ngay: tu_ngay,
|
|
122
|
+
den_ngay: den_ngay,
|
|
123
|
+
ma_vt,
|
|
124
|
+
ma_kho: ma_kho
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
// Lưu ý: Đảm bảo tinhGiaTbPromise KHÔNG được dùng session của Transaction này
|
|
128
|
+
const giaDatas = await tinhGiaTbPromise(query);
|
|
129
|
+
|
|
130
|
+
return giaDatas.map(giaData=>{
|
|
131
|
+
return {
|
|
132
|
+
...giaData,
|
|
133
|
+
id_app,
|
|
134
|
+
ma_kho,
|
|
135
|
+
status: true
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
} catch (err) {
|
|
139
|
+
Logger.error("[tinhgiatb] Lỗi tính giá VT:", query.ma_vt, err.message);
|
|
140
|
+
throw err; // Ném lỗi để dừng quy trình nếu cần
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const bang_gia = await processGiaBan(listVt.map(d=>d.ma_vt))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
// [OPTIMIZATION] Map giá để tra cứu nhanh O(1)
|
|
147
|
+
const mapGiaTB = new Map();
|
|
148
|
+
bang_gia.forEach(g => mapGiaTB.set(g.ma_vt, g.gia));
|
|
149
|
+
|
|
150
|
+
// 4. Cập nhật bảng giá (giatb)
|
|
151
|
+
const thangs = [];
|
|
152
|
+
for (let t = tu_thang; t <= den_thang; t++) thangs.push(t);
|
|
153
|
+
|
|
154
|
+
// [OPTIMIZATION] Bulk Update: Xóa 1 lần và Thêm 1 lần (Nhanh hơn xóa/thêm trong vòng lặp)
|
|
155
|
+
const ma_vts = bang_gia.map(g => g.ma_vt);
|
|
156
|
+
let query_delete_gia = {
|
|
157
|
+
id_app: id_app,
|
|
158
|
+
ma_vt: { $in: ma_vts },
|
|
159
|
+
nam: condition.nam,
|
|
160
|
+
thang: { $in: thangs }
|
|
161
|
+
};
|
|
162
|
+
if (ma_kho) query_delete_gia.ma_kho = ma_kho;
|
|
163
|
+
|
|
164
|
+
await giatb.deleteMany(query_delete_gia);
|
|
165
|
+
|
|
166
|
+
let newGiaEntries = [];
|
|
167
|
+
for (const t of thangs) {
|
|
168
|
+
bang_gia.forEach(gia => {
|
|
169
|
+
newGiaEntries.push({ ...gia, thang: t, nam: condition.nam });
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (newGiaEntries.length > 0) {
|
|
173
|
+
await giatb.insertMany(newGiaEntries);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 5. Lấy danh sách chứng từ cần cập nhật (CHẠY SONG SONG)
|
|
177
|
+
Logger.info(`[tinhgiatb] 🚀 Đang lấy danh sách chứng từ (Parallel Mode)...`);
|
|
178
|
+
|
|
179
|
+
// Khởi tạo các biến chứa kết quả
|
|
180
|
+
let vouchers_x = new Map();
|
|
181
|
+
let vouchers_n = new Map();
|
|
182
|
+
const id_ct_need_delete = [];
|
|
183
|
+
|
|
184
|
+
// Hàm helper để query chứng từ gốc (chạy song song cho từng loại chứng từ)
|
|
185
|
+
const fetchVouchersByMaCt = async (groupedSks) => {
|
|
186
|
+
const results = new Map();
|
|
187
|
+
const deleteIds = [];
|
|
188
|
+
|
|
189
|
+
// Chuyển object grouped thành array để chạy Promise.all
|
|
190
|
+
const entries = Object.entries(groupedSks);
|
|
191
|
+
|
|
192
|
+
// Chạy song song việc query từng bảng (Vd: PXK, PBH chạy cùng lúc)
|
|
193
|
+
await Promise.all(entries.map(async ([ma_ct, listSk]) => {
|
|
194
|
+
try {
|
|
195
|
+
const ctrl = global.controllers[ma_ct.toUpperCase()];
|
|
196
|
+
const Model = ctrl ? ctrl.getProperty("model") : mongoose.models[ma_ct.toLowerCase()];
|
|
197
|
+
|
|
198
|
+
if (!Model) {
|
|
199
|
+
Logger.warn(`⚠️ [tinhgiatb] Không tìm thấy model: ${ma_ct}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ids = [...new Set(listSk.map(s => s.id_ct))];
|
|
204
|
+
|
|
205
|
+
// QUAN TRỌNG: Query này chạy song song nên KHÔNG ĐƯỢC DÙNG SESSION
|
|
206
|
+
// Đảm bảo mongoosePatch không tự gắn session vào đây
|
|
207
|
+
const foundVouchers = await Model.find({ _id: { $in: ids } }).lean();
|
|
208
|
+
|
|
209
|
+
// Map kết quả
|
|
210
|
+
const foundIds = new Set();
|
|
211
|
+
for (const v of foundVouchers) {
|
|
212
|
+
// Chuyển đổi về object mongoose nếu cần save() sau này,
|
|
213
|
+
// nhưng ở bước đọc này dùng lean() cho nhẹ, lát nữa update dùng updateOne sau.
|
|
214
|
+
// Tuy nhiên code logic dưới của bạn đang dùng logic object (v.details),
|
|
215
|
+
// nên nếu dùng lean() thì lát nữa updateOne là chuẩn nhất.
|
|
216
|
+
results.set(v._id.toString(), v);
|
|
217
|
+
foundIds.add(v._id.toString());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check chứng từ rác
|
|
221
|
+
ids.forEach(id => {
|
|
222
|
+
if (!foundIds.has(id.toString())) deleteIds.push(id);
|
|
223
|
+
});
|
|
224
|
+
} catch (err) {
|
|
225
|
+
Logger.error(`❌ Lỗi load chứng từ ${ma_ct}:`, err.message);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return { vouchers: results, deleteIds };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// --- BẮT ĐẦU CHẠY SONG SONG 3 LUỒNG LỚN ---
|
|
234
|
+
await Promise.all([
|
|
235
|
+
// LUỒNG 1: Lấy phiếu xuất
|
|
236
|
+
(async () => {
|
|
237
|
+
let query_sokho_x = {
|
|
238
|
+
id_app: id_app,
|
|
239
|
+
ngay_ct: { $gte: tu_ngay, $lte: den_ngay },
|
|
240
|
+
nxt: 2,
|
|
241
|
+
ma_vt: { $in: ma_vts },
|
|
242
|
+
px_gia_dd: false
|
|
243
|
+
};
|
|
244
|
+
if (ma_kho) query_sokho_x.ma_kho = ma_kho;
|
|
245
|
+
|
|
246
|
+
// Query sokho có thể chạy song song (không session)
|
|
247
|
+
const sks_x = await sokho.find(query_sokho_x).lean();
|
|
248
|
+
const groupedSksX = _.groupBy(sks_x, 'ma_ct');
|
|
249
|
+
|
|
250
|
+
const res = await fetchVouchersByMaCt(groupedSksX);
|
|
251
|
+
res.vouchers.forEach((v, k) => vouchers_x.set(k, v));
|
|
252
|
+
id_ct_need_delete.push(...res.deleteIds);
|
|
253
|
+
})(),
|
|
254
|
+
|
|
255
|
+
// LUỒNG 2: Lấy phiếu nhập (giá TB)
|
|
256
|
+
(async () => {
|
|
257
|
+
let query_sokho_n = {
|
|
258
|
+
id_app: id_app,
|
|
259
|
+
ngay_ct: { $gte: tu_ngay, $lte: den_ngay },
|
|
260
|
+
nxt: 1,
|
|
261
|
+
ma_vt: { $in: ma_vts },
|
|
262
|
+
pn_gia_tb: true,
|
|
263
|
+
ma_ct: { $ne: 'PXC' }
|
|
264
|
+
};
|
|
265
|
+
if (ma_kho) query_sokho_n.ma_kho = ma_kho;
|
|
266
|
+
|
|
267
|
+
const sks_n = await sokho.find(query_sokho_n).lean();
|
|
268
|
+
const groupedSksN = _.groupBy(sks_n, 'ma_ct');
|
|
269
|
+
|
|
270
|
+
const res = await fetchVouchersByMaCt(groupedSksN);
|
|
271
|
+
res.vouchers.forEach((v, k) => vouchers_n.set(k, v));
|
|
272
|
+
id_ct_need_delete.push(...res.deleteIds);
|
|
273
|
+
})(),
|
|
274
|
+
|
|
275
|
+
// LUỒNG 3: Lấy PNC (Nhập điều chuyển)
|
|
276
|
+
(async () => {
|
|
277
|
+
if (ma_kho) {
|
|
278
|
+
let query_sokho_pnc = {
|
|
279
|
+
id_app: id_app,
|
|
280
|
+
ma_ct: "PNC",
|
|
281
|
+
ngay_ct: { $gte: tu_ngay, $lte: den_ngay },
|
|
282
|
+
$or: [{ ma_kho_x: ma_kho }, { "details.ma_kho_x": ma_kho }],
|
|
283
|
+
"details.px_gia_dd": false
|
|
284
|
+
};
|
|
285
|
+
// Query này cũng chạy song song
|
|
286
|
+
const pncList = await global.getModel("pnc").find(query_sokho_pnc).lean();
|
|
287
|
+
pncList.forEach(p => vouchers_n.set(p._id.toString(), p));
|
|
288
|
+
}
|
|
289
|
+
})()
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
// Xóa sổ sách rác
|
|
293
|
+
if (id_ct_need_delete.length > 0) {
|
|
294
|
+
await sokho.deleteMany({ id_app, id_ct: { $in: id_ct_need_delete } });
|
|
295
|
+
await socai.deleteMany({ id_app, id_ct: { $in: id_ct_need_delete } });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 6. Xử lý Logic update (Core Logic)
|
|
299
|
+
// Helper function để tái sử dụng logic tính toán update detail
|
|
300
|
+
const updateDetailPrice = (d) => {
|
|
301
|
+
const gia_chuan = mapGiaTB.get(d.ma_vt) || 0;
|
|
302
|
+
const he_so_qd = getHeSoQuyDoi(d.ma_vt, d.ma_dvt);
|
|
303
|
+
|
|
304
|
+
d.gia_von = d.gia_von_nt = gia_chuan * he_so_qd;
|
|
305
|
+
// Logic cũ: roundBy(sl * gia, f_tien)
|
|
306
|
+
// Lưu ý: check field sl_xuat/sl_nhap/sl_km tuỳ context
|
|
307
|
+
return { gia_chuan, he_so_qd };
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// 6.1 Cập nhật Phiếu Xuất (vouchers_x)
|
|
311
|
+
Logger.info(`[tinhgiatb] Cập nhật ${vouchers_x.size} phiếu xuất...`);
|
|
312
|
+
// Sắp xếp theo thời gian để đảm bảo logic
|
|
313
|
+
const sortedVouchersX = _.sortBy([...vouchers_x.values()], v => (new Date(v.ngay_ct)).getTime());
|
|
314
|
+
|
|
315
|
+
for (const voucher of sortedVouchersX) {
|
|
316
|
+
let isModified = false;
|
|
317
|
+
// -- Details thường --
|
|
318
|
+
if (voucher.details) {
|
|
319
|
+
voucher.details.forEach(d => {
|
|
320
|
+
if ((!condition.ma_vt || condition.ma_vt === d.ma_vt) &&
|
|
321
|
+
!d.px_gia_dd &&
|
|
322
|
+
(!ma_kho || ma_kho === (d.ma_kho || d.ma_kho_x || voucher.ma_kho || voucher.ma_kho_x))) {
|
|
323
|
+
|
|
324
|
+
updateDetailPrice(d);
|
|
325
|
+
d.tien_xuat = d.tien_xuat_nt = Math.roundBy(d.sl_xuat * d.gia_von, f_tien);
|
|
326
|
+
isModified = true;
|
|
327
|
+
}
|
|
328
|
+
// -- Combo --
|
|
329
|
+
if (d.combo && d.combo.length > 0 && !d.px_gia_dd) {
|
|
330
|
+
let totalTienCombo = 0;
|
|
331
|
+
d.combo.forEach(c => {
|
|
332
|
+
if ((!condition.ma_vt || condition.ma_vt === c.ma_vt) &&
|
|
333
|
+
(!ma_kho || ma_kho === (d.ma_kho || d.ma_kho_x || voucher.ma_kho || voucher.ma_kho_x))) {
|
|
334
|
+
updateDetailPrice(c);
|
|
335
|
+
c.tien_xuat = c.tien_xuat_nt = Math.roundBy((c.sl_xuat || 0) * c.gia_von, f_tien);
|
|
336
|
+
isModified = true;
|
|
337
|
+
}
|
|
338
|
+
totalTienCombo += (c.tien_xuat_nt || 0);
|
|
339
|
+
});
|
|
340
|
+
// Update lại detail cha
|
|
341
|
+
d.tien_xuat = d.tien_xuat_nt = totalTienCombo;
|
|
342
|
+
d.gia_von = d.gia_von_nt = Math.roundBy(d.sl_xuat ? d.tien_xuat_nt / d.sl_xuat : 0, f_tien);
|
|
343
|
+
}
|
|
344
|
+
// -- Khuyen Mai Details --
|
|
345
|
+
if (d.promotion && d.promotion.details_km) {
|
|
346
|
+
d.promotion.details_km.forEach(km => {
|
|
347
|
+
if ((!condition.ma_vt || condition.ma_vt === km.ma_vt) && !km.px_gia_dd &&
|
|
348
|
+
(!ma_kho || ma_kho === (km.ma_kho || voucher.ma_kho || voucher.ma_kho_x))) {
|
|
349
|
+
updateDetailPrice(km);
|
|
350
|
+
km.tien_xuat = km.tien_xuat_nt = Math.roundBy((km.sl_xuat || km.sl_km || 0) * km.gia_von, f_tien);
|
|
351
|
+
isModified = true;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// -- Details Doi --
|
|
359
|
+
if (voucher.details_doi) {
|
|
360
|
+
voucher.details_doi.forEach(d => {
|
|
361
|
+
if ((!condition.ma_vt || condition.ma_vt === d.ma_vt) && !d.px_gia_dd &&
|
|
362
|
+
(!ma_kho || ma_kho === (d.ma_kho || voucher.ma_kho || voucher.ma_kho_x))) {
|
|
363
|
+
updateDetailPrice(d);
|
|
364
|
+
d.tien_xuat = d.tien_xuat_nt = Math.roundBy(d.sl_xuat * d.gia_von, f_tien);
|
|
365
|
+
isModified = true;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// -- Save & Post --
|
|
371
|
+
if (isModified) {
|
|
372
|
+
// SỬA ĐỔI: Thay vì voucher.toObject(), ta dùng hàm check an toàn
|
|
373
|
+
const voucherData = getRawData(voucher);
|
|
374
|
+
|
|
375
|
+
// Lưu ý: Khi dùng lean(), voucherData chính là object đang giữ tham chiếu
|
|
376
|
+
// tới các thay đổi ở trên (vì JS pass by reference), nên dữ liệu đã mới nhất.
|
|
377
|
+
|
|
378
|
+
// Xóa _id khỏi payload update để tránh lỗi "Modifying immutable field '_id'"
|
|
379
|
+
// (Mongoose đôi khi kỹ tính chỗ này khi dùng updateOne)
|
|
380
|
+
// eslint-disable-next-line no-unused-vars
|
|
381
|
+
const { _id, ...updatePayload } = voucherData;
|
|
382
|
+
|
|
383
|
+
const ctrl = global.controllers[voucher.ma_ct.toUpperCase()];
|
|
384
|
+
const Model = ctrl ? ctrl.getProperty("model") : mongoose.models[voucher.ma_ct.toLowerCase()];
|
|
385
|
+
|
|
386
|
+
// Dùng updateOne với session (nếu có)
|
|
387
|
+
// Lưu ý: updatePayload là toàn bộ dữ liệu, tương đương với việc ghi đè các field có trong đó
|
|
388
|
+
await Model.updateOne({ _id: voucher._id }, updatePayload);
|
|
389
|
+
|
|
390
|
+
if (ctrl && ctrl.post) {
|
|
391
|
+
// postDataPromise cần object thuần, voucherData đã đáp ứng
|
|
392
|
+
await postDataPromise(voucherData, ctrl);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 6.2 Cập nhật Phiếu Nhập (vouchers_n)
|
|
398
|
+
Logger.info(`[tinhgiatb] Cập nhật ${vouchers_n.size} phiếu nhập...`);
|
|
399
|
+
const sortedVouchersN = _.sortBy([...vouchers_n.values()], v => (new Date(v.ngay_ct)).getTime());
|
|
400
|
+
|
|
401
|
+
for (const voucher of sortedVouchersN) {
|
|
402
|
+
let isModified = false;
|
|
403
|
+
if(voucher.details) {
|
|
404
|
+
voucher.details.forEach(d => {
|
|
405
|
+
const isValid = (!d.combo || d.combo.length == 0) &&
|
|
406
|
+
(!condition.ma_vt || condition.ma_vt === d.ma_vt) &&
|
|
407
|
+
(d.pn_gia_tb || (voucher.ma_ct === "PNC" && !d.px_gia_dd) || (voucher.ma_ct === "PKK" && !d.px_gia_dd)) &&
|
|
408
|
+
(!ma_kho || ma_kho === d.ma_kho || ma_kho === d.ma_kho_x || ma_kho === voucher.ma_kho || ma_kho === voucher.ma_kho_n);
|
|
409
|
+
|
|
410
|
+
if (isValid) {
|
|
411
|
+
updateDetailPrice(d);
|
|
412
|
+
if (voucher.ma_ct === "PNC" || voucher.ma_ct === "PKK") {
|
|
413
|
+
d.tien_xuat_nt = Math.roundBy(d.sl_xuat * d.gia_von, f_tien);
|
|
414
|
+
d.tien_xuat = d.tien_xuat_nt;
|
|
415
|
+
} else {
|
|
416
|
+
d.tien_nhap_nt = Math.roundBy(d.sl_nhap * d.gia_von, f_tien);
|
|
417
|
+
d.tien_nhap = d.tien_nhap_nt;
|
|
418
|
+
}
|
|
419
|
+
isModified = true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Combo
|
|
423
|
+
if(d.combo && d.combo.length > 0 && (d.pn_gia_tb || (voucher.ma_ct ==="PNC" && !d.px_gia_dd) || (voucher.ma_ct ==="PKK" && !d.px_gia_dd))) {
|
|
424
|
+
let totalTienCombo = 0;
|
|
425
|
+
d.combo.forEach(c => {
|
|
426
|
+
if((!condition.ma_vt || condition.ma_vt ===c.ma_vt) && (!ma_kho || ma_kho === (d.ma_kho || voucher.ma_kho|| voucher.ma_kho_n))) {
|
|
427
|
+
updateDetailPrice(c);
|
|
428
|
+
if(voucher.ma_ct ==="PNC" || voucher.ma_ct ==="PKK"){
|
|
429
|
+
c.tien_xuat_nt = Math.roundBy(c.sl_xuat * c.gia_von, f_tien);
|
|
430
|
+
c.tien_xuat = c.tien_xuat_nt;
|
|
431
|
+
} else {
|
|
432
|
+
c.tien_nhap_nt = Math.roundBy(c.sl_nhap * c.gia_von, f_tien);
|
|
433
|
+
c.tien_nhap = c.tien_nhap_nt;
|
|
434
|
+
}
|
|
435
|
+
isModified = true;
|
|
436
|
+
}
|
|
437
|
+
totalTienCombo += (voucher.ma_ct === "PNC" || voucher.ma_ct === "PKK" ? (c.tien_xuat_nt||0) : (c.tien_nhap_nt||0));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (voucher.ma_ct === "PNC" || voucher.ma_ct === "PKK") {
|
|
441
|
+
d.tien_xuat = d.tien_xuat_nt = totalTienCombo;
|
|
442
|
+
d.gia_von = d.gia_von_nt = Math.roundBy(d.sl_xuat ? d.tien_xuat_nt / d.sl_xuat : 0, f_tien);
|
|
443
|
+
} else {
|
|
444
|
+
d.tien_nhap = d.tien_nhap_nt = totalTienCombo;
|
|
445
|
+
d.gia_von = d.gia_von_nt = Math.roundBy(d.sl_nhap ? d.tien_nhap_nt / d.sl_nhap : 0, f_tien);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Details Doi
|
|
452
|
+
if (voucher.details_doi) {
|
|
453
|
+
voucher.details_doi.forEach(d => {
|
|
454
|
+
if ((!condition.ma_vt || condition.ma_vt === d.ma_vt) && (d.pn_gia_tb && (!ma_kho || ma_kho === d.ma_kho))) {
|
|
455
|
+
updateDetailPrice(d);
|
|
456
|
+
d.tien_xuat_nt = Math.roundBy(d.sl_xuat * d.gia_von, f_tien);
|
|
457
|
+
d.tien_xuat = d.tien_xuat_nt;
|
|
458
|
+
isModified = true;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Save & Post
|
|
464
|
+
if (isModified) {
|
|
465
|
+
const voucherData = getRawData(voucher);
|
|
466
|
+
const ctrl = global.controllers[voucher.ma_ct.toUpperCase()];
|
|
467
|
+
const Model = ctrl ? ctrl.getProperty("model") : mongoose.models[voucher.ma_ct.toLowerCase()];
|
|
468
|
+
|
|
469
|
+
await Model.updateOne({ _id: voucher._id }, voucherData);
|
|
470
|
+
if (ctrl && ctrl.post) {
|
|
471
|
+
await postDataPromise(voucherData, ctrl);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 7. Đánh giá chênh lệch (CKVT)
|
|
477
|
+
Logger.info(`[tinhgiatb] Kiểm tra chênh lệch...`);
|
|
478
|
+
let query_ckvt = {
|
|
479
|
+
id_app: condition.id_app,
|
|
480
|
+
ngay: den_ngay,
|
|
481
|
+
chenh_lech: 1
|
|
482
|
+
};
|
|
483
|
+
if (condition.ma_vt) query_ckvt.ma_vt = condition.ma_vt;
|
|
484
|
+
if (condition.ma_kho) query_ckvt.ma_kho = condition.ma_kho;
|
|
485
|
+
|
|
486
|
+
// Cần wrap ckvt vào promise nếu nó là callback style
|
|
487
|
+
const du_cuoi_ky = await new Promise((resolve, reject) => {
|
|
488
|
+
ckvt(query_ckvt, (err, data) => err ? reject(err) : resolve(data));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const validDuCuoiKy = _.filter(du_cuoi_ky, r => r.du00 !== 0 && (r.ton00 == 0 || Math.abs(r.ton00) < 0.001));
|
|
492
|
+
|
|
493
|
+
// Sắp xếp lại danh sách vouchers để phân bổ chênh lệch (Ưu tiên ngày mới nhất)
|
|
494
|
+
const descVouchersX = _.sortBy([...vouchers_x.values()], v => -(new Date(v.ngay_ct)).getTime());
|
|
495
|
+
const descVouchersN = _.sortBy([...vouchers_n.values()], v => -(new Date(v.ngay_ct)).getTime());
|
|
496
|
+
|
|
497
|
+
let chung_tu_cap_nhat_chenh_lech = new Map();
|
|
498
|
+
|
|
499
|
+
for (const vt of validDuCuoiKy) {
|
|
500
|
+
let d_voucher = null;
|
|
501
|
+
|
|
502
|
+
// Tìm phiếu xuất
|
|
503
|
+
d_voucher = descVouchersX.find(x => {
|
|
504
|
+
const det = (x.details || []).find(vc => vc.ma_vt == vt.ma_vt && !vc.px_gia_dd && (!ma_kho || ma_kho === vc.ma_kho || ma_kho === x.ma_kho));
|
|
505
|
+
if (det) {
|
|
506
|
+
det.tien_xuat_nt += vt.du00;
|
|
507
|
+
det.tien_xuat = Math.roundBy(det.tien_xuat_nt, f_tien);
|
|
508
|
+
if (det.sl_xuat) det.gia_von = det.gia_von_nt = Math.roundBy(det.tien_xuat_nt / det.sl_xuat, 0);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Nếu không có, tìm phiếu nhập
|
|
515
|
+
if (!d_voucher) {
|
|
516
|
+
d_voucher = descVouchersN.find(n => {
|
|
517
|
+
const det = (n.details || []).find(vc => vc.ma_vt == vt.ma_vt && !vc.pn_gia_tb && (!ma_kho || ma_kho === vc.ma_kho || ma_kho === n.ma_kho));
|
|
518
|
+
if (det) {
|
|
519
|
+
det.tien_nhap_nt -= vt.du00; // Trừ đi vì đây là chênh lệch
|
|
520
|
+
det.tien_nhap = Math.roundBy(det.tien_nhap_nt, f_tien);
|
|
521
|
+
if (det.sl_nhap) det.gia_von = det.gia_von_nt = Math.roundBy(det.tien_nhap_nt / det.sl_nhap, 0);
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (d_voucher) {
|
|
529
|
+
chung_tu_cap_nhat_chenh_lech.set(d_voucher._id.toString(), d_voucher);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Cập nhật phiếu chênh lệch
|
|
534
|
+
if (chung_tu_cap_nhat_chenh_lech.size > 0) {
|
|
535
|
+
Logger.info(`⚠️ [tinhgiatb] Updating ${chung_tu_cap_nhat_chenh_lech.size} vouchers for discrepancy...`);
|
|
536
|
+
for (const voucher of chung_tu_cap_nhat_chenh_lech.values()) {
|
|
537
|
+
const voucherData = getRawData(voucher);
|
|
538
|
+
const ctrl = global.controllers[voucherData.ma_ct.toUpperCase()];
|
|
539
|
+
const Model = ctrl ? ctrl.getProperty("model") : mongoose.models[voucherData.ma_ct.toLowerCase()];
|
|
540
|
+
|
|
541
|
+
await Model.updateOne({ _id: voucher._id }, voucherData);
|
|
542
|
+
if (ctrl && ctrl.post) {
|
|
543
|
+
await postDataPromise(voucherData, ctrl);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 8. Kết thúc & Trả dữ liệu
|
|
549
|
+
// Join tên vật tư trước khi trả về (nếu cần)
|
|
550
|
+
// Lưu ý: joinModel2 là hàm custom, tôi giữ nguyên logic nhưng gọi wrap
|
|
551
|
+
await new Promise(resolve => {
|
|
552
|
+
bang_gia.joinModel2(id_app, dmvt, [{ where: 'ma_vt', fields: 'ten_vt' }], resolve);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
fn(null, bang_gia);
|
|
556
|
+
|
|
557
|
+
} catch (e) {
|
|
558
|
+
Logger.error("❌ [tinhgiatb] Critical Error:", e);
|
|
559
|
+
fn(e);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
@@ -1,5 +1,153 @@
|
|
|
1
|
-
const dkvt=require(
|
|
2
|
-
|
|
3
|
-
(
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const dkvt = require('./dkvt');
|
|
2
|
+
const sokho = global.getModel('sokho');
|
|
3
|
+
module.exports = async function(condition, fn) {
|
|
4
|
+
// 1. VALIDATE INPUT
|
|
5
|
+
if (!condition || !condition.ma_vt || !condition.tu_ngay || !condition.den_ngay || !condition.id_app) {
|
|
6
|
+
return fn('Lỗi: Báo cáo này yêu cầu các tham số: tu_ngay, den_ngay, id_app, ma_vt');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const { id_app, tu_ngay, den_ngay, ma_kho } = condition;
|
|
11
|
+
|
|
12
|
+
// Chuẩn hóa input ma_vt (Array)
|
|
13
|
+
const isSingle = !Array.isArray(condition.ma_vt);
|
|
14
|
+
const listMaVt = isSingle ? [condition.ma_vt] : condition.ma_vt;
|
|
15
|
+
const validListMaVt = listMaVt.filter(vt => vt); // Loại bỏ null/undefined
|
|
16
|
+
|
|
17
|
+
if (validListMaVt.length === 0) return fn(null, isSingle ? {} : []);
|
|
18
|
+
|
|
19
|
+
// 2. CHẠY SONG SONG: TỒN ĐẦU KỲ & PHÁT SINH TRONG KỲ
|
|
20
|
+
|
|
21
|
+
// --- Task 1: Dư đầu kỳ ---
|
|
22
|
+
const pDauKy = new Promise((resolve, reject) => {
|
|
23
|
+
const queryDk = { ...condition, ngay: tu_ngay, ma_vt: validListMaVt };
|
|
24
|
+
const cb = (err, res) => err ? reject(err) : resolve(res);
|
|
25
|
+
|
|
26
|
+
const result = dkvt(queryDk, cb);
|
|
27
|
+
if (result && result.then) result.then(resolve).catch(reject);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// --- Task 2: Phát sinh trong kỳ ---
|
|
31
|
+
const pPhatSinh = (async () => {
|
|
32
|
+
// A. Xây dựng điều kiện lọc ($match)
|
|
33
|
+
// Lấy: (Nhập kho CÓ giá) HOẶC (Xuất kho giá ĐÍCH DANH)
|
|
34
|
+
const orConditions = [
|
|
35
|
+
// 1. Phiếu Nhập: Lấy phiếu KHÔNG PHẢI giá TB (tức là có giá cụ thể)
|
|
36
|
+
{ nxt: 1, pn_gia_tb: { $ne: true } },
|
|
37
|
+
|
|
38
|
+
// 2. Phiếu Xuất: CHỈ LẤY phiếu giá Đích danh (để trừ ra)
|
|
39
|
+
{ nxt: 2, px_gia_dd: true }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// *Logic riêng cho Kho*: Nếu tính riêng kho, phiếu Nhập chuyển kho (PNC)
|
|
43
|
+
// được coi là đầu vào có giá (dù nó có thể đang tick pn_gia_tb ở kho đích)
|
|
44
|
+
if (ma_kho) {
|
|
45
|
+
orConditions.push({ nxt: 1, ma_ct: "PNC" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const matchQuery = {
|
|
49
|
+
id_app: id_app,
|
|
50
|
+
ngay_ct: { $gte: tu_ngay, $lt: den_ngay },
|
|
51
|
+
ma_vt: { $in: validListMaVt },
|
|
52
|
+
$or: orConditions
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (ma_kho) matchQuery.ma_kho = ma_kho;
|
|
56
|
+
|
|
57
|
+
// B. Thực hiện Aggregate
|
|
58
|
+
return sokho.aggregate([
|
|
59
|
+
{ $match: matchQuery },
|
|
60
|
+
{
|
|
61
|
+
$group: {
|
|
62
|
+
_id: "$ma_vt",
|
|
63
|
+
// CỘNG số lượng nhập, TRỪ số lượng xuất đích danh
|
|
64
|
+
sl_nhap: {
|
|
65
|
+
$sum: {
|
|
66
|
+
$cond: {
|
|
67
|
+
if: { $eq: ["$nxt", 2] }, // Nếu là Xuất (đích danh)
|
|
68
|
+
then: { $multiply: [{ $ifNull: ["$sl_xuat_qd", 0] }, -1] }, // -> TRỪ (Âm)
|
|
69
|
+
else: { $ifNull: ["$sl_nhap_qd", 0] } // Là Nhập -> CỘNG (Dương)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
// CỘNG tiền nhập, TRỪ tiền xuất đích danh
|
|
74
|
+
tien_nhap: {
|
|
75
|
+
$sum: {
|
|
76
|
+
$cond: {
|
|
77
|
+
if: { $eq: ["$nxt", 2] }, // Nếu là Xuất (đích danh)
|
|
78
|
+
then: { $multiply: [{ $ifNull: ["$tien_xuat", 0] }, -1] }, // -> TRỪ (Âm)
|
|
79
|
+
else: { $ifNull: ["$tien_nhap", 0] } // Là Nhập -> CỘNG (Dương)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
// Chờ kết quả
|
|
89
|
+
const [dnData, psData] = await Promise.all([pDauKy, pPhatSinh]);
|
|
90
|
+
|
|
91
|
+
// 3. TỔNG HỢP VÀ TÍNH GIÁ
|
|
92
|
+
|
|
93
|
+
// Map data để xử lý nhanh O(N)
|
|
94
|
+
const dataMap = new Map();
|
|
95
|
+
|
|
96
|
+
// Khởi tạo map
|
|
97
|
+
validListMaVt.forEach(vt => {
|
|
98
|
+
dataMap.set(vt, {
|
|
99
|
+
ma_vt: vt,
|
|
100
|
+
ton_dau: 0, du_dau: 0,
|
|
101
|
+
sl_nhap: 0, tien_nhap: 0,
|
|
102
|
+
tong_sl: 0, tong_tien: 0,
|
|
103
|
+
gia: 0
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Fill tồn đầu
|
|
108
|
+
const arrDn = Array.isArray(dnData) ? dnData : [dnData];
|
|
109
|
+
arrDn.forEach(item => {
|
|
110
|
+
if (item && item.ma_vt && dataMap.has(item.ma_vt)) {
|
|
111
|
+
const cur = dataMap.get(item.ma_vt);
|
|
112
|
+
cur.ton_dau = item.ton00 || 0;
|
|
113
|
+
cur.du_dau = item.du00 || 0;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Fill phát sinh (Đã được cộng/trừ đúng chiều ở Aggregate)
|
|
118
|
+
psData.forEach(item => {
|
|
119
|
+
if (item && item._id && dataMap.has(item._id)) {
|
|
120
|
+
const cur = dataMap.get(item._id);
|
|
121
|
+
cur.sl_nhap = item.sl_nhap || 0;
|
|
122
|
+
cur.tien_nhap = item.tien_nhap || 0;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Tính giá bình quân
|
|
127
|
+
const finalResult = Array.from(dataMap.values()).map(r => {
|
|
128
|
+
// Tổng SL = Tồn đầu + (Nhập trong kỳ - Xuất đích danh)
|
|
129
|
+
r.tong_sl = r.ton_dau + r.sl_nhap;
|
|
130
|
+
|
|
131
|
+
// Tổng Tiền = Dư đầu + (Tiền nhập - Tiền xuất đích danh)
|
|
132
|
+
r.tong_tien = r.du_dau + r.tien_nhap;
|
|
133
|
+
|
|
134
|
+
// Chia giá
|
|
135
|
+
if (r.tong_sl !== 0) {
|
|
136
|
+
// roundBy(giá trị, số lẻ)
|
|
137
|
+
r.gia = Math.roundBy(r.tong_tien / r.tong_sl, condition.round || 0);
|
|
138
|
+
// Có thể cần xử lý giá âm nếu số lượng dương mà tiền âm (do điều chỉnh)
|
|
139
|
+
if(r.tong_sl > 0 && r.tong_tien < 0) r.gia = 0;
|
|
140
|
+
} else {
|
|
141
|
+
r.gia = 0;
|
|
142
|
+
}
|
|
143
|
+
return r;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 4. RETURN
|
|
147
|
+
const output = isSingle ? (finalResult[0] || {}) : finalResult;
|
|
148
|
+
return fn(null, output);
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return fn(error);
|
|
152
|
+
}
|
|
153
|
+
};
|