bep40 commited on
Commit
7eabacd
·
verified ·
1 Parent(s): ff19ffb

FIX v1037.3: Thêm CORS proxy fallback wsrv.nl cho ảnh trong Excel khi fetch trực tiếp fail (Eurogold, Hafele, v.v. không có CORS header)

Browse files
Files changed (1) hide show
  1. qr-payment.js +27 -12
qr-payment.js CHANGED
@@ -1,4 +1,7 @@
1
- /* V.AI STUDIO — QR Payment / Quote Data v1037.2
 
 
 
2
  * FIX v1037.2:
3
  * 1) parsePrompt() extract brand discount (malloca ck35% → itemDiscounts)
4
  * 2) matchItemDiscount() brand matching fallback (Malloca→35%)
@@ -8,7 +11,6 @@
8
  * không qua VAI_QR.parsePrompt, nên comprehensive-fix.js không áp dụng được.
9
  * Thêm guards trong parsePrompt() để skip bare numbers, brand+discount, CK lines.
10
  * Restored from 9db23f9 (v24) — full Excel/PDF export with proper layout
11
- * Fixes: Excel export broken in v1036 (parameter mismatch, missing layout)
12
  */
13
  (function(){
14
  'use strict';
@@ -39,12 +41,10 @@ function vaiProductInfoForQuote(product){
39
 
40
  function compactKey(s){return stripDiacritics(String(s||'')).toLowerCase().replace(/[^a-z0-9]/g,'');}
41
  function itemKeys(it){var arr=[];['model','sku','code','ma','name'].forEach(function(k){if(it&&it[k])arr.push(compactKey(it[k]));});if(it&&it.name){String(it.name).split('|').forEach(function(x){arr.push(compactKey(x));});}return arr.filter(function(x){return x&&x.length>=3;});}
42
- // FIX v1037.2: matchItemDiscount - thêm brand matching fallback
43
  function matchItemDiscount(it,itemDiscounts){
44
  itemDiscounts=itemDiscounts||{};var keys=itemKeys(it);
45
  for(var i=0;i<keys.length;i++){var k=keys[i];if(itemDiscounts[k])return itemDiscounts[k];}
46
  for(var code in itemDiscounts){for(var j=0;j<keys.length;j++){var kk=keys[j];if(code.length>=3&&kk.length>=3&&(kk.indexOf(code)!==-1||code.indexOf(kk)!==-1))return itemDiscounts[code];}}
47
- // Brand matching fallback (vd "malloca" → 35 cho sản phẩm Malloca)
48
  var brandKeys=Object.keys(itemDiscounts).filter(function(k){return !k.match(/\d[A-Za-z0-9._\/-]*$/);});
49
  if(brandKeys.length>0){
50
  var itemBrand='';var idx=(it.idx!=null?it.idx:(it.productIdx!=null?it.productIdx:null));
@@ -69,7 +69,6 @@ function applyPromptDiscounts(qd,parsed){
69
  qd.grandTotal=productTotal;return qd;
70
  }
71
 
72
- // FIX v1037.2: parsePrompt - extract brand discount (malloca ck35%) vào itemDiscounts
73
  function parsePrompt(text){
74
  var fees=[],notes=[],discount=null,deposit=0,discountPercent=0,itemDiscounts={};
75
  if(!text)return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
@@ -77,7 +76,6 @@ function parsePrompt(text){
77
  line=line.trim();if(!line)return;
78
  parseItemDiscountLine(line,itemDiscounts);
79
  if(Object.keys(itemDiscounts).length&&line.match(/\b[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d[A-Za-z0-9._\-\/]*\b/i)&&line.match(/\b(ck|chiết khấu|chiet khau|giảm|giam|discount)\b/i))return;
80
- // Extract brand-level discount BEFORE skipping (vd "malloca ck35%" → itemDiscounts.malloca=35)
81
  var brandCkMatch=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*(?:ck|chiet|chiet khau|giam|discount|chiết khấu)\s*([\d.,]+)/i);
82
  if(brandCkMatch){var brandName=brandCkMatch[1].trim().toLowerCase().replace(/[^a-z0-9]/g,'');var pct=parseFloat(brandCkMatch[2].replace(/\./g,'').replace(',','.'));if(pct>0&&pct<=100)itemDiscounts[brandName]=pct;notes.push(line);return;}
83
  if(line.match(/^(ck|chiết khấu|chiet khau|giảm|giam|discount)/i)){
@@ -91,11 +89,9 @@ function parsePrompt(text){
91
  if(dm){var da=parseFloat(dm[1].replace(/\./g,'').replace(',','.'));var du=(dm[2]||'').toLowerCase();if(du==='k'||du==='nghìn'||du==='nghin')da*=1000;if(du==='triệu'||du==='tr')da*=1000000;deposit=da;}
92
  return;
93
  }
94
- // ===== FIX v1037.1: Skip bare numbers / discount notations =====
95
  if(line.match(/^[\d.,]+$/)){notes.push(line);return;}
96
  var priceUnit=line.match(/^[\d.,]+\s*(k|nghìn|nghin|tr|triệu|m|%|đ|d|vnd)?$/i);
97
  if(priceUnit&&line.replace(/[\d.,\s%\knhìnghintrriệuđdvnđvndKHTRML]/gi,'').trim().length<2){notes.push(line);return;}
98
- // Dòng brand+discount (extracted above) hoặc brand+số
99
  var brandNum=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*[\d.,]/i);
100
  if(brandNum){var textAfterBrand=line.replace(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)/i,'').trim();if(textAfterBrand.match(/^[\d.,\s%(k|nghìn|nghin|tr|triệu|m|đ|d|vnd)]+$/i)){notes.push(line);return;}}
101
  var m=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
@@ -197,7 +193,7 @@ function doOverride(){
197
  window.exportDeliveryExcel=async function(){if(typeof ExcelJS==='undefined')return;var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryExcel(qd,code);};
198
  window.exportDeliveryPDF=async function(){var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryPDF(qd,code);};
199
  window.getQuoteData=function(){var qd=window._origGetQuoteData.apply(this,arguments);if(qd.customer)qd.customer.orderCode=getEffectiveOrderCode();return qd;};
200
- console.log('✅ QR Payment v1037.2 — brand discounts, formula-only Excel, no phantom fees');return true;
201
  }
202
 
203
  async function _doExportPDF(d,code){
@@ -216,6 +212,7 @@ async function _doExportPDF(d,code){
216
  async function _doExportDeliveryPDF(qd,code){var cv=await toCanvas(buildDeliveryHTML(qd,code),700);var img=cv.toDataURL('image/png');var pdf=new jspdf.jsPDF({orientation:'p',unit:'pt',format:'a4'});var margin=25,usableW=595-margin*2;var ratio=cv.height/cv.width;var imgH=usableW*ratio;if(imgH>842-margin*2){imgH=842-margin*2;usableW=imgH/ratio;}pdf.addImage(img,'PNG',margin,margin,usableW,imgH);pdf.save('GH-'+code+'.pdf');}
217
 
218
  // FIX v1037.2: _doExportExcel - Giá CK luôn là formula, ko hằng số
 
219
  async function _doExportExcel(d,qd,code,qr){
220
  var MONEY_FMT='#,##0"đ"';
221
  var NUM_FMT='#,##0';
@@ -250,13 +247,32 @@ async function _doExportExcel(d,qd,code,qr){
250
  var bgColor=i%2===0?'FFF8FAFC':'FFFFFFFF';
251
  var RB={top:{style:'thin',color:{argb:bgColor}},bottom:{style:'thin',color:{argb:bgColor}},left:{style:'thin',color:{argb:bgColor}},right:{style:'thin',color:{argb:bgColor}}};
252
  row.getCell(1).value=it.stt||i+1;row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
253
- if(it.image&&it.image.startsWith('http')){try{var ir=await fetch(it.image,{mode:'cors'});if(ir.ok){var ib=await ir.arrayBuffer();ws.addImage(wb.addImage({buffer:ib,extension:it.image.includes('.png')?'png':'jpeg'}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});}}catch(e){}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
255
  row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center',vertical:'middle'};
256
  row.getCell(5).value=it.specs||'';row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
257
  row.getCell(6).value=Number(it.qty||1);row.getCell(6).numFmt=NUM_FMT;row.getCell(6).alignment={horizontal:'center',vertical:'middle'};
258
  row.getCell(7).value=Number(it.price||0);row.getCell(7).numFmt=MONEY_FMT;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
259
- // Giá CK luôn là formula =G{row}*factor — ko hardcode hằng số
260
  var itemPct=matchItemDiscount(it,d.itemDiscounts||{});
261
  var discountFactor=1.0;
262
  if(itemPct>0&&itemPct<=100){discountFactor=1-itemPct/100;}
@@ -281,7 +297,6 @@ async function _doExportExcel(d,qd,code,qr){
281
  tRow.getCell(1).value='TỔNG CỘNG';tRow.getCell(1).font={bold:true,size:13,color:{argb:'FFFFFFFF'}};tRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};tRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(1).border=TB;
282
  tRow.getCell(6).value={formula:totalQtyFormula,result:totalQtyValue};tRow.getCell(6).numFmt=NUM_FMT;tRow.getCell(6).font={bold:true,size:12,color:{argb:'FFFFFFFF'}};tRow.getCell(6).alignment={horizontal:'center',vertical:'middle'};tRow.getCell(6).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(6).border=TB;
283
  tRow.getCell(7).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(7).border=TB;tRow.getCell(8).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(8).border=TB;
284
- // TỔNG CỘNG = SUM(thành tiền sp) + phụ phí
285
  tRow.getCell(9).value={formula:sumParts.length?sumParts.join('+'):'0',result:Number(d.grandTotal||0)};tRow.getCell(9).numFmt=MONEY_FMT;tRow.getCell(9).font={bold:true,size:14,color:{argb:'FFF0B840'}};tRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};tRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(9).border=TB;tRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(10).border=TB;cr++;
286
  if(d.deposit>0){var DBG='FFF0FDF4';var DB={top:{style:'thin',color:{argb:DBG}},bottom:{style:'thin',color:{argb:DBG}},left:{style:'thin',color:{argb:DBG}},right:{style:'thin',color:{argb:DBG}}};ws.mergeCells(cr,1,cr,8);var dRow=ws.getRow(cr);dRow.height=24;dRow.getCell(1).value='Đã cọc';dRow.getCell(1).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};dRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(1).border=DB;dRow.getCell(9).value=Number(d.deposit||0);dRow.getCell(9).numFmt=MONEY_FMT;dRow.getCell(9).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};dRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(9).border=DB;dRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(10).border=DB;var depositRowNum=cr;cr++;var RBG='FFFEF2F2';var RBD={top:{style:'thin',color:{argb:RBG}},bottom:{style:'thin',color:{argb:RBG}},left:{style:'thin',color:{argb:RBG}},right:{style:'thin',color:{argb:RBG}}};ws.mergeCells(cr,1,cr,8);var rRow=ws.getRow(cr);rRow.height=26;rRow.getCell(1).value='CÒN LẠI';rRow.getCell(1).font={bold:true,size:12,color:{argb:'FFDC2626'}};rRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};rRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(1).border=RBD;rRow.getCell(9).value={formula:'MAX(I'+totalRowNum+'-I'+depositRowNum+',0)',result:Number(d.remaining||0)};rRow.getCell(9).numFmt=MONEY_FMT;rRow.getCell(9).font={bold:true,size:13,color:{argb:'FFDC2626'}};rRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};rRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(9).border=RBD;rRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(10).border=RBD;cr++;}
287
  if(d.notes&&d.notes.length){var NBG='FFF0FDF4';var NB2={top:{style:'thin',color:{argb:NBG}},bottom:{style:'thin',color:{argb:NBG}},left:{style:'thin',color:{argb:NBG}},right:{style:'thin',color:{argb:NBG}}};ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='📝 Ghi chú: '+d.notes.join('; ');ws.getCell('A'+cr).font={italic:true,size:9,color:{argb:'FF166534'}};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:NBG}};ws.getCell('A'+cr).border=NB2;cr++;}
 
1
+ /* V.AI STUDIO — QR Payment / Quote Data v1037.3
2
+ * FIX v1037.3: Thêm CORS proxy fallback wsrv.nl cho ảnh trong Excel
3
+ * - fetch trực tiếp fail (CORS) → fallback qua wsrv.nl
4
+ * - Tested: Eurogold, Hafele images đều load được với ACL=*
5
  * FIX v1037.2:
6
  * 1) parsePrompt() extract brand discount (malloca ck35% → itemDiscounts)
7
  * 2) matchItemDiscount() brand matching fallback (Malloca→35%)
 
11
  * không qua VAI_QR.parsePrompt, nên comprehensive-fix.js không áp dụng được.
12
  * Thêm guards trong parsePrompt() để skip bare numbers, brand+discount, CK lines.
13
  * Restored from 9db23f9 (v24) — full Excel/PDF export with proper layout
 
14
  */
15
  (function(){
16
  'use strict';
 
41
 
42
  function compactKey(s){return stripDiacritics(String(s||'')).toLowerCase().replace(/[^a-z0-9]/g,'');}
43
  function itemKeys(it){var arr=[];['model','sku','code','ma','name'].forEach(function(k){if(it&&it[k])arr.push(compactKey(it[k]));});if(it&&it.name){String(it.name).split('|').forEach(function(x){arr.push(compactKey(x));});}return arr.filter(function(x){return x&&x.length>=3;});}
 
44
  function matchItemDiscount(it,itemDiscounts){
45
  itemDiscounts=itemDiscounts||{};var keys=itemKeys(it);
46
  for(var i=0;i<keys.length;i++){var k=keys[i];if(itemDiscounts[k])return itemDiscounts[k];}
47
  for(var code in itemDiscounts){for(var j=0;j<keys.length;j++){var kk=keys[j];if(code.length>=3&&kk.length>=3&&(kk.indexOf(code)!==-1||code.indexOf(kk)!==-1))return itemDiscounts[code];}}
 
48
  var brandKeys=Object.keys(itemDiscounts).filter(function(k){return !k.match(/\d[A-Za-z0-9._\/-]*$/);});
49
  if(brandKeys.length>0){
50
  var itemBrand='';var idx=(it.idx!=null?it.idx:(it.productIdx!=null?it.productIdx:null));
 
69
  qd.grandTotal=productTotal;return qd;
70
  }
71
 
 
72
  function parsePrompt(text){
73
  var fees=[],notes=[],discount=null,deposit=0,discountPercent=0,itemDiscounts={};
74
  if(!text)return{fees:fees,notes:notes,discount:discount,deposit:deposit,discountPercent:discountPercent,itemDiscounts:itemDiscounts};
 
76
  line=line.trim();if(!line)return;
77
  parseItemDiscountLine(line,itemDiscounts);
78
  if(Object.keys(itemDiscounts).length&&line.match(/\b[A-Za-z]{1,8}[A-Za-z0-9._\-\/]{1,30}\d[A-Za-z0-9._\-\/]*\b/i)&&line.match(/\b(ck|chiết khấu|chiet khau|giảm|giam|discount)\b/i))return;
 
79
  var brandCkMatch=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*(?:ck|chiet|chiet khau|giam|discount|chiết khấu)\s*([\d.,]+)/i);
80
  if(brandCkMatch){var brandName=brandCkMatch[1].trim().toLowerCase().replace(/[^a-z0-9]/g,'');var pct=parseFloat(brandCkMatch[2].replace(/\./g,'').replace(',','.'));if(pct>0&&pct<=100)itemDiscounts[brandName]=pct;notes.push(line);return;}
81
  if(line.match(/^(ck|chiết khấu|chiet khau|giảm|giam|discount)/i)){
 
89
  if(dm){var da=parseFloat(dm[1].replace(/\./g,'').replace(',','.'));var du=(dm[2]||'').toLowerCase();if(du==='k'||du==='nghìn'||du==='nghin')da*=1000;if(du==='triệu'||du==='tr')da*=1000000;deposit=da;}
90
  return;
91
  }
 
92
  if(line.match(/^[\d.,]+$/)){notes.push(line);return;}
93
  var priceUnit=line.match(/^[\d.,]+\s*(k|nghìn|nghin|tr|triệu|m|%|đ|d|vnd)?$/i);
94
  if(priceUnit&&line.replace(/[\d.,\s%\knhìnghintrriệuđdvnđvndKHTRML]/gi,'').trim().length<2){notes.push(line);return;}
 
95
  var brandNum=line.match(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)\b.*[\d.,]/i);
96
  if(brandNum){var textAfterBrand=line.replace(/^(malloca|eurogold|grob|canzy|demax|hafele|garis|boss)/i,'').trim();if(textAfterBrand.match(/^[\d.,\s%(k|nghìn|nghin|tr|triệu|m|đ|d|vnd)]+$/i)){notes.push(line);return;}}
97
  var m=line.match(/([\d.,]+)\s*(k|K|nghìn|nghin|triệu|tr|đ|d|vnd)?/);
 
193
  window.exportDeliveryExcel=async function(){if(typeof ExcelJS==='undefined')return;var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryExcel(qd,code);};
194
  window.exportDeliveryPDF=async function(){var qd=window._origGetQuoteData(),code=getEffectiveOrderCode();await _doExportDeliveryPDF(qd,code);};
195
  window.getQuoteData=function(){var qd=window._origGetQuoteData.apply(this,arguments);if(qd.customer)qd.customer.orderCode=getEffectiveOrderCode();return qd;};
196
+ console.log('✅ QR Payment v1037.3CORS proxy, brand discounts, formula Excel, no phantom fees');return true;
197
  }
198
 
199
  async function _doExportPDF(d,code){
 
212
  async function _doExportDeliveryPDF(qd,code){var cv=await toCanvas(buildDeliveryHTML(qd,code),700);var img=cv.toDataURL('image/png');var pdf=new jspdf.jsPDF({orientation:'p',unit:'pt',format:'a4'});var margin=25,usableW=595-margin*2;var ratio=cv.height/cv.width;var imgH=usableW*ratio;if(imgH>842-margin*2){imgH=842-margin*2;usableW=imgH/ratio;}pdf.addImage(img,'PNG',margin,margin,usableW,imgH);pdf.save('GH-'+code+'.pdf');}
213
 
214
  // FIX v1037.2: _doExportExcel - Giá CK luôn là formula, ko hằng số
215
+ // FIX v1037.3: Thêm CORS proxy fallback wsrv.nl cho ảnh (Eurogold, Hafele...)
216
  async function _doExportExcel(d,qd,code,qr){
217
  var MONEY_FMT='#,##0"đ"';
218
  var NUM_FMT='#,##0';
 
247
  var bgColor=i%2===0?'FFF8FAFC':'FFFFFFFF';
248
  var RB={top:{style:'thin',color:{argb:bgColor}},bottom:{style:'thin',color:{argb:bgColor}},left:{style:'thin',color:{argb:bgColor}},right:{style:'thin',color:{argb:bgColor}}};
249
  row.getCell(1).value=it.stt||i+1;row.getCell(1).alignment={horizontal:'center',vertical:'middle'};
250
+ // FIX v1037.3: Load ảnh với CORS proxy fallback wsrv.nl
251
+ if(it.image&&it.image.startsWith('http')){
252
+ try{
253
+ var ir=await fetch(it.image,{mode:'cors'});
254
+ if(!ir.ok)throw new Error('HTTP '+ir.status);
255
+ var ib=await ir.arrayBuffer();
256
+ var ext=it.image.match(/\.(png|jpe?g)/i);ext=ext?ext[1].startsWith('png')?'png':'jpeg':'jpeg';
257
+ ws.addImage(wb.addImage({buffer:ib,extension:ext}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
258
+ }catch(e1){
259
+ // Fallback: dùng wsrv.nl (CORS proxy, tested: ✅ Eurogold, Hafele)
260
+ try{
261
+ var proxyUrl='https://wsrv.nl/?url='+encodeURIComponent(it.image)+'&w=200&output='+(it.image.match(/\.png/i)?'png':'jpg');
262
+ var ir2=await fetch(proxyUrl);
263
+ if(ir2.ok){
264
+ var ib2=await ir2.arrayBuffer();
265
+ var ext2=it.image.match(/\.(png|jpe?g)/i);ext2=ext2?ext2[1].startsWith('png')?'png':'jpeg':'jpeg';
266
+ ws.addImage(wb.addImage({buffer:ib2,extension:ext2}),{tl:{col:1,row:cr-1},ext:{width:46,height:46}});
267
+ }
268
+ }catch(e2){/* skip image if all fails */}
269
+ }
270
+ }
271
  row.getCell(3).value=it.name||'';row.getCell(3).font={bold:true,size:9};row.getCell(3).alignment={wrapText:true,vertical:'middle'};
272
  row.getCell(4).value=it.model||'';row.getCell(4).alignment={horizontal:'center',vertical:'middle'};
273
  row.getCell(5).value=it.specs||'';row.getCell(5).font={size:8,color:{argb:'FF64748B'}};row.getCell(5).alignment={wrapText:true,vertical:'middle'};
274
  row.getCell(6).value=Number(it.qty||1);row.getCell(6).numFmt=NUM_FMT;row.getCell(6).alignment={horizontal:'center',vertical:'middle'};
275
  row.getCell(7).value=Number(it.price||0);row.getCell(7).numFmt=MONEY_FMT;row.getCell(7).alignment={horizontal:'right',vertical:'middle'};
 
276
  var itemPct=matchItemDiscount(it,d.itemDiscounts||{});
277
  var discountFactor=1.0;
278
  if(itemPct>0&&itemPct<=100){discountFactor=1-itemPct/100;}
 
297
  tRow.getCell(1).value='TỔNG CỘNG';tRow.getCell(1).font={bold:true,size:13,color:{argb:'FFFFFFFF'}};tRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};tRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(1).border=TB;
298
  tRow.getCell(6).value={formula:totalQtyFormula,result:totalQtyValue};tRow.getCell(6).numFmt=NUM_FMT;tRow.getCell(6).font={bold:true,size:12,color:{argb:'FFFFFFFF'}};tRow.getCell(6).alignment={horizontal:'center',vertical:'middle'};tRow.getCell(6).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(6).border=TB;
299
  tRow.getCell(7).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(7).border=TB;tRow.getCell(8).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(8).border=TB;
 
300
  tRow.getCell(9).value={formula:sumParts.length?sumParts.join('+'):'0',result:Number(d.grandTotal||0)};tRow.getCell(9).numFmt=MONEY_FMT;tRow.getCell(9).font={bold:true,size:14,color:{argb:'FFF0B840'}};tRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};tRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(9).border=TB;tRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:TBG}};tRow.getCell(10).border=TB;cr++;
301
  if(d.deposit>0){var DBG='FFF0FDF4';var DB={top:{style:'thin',color:{argb:DBG}},bottom:{style:'thin',color:{argb:DBG}},left:{style:'thin',color:{argb:DBG}},right:{style:'thin',color:{argb:DBG}}};ws.mergeCells(cr,1,cr,8);var dRow=ws.getRow(cr);dRow.height=24;dRow.getCell(1).value='Đã cọc';dRow.getCell(1).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};dRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(1).border=DB;dRow.getCell(9).value=Number(d.deposit||0);dRow.getCell(9).numFmt=MONEY_FMT;dRow.getCell(9).font={bold:true,size:11,color:{argb:'FF166534'}};dRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};dRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(9).border=DB;dRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:DBG}};dRow.getCell(10).border=DB;var depositRowNum=cr;cr++;var RBG='FFFEF2F2';var RBD={top:{style:'thin',color:{argb:RBG}},bottom:{style:'thin',color:{argb:RBG}},left:{style:'thin',color:{argb:RBG}},right:{style:'thin',color:{argb:RBG}}};ws.mergeCells(cr,1,cr,8);var rRow=ws.getRow(cr);rRow.height=26;rRow.getCell(1).value='CÒN LẠI';rRow.getCell(1).font={bold:true,size:12,color:{argb:'FFDC2626'}};rRow.getCell(1).alignment={horizontal:'left',vertical:'middle',indent:1};rRow.getCell(1).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(1).border=RBD;rRow.getCell(9).value={formula:'MAX(I'+totalRowNum+'-I'+depositRowNum+',0)',result:Number(d.remaining||0)};rRow.getCell(9).numFmt=MONEY_FMT;rRow.getCell(9).font={bold:true,size:13,color:{argb:'FFDC2626'}};rRow.getCell(9).alignment={horizontal:'right',vertical:'middle'};rRow.getCell(9).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(9).border=RBD;rRow.getCell(10).fill={type:'pattern',pattern:'solid',fgColor:{argb:RBG}};rRow.getCell(10).border=RBD;cr++;}
302
  if(d.notes&&d.notes.length){var NBG='FFF0FDF4';var NB2={top:{style:'thin',color:{argb:NBG}},bottom:{style:'thin',color:{argb:NBG}},left:{style:'thin',color:{argb:NBG}},right:{style:'thin',color:{argb:NBG}}};ws.mergeCells(cr,1,cr,10);ws.getCell('A'+cr).value='📝 Ghi chú: '+d.notes.join('; ');ws.getCell('A'+cr).font={italic:true,size:9,color:{argb:'FF166534'}};ws.getCell('A'+cr).fill={type:'pattern',pattern:'solid',fgColor:{argb:NBG}};ws.getCell('A'+cr).border=NB2;cr++;}