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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "flexbiz-server",
3
3
  "main": "./server/app.js",
4
4
  "description": "Flexible Server",
5
- "version": "12.5.49",
5
+ "version": "12.5.50",
6
6
  "author": {
7
7
  "name": "Van Truong Pham",
8
8
  "email": "invncur@gmail.com"
@@ -1,30 +1,561 @@
1
- const ckvt=require("./ckvt"),sokho=global.getModel("sokho"),socai=global.getModel("socai"),dmvt=global.getModel("dmvt"),giatb=global.getModel("giatb"),dmqddvt=global.getModel("dmqddvt"),tinhgiatb1vt=require("./tinhgiatb1vt"),_=require("lodash"),Controller=require("../controllers/controller"),moment=require("moment"),{getCurrentSession}=require("./sessionContext"),async=require("async"),getRawData=$doc$$=>typeof $doc$$.toObject==="function"?$doc$$.toObject():$doc$$,tinhGiaTbPromise=$query$$=>new Promise(($resolve$$,
2
- $reject$$)=>{tinhgiatb1vt($query$$,($err$$,$data$$)=>{if($err$$)return $reject$$($err$$);$resolve$$($data$$)})}),postDataPromise=($data$$,$ctrl$$)=>new Promise(($resolve$$,$reject$$)=>{Controller.postData($data$$,$ctrl$$,($err$$,$rs$$)=>{if($err$$)return $reject$$($err$$);$resolve$$($rs$$)},{kiem_tra_han_muc_cong_no:!1})});
3
- module.exports=async function($condition$$,$fn$$){try{if(!($condition$$&&$condition$$.tu_thang&&$condition$$.den_thang&&$condition$$.nam&&$condition$$.id_app))throw Error("L\u1ed7i: T\u00ednh n\u0103ng n\u00e0y y\u00eau c\u1ea7u c\u00e1c tham s\u1ed1: tu_thang, den_thang, nam, id_app");var $t_tu_thang$$=Number($condition$$.tu_thang),$d_voucher_den_thang$$=Number($condition$$.den_thang);const $ma_kho$$=$condition$$.ma_kho,$id_app$$=$condition$$.id_app,$tu_ngay$$=moment(new Date($condition$$.nam,$t_tu_thang$$-
4
- 1,15)).startOf("month").toDate(),$den_ngay$$=moment(new Date($condition$$.nam,$d_voucher_den_thang$$-1,15)).endOf("month").toDate();var $session_thangs$$=getCurrentSession(),$app_query_dmvt$$=await global.getModel("app").findOne({_id:$id_app$$},{options:1}).lean();if(!$app_query_dmvt$$)throw Error("C\u00f4ng ty n\u00e0y kh\u00f4ng t\u1ed3n t\u1ea1i");const $f_tien$$=($app_query_dmvt$$.options||{}).f_tien||0;$app_query_dmvt$$={id_app:$id_app$$,gia_xuat:"1"};$condition$$.ma_nvt&&($app_query_dmvt$$.ma_nvt=
5
- $condition$$.ma_nvt);$condition$$.ma_ncc&&($app_query_dmvt$$.ma_ncc=$condition$$.ma_ncc);$condition$$.ma_vt&&($app_query_dmvt$$.ma_vt=$condition$$.ma_vt);Logger.info(`\ud83d\udd25[tinhgiatb] Start | Kho: ${$ma_kho$$} | Session: ${$session_thangs$$?._debugId}`);const $dmqdList$$=await dmqddvt.find({id_app:$id_app$$}).lean(),$mapQuyDoi$$=new Map;$dmqdList$$.forEach($qd$$=>{$mapQuyDoi$$.set(`${$qd$$.ma_vt}-${$qd$$.ma_dvt}`,($qd$$.mau?$qd$$.tu/$qd$$.mau:$qd$$.ty_le_qd)||1)});const $listVt$$=await dmvt.find($app_query_dmvt$$).lean();
6
- Logger.info(`\ud83d\ude80 [tinhgiatb] B\u1eaft \u0111\u1ea7u t\u00ednh gi\u00e1 cho ${$listVt$$.length} v\u1eadt t\u01b0 (Ch\u1ea1y song song)...`);const $processOneVt$$=async $query$$=>{$query$$={id_app:$id_app$$,tu_ngay:$tu_ngay$$,den_ngay:$den_ngay$$,ma_vt:$query$$.ma_vt,ma_kho:$ma_kho$$};try{return{...(await tinhGiaTbPromise($query$$)),id_app:$id_app$$,ma_kho:$ma_kho$$,status:!0}}catch($err$$){throw Logger.error("[tinhgiatb] L\u1ed7i t\u00ednh gi\u00e1 VT:",$query$$.ma_vt,$err$$.message),$err$$;
7
- }},$bang_gia$$=await new Promise(($resolve$$,$reject$$)=>{async.mapLimit($listVt$$,100,async $vt$$=>await $processOneVt$$($vt$$),($err$$,$results$$)=>{if($err$$)return $reject$$($err$$);$resolve$$($results$$)})}),$mapGiaTB$$=new Map;$bang_gia$$.forEach($g$$=>$mapGiaTB$$.set($g$$.ma_vt,$g$$.gia));for($session_thangs$$=[];$t_tu_thang$$<=$d_voucher_den_thang$$;$t_tu_thang$$++)$session_thangs$$.push($t_tu_thang$$);const $ma_vts$$=$bang_gia$$.map($g$$=>$g$$.ma_vt);let $query_delete_gia$$={id_app:$id_app$$,
8
- ma_vt:{$in:$ma_vts$$},nam:$condition$$.nam,thang:{$in:$session_thangs$$}};$ma_kho$$&&($query_delete_gia$$.ma_kho=$ma_kho$$);await giatb.deleteMany($query_delete_gia$$);let $newGiaEntries$$=[];for(const $t$$ of $session_thangs$$)$bang_gia$$.forEach($gia$$=>{$newGiaEntries$$.push({...$gia$$,thang:$t$$,nam:$condition$$.nam})});$newGiaEntries$$.length>0&&await giatb.insertMany($newGiaEntries$$);Logger.info("[tinhgiatb] \ud83d\ude80 \u0110ang l\u1ea5y danh s\u00e1ch ch\u1ee9ng t\u1eeb (Parallel Mode)...");
9
- let $vouchers_x$$=new Map,$vouchers_n$$=new Map;const $id_ct_need_delete$$=[],$fetchVouchersByMaCt$$=async $entries_groupedSks$$=>{const $results$$=new Map,$deleteIds$$=[];$entries_groupedSks$$=Object.entries($entries_groupedSks$$);await Promise.all($entries_groupedSks$$.map(async([$ma_ct$$,$listSk$$])=>{try{const $ctrl$$=global.controllers[$ma_ct$$.toUpperCase()],$Model$$=$ctrl$$?$ctrl$$.getProperty("model"):mongoose.models[$ma_ct$$.toLowerCase()];if($Model$$){var $ids$$=[...(new Set($listSk$$.map($s$$=>
10
- $s$$.id_ct)))],$foundVouchers$$=await $Model$$.find({_id:{$in:$ids$$}}).lean(),$foundIds$$=new Set;for(const $v$$ of $foundVouchers$$)$results$$.set($v$$._id.toString(),$v$$),$foundIds$$.add($v$$._id.toString());$ids$$.forEach($id$$=>{$foundIds$$.has($id$$.toString())||$deleteIds$$.push($id$$)})}else Logger.warn(`\u26a0\ufe0f [tinhgiatb] Kh\u00f4ng t\u00ecm th\u1ea5y model: ${$ma_ct$$}`)}catch($err$$){throw Logger.error(`\u274c L\u1ed7i load ch\u1ee9ng t\u1eeb ${$ma_ct$$}:`,$err$$.message),$err$$;
11
- }}));return{vouchers:$results$$,deleteIds:$deleteIds$$}};await Promise.all([(async()=>{var $groupedSksX_query_sokho_x_res_sks_x$$={id_app:$id_app$$,ngay_ct:{$gte:$tu_ngay$$,$lte:$den_ngay$$},nxt:2,ma_vt:{$in:$ma_vts$$},px_gia_dd:!1};$ma_kho$$&&($groupedSksX_query_sokho_x_res_sks_x$$.ma_kho=$ma_kho$$);$groupedSksX_query_sokho_x_res_sks_x$$=await sokho.find($groupedSksX_query_sokho_x_res_sks_x$$).lean();$groupedSksX_query_sokho_x_res_sks_x$$=_.groupBy($groupedSksX_query_sokho_x_res_sks_x$$,"ma_ct");
12
- $groupedSksX_query_sokho_x_res_sks_x$$=await $fetchVouchersByMaCt$$($groupedSksX_query_sokho_x_res_sks_x$$);$groupedSksX_query_sokho_x_res_sks_x$$.vouchers.forEach(($v$$,$k$$)=>$vouchers_x$$.set($k$$,$v$$));$id_ct_need_delete$$.push(...$groupedSksX_query_sokho_x_res_sks_x$$.deleteIds)})(),(async()=>{var $groupedSksN_query_sokho_n_res$$={id_app:$id_app$$,ngay_ct:{$gte:$tu_ngay$$,$lte:$den_ngay$$},nxt:1,ma_vt:{$in:$ma_vts$$},pn_gia_tb:!0,ma_ct:{$ne:"PXC"}};$ma_kho$$&&($groupedSksN_query_sokho_n_res$$.ma_kho=
13
- $ma_kho$$);$groupedSksN_query_sokho_n_res$$=await sokho.find($groupedSksN_query_sokho_n_res$$).lean();$groupedSksN_query_sokho_n_res$$=_.groupBy($groupedSksN_query_sokho_n_res$$,"ma_ct");$groupedSksN_query_sokho_n_res$$=await $fetchVouchersByMaCt$$($groupedSksN_query_sokho_n_res$$);$groupedSksN_query_sokho_n_res$$.vouchers.forEach(($v$$,$k$$)=>$vouchers_n$$.set($k$$,$v$$));$id_ct_need_delete$$.push(...$groupedSksN_query_sokho_n_res$$.deleteIds)})(),(async()=>{if($ma_kho$$){let $query_sokho_pnc$$=
14
- {id_app:$id_app$$,ma_ct:"PNC",ngay_ct:{$gte:$tu_ngay$$,$lte:$den_ngay$$},$or:[{ma_kho_x:$ma_kho$$},{"details.ma_kho_x":$ma_kho$$}],"details.px_gia_dd":!1};(await global.getModel("pnc").find($query_sokho_pnc$$).lean()).forEach($p$$=>$vouchers_n$$.set($p$$._id.toString(),$p$$))}})()]);$id_ct_need_delete$$.length>0&&(await sokho.deleteMany({id_app:$id_app$$,id_ct:{$in:$id_ct_need_delete$$}}),await socai.deleteMany({id_app:$id_app$$,id_ct:{$in:$id_ct_need_delete$$}}));const $updateDetailPrice$$=$d$$=>
15
- {const $gia_chuan$$=$mapGiaTB$$.get($d$$.ma_vt)||0;var $JSCompiler_inline_result$jscomp$0_ma_vt$$=$d$$.ma_vt;var $ma_dvt$$=$d$$.ma_dvt;$JSCompiler_inline_result$jscomp$0_ma_vt$$=$ma_dvt$$?$mapQuyDoi$$.get(`${$JSCompiler_inline_result$jscomp$0_ma_vt$$}-${$ma_dvt$$}`)||1:1;$d$$.gia_von=$d$$.gia_von_nt=$gia_chuan$$*$JSCompiler_inline_result$jscomp$0_ma_vt$$;return{gia_chuan:$gia_chuan$$,he_so_qd:$JSCompiler_inline_result$jscomp$0_ma_vt$$}};Logger.info(`[tinhgiatb] C\u1eadp nh\u1eadt ${$vouchers_x$$.size} phi\u1ebfu xu\u1ea5t...`);
16
- const $sortedVouchersX$$=_.sortBy([...$vouchers_x$$.values()],$v$$=>(new Date($v$$.ngay_ct)).getTime());for(const $voucher$$ of $sortedVouchersX$$){let $isModified$$=!1;$voucher$$.details&&$voucher$$.details.forEach($d$$=>{$condition$$.ma_vt&&$condition$$.ma_vt!==$d$$.ma_vt||$d$$.px_gia_dd||$ma_kho$$&&$ma_kho$$!==($d$$.ma_kho||$d$$.ma_kho_x||$voucher$$.ma_kho||$voucher$$.ma_kho_x)||($updateDetailPrice$$($d$$),$d$$.tien_xuat=$d$$.tien_xuat_nt=Math.roundBy($d$$.sl_xuat*$d$$.gia_von,$f_tien$$),$isModified$$=
17
- !0);if($d$$.combo&&$d$$.combo.length>0&&!$d$$.px_gia_dd){let $totalTienCombo$$=0;$d$$.combo.forEach($c$$=>{$condition$$.ma_vt&&$condition$$.ma_vt!==$c$$.ma_vt||$ma_kho$$&&$ma_kho$$!==($d$$.ma_kho||$d$$.ma_kho_x||$voucher$$.ma_kho||$voucher$$.ma_kho_x)||($updateDetailPrice$$($c$$),$c$$.tien_xuat=$c$$.tien_xuat_nt=Math.roundBy(($c$$.sl_xuat||0)*$c$$.gia_von,$f_tien$$),$isModified$$=!0);$totalTienCombo$$+=$c$$.tien_xuat_nt||0});$d$$.tien_xuat=$d$$.tien_xuat_nt=$totalTienCombo$$;$d$$.gia_von=$d$$.gia_von_nt=
18
- Math.roundBy($d$$.sl_xuat?$d$$.tien_xuat_nt/$d$$.sl_xuat:0,$f_tien$$)}$d$$.promotion&&$d$$.promotion.details_km&&$d$$.promotion.details_km.forEach($km$$=>{$condition$$.ma_vt&&$condition$$.ma_vt!==$km$$.ma_vt||$km$$.px_gia_dd||$ma_kho$$&&$ma_kho$$!==($km$$.ma_kho||$voucher$$.ma_kho||$voucher$$.ma_kho_x)||($updateDetailPrice$$($km$$),$km$$.tien_xuat=$km$$.tien_xuat_nt=Math.roundBy(($km$$.sl_xuat||$km$$.sl_km||0)*$km$$.gia_von,$f_tien$$),$isModified$$=!0)})});$voucher$$.details_doi&&$voucher$$.details_doi.forEach($d$$=>
19
- {$condition$$.ma_vt&&$condition$$.ma_vt!==$d$$.ma_vt||$d$$.px_gia_dd||$ma_kho$$&&$ma_kho$$!==($d$$.ma_kho||$voucher$$.ma_kho||$voucher$$.ma_kho_x)||($updateDetailPrice$$($d$$),$d$$.tien_xuat=$d$$.tien_xuat_nt=Math.roundBy($d$$.sl_xuat*$d$$.gia_von,$f_tien$$),$isModified$$=!0)});if($isModified$$){const $voucherData$$=getRawData($voucher$$),{_id:$_id$$,...$updatePayload$$}=$voucherData$$,$ctrl$$=global.controllers[$voucher$$.ma_ct.toUpperCase()];await ($ctrl$$?$ctrl$$.getProperty("model"):mongoose.models[$voucher$$.ma_ct.toLowerCase()]).updateOne({_id:$voucher$$._id},
20
- $updatePayload$$);$ctrl$$&&$ctrl$$.post&&await postDataPromise($voucherData$$,$ctrl$$)}}Logger.info(`[tinhgiatb] C\u1eadp nh\u1eadt ${$vouchers_n$$.size} phi\u1ebfu nh\u1eadp...`);const $sortedVouchersN$$=_.sortBy([...$vouchers_n$$.values()],$v$$=>(new Date($v$$.ngay_ct)).getTime());for(const $voucher$$ of $sortedVouchersN$$){let $isModified$$=!1;$voucher$$.details&&$voucher$$.details.forEach($d$$=>{$d$$.combo&&$d$$.combo.length!=0||$condition$$.ma_vt&&$condition$$.ma_vt!==$d$$.ma_vt||!($d$$.pn_gia_tb||
21
- $voucher$$.ma_ct==="PNC"&&!$d$$.px_gia_dd||$voucher$$.ma_ct==="PKK"&&!$d$$.px_gia_dd)||$ma_kho$$&&$ma_kho$$!==$d$$.ma_kho&&$ma_kho$$!==$d$$.ma_kho_x&&$ma_kho$$!==$voucher$$.ma_kho&&$ma_kho$$!==$voucher$$.ma_kho_n||($updateDetailPrice$$($d$$),$voucher$$.ma_ct==="PNC"||$voucher$$.ma_ct==="PKK"?($d$$.tien_xuat_nt=Math.roundBy($d$$.sl_xuat*$d$$.gia_von,$f_tien$$),$d$$.tien_xuat=$d$$.tien_xuat_nt):($d$$.tien_nhap_nt=Math.roundBy($d$$.sl_nhap*$d$$.gia_von,$f_tien$$),$d$$.tien_nhap=$d$$.tien_nhap_nt),$isModified$$=
22
- !0);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)){let $totalTienCombo$$=0;$d$$.combo.forEach($c$$=>{$condition$$.ma_vt&&$condition$$.ma_vt!==$c$$.ma_vt||$ma_kho$$&&$ma_kho$$!==($d$$.ma_kho||$voucher$$.ma_kho||$voucher$$.ma_kho_n)||($updateDetailPrice$$($c$$),$voucher$$.ma_ct==="PNC"||$voucher$$.ma_ct==="PKK"?($c$$.tien_xuat_nt=Math.roundBy($c$$.sl_xuat*$c$$.gia_von,$f_tien$$),$c$$.tien_xuat=$c$$.tien_xuat_nt):
23
- ($c$$.tien_nhap_nt=Math.roundBy($c$$.sl_nhap*$c$$.gia_von,$f_tien$$),$c$$.tien_nhap=$c$$.tien_nhap_nt),$isModified$$=!0);$totalTienCombo$$+=$voucher$$.ma_ct==="PNC"||$voucher$$.ma_ct==="PKK"?$c$$.tien_xuat_nt||0:$c$$.tien_nhap_nt||0});$voucher$$.ma_ct==="PNC"||$voucher$$.ma_ct==="PKK"?($d$$.tien_xuat=$d$$.tien_xuat_nt=$totalTienCombo$$,$d$$.gia_von=$d$$.gia_von_nt=Math.roundBy($d$$.sl_xuat?$d$$.tien_xuat_nt/$d$$.sl_xuat:0,$f_tien$$)):($d$$.tien_nhap=$d$$.tien_nhap_nt=$totalTienCombo$$,$d$$.gia_von=
24
- $d$$.gia_von_nt=Math.roundBy($d$$.sl_nhap?$d$$.tien_nhap_nt/$d$$.sl_nhap:0,$f_tien$$))}});$voucher$$.details_doi&&$voucher$$.details_doi.forEach($d$$=>{$condition$$.ma_vt&&$condition$$.ma_vt!==$d$$.ma_vt||!$d$$.pn_gia_tb||$ma_kho$$&&$ma_kho$$!==$d$$.ma_kho||($updateDetailPrice$$($d$$),$d$$.tien_xuat_nt=Math.roundBy($d$$.sl_xuat*$d$$.gia_von,$f_tien$$),$d$$.tien_xuat=$d$$.tien_xuat_nt,$isModified$$=!0)});if($isModified$$){const $voucherData$$=getRawData($voucher$$),$ctrl$$=global.controllers[$voucher$$.ma_ct.toUpperCase()];
25
- await ($ctrl$$?$ctrl$$.getProperty("model"):mongoose.models[$voucher$$.ma_ct.toLowerCase()]).updateOne({_id:$voucher$$._id},$voucherData$$);$ctrl$$&&$ctrl$$.post&&await postDataPromise($voucherData$$,$ctrl$$)}}Logger.info("[tinhgiatb] Ki\u1ec3m tra ch\u00eanh l\u1ec7ch...");let $query_ckvt$$={id_app:$condition$$.id_app,ngay:$den_ngay$$,chenh_lech:1};$condition$$.ma_vt&&($query_ckvt$$.ma_vt=$condition$$.ma_vt);$condition$$.ma_kho&&($query_ckvt$$.ma_kho=$condition$$.ma_kho);const $du_cuoi_ky$$=await new Promise(($resolve$$,
26
- $reject$$)=>{ckvt($query_ckvt$$,($err$$,$data$$)=>$err$$?$reject$$($err$$):$resolve$$($data$$))}),$validDuCuoiKy$$=_.filter($du_cuoi_ky$$,$r$$=>$r$$.du00!==0&&($r$$.ton00==0||Math.abs($r$$.ton00)<.001)),$descVouchersX$$=_.sortBy([...$vouchers_x$$.values()],$v$$=>-(new Date($v$$.ngay_ct)).getTime()),$descVouchersN$$=_.sortBy([...$vouchers_n$$.values()],$v$$=>-(new Date($v$$.ngay_ct)).getTime());let $chung_tu_cap_nhat_chenh_lech$$=new Map;for(const $vt$$ of $validDuCuoiKy$$)$d_voucher_den_thang$$=null,
27
- ($d_voucher_den_thang$$=$descVouchersX$$.find($x$$=>{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));return $det$$?($det$$.tien_xuat_nt+=$vt$$.du00,$det$$.tien_xuat=Math.roundBy($det$$.tien_xuat_nt,$f_tien$$),$det$$.sl_xuat&&($det$$.gia_von=$det$$.gia_von_nt=Math.roundBy($det$$.tien_xuat_nt/$det$$.sl_xuat,0)),!0):!1}))||($d_voucher_den_thang$$=$descVouchersN$$.find($n$$=>{const $det$$=($n$$.details||
28
- []).find($vc$$=>$vc$$.ma_vt==$vt$$.ma_vt&&!$vc$$.pn_gia_tb&&(!$ma_kho$$||$ma_kho$$===$vc$$.ma_kho||$ma_kho$$===$n$$.ma_kho));return $det$$?($det$$.tien_nhap_nt-=$vt$$.du00,$det$$.tien_nhap=Math.roundBy($det$$.tien_nhap_nt,$f_tien$$),$det$$.sl_nhap&&($det$$.gia_von=$det$$.gia_von_nt=Math.roundBy($det$$.tien_nhap_nt/$det$$.sl_nhap,0)),!0):!1})),$d_voucher_den_thang$$&&$chung_tu_cap_nhat_chenh_lech$$.set($d_voucher_den_thang$$._id.toString(),$d_voucher_den_thang$$);if($chung_tu_cap_nhat_chenh_lech$$.size>
29
- 0){Logger.info(`\u26a0\ufe0f [tinhgiatb] Updating ${$chung_tu_cap_nhat_chenh_lech$$.size} vouchers for discrepancy...`);for(const $voucher$$ of $chung_tu_cap_nhat_chenh_lech$$.values()){const $voucherData$$=getRawData($voucher$$),$ctrl$$=global.controllers[$voucherData$$.ma_ct.toUpperCase()];await ($ctrl$$?$ctrl$$.getProperty("model"):mongoose.models[$voucherData$$.ma_ct.toLowerCase()]).updateOne({_id:$voucher$$._id},$voucherData$$);$ctrl$$&&$ctrl$$.post&&await postDataPromise($voucherData$$,$ctrl$$)}}await new Promise($resolve$$=>
30
- {$bang_gia$$.joinModel2($id_app$$,dmvt,[{where:"ma_vt",fields:"ten_vt"}],$resolve$$)});$fn$$(null,$bang_gia$$)}catch($e$$){Logger.error("\u274c [tinhgiatb] Critical Error:",$e$$),$fn$$($e$$)}};
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("./dkvt"),sokho=global.getModel("sokho"),async=require("async");
2
- module.exports=function($condition$$,$fn$$){if(!($condition$$&&$condition$$.ma_vt&&$condition$$.tu_ngay&&$condition$$.den_ngay&&$condition$$.id_app))return $fn$$("L\u1ed7i: B\u00e1o c\u00e1o n\u00e0y y\u00eau c\u1ea7u c\u00e1c tham s\u1ed1: tu_ngay,den_ngay,id_app,ma_vt");let $ma_vt$$=$condition$$.ma_vt,$tu_ngay$$=$condition$$.tu_ngay,$den_ngay$$=$condition$$.den_ngay,$id_app$$=$condition$$.id_app;async.parallel({dn:function($callback$$){let $query$$={ngay:$tu_ngay$$,id_app:$id_app$$};$condition$$.ma_kho&&
3
- ($query$$.ma_kho=$condition$$.ma_kho);$query$$.ma_vt=$condition$$.ma_vt;dkvt($query$$,function($error$$,$du_dau_result$$){if($error$$)return $callback$$($error$$);$error$$=$du_dau_result$$.csum("ton00");$du_dau_result$$=$du_dau_result$$.csum("du00");$callback$$(null,{ton_dau:$error$$,du_dau:$du_dau_result$$})})},ps:function($callback$$){let $query$$={id_app:$id_app$$,ngay_ct:{$gte:$tu_ngay$$,$lt:$den_ngay$$},ma_vt:$ma_vt$$,$or:[{nxt:1,pn_gia_tb:!1},{nxt:2,px_gia_dd:!0}]};$condition$$.ma_kho&&($query$$.ma_kho=
4
- $condition$$.ma_kho,$query$$.$or.push({nxt:1,pn_gia_tb:!0,ma_ct:"PNC"}));sokho.find($query$$).lean().then(function($pss_tien_nhap$$){$pss_tien_nhap$$=$pss_tien_nhap$$.map($ps$$=>{$ps$$.sl_nhap=($ps$$.sl_nhap_qd||0)-($ps$$.sl_xuat_qd||0);return $ps$$});let $sl_nhap$$=$pss_tien_nhap$$.csum("sl_nhap");$pss_tien_nhap$$=$pss_tien_nhap$$.csum("tien_nhap")-$pss_tien_nhap$$.csum("tien_xuat");$callback$$(null,{sl_nhap:$sl_nhap$$,tien_nhap:$pss_tien_nhap$$})}).catch($e$$=>{$callback$$($e$$)})}},function($error$$,
5
- $results$$){if($error$$)$fn$$($error$$);else{$error$$=$results$$.ps.sl_nhap+$results$$.dn.ton_dau;var $tong_tien$$=$results$$.ps.tien_nhap+$results$$.dn.du_dau,$gia$$=$error$$?Math.roundBy($tong_tien$$/$error$$,0):0;return $fn$$(null,{ma_vt:$ma_vt$$,ton_dau:$results$$.dn.ton_dau,du_dau:$results$$.dn.du_dau,sl_nhap:$results$$.ps.sl_nhap,tien_nhap:$results$$.ps.tien_nhap,tong_sl:$error$$,tong_tien:$tong_tien$$,gia:$gia$$})}})};
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
+ };