From ec53246e2357953c52b25698ee9e0ca6d846b772 Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Wed, 3 Dec 2025 13:53:46 +0700 Subject: [PATCH] change the flow of advance payment, now creating advance payment only from purchase order form --- README.md | 104 +++-- __init__.py | 2 +- __manifest__.py | 54 ++- __pycache__/__init__.cpython-310.pyc | Bin 0 -> 252 bytes __pycache__/__init__.cpython-312.pyc | Bin 0 -> 210 bytes data/product_data.xml | 18 +- models/__init__.py | 4 +- models/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 318 bytes models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 298 bytes .../account_payment.cpython-310.pyc | Bin 0 -> 1347 bytes .../account_payment.cpython-312.pyc | Bin 0 -> 3610 bytes .../purchase_order.cpython-310.pyc | Bin 0 -> 6217 bytes .../purchase_order.cpython-312.pyc | Bin 0 -> 11668 bytes .../res_config_settings.cpython-310.pyc | Bin 0 -> 768 bytes .../res_config_settings.cpython-312.pyc | Bin 0 -> 840 bytes models/account_payment.py | 98 ++-- models/purchase_order.py | 423 ++++++++++-------- models/res_config_settings.py | 24 +- security/ir.model.access.csv | 4 +- views/purchase_advance_payment_views.xml | 72 ++- views/purchase_order_views.xml | 107 +++-- views/res_config_settings_views.xml | 66 +-- wizard/__init__.py | 2 +- wizard/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 251 bytes wizard/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 210 bytes ...ate_advance_payment_wizard.cpython-312.pyc | Bin 0 -> 7908 bytes wizard/create_advance_payment_wizard.py | 189 ++++++++ .../create_advance_payment_wizard_views.xml | 42 ++ wizard/link_advance_payment_wizard.py | 100 ----- wizard/link_advance_payment_wizard_views.xml | 41 -- 30 files changed, 789 insertions(+), 561 deletions(-) create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/__init__.cpython-310.pyc create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/account_payment.cpython-310.pyc create mode 100644 models/__pycache__/account_payment.cpython-312.pyc create mode 100644 models/__pycache__/purchase_order.cpython-310.pyc create mode 100644 models/__pycache__/purchase_order.cpython-312.pyc create mode 100644 models/__pycache__/res_config_settings.cpython-310.pyc create mode 100644 models/__pycache__/res_config_settings.cpython-312.pyc create mode 100644 wizard/__pycache__/__init__.cpython-310.pyc create mode 100644 wizard/__pycache__/__init__.cpython-312.pyc create mode 100644 wizard/__pycache__/create_advance_payment_wizard.cpython-312.pyc create mode 100644 wizard/create_advance_payment_wizard.py create mode 100644 wizard/create_advance_payment_wizard_views.xml delete mode 100644 wizard/link_advance_payment_wizard.py delete mode 100644 wizard/link_advance_payment_wizard_views.xml diff --git a/README.md b/README.md index 6330268..73eeb1f 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,78 @@ -# Purchase Advance Payment - -This module allows linking payments to purchase orders as advance payments. - -## Features - -- Link payments to purchase orders as advance payments -- Automatically subtract advance payments from the total when the PO is fully billed -- Create deposit products on the PO so the final invoice includes the deposit product -- Track advance payments linked to purchase orders - -## Usage - -1. Create a purchase order -2. Create a payment and link it to the purchase order as an advance payment -3. When the payment is posted, it will automatically be applied as a deposit to the purchase order -4. The deposit will appear as a negative line item on the purchase order -5. When creating the vendor bill, the deposit will be included - -## Configuration - -No additional configuration is required. - -## Known Issues - -- None \ No newline at end of file +# Purchase Advance Payment + +This module allows creating advance payments directly from purchase orders. +When an advance payment is created, a deposit line is automatically added to the PO. +The journal entry uses the expense account from the default advance payment product +and the outstanding payment account from the selected cash/bank journal. + +## Features + +- Create advance payments directly from Purchase Order form +- Automatic deposit line creation in PO +- Proper accounting entries using: + - Expense account from default advance payment product + - Outstanding payment account from selected journal +- Payment remains in draft state for review before posting +- View all advance payments linked to a PO +- Deposit line automatically updates when payment is posted + +## Configuration + +1. **Set Default Advance Payment Product** + - Go to **Purchase > Configuration > Settings** + - Scroll to **Advance Payment** section + - Set the **Default Deposit Product** - this product's expense account will be used for advance payment journal entries + +2. **Configure Outstanding Payment Accounts** (Choose one option) + + **Option A: Journal-Specific** (for multiple banks) + - Go to **Accounting > Configuration > Journals** + - Open each journal > **Outgoing Payments** tab + - Set **Outstanding Payments Account** in payment methods + + **Option B: Company Default** (for single bank) + - Go to **Accounting > Configuration > Settings** + - Set **Outstanding Payments Account** in Default Accounts section + + Note: Journal-specific accounts take priority over company default. + +## Usage + +### Creating an Advance Payment + +1. Open a Purchase Order (must be in confirmed state) +2. Go to **Advance Payments** tab +3. Click **Create Advance Payment** button +4. Fill in the wizard: + - **Journal**: Select cash or bank journal + - **Amount**: Enter advance payment amount + - **Date**: Payment date + - **Memo**: Optional description +5. Click **Confirm** +6. The payment is created in draft state for review +7. A deposit line is automatically added to the PO with negative amount +8. Review the payment and click **Confirm** to post it + +### Journal Entry Accounts + +When the payment is posted, the journal entry will use: +- **Debit**: Expense account from the default advance payment product +- **Credit**: Outstanding payment account from the selected journal + +### Vendor Bill Creation + +When creating a vendor bill from the PO: +1. The deposit line will be included in the bill +2. The deposit reduces the total amount payable +3. The advance payment is properly accounted for + +## Technical Details + +- Module creates a deposit product line in the PO with negative price +- Payment model is extended to handle advance payment accounting +- Journal entries are customized to use correct accounts +- Deposit line updates automatically when payment is posted + +## Known Issues + +- None diff --git a/__init__.py b/__init__.py index c536983..c6f023c 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -from . import models +from . import models from . import wizard \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py index 89e5bcb..ba3c141 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,25 +1,31 @@ -{ - 'name': 'Purchase Advance Payment', - 'version': '17.0.1.0.0', - 'category': 'Purchase', - 'summary': 'Link payments to purchase orders as advance payments', - 'description': """ - This module allows linking payments to purchase orders as advance payments. - When a PO is fully billed, the total is subtracted by the advance payment made. - After payment is linked to the PO, a deposit product is created so the final - invoice/vendor bills will include the deposit product. - """, - 'author': 'Suherdy Yacob', - 'depends': ['purchase', 'account'], - 'data': [ - 'security/ir.model.access.csv', - 'data/product_data.xml', - 'wizard/link_advance_payment_wizard_views.xml', - 'views/purchase_advance_payment_views.xml', - 'views/purchase_order_views.xml', - 'views/res_config_settings_views.xml', - ], - 'installable': True, - 'auto_install': False, - 'license': 'LGPL-3', +{ + 'name': 'Purchase Advance Payment', + 'version': '17.0.2.0.0', + 'category': 'Purchase', + 'summary': 'Create advance payments directly from purchase orders', + 'description': """ + This module allows creating advance payments directly from purchase orders. + When an advance payment is created, a deposit line is automatically added to the PO. + The journal entry uses the expense account from the default advance payment product + and the outstanding payment account from the selected cash/bank journal. + + Features: + - Create advance payments from PO form + - Automatic deposit line creation + - Proper accounting with configurable accounts + - Payment remains in draft for review + """, + 'author': 'Suherdy Yacob', + 'depends': ['purchase', 'account'], + 'data': [ + 'security/ir.model.access.csv', + 'data/product_data.xml', + 'wizard/create_advance_payment_wizard_views.xml', + 'views/purchase_advance_payment_views.xml', + 'views/purchase_order_views.xml', + 'views/res_config_settings_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', } \ No newline at end of file diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ce47c3a1f9a6a5fa774f80f9240b69de1ebf6a9 GIT binary patch literal 252 zcmYk0F=_)b5Jjb3Cxl>w50Ey+rdW|GO)x$|Ft~FgCK`bjR+>fHwUKY*GRJYdtt($4 zRYqL~=Fj^ud_JvK&w}UjdUbDnZ^itdip`ccy|AD_*$XcZQWmb5t3$RH5+6l<0r5g!T{`8CRsw<=A}M$BynL*H8u1BBL4+o9U!(NQoQ zsR)VQKa`o$NFER=BsEOYI7n6l9w8X32gu%rQEA - - - - - Deposit - - - + + + + + + Deposit + + + \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index 52cd9e9..e4d7a59 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,3 +1,3 @@ -from . import purchase_order -from . import account_payment +from . import purchase_order +from . import account_payment from . import res_config_settings \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86e34f4da247654f415047a51eba1ac01e4a5879 GIT binary patch literal 318 zcmYk1O-ckY5QUTOR%Dn_!9!?oI=FHpGA9thT`vqV$xI@hRFfYe;Dx-7%ig;33a+dO z4i?nQSC0x+1)I%^V7y&^m?wNcYw-qe>e&GI@4u{J2u)AP*0RskG=&(XeoH1ur_QyGM zUgZd5QO>@Q$4i`6{or-2f|cZr_3Ruet!Z#k_G;{`i)A-p*lc95F*a@!?#p5V zpYtugNHSx2g&iyW+?i|9kFh4F$yh;#=<-VFAw=qwG30{yk5%SLIc*PVqT`J=2q_!a kL@C$(IL&X^99nzp(>J7thZW0rj)+@0*}~}uocTFhzce6HXaE2J literal 0 HcmV?d00001 diff --git a/models/__pycache__/account_payment.cpython-310.pyc b/models/__pycache__/account_payment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15cca60f13f41f439e5ae5a85ebaf9e279521fbe GIT binary patch literal 1347 zcmaJ>&5qkP5GEyA)_QGklNv?Q0);Mxqad)+UV3SRr2l(Jy6E0i1O%(0V;K^uCFKA% zHb8-0Ag8{=?r|Tb$6k5usjpC=XovbqH#w972b|#y`OP;Ys>x(TaQyc1@9X1$kUyMq zd!UUx!fEcK00OiiB~5ARrC#Z$J|*7~@W8($;0wA5F8p)=!6``wf1*E0d>fVuL}6^W z%0&Uj`n<|Bw|9FCI>r2m3qeu}B=vx%z6dr0cRmC#_|;EC)Q1ql5F$80JrW03BN$y% zIE3*fxuEIcE3%?6f$3$Gj$e_0q!UDQWT%g_Ol@U-!gr;R_5O&r`l9L#(`u`;H8*0R zGzh()o^*QrL!*EH)4h4cbuEQvIm{o#^Dj{RXGtE!_vWVNwQzg6M2P|28O{Vsig}Ve zOFSEzTIce)4MCMWm;1ZNCVmVrxXeV{0sMCV%`qU*9BDIgzKZL0Zt%JWeT+*kW8Q`I z)|JvRkNZ30CXd9|A#!SmAXa=^)HYa)qOx~-FV=WrMj5?T5V!VleudM#kK&wch#on) zq348oKu)n6PRR{@iZ{3xvPZmh1T!s64qIN>ur(yq21XRC8={ZU@2|7vT9smHwrinb zx7hJa{rsrp6_<-lEpk|%?CQ0W8Me(*fl{A*w#>GsMy3EMG|Sh>yYyL?{<3YfWmnkV zyr_2eLx$(AxqL3z>*zl3cU_8(qR^Yt;Gg~dHydT~KcR0>=Hn6lGz(N}azU=>HF=4I zeNCQCUV7)=#?v#Dek-qnYeLD{@SL6!eTX5+zz)o|651Z}tj?8Wl`^%xn;Z64vMuGa z&Wl%7v11_6JFl@GQ#{N1SO`1p$ho2RfU&IL#xT~@LBE4e+GX$RYsM%>H1fuD}%lwNST literal 0 HcmV?d00001 diff --git a/models/__pycache__/account_payment.cpython-312.pyc b/models/__pycache__/account_payment.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57fa516cf3a6f316b13d26e02986aa4e96c78f92 GIT binary patch literal 3610 zcma)9O>7&-6`tji%O6Q3EjgA%ThdZ?Dy?l$X^=FjD%nzCMONcq*lA$a&UVEaNz0PE z?CespWC~FM6;S{Oa1Mp+UYwwZ*sy^XJ^9c>b83KIxMm9yixySTUV3Bf1PyZPo8>NP zu~OPS%)FWRX5P=t``+xIy1F6=zF+)pO#O8iLjPa`?+duZ)7OC5LKtB#hw@yS%lp#4 zJfG${-iL@k>sbBl5(IW&9{V2#nmPqXGP+ZWjj(`&IP@^wlnjAn zIJ-GQT8{u@PLv?M0(p{VAI+u=KLQ>3*PG>TDQ8Om&pMlZNy@w_uW{o~P*f`7B%A_LY#_oLxaH=stH2Eb9a* zJEr8>!N`;tDlRHHTWCy3ob5MAZoy!NiQk;e6b%!?g&~f{Skrtr2C^H2vBpgvb5GZs zP8C+{gR-u{0c(pyZm#y6Up>0>dZfNZwg=2H4nwn!wmV|gj^yKxWGyjRO}tD>aTOg0S*Cg7Zk`IXvulhnottxr#P?{1nc^|*;b!T zv}li_hyQCWK{r%WE>Im8GbVuDYRsimZhwxl zbh87P%&VpyX~DW3@t{P-wveF&tESXr2aIBYP9fR1ss+smdunm-$E3#(-xn3ZeLBIh!lz~dK$f)F= zVoi}qRxeUb$uV(JFPifJxY)x+8N#=u zjFp_J zBxkG1i&pYtEk5~_=Y~&~I|rW#C_W5@9P`6pINsisUTWtRJneIcMjV@-k6R_b&$MXeMC*5|!Px20E} zAff*vcYAJoX!P#dowa*!{Z^?Az53|8kB27y(ErDQPX|6L{dxLoZG5gee#RO<^HKQT z@yFsT+sUJ!417Fr@A{*CmE;c|DV5~O+W6V(_yueHf-6$*VjI`vC_Y#Y4N!*jJ02md zOgg3VS3tCtN+^n~RC;A=jVmF5&hG(q`bxgt$|ZnT95}m_OW+Lb&dG*Baogu<0QYea-S0;d|;8)WjVVVaBrG+%F^T1;F&z1=r*Teus$ z6RU;#YO$eO?8=uxf4@-nKUeQWiP75!?hf7=ti=x3;?LE(E(0$CyaT`!y4NqgclGC2 z%iYgc0^j+<=L<}8PkAJa)E)Ax(2>C4zt3>NKu;Y(;{?Ys;hZGcejV#NWjEFFojUFX z&az)BN9I0i$TR5qCN$9q;0ZbdL*|vi(5TLH9QSv0#6m~D6s9?@=hlz5u6}s+E5vAL G`F{X~vx^=8 literal 0 HcmV?d00001 diff --git a/models/__pycache__/purchase_order.cpython-310.pyc b/models/__pycache__/purchase_order.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b81c580e68d4ce22f625482af080ebff668feb2 GIT binary patch literal 6217 zcmbVQ&5zs073Yu?MRB#evb}429Vbyf98^u9O@g9nQ`h-&oTRPSM&hQefRvy$qg}2k zQXW$4T|q{Y+CWaZ6}`j-c8|XG(o6r09_QE|iu6>V1qvAX=loPM=aWyV_uw|V8R!7GAox_7Kj4HSo0L8%rLkGt;~t=i|{Y_u%p^b;r)qa@@`KFYKL}$JiB|+wQll~x(qDwO;e@@X!|AK}vI*c0~le3N`?9B={ zxkY}f8?&wY4EfBQ9O@*V)Ph)j6Z(Y?HuP#{sd?zH0??L z6lhEQX?_m0B8xhqAj7q~eO?LXH*&MYo;k{$kMWCc`*u4}zV(h%REhIbXk6+s|nHTFa7kNL^J)Ie3lMN~9PqzSgImHs2L#$cJEb`hbxh z@ntdcU~wQ5KJ4PF^5tW}3xjnmnJ;GV|To8zLI0 z6SG?mc{*A-g4ovb8J#(5DA2p@vI&DT(XOJrfs(dSjg5=O2gh%-44%TxyT+JpvR!R= zca1%E5f*cYjhS!VG`RsiSJn=(BbF(Zlc+uh!w;O>n6hcSZ!jaDcAu@m6`XuOP@vg< zUR8_lzi-GDOnfj{-AMXkH63mU$#>d2K{vVnYCjkRal4zeL%w=-C)-HkE?jOk;Ysqs zORL=>6xI(oPvUfS-<)P7v6?&S!MHZJ_FzZV&aaDDY!BpxbF_UImHClvIL!Nc^vaYq zw+{^9C`)RekCJW>r5D=!9H*p23Lq)HN6Qd;jg3u+`-TaDnrm_nlopdU{ZMS6<&jrs zKvVAdDCl43!G&kgONo(Zj4ph~4Hm3gwzAS;UoPnddSR3aDY#mm5nk>o_f2J|BI>D% zmfg($~b*Q4kDpDn6-ZXNlGvI$mGGNjM8P_*F!_jALo<(5W9 zdm5(eo4LKoWxN3Qei{$WN-Y-LOV5>0&=48kfuQ9vnuU@QD!XQb;Uk`T*hfqw!J)lY zz(Vi!DjH*B*VuzMK=0&SY>(|)d&aK4hfuU)+-3yx7^`eLJ!>7l!z<_M-71O;$VU+3 z?mZjy8t4pGThMYqJbRFc&xGvp|-oTrYP472NiHm=-sI2c5skg$Ul zE_%T*%KSlr*!zI2j_(fzGK+--NUNm-ng((|+Zl*SYW}9C0$JlsY-g>S@}631}qy0)XgGol%=QTS#Cl6t`xfUSnK( z9J5_^nk_NgbeM_1_l50@zCW$oZ(=j0UrI3uEwp~O(3sROXDi083GG_k++$>)@^l8> z0Y(kKLRx;0bX!S(#4S9X&x}8s6y$R)^V+&WHo(1m&TSLpjL(5zbxgdhv9;+QaB0;? z&%XiDWuh6BQKVS}A{}Y6jWBJNT5aZW_(Ujyr*SV_ho#7%FW~s2XUmsn#n&!^lV7l< zD7_!zD41Qn(o|KXJ6mB_T)QNnf-pWO@6p!Ra+Doo31w?*nA=|^xq(SR1Z%jV7Q%Qd z!EAq!L}7PF9V<=K&w_1{Vns>nYt`f`@qM1E7g4F&x`55nHz+GaLIGpdp?6}h@@0C_ z>1rWqvufG7DV-{V2lXFfG3nJr>OfMLvod@6%MmY{!_51J{RH2E5`j z=w%a;vkXiXq+!E>kg7iUqVn=AF$w`gkAtsrhA}!fN`+cLc5QxYzSIJ}n_`DG-|t32 zn)?0()?WdScTsfE@))7L$80t_Gp&WmJxmFmgiV3_4dU@9RMQx6gUR{KX_|TIbj2vqaz0^Fra$R+G9AR)HGti^EcuDoSrp}8Z^V& z^avC5%vv_LiIg8v<$#BGucmQiLF33G;YiLA=<$=+5CnD(C8R;Rk0vC-W*9fK4KV{G z+QCOJtR;u0_b9?V41d~FLYy=-`8I^8O-#N+t)Ejh6QIem1ko>}8vS5eRfQ`C^i8FG z9=5N~X-)ZKR5)DBmCk-M+d;sQ1Vh6(%#>SV&*;>}BB3wS^#b*-A*Q*xO(G~_u0974 zT9MbwU!gi+KTP`a*F^pqk?S0g?GK_Ljs=&$0qMbs5*qW1F;@^z}- zqKcA=+zl$XWC#eSkXfJ|7Nc#DfhzYRmdZjvuoWk_a_}UJMQ)@c>ay4l(+p>+QeZx< zj2#wGN+39+1hsUOpgUli5-hubIKY|ZvW8`wHh>QKG;_bSm+b#g%cN0V1N#2kn6u5% zli#k#DXSq_Pza_kOn;9`qugAc3L>qxhGK7Fo?4-3k7a5QW4+;->PWS$g$Kd z_@t^@wA5S~uPIZbJC+GhT}Ssrl#~*5*CIi~ z?^66If28dFgX<5`p%W%VzpR5Bax8>6wrSEc4|LorP}IJsG4H0uk$JU{^<4_Lr}c0k zgc9qDZ~RBT`oCV$fRX1krgDq-cT^QH0R^RNNTVC2T*!lns2ZhHYo{WgB=?s-s>1(d zgtoG2#*CNA5F>v;NgJpisSFj=Xo$W)V*=jh=gl;~p%;-63G0hY*Wooh+e= z=R`iAUxn@8RTX)wJOO@(9E6;NN~#^i$Pb+v0|J_396EI(V96EE2=UGvl!W9?f<7HN z3Adclo8N+4X>%jk63uQRaiN(tOK+HkFiHVsLKQ_@?L2aT7BV)S$!pj;gKS;U&FKF5 zl`u{_jP{l?P}v&#C}Eq$9KxIXC@J0Lxn@0&J%lmS_Yb6n_QO}3iZGNT5cp_W=9diz z2Y6ZIID(gP1rjqy7r&##D3{1#10h((Sg{n2MhY#VhEM055?qj|;HrpX_pBT~2JO!% z`X0;0F_#V-GqE+7!6AN^TAs{^<&df^s%}w5pRcN~CrKoNI0rA1wxp%xm@4h6GyZxT z1G^~sse8g-oi7)hrMjf?H8_b)w|J7otQV3RKo*JE7TaAhpj(YpCq792iJD;}sU#>+ omCJPVu3cI4o7jln4FL!4&2p@}K@ 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/res_config_settings.cpython-312.pyc b/models/__pycache__/res_config_settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c38333eb00b45ed6b2260dbc83c3a88ad54dc9c1 GIT binary patch literal 840 zcmZ`&&ui2`6rM@4vuSnRs<`bIq&e(iQ|Usz2%=K7sFxN|4-x`H$jt7hz01ki$kVkv=YsSqgO5g^JDAS#6idILP)Pu0q|TA60Hz88V13KT?y z`dpx~M}5MD>cpOV4g#QE2a6r)90Lk0NPwjfXsHUI+CjLNZ?=VwaVT>bi)Ds7cqXBI)42{$6h$4%uARi#T5FU6J|G~kcun+5`GI7h2XzmklS{gCDSN$=m!C zy6#$0Q({+(8vRbC)IGK?L-{`deqEZMDunP07|g-o3@t%)f 0 and line_vals.get('partner_id'): + line_vals['account_id'] = expense_account.id + # The credit line (outstanding payment) + elif line_vals.get('credit', 0) > 0: + line_vals['account_id'] = outstanding_account.id + + return line_vals_list + + def action_post(self): + res = super().action_post() + # When an advance payment is posted, update deposit line in purchase order + for payment in self: + if payment.is_advance_payment and payment.purchase_order_id: + # Update the deposit line with the actual posted amount + payment.purchase_order_id._update_deposit_line() return res \ No newline at end of file diff --git a/models/purchase_order.py b/models/purchase_order.py index cbec487..c42ff63 100644 --- a/models/purchase_order.py +++ b/models/purchase_order.py @@ -1,187 +1,238 @@ -from odoo import models, fields, api -from odoo.exceptions import UserError -from odoo.tools import float_compare - - -class PurchaseOrder(models.Model): - _inherit = 'purchase.order' - - advance_payment_ids = fields.One2many( - 'account.payment', - 'purchase_order_id', - string='Advance Payments', - domain=[('state', '=', 'posted')] - ) - - advance_payment_total = fields.Monetary( - string='Advance Payment Total', - compute='_compute_advance_payment_total', - store=True - ) - - amount_residual = fields.Monetary( - string='Amount Residual', - compute='_compute_amount_residual', - store=True - ) - - deposit_product_id = fields.Many2one( - 'product.product', - string='Deposit Product', - help='Product used for advance payment deposit' - ) - - @api.depends('advance_payment_ids', 'advance_payment_ids.state', 'advance_payment_ids.amount') - def _compute_advance_payment_total(self): - for order in self: - order.advance_payment_total = sum( - payment.amount for payment in order.advance_payment_ids.filtered(lambda p: p.state == 'posted') - ) - - @api.depends('amount_total', 'advance_payment_total') - def _compute_amount_residual(self): - for order in self: - order.amount_residual = order.amount_total - order.advance_payment_total - - def action_view_advance_payments(self): - self.ensure_one() - action = self.env.ref('account.action_account_payments').sudo().read()[0] - action['domain'] = [('id', 'in', self.advance_payment_ids.ids)] - action['context'] = { - 'default_purchase_order_id': self.id, - 'default_partner_id': self.partner_id.id, - 'default_payment_type': 'outbound', - 'default_partner_type': 'supplier', - } - return action - - def action_create_deposit_product(self): - """Create a deposit product for this purchase order""" - self.ensure_one() - # Check if there's a default deposit product in settings - default_deposit_product = self.env['ir.config_parameter'].sudo().get_param( - 'purchase_advance_payment.deposit_product_id') - if default_deposit_product: - self.deposit_product_id = int(default_deposit_product) - return self.deposit_product_id - - # If no default product, create one - if not self.deposit_product_id: - product_vals = { - 'name': f'Deposit for PO {self.name}', - 'type': 'service', - 'purchase_ok': True, - 'sale_ok': False, - 'invoice_policy': 'order', # Ordered quantities for deposit - 'supplier_taxes_id': [(6, 0, [])], # No supplier taxes - } - deposit_product = self.env['product.product'].create(product_vals) - self.deposit_product_id = deposit_product.id - return self.deposit_product_id - - def button_draft(self): - res = super().button_draft() - # Remove deposit lines when resetting to draft - for order in self: - deposit_lines = order.order_line.filtered(lambda l: l.is_deposit) - deposit_lines.unlink() - return res - - def action_apply_deposit(self): - """Apply advance payment as deposit line in the purchase order""" - self.ensure_one() - if self.advance_payment_total <= 0: - raise UserError("No advance payment found for this purchase order.") - - # Create or update deposit product - if not self.deposit_product_id: - self.action_create_deposit_product() - - # Check if deposit line already exists - existing_deposit_line = self.order_line.filtered(lambda l: l.is_deposit) - - if existing_deposit_line: - # Update existing deposit line - existing_deposit_line.write({ - 'product_qty': 1, - 'price_unit': -self.advance_payment_total, # Negative value for deposit - 'taxes_id': [(6, 0, [])], # No taxes - }) - else: - # Create new deposit line - deposit_vals = { - 'order_id': self.id, - 'product_id': self.deposit_product_id.id, - 'name': f'Deposit payment for PO {self.name}', - 'product_qty': 1, - 'product_uom': self.deposit_product_id.uom_id.id, - 'price_unit': -self.advance_payment_total, # Negative value for deposit - 'is_deposit': True, - 'date_planned': fields.Datetime.now(), - 'taxes_id': [(6, 0, [])], # No taxes - } - self.env['purchase.order.line'].create(deposit_vals) - - return True - - def action_create_invoice(self): - """Override to ensure deposit line is included in vendor bill""" - # Apply deposit before creating invoice - for order in self: - if order.advance_payment_total > 0: - order.action_apply_deposit() - - # Call super to create the invoice - invoices = super().action_create_invoice() - - # Ensure deposit lines have quantity 1 in the created invoices - if 'res_id' in invoices and invoices['res_id']: - # Single invoice - invoice = self.env['account.move'].browse(invoices['res_id']) - self._fix_deposit_line_quantities(invoice) - elif 'domain' in invoices and invoices['domain']: - # Multiple invoices - invoice_ids = self.env['account.move'].search(invoices['domain']) - for invoice in invoice_ids: - self._fix_deposit_line_quantities(invoice) - - return invoices - - def _fix_deposit_line_quantities(self, invoice): - """Fix deposit line quantities in the invoice""" - for line in invoice.invoice_line_ids: - if line.purchase_line_id and line.purchase_line_id.is_deposit: - line.write({ - 'quantity': 1.0, - 'tax_ids': [(6, 0, [])] # No taxes - }) - - -class PurchaseOrderLine(models.Model): - _inherit = 'purchase.order.line' - - is_deposit = fields.Boolean( - string='Is Deposit', - default=False, - help='Identifies if this line is a deposit payment' - ) - - def _prepare_account_move_line(self, move=False): - """Override to ensure deposit lines have correct quantity in vendor bill""" - self.ensure_one() - res = super()._prepare_account_move_line(move) - - # If this is a deposit line, ensure quantity is 1 and no taxes - if self.is_deposit: - res['quantity'] = 1 - res['tax_ids'] = [(6, 0, [])] # No taxes - - return res - - def _get_invoice_qty(self): - """Override to ensure deposit lines have correct quantity for invoicing""" - self.ensure_one() - if self.is_deposit: - # For deposit lines, always invoice quantity 1 regardless of received qty - return 1.0 +from odoo import models, fields, api +from odoo.exceptions import UserError +from odoo.tools import float_compare + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + advance_payment_ids = fields.One2many( + 'account.payment', + 'purchase_order_id', + string='Advance Payments', + domain=[('state', '=', 'posted')] + ) + + advance_payment_total = fields.Monetary( + string='Advance Payment Total', + compute='_compute_advance_payment_total', + store=True + ) + + amount_residual = fields.Monetary( + string='Amount Residual', + compute='_compute_amount_residual', + store=True + ) + + deposit_product_id = fields.Many2one( + 'product.product', + string='Deposit Product', + help='Product used for advance payment deposit' + ) + + @api.depends('advance_payment_ids', 'advance_payment_ids.state', 'advance_payment_ids.amount') + def _compute_advance_payment_total(self): + for order in self: + order.advance_payment_total = sum( + payment.amount for payment in order.advance_payment_ids.filtered(lambda p: p.state == 'posted') + ) + + @api.depends('amount_total', 'advance_payment_total') + def _compute_amount_residual(self): + for order in self: + order.amount_residual = order.amount_total - order.advance_payment_total + + def action_view_advance_payments(self): + self.ensure_one() + action = self.env.ref('account.action_account_payments').sudo().read()[0] + action['domain'] = [('id', 'in', self.advance_payment_ids.ids)] + action['context'] = { + 'default_purchase_order_id': self.id, + 'default_partner_id': self.partner_id.id, + 'default_payment_type': 'outbound', + 'default_partner_type': 'supplier', + } + return action + + def action_create_deposit_product(self): + """Create a deposit product for this purchase order""" + self.ensure_one() + # Check if there's a default deposit product in settings + default_deposit_product = self.env['ir.config_parameter'].sudo().get_param( + 'purchase_advance_payment.deposit_product_id') + if default_deposit_product: + self.deposit_product_id = int(default_deposit_product) + return self.deposit_product_id + + # If no default product, create one + if not self.deposit_product_id: + product_vals = { + 'name': f'Deposit for PO {self.name}', + 'type': 'service', + 'purchase_ok': True, + 'sale_ok': False, + 'invoice_policy': 'order', # Ordered quantities for deposit + 'supplier_taxes_id': [(6, 0, [])], # No supplier taxes + } + deposit_product = self.env['product.product'].create(product_vals) + self.deposit_product_id = deposit_product.id + return self.deposit_product_id + + def button_draft(self): + res = super().button_draft() + # Remove deposit lines when resetting to draft + for order in self: + deposit_lines = order.order_line.filtered(lambda l: l.is_deposit) + deposit_lines.unlink() + return res + + def _update_deposit_line(self): + """Update deposit line based on posted advance payments""" + self.ensure_one() + + # Calculate total posted advance payments + posted_advance_total = sum( + payment.amount for payment in self.advance_payment_ids.filtered(lambda p: p.state == 'posted') + ) + + if posted_advance_total <= 0: + # Remove deposit line if no posted payments + deposit_lines = self.order_line.filtered(lambda l: l.is_deposit) + deposit_lines.unlink() + return + + # Get or create deposit product + deposit_product_id = self.env['ir.config_parameter'].sudo().get_param( + 'purchase_advance_payment.deposit_product_id') + + if not deposit_product_id: + raise UserError( + "Please configure a default advance payment product in Purchase settings." + ) + + deposit_product = self.env['product.product'].browse(int(deposit_product_id)) + + # Check if deposit line already exists + existing_deposit_line = self.order_line.filtered(lambda l: l.is_deposit) + + if existing_deposit_line: + # Update existing deposit line + existing_deposit_line.write({ + 'product_qty': 1, + 'price_unit': -posted_advance_total, # Negative value for deposit + 'taxes_id': [(6, 0, [])], # No taxes + }) + else: + # Create new deposit line + deposit_vals = { + 'order_id': self.id, + 'product_id': deposit_product.id, + 'name': f'Advance payment for {self.name}', + 'product_qty': 1, + 'product_uom': deposit_product.uom_id.id, + 'price_unit': -posted_advance_total, # Negative value for deposit + 'is_deposit': True, + 'date_planned': fields.Datetime.now(), + 'taxes_id': [(6, 0, [])], # No taxes + } + self.env['purchase.order.line'].create(deposit_vals) + + def action_apply_deposit(self): + """Apply advance payment as deposit line in the purchase order""" + self.ensure_one() + if self.advance_payment_total <= 0: + raise UserError("No advance payment found for this purchase order.") + + # Create or update deposit product + if not self.deposit_product_id: + self.action_create_deposit_product() + + # Check if deposit line already exists + existing_deposit_line = self.order_line.filtered(lambda l: l.is_deposit) + + if existing_deposit_line: + # Update existing deposit line + existing_deposit_line.write({ + 'product_qty': 1, + 'price_unit': -self.advance_payment_total, # Negative value for deposit + 'taxes_id': [(6, 0, [])], # No taxes + }) + else: + # Create new deposit line + deposit_vals = { + 'order_id': self.id, + 'product_id': self.deposit_product_id.id, + 'name': f'Deposit payment for PO {self.name}', + 'product_qty': 1, + 'product_uom': self.deposit_product_id.uom_id.id, + 'price_unit': -self.advance_payment_total, # Negative value for deposit + 'is_deposit': True, + 'date_planned': fields.Datetime.now(), + 'taxes_id': [(6, 0, [])], # No taxes + } + self.env['purchase.order.line'].create(deposit_vals) + + return True + + def action_create_invoice(self): + """Override to ensure deposit line is included in vendor bill""" + # Apply deposit before creating invoice + for order in self: + if order.advance_payment_total > 0: + order.action_apply_deposit() + + # Call super to create the invoice + invoices = super().action_create_invoice() + + # Ensure deposit lines have quantity 1 in the created invoices + if 'res_id' in invoices and invoices['res_id']: + # Single invoice + invoice = self.env['account.move'].browse(invoices['res_id']) + self._fix_deposit_line_quantities(invoice) + elif 'domain' in invoices and invoices['domain']: + # Multiple invoices + invoice_ids = self.env['account.move'].search(invoices['domain']) + for invoice in invoice_ids: + self._fix_deposit_line_quantities(invoice) + + return invoices + + def _fix_deposit_line_quantities(self, invoice): + """Fix deposit line quantities in the invoice""" + for line in invoice.invoice_line_ids: + if line.purchase_line_id and line.purchase_line_id.is_deposit: + line.write({ + 'quantity': 1.0, + 'tax_ids': [(6, 0, [])] # No taxes + }) + + +class PurchaseOrderLine(models.Model): + _inherit = 'purchase.order.line' + + is_deposit = fields.Boolean( + string='Is Deposit', + default=False, + help='Identifies if this line is a deposit payment' + ) + + def _prepare_account_move_line(self, move=False): + """Override to ensure deposit lines have correct quantity in vendor bill""" + self.ensure_one() + res = super()._prepare_account_move_line(move) + + # If this is a deposit line, ensure quantity is 1 and no taxes + if self.is_deposit: + res['quantity'] = 1 + res['tax_ids'] = [(6, 0, [])] # No taxes + + return res + + def _get_invoice_qty(self): + """Override to ensure deposit lines have correct quantity for invoicing""" + self.ensure_one() + if self.is_deposit: + # For deposit lines, always invoice quantity 1 regardless of received qty + return 1.0 return super()._get_invoice_qty() \ No newline at end of file diff --git a/models/res_config_settings.py b/models/res_config_settings.py index 5d0a8e8..f221f6b 100644 --- a/models/res_config_settings.py +++ b/models/res_config_settings.py @@ -1,13 +1,13 @@ -from odoo import models, fields, api - - -class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' - - deposit_product_id = fields.Many2one( - 'product.product', - string='Default Deposit Product', - domain=[('type', '=', 'service')], - config_parameter='purchase_advance_payment.deposit_product_id', - help='Default product used for advance payment deposits' +from odoo import models, fields, api + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + deposit_product_id = fields.Many2one( + 'product.product', + string='Default Deposit Product', + domain=[('type', '=', 'service')], + config_parameter='purchase_advance_payment.deposit_product_id', + help='Default product used for advance payment deposits' ) \ No newline at end of file diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index 2f7f514..6164d41 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,2 +1,2 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_link_advance_payment_wizard,access.link.advance.payment.wizard,model_link_advance_payment_wizard,base.group_user,1,1,1,1 \ No newline at end of file +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_create_advance_payment_wizard,access.create.advance.payment.wizard,model_create_advance_payment_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/views/purchase_advance_payment_views.xml b/views/purchase_advance_payment_views.xml index 073c861..c93f739 100644 --- a/views/purchase_advance_payment_views.xml +++ b/views/purchase_advance_payment_views.xml @@ -1,40 +1,34 @@ - - - - - - account.payment.form.inherit.advance.payment - account.payment - - - - - - - - - -