From 3448fbaceb4aad88ca52a419e4876c8ae1e54c69 Mon Sep 17 00:00:00 2001 From: Abdul Aziz Amrullah Date: Tue, 24 Mar 2026 15:15:58 +0700 Subject: [PATCH] first commit --- README.md | 49 ++++- __init__.py | 1 + __manifest__.py | 18 ++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 195 bytes models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 205 bytes models/__pycache__/pos_order.cpython-312.pyc | Bin 0 -> 9418 bytes security/ir.model.access.csv | 2 + wizard/__init__.py | 1 + wizard/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 216 bytes .../pos_export_bc_wizard.cpython-311.pyc | Bin 0 -> 11748 bytes .../pos_export_bc_wizard.cpython-312.pyc | Bin 0 -> 11817 bytes wizard/pos_export_bc_wizard.py | 195 ++++++++++++++++++ wizard/pos_export_bc_wizard_views.xml | 40 ++++ 13 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/pos_order.cpython-312.pyc create mode 100644 security/ir.model.access.csv create mode 100644 wizard/__init__.py create mode 100644 wizard/__pycache__/__init__.cpython-312.pyc create mode 100644 wizard/__pycache__/pos_export_bc_wizard.cpython-311.pyc create mode 100644 wizard/__pycache__/pos_export_bc_wizard.cpython-312.pyc create mode 100644 wizard/pos_export_bc_wizard.py create mode 100644 wizard/pos_export_bc_wizard_views.xml diff --git a/README.md b/README.md index be7680a..3d1c4c3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,50 @@ # pos_export_bc -This module adds a wizard in the POS backend to export POS Orders (Sales) into a specific BC Excel format \ No newline at end of file +POS Export BC Format Module +This module adds a new wizard in the POS backend to export sales data into a specific Excel format ("BC Format"). + +User Review Required + +NOTE +The current plan maps the requested Excel columns to standard Odoo POS fields as closely as possible. Columns like "Takeaway Charge", "Packaging Fee", "Service" will be left empty or 0 if there's no clear standard Odoo field for them. If you have custom fields for these on the pos.order model, please let me know so I can map them accurately. "Price Type" is mapped to the POS Order's Pricelist name. "Table/Customer" will prefer the Table name (if dining in) or Customer name. "Dinein" will be inferred based on whether a Table is set ("dinein") or not ("takeaway"). + +Proposed Changes +POS Export Module (pos_export_bc) +[NEW] pos_export_bc/__init__.py +Initialize the module directories (models, wizard). + +[NEW] pos_export_bc/__manifest__.py +Define module metadata, dependencies (point_of_sale), and data files to load (wizard view, security). + +[NEW] pos_export_bc/wizard/__init__.py +Import the wizard models. + +[NEW] pos_export_bc/wizard/pos_export_bc_wizard.py +A TransientModel (pos.export.bc.wizard) that: + +Prompts for start_date and end_date. +Has an action_export_bc method to query pos.order within the date range. +Uses xlsxwriter (or similar standard library) via io.BytesIO to generate the Excel file. +Generates 2 sheets: "Invoice" (orders where amount_total >= 0) and "Refund" (orders where amount_total < 0). +Writes the specific headers and loops through every order line. Order-level values (Subtotal, Tax, Charge, etc.) are only written on the first row of each order to match the screenshot format. +Returns a UI action to download the generated file. +[NEW] pos_export_bc/wizard/pos_export_bc_wizard_views.xml +Defines the Form view for the wizard containing the start_date, end_date inputs, and the "Export to BC Format" button. Also defines an Action and a Menu Item under Point of Sale > Reporting > Export BC Format. + +[NEW] pos_export_bc/security/ir.model.access.csv +Grants read/write access to the pos.export.bc.wizard model so users can open and run it. + +Verification Plan +Automated Tests +This is a UI export tool, so no complex backend automated tests will be added initially unless requested. We will rely on manual verification to ensure the Excel output exactly matches the user's expected visual layout. + +Manual Verification +1. Install the pos_export_bc module locally or on the Odoo instance. +2. Go to Point of Sale > Reporting > Export BC Format. +3. Select a date range that contains some existing POS orders. +4. Click Export to BC Format. +5. Download the Excel file and open it. +6. Verify the file has "Invoice" and "Refund" sheets. +7. Verify the headers match exactly: No, Date, Outlet, Table/Customer, Invoice, Category, SKU, Product, etc. +8. Verify "MIE MAPAN" and "INVOICES" titles are present at the top. +9. Verify order-level data is correctly row-grouped as per the provided screenshot. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4027237 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..6c3682a --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'POS Export BC Format', + 'version': '1.0', + 'category': 'Point of Sale', + 'summary': 'Export POS orders to BC Excel format', + 'description': """ + This module adds a wizard in the POS backend to export POS Orders + into a specific BC Excel format ("MIE MAPAN INVOICES"). + """, + 'depends': ['point_of_sale'], + 'data': [ + 'security/ir.model.access.csv', + 'wizard/pos_export_bc_wizard_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0dcbfbbb128db8887b7ef4fc452750fc282e077 GIT binary patch literal 195 zcmX@j%ge<81TT~}W{Lsn#~=<2FhLog1%Qm{3@HpLj5!Rsj8Tk?43$ip%r6;%!kUb? z*vd1j5{pv&G?{KO6fpzERx*4B>HOu8Vil8{Sdf^fkd~Q~8dFe|pOu2KczG$)vkyGXduYG TVi4m4Gb1D8JqD2?HXsK86_hcs literal 0 HcmV?d00001 diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55beb315f14cfbf1033069893fccc52fab2d1647 GIT binary patch literal 205 zcmX@j%ge<81eYW?W=aC-#~=<2FhLog1%Qm{3@HpLj5!Rsj8Tk?43$ip%r6;%!kUb? zI1BQN`AO`?zN;SOz literal 0 HcmV?d00001 diff --git a/models/__pycache__/pos_order.cpython-312.pyc b/models/__pycache__/pos_order.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fadd685d7d9935c2eaffc439035472a8f33ccf8 GIT binary patch literal 9418 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 q7=r(IM~xBX-(oVEIaO-Nzo)H literal 0 HcmV?d00001 diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..fd9fea5 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pos_export_bc_wizard,pos.export.bc.wizard,model_pos_export_bc_wizard,base.group_user,1,1,1,1 diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..0985a1b --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1 @@ +from . import pos_export_bc_wizard diff --git a/wizard/__pycache__/__init__.cpython-312.pyc b/wizard/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38a0d8e6f8de26c76430f3c1a04ff4020199978f GIT binary patch literal 216 zcmX@j%ge<81Z>J1GgW}}V-N=hn4pZ$0zk%eh7^Vr#vF!R#wbQchDs()=9i2>VNJ$c zA_e)y@u?LB`9&r1Ny+i$nN^8JDSnzvw-}0;fhtxqdN&#z literal 0 HcmV?d00001 diff --git a/wizard/__pycache__/pos_export_bc_wizard.cpython-311.pyc b/wizard/__pycache__/pos_export_bc_wizard.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1392c63580f402408d678c1de9fbe791a87317cd GIT binary patch literal 11748 zcmcgyeM}rlcJKNA9$@GhdKf;2FM9{WfNijivB7+Uu?@Ciz2Cr3+iUS+ac?Q*x4!XJ%;@f6|$FVH}lQMkogcJ zF;W)5W~8TVMh0bhKpvFO$Yl~uF{1#QBA^VaW>i7-j5?^9(FCsUy67rUppj@^~Xu@Q_^y^E1*O0!_js`jY@!#;<9 z>6$4aWmhqmCWDHXTrxpRdKrrG2Pv0QPT(Zz)VO{l07dsFx1*5|$}Q5};LSDg~4vwy%j z9HxU_#<|->f0gmljB^kiHRVzW>Ig&oL-T@`rrufh(-bLaNNUcz9AKi_p%5wZ`D9t! z;Awz=Gyvb60$)799_SwE9~d4OADAAPACx>OeNgtm@*%d0rGVw->r)_oo+HaJMxHBR zkt!xT0zhPP{={Hvd8b11W1^Z8pzTW^-V(Et}udvLZ__ ze3IA`yqGVp+*cQUG3Wn-E~CCrJEL}{WdVVN_K)_1jWHEAO&V#+xM zJc*cUPBAZ!Yvfo=P1<7`vT|M)1yhShRfdW64ydnsbv?{-qR>OxeDPe7VKG_Co~!x$YwsS1x$tLnpOuLYKm!RLnA1TN;NY`Tp>}JiEqPSKl28hOxT&-#@vF=AMymLV3=N+64X8_1lY zC}x=k=U$=afE^U|t^{E)1m&eLvUAWMqWmF2iBK9+GaL*@^%Kbd&N(Omn>R=ais`9= zAIk(a6ZMDY!oJ)V3*JT>EE=6}{u8!~KQ;gyX-h%ztAiz%e{IeRj6f7Jh^AM^j+X#W4zet%p_J=CUdl9O`vFL8eHVk;9OM|pUbn=@ z%ScGUZGcR%N57O9y8H$vKsO=@k-#%fzZ6)#qn{F zwQ(e^z~4#5%~ZU4zqbb9g*~R*yMn|FbPKe|ZP)qn#4u=48Z*3ljsr;BGoM$R_P&p= z(Zm@uzNE=M>kep97b`gccYUn%CGP5l90{74Vx1G}oLS$->tNjKvZZ_vz}XP52hRFzUAzHG4cQWHDXc53i2@iS8>6tIfR?s+ z<7+%`h}FF0o&7y;jMW~1yD8>80QaF--2u1{$LbHj{YI=|)pdaR=!!MIg1;&$qjz58 z%jQ_wD_ZUE%_A|(f%scuMo8AIWP82_949vT7848lk>ifm=jc_o96A{lhn$e>xCLC7|Pjt3cZ zD##${ok18rBep;gErXzQ1|j5(C=X%042lm48lN|E8=^KD1kqfLg2_vgp1WasQJgY@ z0iqo9l!x|)<{`uY-z(!y4Tu5xZVUSSODSk*Y6)U7 z;FqNpCQ75wm!S7XkeFyVLAMl#FS0Jd7n2ZpAL>o$jiISI zil#y!P!U-U3aSA3IRX;JY2;xbWMa(b#dwM#J4}8uLSI56l?!1Q7M(yFiY2rm;)k|qomdX} zA-+TIMs-~6f?jm3M>JMVhJ#+vMkTr12oW}#@sNxMTrKFS5Sh(uAiCuNZwu<%loy4= zG<5i`L=e}zh@cgpo=})RCpu1C0X)bOK^+#uSAt4h1_f<)N(*LbUCfm={%kxeySfSH ztme`pD=3hQ)jlx>1!F3Pn3jN~r^U#U#~WmXlC00AS0f@inVuA#k321y{1F&s8vKNY zPf&`fR`jhHLyIgfBWH>+G|^G?ABi?e9%aNBnwS@K63GWKG=n(nq(Glw6n&Q+Mw8^U z-6)`-k%|$)1RkA*k>72kj8K6&!Mr;N*~TTv7k-HUW!a2HC28(3Q9+Ado7r%9G13mo zI)9t%e*O{{Ji$<~UPn*icX z*vlGw|4WTEwys~}8anxg&K*o<>J^g@N|Ks2Te{Txq2Y&ywZ65x>*@^?SK7mu_ORmp z!d8(ONn7oY-dbDat#+&!?Wg#a4H6)x&)C@OJg(t?J9{mFryf3|~FN!p*EC z^)Ku-k862*D{DW#VM(gfZC#twPj9kq=i&Zx`Kv#G$C~P~nJ130aQi6e$>HQ3M|ej& z>o~E|pVX%74zYE|*562))0MS+Wh-0RyLGO78Mqej|oH?2<{kmG8Gct{;XykiJ8 zYlIdy2X8x+RHW@Sy!}X0{k)=5+O*cE`PLrDa+SS2q!qn7>cK1AM`b8`2o`5?^ z@wR3b?&&u{fcx0Sn?DJz2iK-j!+&yf?dG#Gu&w^kXReQ3>qC6Y+3l9Wt(HM{=qA^4 zi*LEb)qD7QPqHN4bYy+`Nt|su19x)yQ8XESRu3IDHM929Ys4C{p$Df_R6UCG6)v`- zZT(78`NG+lG^MNR9*^%}$II%zMoDrojoUvQ|KWH_vtG77%Hf?n-pS&f>54iSdh5I_ zO;m3aEn7s(`n3)3X61I*_*U0Ad-WRIHO_Ut#dp2M&QKgN#}jibG54ZC)-Z=3=ken# ze!Q@UOB~+KdbzV0|*cZ#hW+8j%s zPggk~Pb7!Z;}c({z>gd#}{Y0x^vHtb9KWk+_${Ru|oX|d{JQ40Ef5pcsq-? z7wUh3!@GFAi^aPNsjuce&*3L|{3MH?%=22-r#CKgct4N#vv_|Y{raYf!-si%n8o1* zYWy0ju)1ML{><>FhSbRVl?`I^EJuv-#28DA71C>-Rdd7yPfW1HL?OL=BghehJTb@; zgN5`m=%484iGG&o|9#MHo1o7KPmHj{NTGauBhC>+JTb%)LxuEmj_BlxPL}9QJDOl7 z+5<9}x!YCkTUG5F6`S>&3)?-DTRoHPTQ}IANv`K6-*c1oEO1qeeAOabwOA;ta`Prf zjPt}eONuvk0tsFxp9s-!4oG~;zS|U#u2A@;uK4qDx|)e*TNAUJkh}t z9fiDhj_BrzZkFgSq}Fi6Ii5Jj66apL(Yj`OfjidT`QSowX!d)n@VNOyRN z3f&6^q>Vh%8lIa2>_vusfKNk=JF-LrY7#M4FyI!SE+zea=v`1sXacZ<7cf*1U@0O* zEl4TxR24x_&oUi|n%eGCvu%J^8Iai)HTRO?yP-hXOP*!uWs3fpWZ*v_sS4nS?_=mg z9pkD+NG`HW!#Neejz0gLl}3P{M$jKXp@jSpT+?Q;iy-%h1x^17Lq*0W1nsq5d;w?| zK)Pfq%^(qN4+p_*fKh|!)1HWH6+wV(S`i|`=TdqF2|>`D zzKdF@e1UKTU|EEs76I#l?+}Qbg^$1|It1P9i4MTu2=pqES3ybARW4dD0?j3WVx=M! zLp^0NlR-q-OjHE6I?32&#wTcyt&pMr9(8Px;IIdE1y<%J1-*fE`zcEP5+#3yk}pv5 z*C_cLlx(8pZ&C7hkVMd-=z#OgrZmfmQRgnka(%M~z-nX_EsEmv6Mm0y*WKdSto>QPlf{k*)IFF%yJvtihrV%z$;@&UelAfW|J zX0O{Oj%*P}(m0ViwXyPL-;P4ws^7sN`5Kek4ZlW7!UUXzbGxE>tDQ5#BbEFr-0>!@cEjZy4F0t8Cj8=a}Xl)2xo+z}vn|8Y0u6d9Rprn_ zHc4o9ELer((d|b;z&xz2ZM`XBcy6&hYE2n9OABvlNoam$ujB2m^~NV{KR@;K%y##M zt?mn4_a(mj5@(;_?Gp*}^NL!&;_$liiRowezKCs~9NjuO%AGvVpFGc1jPn)a2_vj= z+j!#^-k7>2!GUeOcMI?RN98ZH96rY5W83)UE&MWj{U(cF=I~oQek-9%TkPAG`YlU+ z>I`RT=Pm8qmhLS}_s<8Pj&hbE-ZHdpxwvJy$WC8pEf+b<3~!kMdz5d>bg*px9@0St zW^YQ7>tk%iiOs5K%$L{LQ`4;NO&AAldBTt!Tl+ri9${NYx!O@qJH~6rSnZfpHM+LQ zwhp0js2S$8BfNHm)sDQ-mcDO(Xy&v8uO-l!Y0d8&9vYH;tLFF22{R14trAdon76vO zt({xe&NO~0HT8*y#V^6V+4!_o%0UVzkmpthZ*5A=eYW`V;-3dU33ApB-U_?X_jYuu zlW53K!lY8!U{XPn(C?IDR#&?GT@}o! zh5iy%{4+{^3CWK!X_FH}QYbP*qXb1_A^}M5=l;^h`ujua^7i`|UX)cOZI2wO3!5EZ z+P-q|W3z0Tk1O->Wxo68(x{}SoKk&@ey(J5%eOJ35Np_ z`k#PTFpLG!&s@cy=nCd3i2OwSu-#omzu^__rvDv@JdYCOkZ-kTB$xNlJ5ZFtf8;+P z*-^-3vNUG8FaD)5-F@*dja9S7cN#O?7yn*hhuQDC(^wN*e5bK8Hhb?V6*5~|X=MxU z-=sAbSdeA5e3CA+-an82kd8K3%y-oDavAvi)#;j=CtAN2D}TKYO9cJs;3vv9>@PH3 KicMLU;(q|}+&{4Z literal 0 HcmV?d00001 diff --git a/wizard/__pycache__/pos_export_bc_wizard.cpython-312.pyc b/wizard/__pycache__/pos_export_bc_wizard.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72c11abfac9260e08a72f156117edfda5b4b8b4b GIT binary patch literal 11817 zcmcIqYit`=b{@XpdQc=qih58l%alb*w)~20$=1`7Uy^M(P88V;#Ti+oNRBcyvMq+} z20;K*L4eRHTFKZ1R<=K)G%2D53Pjna8+&()WVb(-BXUdbBn!Ch0#($b(bB%P2-1et6?1~OSt9#TvwLdpqcNHw7fsVCHYzC5T2X(zN>SlxtN zBb3rl=%v^R3|G8{;Yvaks1}>ghabp<_(bV~q!A+wO&FzsdL$G2=dm~Hu46Z3&tumm zPSqxYopnk$HST2y#vdY_a!%%*^K;5+KM}-fP8lL`B1m%*51=#`X@VN0D3bE!+rqHi zuzv{BC5*t(P$#6&oD7#v$fX#e2$<06HMkbny{*Wh%btqXE>k7S0&QX}ALdH7=Vf9H$38TP`7y;dZEHou z|6|+AxHhhf>*Izv7B7jH#>?X6apSZWH@~Kisc;Kk8C6WnAhbr6)3U457}({g3b#eo zc-2)&1coEdoCM=@?2uFacn+-fz2eC8p*Z&vEGh|f=H+4xuMx*DL3Xgn`p0W+01g}w zOXNemuJ{W5E`~cGr^p(DocebZcms1ttR-+*jO9b<>5+U&jEN%=`-?Z?O?jIX)8Nhc zuBaw(RK({)Xtm`X8NM5LzAd|eIa|+0j*ZU{jsDL+F{ zEe=2J5H}dNV=PE`X~N+T(~LJ5bod!}q$9s8IYRR^LRBaqey(-$8hR%^WHGyXBHybF$#c=@r60)9UqTq z1X2Gy;4aKhF(jy$QxEuQA2}apIN7-O2B*3}P}iVfq;%XnOL(t)7aaXFUg|0lF^s_o zy;p$@IED!5!FeczBWj=&0Gxcx>&H3SnD<6Rg`$pLhEtv=n0X46d!CpEICRvz5Q53z zr~@Pxdr z+6Dvs;E!T-Q5k%-`l$T7*gLX3n$d`DEjq=%3}BbRKuv;vV~Ut^n>GzmMO3-C`vbX9 zUV^=0mt%NIOtrYDn4Q8)d7GDGT4JjhSW&#}!*a1lF*Rs`i!sJ1#)?_v4=coR2P#EeK3r@UbHftc6w^VgMVLw;x1;7uZm}10OpjZj zRFR|5HH>P2oE-bem!(jNBJzMEj}c?gFLTTQ_+s4UY5gjR8iYPZOJb!^zDcYxA40w! zz5#8q@x{uR77>Y8zJ^e{#dJO#z#iXaV&%9sW(0I=9wo+N6(BFdNn)mgwrqS`Zm~>0 zEYg-4uYwwj>s}8z9r@B?4D@M?S#bMP%k2}<`7o@DnxiKEk8^BYKZ~W9M$gtwJTb>1C$|JR)DGv^yT{_#sYAPef*&s zqdUgp)x6CuVgvZK;J{n9sjK++(zXqH= z1Q>cum5{x0SB^zBTWL)~@x~W&YyoqgI2%#+?(OOw+8fcF85wk(=^g7uqJ1FR$Eioo zK07+nKX@Th(u}*BL$2oM9UUEA9UTSfeVttgp5dguot$zEj(jA}N&7lENf#&W?~KSC zF2^Q1QEZ~)R78WE+x%HQQi5&)Jbc{YrG4P>SMUi6aeE4prXddF)C@XJd;It#4r_E! zM2?z=`{8v8lfXql6uS&A+?WI$BOrVH`D@9N1P(MtG~~aN!EJ;DT||`3q4Tzf-a1!1 zrJU5=!AXKNs^8)G_5$8D8S>6~!;WcxkeHmK$N=GECP|zm_desf);a0p5BZZYD39Q= z_e}XF1&{rsLXRMH&n-Q(unE*^S@ zAeg)`SJ#WsV;Y@y{uBHCD_F92@Dux5@A6BTy7phxwSQv&1C8e3Rf|<}6+-@bM4{;1 z%qw&pA~}URwpS?5d*o@!8x>DgcmpV0!Yf=#A*CX%E4?A)yKqXW6CmLvb6e`BMj%O{ zeg6MaF=)JWJ-h_PRE@H(o>j@+`ps}T!yx&Kzv)0Pq+;i8 zP!o=Ou9MU(zfd_Hc)+d_9?BcO3hpPkmzqZRew>0|%$$Y*w;Mqr@0pAUUxSM)@DKWC zIBniEa4L$J13w+8a&Cc%PzRwIPV1#nH4zeYtMQTHY5!HwJE$BfUgZ&P6Dh>CIX&*D zq5Oh}&(qGqJsJ3=5ZZvgT8fw^C?X8i%jo$KrwGDC(NI6XLOl#P*YskX_q^%E+4kc! zg=i!-idvBeNSIzF*orvw4ffM3oAL33LUWKJa{Y0dy@oq{!m42+oEm*qQzSV{qbm%@&Yo9t4{u#LUBNCAIrJxPKj^?s z|2ORCu+wBs)frPOYieEXN|_GE2OlUggE6D8Vf8i3Gpo9_$+W&Nt1r(bj{Q#h< zTQl~3tbJdyvp;1Yh@Z~dnlm;xYjY>tkELwK<0qlio2M2}Ez6f@()yNs%9qwRGEZj~lf%a&}Jc}aImx7@pYeO0+;_(fUI0~ywI z5ZbS_#!qC;wx#E8J-0l|np-pGy{vif+OFjBv7~u#%6u+9bRRIoU)kysitH6%=1O?u zN_hEVrpCqAxH2^d*qQ^Mj(_&tC(k8o4y0;^GBxMensdqX&!=l9lFULvd*4=jr|x!L z#^z#et_@q~nlYiw9_qb&@$<{~F0+TuCofHAE)g4-h-Jl{lG`O2Yv+cwGdUZA*6faq zeGhBjld*TO_Kpqv!L`1GI$OVMqkiA&?nFtpsxDLIVyj#mRY%s$A6I?9Dxvzdy_T&V zTsMDa|A9SKJD9Q$q7qF2w%9Y4U94qSLYB4FW^8*{+n$8-8#QLNvsI(37c;J&4OdU9 zYBW_jnviGRJ$IWwcinTb?$HF1v9xYjT9X%_g|hqm@6LZ7yBA~kk0T{?Y}9llgL6rG zVI#PZe8H2Pn%Q_^<^hHsm-b3w@D01(bH3|b9n7?Kvu)j(wgI+nAUSwB)pjM-;7OEb zoA<2Fe-wK^#x@^K%r8aWirj5T$RAM|Xsg1u{Op~{+mqkD!dC3b)->N5x?j_h7|K>w zFU4-fGL=rY(z#LDzIq;tI+_xOY<2yeQ}3Kg3}j8VrBk;~t*BPZSBKN41HY`S&o;U; zjR)DrgULfDQ;nyV)mdvz#@fnSTUQUQ^{(AWTaVu_C^eKebr#lgHf=gmm_CsK z`sbxTEzJxQ8^gphbLTs^zmusu%GMoSKgZS`%hU~Q)D55s9nSVWmz;btGdasn&SoZQ z7XJF^FknZ+(yxVpPhFP&2Bj`bmz(ai-)_%T^=wr2fLeW0LYnB$HnzNX?A>FT z#zSo5p^e7w^?ixc*{a$*yKnDK^xv=Fm8tJ!>pL^`huQkW8})sC&zo?o-Bcc-l*g_7F4HEHW; zp`>Chl(r5OO3I;fYu}$Qv8+Q|CkpE^t;N#T!9qzz+IrxZ_U3HM-b_mu+tQUhayHd6 zx;zXkFH`MitKF;D*QM*j>FP&HRjpr6TTd0%^JJgv($?NWgeh%3SeUk?t%nQKkLMfH z*8PR~wzTy~Vfyj>V(I(4UCW01Cj0VBw@$xx;sGhaYIZ-ElVIhx<=$I{x71&gQmo4H z5X?VDy$r8!r9Isd>CtYACx0?3jxyki2jl56)d4Z5Amt=S1sNi_5rdy{dl|;-o8j*j zRq)#;PhY=>lY1D_6H&O)EisYmX52Jz0WZ#zo>0)uhfX>53N*{fh0Pp@ix)^u2pA+1M2x66{IQEhp=X{7 zMhxxOiK%v=mJHO{9w|A7lh?yR(u;R9)I33f8yk)6XQ);g;Nb=n-M2E%Iz+ALc?u4; zz&f<~>v9UQbPDlxU}*vsL;OiC@(51qCplH$0z=Rvqn!HE)?N=(6!^3-RB8cL@e!&* z3QRbLB5IKbQ~bdJxLgR-25dHjZfJROA`1bBQuCY*ZoR3$K$RdCO+`@!3SW?+|OyI4(vmD*Ok6`Z1LFg_?^qQFK_5tw*X_Q>T@aM`K12*ugfczs%};N zWp!NnRYgsvVi#MnYvtOSZhbu2-j}NAkE?-|*y=OZJ*;(4)?{5dyteQ~@7FS^OZyO$ z+H`RPaq}rN=Hu*lSPOf()+lErs6LG_D zVW1R<^9lKQg>;`1>0lN53D<92#a14X0qo zt~?djgJsH?npjiQ%9WI9f5vo-H68nj{B!j^b;>l7F`Z*g=aSD~PMXf8OjqKXtkIS+ zHn7Hqm7^(RN5*)BH6HnN;IrXRhEvADjPVR>Jd?cmeA0L(Wt@nsekTt}G_}9Q(EGs^ z8D?u*vRDZ3KmuVkha*dYy39R9>P93NA@CQSWhtqmI@I%{qIB|G5 zy~l%ohl9>NkWvU|N4*CpXdmUDLpGN}jynn+^f?1>5N|MGrg85A#iEQ2^!hJ&y^rxt z30@rJiJ;I6c^+tVP<{=|>Kk4e%vN-~a{7LGb;7b_zhz%Jy}tho%g^mUwP!}A*paDp zx$l*e+0v@`wKrd0e0imHjk){m=P%rQAv1V|9lVk*^}I6tP#%&hgi-MgILoMiMUAD< z3mHMdFv^jWjzUf^q5sHTBADM&lmkDo^%4B=Xx=h&T3*VKWRRwkfaP=}A@s9c{s+ih z$vAj0Xg}-)XV5Q{`G%=)P?bo#f`L&V6_naTeG4)P{L{!c@IWS!NWR1jUt*eHVKrZ3 zy8pmhQdrBcu;wqZ@&|I6#PX%w{EeECO0>($jMc?jUB882(@M0zf1twD6~DnG5=*w+ e{OUy(&IdJ|~-YduM$yL22vU>&zy#E*Ai^F9A literal 0 HcmV?d00001 diff --git a/wizard/pos_export_bc_wizard.py b/wizard/pos_export_bc_wizard.py new file mode 100644 index 0000000..55093c1 --- /dev/null +++ b/wizard/pos_export_bc_wizard.py @@ -0,0 +1,195 @@ +import base64 +import io +from datetime import datetime +import pytz + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +import odoo.tools + +try: + import xlsxwriter +except ImportError: + xlsxwriter = None + +class PosExportBcWizard(models.TransientModel): + _name = 'pos.export.bc.wizard' + _description = 'POS Export BC Format Wizard' + + start_date = fields.Date(string="Start Date", required=True, default=fields.Date.context_today) + end_date = fields.Date(string="End Date", required=True, default=fields.Date.context_today) + + def action_export_bc(self): + self.ensure_one() + if not xlsxwriter: + raise UserError(_("The Python library 'xlsxwriter' is required. Please install it.")) + + if self.start_date > self.end_date: + raise UserError(_("Start Date must be earlier or equal to End Date.")) + + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + + # Define formats + header_format = workbook.add_format({ + 'bold': True, 'align': 'center', 'valign': 'vcenter', 'border': 1 + }) + title_format = workbook.add_format({ + 'bold': True, 'align': 'center', 'valign': 'vcenter', 'font_color': 'red', 'size': 14 + }) + subtitle_format = workbook.add_format({ + 'bold': True, 'align': 'center', 'valign': 'vcenter', 'font_color': 'black', 'size': 12 + }) + date_format = workbook.add_format({'num_format': 'dd-mm-yyyy'}) + date_time_format = workbook.add_format({'num_format': 'dd-mm-yyyy hh:mm:ss'}) + number_format = workbook.add_format({'num_format': '#,##0.00'}) + + headers = [ + "No", "Date", "Outlet", "Table/Customer", "Invoice", "Category", "SKU", "Product", "Quantity", + "Price Type", "Price", "Price Cut", "Subtotal", "Discount", "Tax", "Service", "Takeaway Charge", + "Packaging Fee", "Rounding", "Charge", "Paid", "Pax", "Paid At", "Return", "Refund", "Payment", + "Note", "Dinein", "User", "Promo", "Order from", "Nama Penerima", "Alamat Penerima", "Link Maps" + ] + + # Datetime timezone conversion + # We need these in UTC for the domain search + user_tz_str = self.env.user.tz or 'UTC' + user_tz = pytz.timezone(user_tz_str) + + start_datetime = datetime.combine(self.start_date, datetime.min.time()) + end_datetime = datetime.combine(self.end_date, datetime.max.time()) + + start_utc = user_tz.localize(start_datetime).astimezone(pytz.UTC).replace(tzinfo=None) + end_utc = user_tz.localize(end_datetime).astimezone(pytz.UTC).replace(tzinfo=None) + + def write_sheet(sheet_name, domain): + sheet = workbook.add_worksheet(sheet_name) + + # Title + sheet.merge_range('A1:AF1', 'MIE MAPAN', title_format) + sheet.merge_range('A2:AF2', 'INVOICES', subtitle_format) + + # Period + start_dt_str = start_datetime.strftime('%d-%m-%Y 00:00') + end_dt_str = end_datetime.strftime('%d-%m-%Y 23:59') + sheet.write('A3', 'Period') + sheet.write('B3', ':') + sheet.write('C3', f"{start_dt_str} - {end_dt_str}") + + # Headers + for col_num, header in enumerate(headers): + sheet.write(4, col_num, header, header_format) + + row_num = 5 + orders = self.env['pos.order'].search(domain, order='date_order asc') + + order_no = 1 + for order in orders: + local_date = order.date_order.replace(tzinfo=pytz.UTC).astimezone(user_tz) if order.date_order else False + + outlet = order.config_id.name or '' + table = order.table_id.display_name if 'table_id' in order._fields and order.table_id else '' + customer = order.partner_id.name or '' + table_customer = table or customer + invoice = order.pos_reference or order.name + + subtotal = sum(l.price_subtotal for l in order.lines) + discount_order = 0.0 # Standard Odoo doesn't have an order level discount easily isolated from line discounts + tax = order.amount_tax + charge = order.amount_total + # In Odoo, payment amount can be negative for change. + # To get the total amount tendered before change, we sum only the positive payments. + # The change/return is typically stored in order.amount_return + paid = sum(p.amount for p in order.payment_ids if p.amount > 0) + pax = order.customer_count if 'customer_count' in order._fields else 1 + return_amt = order.amount_return if 'amount_return' in order._fields else (paid - charge if paid > charge else 0) + + payment_methods = ', '.join(order.payment_ids.mapped('payment_method_id.name')) + note = order.note if 'note' in order._fields else '' + # dinein = 'dinein' if table else 'takeaway' + preset = order.preset_id.name + if "dine" in preset.lower(): + dinein = "dinein" + else: + dinein = "takeaway" + user = order.user_id.name or '' + + is_first_line = True + for line in order.lines: + sheet.write(row_num, 0, order_no) + if local_date: + date_str = local_date.strftime('%d-%m-%Y %H:%M:%S') + sheet.write_string(row_num, 1, date_str) + + sheet.write(row_num, 2, outlet) + sheet.write(row_num, 3, table_customer) + sheet.write(row_num, 4, invoice) + + category = line.product_id.pos_categ_ids[0].name if line.product_id.pos_categ_ids else '' + sku = line.product_id.x_studio_popcorn_sku if 'x_studio_popcorn_sku' in line.product_id._fields and line.product_id.x_studio_popcorn_sku else '' + product_name = line.product_id.name or '' + qty = line.qty + + price_type = order.pricelist_id.name or 'DEFAULT' + price = line.price_unit + price_cut = (line.price_unit * line.discount / 100) if line.discount else 0.0 + + sheet.write(row_num, 5, category) + sheet.write(row_num, 6, sku) + sheet.write(row_num, 7, product_name) + sheet.write(row_num, 8, qty) + sheet.write(row_num, 9, price_type) + sheet.write(row_num, 10, price, number_format) + sheet.write(row_num, 11, price_cut, number_format) + + if is_first_line: + sheet.write(row_num, 12, subtotal, number_format) + sheet.write(row_num, 13, discount_order, number_format) + sheet.write(row_num, 14, tax, number_format) + sheet.write(row_num, 15, 0, number_format) # Service + sheet.write(row_num, 16, 0, number_format) # Takeaway Charge + sheet.write(row_num, 17, 0, number_format) # Packaging Fee + sheet.write(row_num, 18, 0, number_format) # Rounding + sheet.write(row_num, 19, charge, number_format) + sheet.write(row_num, 20, paid, number_format) + sheet.write(row_num, 21, pax) + if local_date: + date_str = local_date.strftime('%d-%m-%Y %H:%M:%S') + sheet.write_string(row_num, 22, date_str) + sheet.write(row_num, 23, return_amt, number_format) + sheet.write(row_num, 24, 0, number_format) # Refund + sheet.write(row_num, 25, payment_methods) + sheet.write(row_num, 26, note) + sheet.write(row_num, 27, dinein) + sheet.write(row_num, 28, user) + sheet.write(row_num, 29, "") # Promo + sheet.write(row_num, 30, "cashier") # Order from + sheet.write(row_num, 31, "") + sheet.write(row_num, 32, "") + sheet.write(row_num, 33, "") + + is_first_line = False + + row_num += 1 + order_no += 1 + + domain_base = [('date_order', '>=', start_utc), ('date_order', '<=', end_utc)] + write_sheet('Invoice', domain_base + [('amount_total', '>=', 0)]) + write_sheet('Refund', domain_base + [('amount_total', '<', 0)]) + + workbook.close() + output.seek(0) + + # Save as an ir.attachment and return action to download + attachment = self.env['ir.attachment'].create({ + 'name': f"POS_BC_{self.start_date}_to_{self.end_date}.xlsx", + 'type': 'binary', + 'datas': base64.b64encode(output.read()), + 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }) + + return { + 'type': 'ir.actions.act_url', + 'url': f'/web/content/{attachment.id}?download=true', + 'target': 'self', + } diff --git a/wizard/pos_export_bc_wizard_views.xml b/wizard/pos_export_bc_wizard_views.xml new file mode 100644 index 0000000..e5ee376 --- /dev/null +++ b/wizard/pos_export_bc_wizard_views.xml @@ -0,0 +1,40 @@ + + + + + pos.export.bc.wizard.form + pos.export.bc.wizard + +
+ + + + + + + + + + +
+
+
+
+
+ + + Export BC Format + pos.export.bc.wizard + form + new + + + + +