From aaade5784b8fda49b9eaa43ff8f7afb63342c752 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sat, 20 Jun 2026 12:55:40 +0700 Subject: [PATCH] refactored to 17.0 --- README.md | 41 +++ __init__.py | 132 +++++++ __manifest__.py | 22 ++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 5434 bytes __pycache__/__manifest__.cpython-312.pyc | Bin 0 -> 865 bytes models/__init__.py | 5 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 274 bytes .../account_journal.cpython-312.pyc | Bin 0 -> 1619 bytes .../account_payment.cpython-312.pyc | Bin 0 -> 4770 bytes .../pos_payment_method.cpython-312.pyc | Bin 0 -> 2225 bytes .../__pycache__/pos_session.cpython-312.pyc | Bin 0 -> 13926 bytes models/account_journal.py | 36 ++ models/account_payment.py | 87 +++++ models/pos_payment_method.py | 54 +++ models/pos_session.py | 324 ++++++++++++++++++ views/account_journal_views.xml | 31 ++ views/pos_payment_method_views.xml | 31 ++ wizard/__init__.py | 2 + wizard/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 167 bytes .../account_payment_register.cpython-312.pyc | Bin 0 -> 6165 bytes wizard/account_payment_register.py | 122 +++++++ 21 files changed, 887 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/__manifest__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/account_journal.cpython-312.pyc create mode 100644 models/__pycache__/account_payment.cpython-312.pyc create mode 100644 models/__pycache__/pos_payment_method.cpython-312.pyc create mode 100644 models/__pycache__/pos_session.cpython-312.pyc create mode 100644 models/account_journal.py create mode 100644 models/account_payment.py create mode 100644 models/pos_payment_method.py create mode 100644 models/pos_session.py create mode 100644 views/account_journal_views.xml create mode 100644 views/pos_payment_method_views.xml create mode 100644 wizard/__init__.py create mode 100644 wizard/__pycache__/__init__.cpython-312.pyc create mode 100644 wizard/__pycache__/account_payment_register.cpython-312.pyc create mode 100644 wizard/account_payment_register.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..76fbb55 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Account Shared Bank Cash + +## Overview + +This module mirrors the parent company's Bank and Cash accounts to branch companies and provides: + +1. **Mirrored Bank/Cash Accounts**: Automatically clones parent company bank/cash accounts and journals to branches on install +2. **POS Inter-Company Clearing**: When a POS session closes, creates automated clearing entries between the branch and parent company +3. **Centralized Vendor Payment**: Branch vendor bills can be paid via parent company's bank journal with inter-company clearing + +## Configuration + +### 1. POS Inter-Company Clearing + +On each `POS Payment Method`: +- **Inter-Company Clearing Account**: The RK account in the branch company +- **Clearing Journal**: A miscellaneous journal for the clearing entry +- **Parent Company**: The parent company for the mirror entry +- **Parent Bank Journal**: The parent's bank journal (outstanding receipt account will be debited) +- **Parent Hubungan RK Account**: The parent's inter-company liability account +- **Parent Clearing Journal**: A miscellaneous journal in the parent for the mirror entry + +### 2. Centralized Vendor Payment + +On each `Account Journal` (Bank/Cash): +- **Is Centralized Payment**: Enable centralized mode +- **Parent Company**: The parent company +- **Parent Bank Journal**: The parent's actual bank journal +- **Parent Inter-Company Account**: The RK account in the parent (Receivable from branch) +- **Branch Inter-Company Account**: The RK account in the branch (Liability to parent) + +## Technical Notes + +- **Odoo 17 Compatibility**: Uses mirror account strategy since Odoo 17 `account.account` uses single `company_id` +- **Post-init hook**: Automatically clones parent bank/cash accounts and journals to branches +- **Uninstall hook**: Removes mirrored accounts that have no journal items + +## Dependencies + +- `point_of_sale` +- `account` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..425a749 --- /dev/null +++ b/__init__.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +from . import models +from . import wizard +from odoo import api, SUPERUSER_ID +import logging + +_logger = logging.getLogger(__name__) + + +def _auto_share_accounts_post_init(cr, registry): + """ + Automatically mirror Bank & Cash accounts from the Parent Company (ID 2) + to branch companies that don't have their own Chart of Accounts. + + Also auto-creates Bank journals in each branch for the mirrored accounts. + + Odoo 17 Strategy: Since account.account uses a single company_id (M2O), + we CREATE new account records in each branch company with the same code/name + as the parent's bank/cash accounts (instead of sharing via M2M in Odoo 19). + """ + env = api.Environment(cr, SUPERUSER_ID, {}) + + parent_company = env['res.company'].browse(2) + if not parent_company.exists(): + _logger.warning("Parent company (ID=2) not found, skipping auto-share.") + return + + branch_companies = env['res.company'].search([('id', '!=', 2)]) + + # Find parent's bank/cash accounts + parent_bank_cash_accounts = env['account.account'].search([ + ('company_id', '=', parent_company.id), + ('account_type', '=', 'asset_cash'), + ]) + + if not parent_bank_cash_accounts: + _logger.info("No bank/cash accounts found in parent company, skipping.") + return + + for branch in branch_companies: + # Check if branch already has its own accounts + existing_branch_accounts = env['account.account'].search([ + ('company_id', '=', branch.id), + ('account_type', '=', 'asset_cash'), + ]) + + if existing_branch_accounts: + _logger.info("Branch %s already has bank/cash accounts, skipping.", branch.name) + continue + + # Clone parent's bank/cash accounts to this branch + for parent_acc in parent_bank_cash_accounts: + # Check if an account with the same code already exists in this branch + existing = env['account.account'].search([ + ('company_id', '=', branch.id), + ('code', '=', parent_acc.code), + ], limit=1) + + if not existing: + env['account.account'].create({ + 'name': parent_acc.name, + 'code': parent_acc.code, + 'account_type': parent_acc.account_type, + 'company_id': branch.id, + 'reconcile': parent_acc.reconcile, + 'currency_id': parent_acc.currency_id.id if parent_acc.currency_id else False, + 'note': "Auto-mirrored from parent company (%s)" % parent_company.name, + }) + _logger.info( + "Created mirror account %s (%s) in branch %s", + parent_acc.code, parent_acc.name, branch.name + ) + + # Auto-create bank journals in branch for mirrored accounts + parent_bank_journals = env['account.journal'].search([ + ('company_id', '=', parent_company.id), + ('type', 'in', ('bank', 'cash')), + ]) + + for parent_journal in parent_bank_journals: + existing_journal = env['account.journal'].search([ + ('company_id', '=', branch.id), + ('code', '=', parent_journal.code), + ], limit=1) + + if not existing_journal: + # Find the mirrored account in the branch + branch_account = env['account.account'].search([ + ('company_id', '=', branch.id), + ('code', '=', parent_journal.default_account_id.code), + ], limit=1) + + if branch_account: + env['account.journal'].create({ + 'name': parent_journal.name, + 'code': parent_journal.code, + 'type': parent_journal.type, + 'company_id': branch.id, + 'default_account_id': branch_account.id, + }) + _logger.info( + "Created mirror journal %s (%s) in branch %s", + parent_journal.code, parent_journal.name, branch.name + ) + + +def _cleanup_shared_accounts_uninstall(cr, registry): + """ + Remove the auto-mirrored accounts upon uninstallation. + Only removes accounts tagged with the auto-mirror note and having no journal items. + """ + env = api.Environment(cr, SUPERUSER_ID, {}) + + mirrored_accounts = env['account.account'].search([ + ('company_id', '!=', 2), + ('note', 'ilike', 'Auto-mirrored from parent company'), + ('account_type', '=', 'asset_cash'), + ]) + + for acc in mirrored_accounts: + # Only delete if no journal items reference this account + has_items = env['account.move.line'].search([ + ('account_id', '=', acc.id), + ], limit=1) + if not has_items: + _logger.info("Removing mirrored account %s from company %s", acc.code, acc.company_id.name) + acc.unlink() + else: + _logger.warning( + "Cannot remove mirrored account %s from company %s: has journal items", + acc.code, acc.company_id.name + ) diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..2d5b5d6 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Account Shared Bank Cash', + 'version': '17.0.1.0', + 'category': 'Accounting', + 'summary': 'Mirror parent bank/cash accounts to branches with inter-company clearing', + 'description': """ +This module mirrors the parent company's Bank and Cash accounts and journals to branch companies, +and provides automated inter-company clearing for POS sessions and centralized vendor payments. + """, + 'author': 'Suherdy Yacob', + 'depends': ['point_of_sale', 'account'], + 'data': [ + 'views/pos_payment_method_views.xml', + 'views/account_journal_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', + 'post_init_hook': '_auto_share_accounts_post_init', + 'uninstall_hook': '_cleanup_shared_accounts_uninstall', +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ae7cadbaf41a11278487813ff7dc9854add084f GIT binary patch literal 5434 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 literal 0 HcmV?d00001 diff --git a/__pycache__/__manifest__.cpython-312.pyc b/__pycache__/__manifest__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff031e4b39f910162d1fa8a7351a2c9dc7ded9e3 GIT binary patch literal 865 zcmZuv&u`N(7){!(4Jm1cDElFy&Dv$E*t$(1kPt!wq`_gpr0%kl<;HGfme`S(XNWw zE)<_EYEhfkXq_(6W!j+4QH?tE7R)Yf(c9FcZR&saY-_U8y_2uM2m+aj6!k-*7)39M zI7hFD4s+-5N$;R{*gG(8h2dh9zj@1*k_sh&2G#)J`vJfaS?Fk#N;FVJ1R>Mtf~O(k zB4uhXka0r91O*Wzs#N;so`zhbSkf$FD4xp#J7ibd6=O#)Oc6on#%^vZcRt8W2@?Ht zEn4t6)4OhoNR%9N3T8-_%9x}O^8b#|5OSTo?<38$=28Fy=mEr3B;q;n#!S$8_DKx8 zy5}Mox!2D^rsxEnkw6ZNmq^gZa)>pFm}ypx$+yRxUFiKpYMhX{0FGH2N{Z(Py^nE} zZ!Itt1y|v20H;TJT3C&FV+8GX|JU8uY7xVSIua@e->z$QP`O?QYCV~$cgu%UJa_Kx< zJd@{Y4bE-&^bt(cx^3IjhUM)P{zkEKzwpndwR*>yI+nj#tZaSR_~AeJ>FgFxce-5n c8nEA4FYc}v{#vp6xLAEU+v(Ufbq}Qe0>R%c2><{9 literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..2964ed2 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import account_journal +from . import account_payment +from . import pos_payment_method +from . import pos_session diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f2956d43515da023915fb6daa4e23cdd37b414c GIT binary patch literal 274 zcmX@j%ge<81blCPXZ8c>#~=<2FhLogO@NH)3@HpLj5!Rsj8Tk?AT|?_%@oB1W-|lX z%u&o>HVcr=62-#EP|2#v_L32(Rg>`+e`0cSeraAwd{%yGQC?yWvQR-{Wo~L-2~bF= zAip>s%8k!WEy>7F0g7^iM2l04i!<}{{4`l_F%&Ta4J~2;5v(AB4M?nH_zWZ&ehKB~ zr=;c->&M4u=4F<|$LkeT{^GC!3YO-i+7)pFm4IASEC?h%Ff%eTK4p--3qlI_7$l20 GfuaEY(?om# literal 0 HcmV?d00001 diff --git a/models/__pycache__/account_journal.cpython-312.pyc b/models/__pycache__/account_journal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..373640ca131f98e65565efeab753682ea139371e GIT binary patch literal 1619 zcmah}&2Jnv6!(1W>}0dMLn(<^D8(vOb_b=4gv5;^kxHd0GzwMK!}Me19XnmO^Rakl zsdi31^beq?&=YV%DEUvgfVhl&LgE4vhe%n3R4&*v9ug^=VrgtYzu$ZB=l99&?skQ~QlayWTPHlg$78}s=^6#u6p5fHepK^2 zL1UPoRX#T)tWLu9JA=Lo>QmfbhyBZ9s<`?`3U?h2`q^wkB!b3b9`t26DF!GV_g(ki z)qE&f9w$@hhLRH#)TQayd z9Zlm0D26vb6>Q3yFD?Si5{Su&W^izm1cZKxMj?SaEQ#QVp*R@3L)R!dvbG>oF*>(V zs^DS*TmM6#@>_R-BoHuyC48qttuGkI7Qz#VS6ZEpL1j3VOuyyg4i|DJ|&-}e14 zMA4PT_Gv8Ifo4qpAH2DdBq2dD@6yx{j#`@AH$?BfpTvZpIZ@;Pj#Sk|Kk)MkMP;_@ zc2GGj_0@eu^o9B#z@eZ6Ui7MG>Al)gMHo(I%$A9jlO?|NxtN}U!huh&{krnbSGRZX zKf7Nz{h|}h8^)^jWb42Ny}{SkH_mgXuvZG_?V|JfylM8VpUjnm)-heLa4r_konrOP zqH}q^Y*mOZSo`4H+u!@&`GqYImrAuiUuvug2Nw?7i`4f@_0Q%7)~mU0xlL{+I7yg% z=*pZk`G_%j3EsFNAK~g|uglZY?hUbfVSiW^@FVt?5H<0oa&us4n)bH^w81Z+Kex_n Qmbm)eXLtVsVq2R33mQGoivR!s 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..2eadbf9b9c516be487041f057783cb1beb5635b4 GIT binary patch literal 4770 zcmcIoU2GHC6}~eb+p%LiAvR9Pk27qDg9|v2T?isPjp44vQOJSm3>;OJ~V@?jyt6iQY%%J`d~IxU2R`_?s&#F zWU^an+mSQ(-gD1AbM86kp6}fJ*6Vd6cwYO*zmusJg#JPX=3_THYwts61qn!?B$TFN zRGN;_XutgkyCxZy1Inr4{lw{43NQ#mmYc@WY)F>`OX|5m2B0h$37Ei@Z8KgDD zptUFrK7fsgC`Jh=MhjHTwu};#KwoW&F#@CaZC70}`!c#h#T-9C5xeFai^sF`nfyt9 zAuVR|g%-X#j^?U_xUnDoi<^wkt<8b$eoC#NB1$#R>r4pjcoms;Sfq;7QFQ(YLW^{f zPSFNSLz7|}21XZC*E{o0lb&~(J%KjmZXiqdw#i*>3l3D%=EoF+@*Z>kT2m4Iqz7gV z!lI4ndV@ay#y4nd;q^sk18v&OFV)*c`v$y$I*-{~bmVJsliFfZ>#bk|Sr$76YG<9x z>{)U$mQ=eX)sgBpW!2mJU|sT1sA$J@(IM}!^c(YC{RWX1s(z{QE!c@!Qd=8o)qb(| z#Z=$=S-}A|+P0ovWKv-Zs@;m(5S7k`-F+v2Bxn+?LX*%0`fXpY$rVcOpisk?Yx%}H z5{R^-!%BPL|K57}K;5~XYPsIhQiA&j!vKZ#rB73B2kOO42YZT^*P4rRG7_bvndm6-dPmoUGu%%kn<>m^O z&j{?hCpOT_V}g1*dr8#nM9GChw}rC$hqw335lo`2(7pi+)D29APm7w{^cgtWE{HS9 zyyga9=QAP(zhfT=iP-UGXfQbsnj?-yA(_V=grd(nG#62d%P-_ajS+Y-GNEGv(wYs6 z39U&=W<-!C7lOu~-+~3~N$|WV5QQThT2mq^!Aeo6t_#iRKI4+aXJ*jVRmG&Tz$CVH z)ik?2pA)gx+z?ZkgitMR8^SyU+asa8foRRiN!i(1kj5l4;CZqajfq1|r`g2JCCzg= znV&Pyfo=}6<1WHQgTV;EY7Rb4Dl*+jTALsy_<1SM)zy!+zmv~v`IGI#otnn;iQVU`NyoJuO7fQ&+ zqe;9dCn1);T0&2I`|d2>Ui_m+ShLZrd$p;%;zk`izeJ3yZF#)x?^ON$ioaj=A1(Qh zJ_+_d32a^so+$;+uAZAw&!v@fY4sdd;1|FkpuMev=uK@EJFI^0LoI=8vx;wP$=A1R zt2jXH8rtDHMuFU(ZEDY;(le;`j43^1%TwzK?10J+DeRESzN4`3ta^I>>hXU#d2O=X znkcunuXt{HZq2Hp-AZV;8rrLb_C9!D9iC8zC(5lMwY5)a?OW~Nr}mF1{UgtvjMw+f zg95K8kpt!y&Xxl2txm_)>5MX+QKw}EegU~*3qI7&6EqCb5li>rGkN`ogJ&HhgMjie05%Hd%(d_V~wC~w_< zFLXCl4)0RK`<3v1H9Vq(N6O*X)bOwp9)8yB=nhtpV{?1Ohl1_jA`=#kE8%f9JfVaq z%CAK3Mej!c3(1>WdfJ|OgJ&t<8VYu`ET5=Ek+(zj^eLXc-+TI>RQfQ6l(P^yD@>rxOpdnJ|fo(GG03ca6>LFc|z5nYj(H*z*VM56=@TC z;SykpkSPl+#7n^2WJ;^I8%VGKe1fLZ6L+sx?&}z$OzOy_}cZDQJ~O;%Wg9gqmC*OH)cT{t(VJTk>oB;M-&}4 z%H{4cFfnfxV)PVmn-%kz)V!FK!?+h-5eMD{jpo0MlX;QLCK8-3i^$NtkksQM zH^WO%6^M8@VRY4|@m?6qBvN#H7+Dg0j0(}0vgEfYJ4pmxhZkhd=WZjv?Ook>{L$+r z-+Rlpa&yavhprt`n;d+}==#)BiMj1L&@}DV3hlm6m2++DZc bEV=uaoX;Io6y?8hdS&|N^gj{l>tg={U>6fa literal 0 HcmV?d00001 diff --git a/models/__pycache__/pos_payment_method.cpython-312.pyc b/models/__pycache__/pos_payment_method.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c5dee852aa786c29aaf05efc9fb2f9d84930c7c GIT binary patch literal 2225 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 OZO1Lg?+6?hnEf9m`k!3@ literal 0 HcmV?d00001 diff --git a/models/account_journal.py b/models/account_journal.py new file mode 100644 index 0000000..2941df9 --- /dev/null +++ b/models/account_journal.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + is_centralized = fields.Boolean( + string='Is Centralized Payment', + help="If checked, payments made using this journal will be recorded in the parent company." + ) + parent_company_id = fields.Many2one( + 'res.company', + string='Parent Company', + help="The parent company where the actual bank payment will be recorded." + ) + parent_journal_id = fields.Many2one( + 'account.journal', + string='Parent Bank Journal', + domain="[('company_id', '=', parent_company_id), ('type', 'in', ('bank', 'cash'))]", + check_company=False, + help="The actual bank journal in the parent company." + ) + parent_intercompany_account_id = fields.Many2one( + 'account.account', + string='Parent Inter-company Account', + check_company=False, + help="The Hubungan RK account in the parent company to debit (Receivable from branch)." + ) + branch_intercompany_account_id = fields.Many2one( + 'account.account', + string='Branch Inter-company Account', + check_company=False, + help="The Hubungan RK account in the branch company to credit (Liability to parent)." + ) diff --git a/models/account_payment.py b/models/account_payment.py new file mode 100644 index 0000000..8472a97 --- /dev/null +++ b/models/account_payment.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class AccountPayment(models.Model): + _inherit = 'account.payment' + + def action_post(self): + # 1. Capture info before super() because we might need to create extra moves + centralized_payments = self.filtered( + lambda p: p.journal_id.is_centralized and p.company_id != p.journal_id.parent_company_id + ) + + # 2. Validate centralized payments before posting + for payment in centralized_payments: + if not payment.journal_id.branch_intercompany_account_id: + raise UserError( + _("Please configure the Branch Inter-company Account on journal %s") % payment.journal_id.name + ) + + # 3. Call super() to post the original payment(s) + res = super().action_post() + + # 4. Handle Centralized entries in Parent + for payment in centralized_payments: + parent_company = payment.journal_id.parent_company_id + parent_journal = payment.journal_id.parent_journal_id + parent_rk_account = payment.journal_id.parent_intercompany_account_id + + _logger.info( + "Centralized Payment for %s | Company: %s -> Parent: %s | Parent Journal: %s | RK: %s", + payment.name, payment.company_id.name, parent_company.name, + parent_journal.name, parent_rk_account.code + ) + + if not parent_journal or not parent_rk_account: + raise UserError( + _("Please configure the Parent Journal and RK Account on journal %s") % payment.journal_id.name + ) + + # Create the mirroring entry in Parent + # Debit: Parent RK (Receivable from Branch) + # Credit: Bank + parent_move = self.env['account.move'].with_company(parent_company).create({ + 'move_type': 'entry', + 'date': payment.date, + 'company_id': parent_company.id, + 'journal_id': parent_journal.id, + 'ref': _("Centralized Payment: %s (%s)") % (payment.name, payment.company_id.name), + 'line_ids': [ + (0, 0, { + 'name': payment.ref or _("Centralized Payment: %s") % payment.name, + 'account_id': parent_rk_account.id, + 'debit': payment.amount, + 'partner_id': payment.partner_id.id, + 'company_id': parent_company.id, + }), + (0, 0, { + 'name': payment.ref or _("Centralized Payment: %s") % payment.name, + 'account_id': parent_journal.default_account_id.id, + 'credit': payment.amount, + 'partner_id': payment.partner_id.id, + 'company_id': parent_company.id, + }), + ] + }) + parent_move.action_post() + _logger.info("Parent Move Posted: %s", parent_move.name) + + return res + + def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None): + """ + Override to use the Branch RK account for the liquidity line + when the payment is centralized. + """ + res = super()._prepare_move_line_default_vals(write_off_line_vals, force_balance) + if self.journal_id.is_centralized and self.company_id != self.journal_id.parent_company_id: + # Update the account if it matches the journal's default (liquidity account). + for line_vals in res: + if line_vals.get('account_id') == self.journal_id.default_account_id.id: + line_vals['account_id'] = self.journal_id.branch_intercompany_account_id.id + return res diff --git a/models/pos_payment_method.py b/models/pos_payment_method.py new file mode 100644 index 0000000..ac12b2f --- /dev/null +++ b/models/pos_payment_method.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields + + +class PosPaymentMethod(models.Model): + _inherit = 'pos.payment.method' + + # --- Branch-side clearing configuration --- + intercompany_clearing_account_id = fields.Many2one( + 'account.account', + string='Inter-Company Clearing Account', + domain="[('account_type', 'in', ['asset_receivable', 'liability_payable', 'asset_current'])]", + help="If specified, an automatic clearing entry will be generated when a POS session closes. " + "This is used to transfer the balance from a shared parent bank account to an inter-company account." + ) + + intercompany_clearing_journal_id = fields.Many2one( + 'account.journal', + string='Clearing Journal', + domain="[('type', '=', 'general')]", + help="Journal to use for the automated inter-company clearing entries." + ) + + # --- Parent-side mirror entry configuration --- + parent_company_id = fields.Many2one( + 'res.company', + string='Parent Company', + help="The parent company where the mirror clearing entry will be created. " + "Leave empty to auto-detect from the branch company's parent." + ) + + parent_bank_journal_id = fields.Many2one( + 'account.journal', + string='Parent Bank Journal', + domain="[('type', '=', 'bank')]", + help="The bank journal in the parent company. Its outstanding receipt account will be debited " + "in the mirror entry, allowing reconciliation with the parent's bank statement." + ) + + parent_intercompany_account_id = fields.Many2one( + 'account.account', + string='Parent Hubungan RK Account', + domain="[('reconcile', '=', True)]", + help="The Hubungan RK liability account (e.g., 229101) in the parent company to credit. " + "This represents the parent's liability to the branch." + ) + + parent_clearing_journal_id = fields.Many2one( + 'account.journal', + string='Parent Clearing Journal', + domain="[('type', '=', 'general')]", + help="Journal in the parent company to use for the mirror clearing entry." + ) diff --git a/models/pos_session.py b/models/pos_session.py new file mode 100644 index 0000000..efb9105 --- /dev/null +++ b/models/pos_session.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, Command, _ +import logging + +_logger = logging.getLogger(__name__) + + +class PosSession(models.Model): + _inherit = 'pos.session' + + def _validate_session(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None): + res = super(PosSession, self)._validate_session(balancing_account, amount_to_balance, bank_payment_method_diffs) + + # After the standard validation and account move creation, we create the inter-company clearing moves + self._create_intercompany_clearing_moves() + + return res + + def _create_bank_payment_moves(self, data): + """Override to skip account.payment creation for intercompany payment methods.""" + intercompany_pms = self.payment_method_ids.filtered( + lambda pm: pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id + ) + + if not intercompany_pms: + return super()._create_bank_payment_moves(data) + + combine_receivables_bank = data.get('combine_receivables_bank', {}) + MoveLine = data.get('MoveLine') + + # Split the data into intercompany and standard + standard_combine = {} + intercompany_combine = {} + + for pm, amounts in combine_receivables_bank.items(): + if pm in intercompany_pms: + intercompany_combine[pm] = amounts + else: + standard_combine[pm] = amounts + + # Call super with only standard payments + data['combine_receivables_bank'] = standard_combine + res_data = super()._create_bank_payment_moves(data) + + # Restore original data + data['combine_receivables_bank'] = combine_receivables_bank + + # Manually handle intercompany ones: create the line in main move but skip account.payment + for pm, amounts in intercompany_combine.items(): + combine_receivable_line = MoveLine.create( + self._get_combine_receivable_vals(pm, amounts['amount'], amounts['amount_converted']) + ) + res_data['payment_method_to_receivable_lines'][pm] = combine_receivable_line + + return res_data + + def _create_intercompany_clearing_moves(self): + for session in self: + if session.state != 'closed' or not session.move_id: + continue + + # Dictionary to accumulate amounts per payment method + clearing_amounts = {} + + # Find all orders and payments for this session + orders = session.order_ids + + for order in orders: + for payment in order.payment_ids: + pm = payment.payment_method_id + if pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id: + if pm not in clearing_amounts: + clearing_amounts[pm] = 0.0 + clearing_amounts[pm] += payment.amount + + # Group PMs by their clearing journal + journal_to_pms = {} + for pm, amount in clearing_amounts.items(): + if session.currency_id.is_zero(amount): + continue + journal = pm.intercompany_clearing_journal_id + if journal not in journal_to_pms: + journal_to_pms[journal] = [] + journal_to_pms[journal].append(pm) + + for clearing_journal, pms in journal_to_pms.items(): + aggregated_data = {} # Key: (receivable_account, intercompany_account) + pm_level_data = [] # For parent mirror entries + + for pm in pms: + amount = clearing_amounts[pm] + receivable_account = self._get_receivable_account(pm) + if not receivable_account: + continue + + intercompany_account = pm.intercompany_clearing_account_id + + # Convert amount to company currency if needed + amount_company_curr = amount + if session.currency_id != session.company_id.currency_id: + amount_company_curr = session.currency_id._convert( + amount, session.company_id.currency_id, session.company_id, + session.stop_at or fields.Date.context_today(session) + ) + + # Store PM level data for parent mirror + pm_level_data.append({ + 'pm': pm, + 'amount': amount, + 'amount_company_curr': amount_company_curr, + }) + + # Aggregate for branch move + key = (receivable_account, intercompany_account) + if key not in aggregated_data: + aggregated_data[key] = { + 'total_amount': 0.0, + 'total_company_curr': 0.0, + 'pms': [], + 'receivable_account': receivable_account, + 'intercompany_account': intercompany_account, + } + aggregated_data[key]['total_amount'] += amount + aggregated_data[key]['total_company_curr'] += amount_company_curr + aggregated_data[key]['pms'].append(pm) + + if not aggregated_data: + continue + + line_ids = [] + for key, agg_data in aggregated_data.items(): + # CREDIT: Total AR in Transit + line_ids.append(Command.create({ + 'name': _("Total Clearing - %s") % session.name, + 'account_id': agg_data['receivable_account'].id, + 'credit': agg_data['total_company_curr'], + 'debit': 0.0, + 'currency_id': session.currency_id.id, + 'amount_currency': -agg_data['total_amount'], + })) + + # DEBIT: Total Hubungan RK + line_ids.append(Command.create({ + 'name': _("Total Due from Parent - %s") % session.name, + 'account_id': agg_data['intercompany_account'].id, + 'credit': 0.0, + 'debit': agg_data['total_company_curr'], + 'currency_id': session.currency_id.id, + 'amount_currency': agg_data['total_amount'], + })) + + # --- BRANCH SIDE: Aggregated Clearing Move (Target: 2 items) --- + move_vals = { + 'journal_id': clearing_journal.id, + 'date': session.stop_at or fields.Date.context_today(session), + 'ref': _("Inter-company clearing for %s") % session.name, + 'move_type': 'entry', + 'company_id': session.company_id.id, + 'line_ids': line_ids, + } + + try: + clearing_move = self.env['account.move'].sudo().with_company(session.company_id).create(move_vals) + clearing_move._post() + + # 1. Reconcile aggregated lines with session move + for key, agg_data in aggregated_data.items(): + receivable_account = agg_data['receivable_account'] + pm_list = agg_data['pms'] + + try: + # Find the aggregated credit line + clearing_credit_line = clearing_move.line_ids.filtered( + lambda l: l.account_id == receivable_account and l.credit > 0 + ) + # Find all matching debit lines in the session move + # Odoo 17 names these lines as "SessionName - PMName" + pos_debit_lines = session.move_id.line_ids.filtered( + lambda l: l.account_id == receivable_account and l.debit > 0 and + any(l.name.endswith(" - %s" % pm.name) for pm in pm_list) + ) + + if clearing_credit_line and pos_debit_lines: + (clearing_credit_line + pos_debit_lines).reconcile() + except Exception as re_e: + _logger.warning( + "Could not auto-reconcile aggregated clearing lines for session %s: %s", + session.name, re_e + ) + + # 2. Create parent mirror move + self._create_aggregated_parent_mirror_move(session, pm_level_data, clearing_move) + + except Exception as e: + _logger.error( + "Failed to create/post aggregated inter-company clearing move for session %s: %s", + session.name, e + ) + + def _get_related_account_moves(self): + res = super()._get_related_account_moves() + for session in self: + clearing_moves = self.env['account.move'].sudo().search([ + ('company_id', '=', session.company_id.id), + ('ref', '=', _("Inter-company clearing for %s") % session.name) + ]) + res |= clearing_moves + return res + + def _create_aggregated_parent_mirror_move(self, session, pm_level_data, branch_clearing_move): + """Create a single mirror journal entry in the parent company, with separate lines per PM. + + For each PM: + Debit: Outstanding receipt account of the parent bank journal + Credit: Hubungan RK liability (229101) in the parent + """ + parent_groups = {} + for data in pm_level_data: + pm = data['pm'] + parent_company = pm.parent_company_id or session.company_id.parent_id + if not parent_company: + _logger.info("No parent company configured for PM %s, skipping parent mirror entry.", pm.name) + continue + + parent_bank_journal = pm.parent_bank_journal_id + parent_rk_account = pm.parent_intercompany_account_id + parent_clearing_journal = pm.parent_clearing_journal_id + + if not parent_bank_journal or not parent_rk_account or not parent_clearing_journal: + _logger.warning("Parent clearing not fully configured for PM %s. Skipping.", pm.name) + continue + + outstanding_receipt_account = None + for pml in parent_bank_journal.inbound_payment_method_line_ids: + if pml.payment_account_id: + outstanding_receipt_account = pml.payment_account_id + break + + if not outstanding_receipt_account: + _logger.warning( + "No outstanding receipt account found on parent bank journal %s. Skipping PM %s.", + parent_bank_journal.name, pm.name + ) + continue + + group_key = (parent_company.id, parent_clearing_journal.id) + if group_key not in parent_groups: + parent_groups[group_key] = { + 'parent_company': parent_company, + 'parent_clearing_journal': parent_clearing_journal, + 'lines_data': [] + } + + parent_groups[group_key]['lines_data'].append({ + 'pm': pm, + 'amount': data['amount'], + 'amount_company_curr': data['amount_company_curr'], + 'outstanding_receipt_account': outstanding_receipt_account, + 'parent_rk_account': parent_rk_account, + }) + + entry_date = session.stop_at or fields.Date.context_today(session) + + for group_key, group_data in parent_groups.items(): + parent_company = group_data['parent_company'] + parent_clearing_journal = group_data['parent_clearing_journal'] + + line_ids = [] + + for line_data in group_data['lines_data']: + pm = line_data['pm'] + amount = line_data['amount'] + + parent_currency = parent_company.currency_id + if session.currency_id != parent_currency: + amount_parent_curr = session.currency_id._convert( + amount, parent_currency, parent_company, entry_date + ) + else: + amount_parent_curr = amount + + line_ids.append(Command.create({ + 'name': _("POS Receipt: %s (%s)") % (pm.name, session.company_id.name), + 'account_id': line_data['outstanding_receipt_account'].id, + 'debit': amount_parent_curr, + 'credit': 0.0, + 'currency_id': session.currency_id.id, + 'amount_currency': amount, + })) + + line_ids.append(Command.create({ + 'name': _("Due to Branch: %s (%s)") % (pm.name, session.company_id.name), + 'account_id': line_data['parent_rk_account'].id, + 'debit': 0.0, + 'credit': amount_parent_curr, + 'currency_id': session.currency_id.id, + 'amount_currency': -amount, + })) + + if not line_ids: + continue + + parent_move_vals = { + 'journal_id': parent_clearing_journal.id, + 'date': entry_date, + 'ref': _("POS Mirror: %s - %s") % (session.name, session.company_id.name), + 'move_type': 'entry', + 'company_id': parent_company.id, + 'line_ids': line_ids, + } + + try: + parent_move = self.env['account.move'].sudo().with_company(parent_company).create(parent_move_vals) + parent_move._post() + _logger.info( + "Created aggregated parent mirror entry %s in company %s for session %s (Lines: %s)", + parent_move.name, parent_company.name, session.name, len(line_ids) + ) + except Exception as e: + _logger.error( + "Failed to create aggregated parent mirror entry for session %s in company %s: %s", + session.name, parent_company.name, e + ) diff --git a/views/account_journal_views.xml b/views/account_journal_views.xml new file mode 100644 index 0000000..feb4959 --- /dev/null +++ b/views/account_journal_views.xml @@ -0,0 +1,31 @@ + + + + account.journal.form.inherit.shared.bank + account.journal + + + + + + + + + + + + + + + + + + + + + + diff --git a/views/pos_payment_method_views.xml b/views/pos_payment_method_views.xml new file mode 100644 index 0000000..816ef2f --- /dev/null +++ b/views/pos_payment_method_views.xml @@ -0,0 +1,31 @@ + + + + pos.payment.method.form.inherit.shared.bank + pos.payment.method + + + + + + + + + + + + + + + + + + + + + diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..353adbd --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import account_payment_register diff --git a/wizard/__pycache__/__init__.cpython-312.pyc b/wizard/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffe982a5cef17b41bdb20b65a014fdba982d1350 GIT binary patch literal 167 zcmX@j%ge<81VwLtXW9el#~=<2FhLog1%Qm{3@HpLj5!Rsj8Tk?43$ip%r6;%!kUb? zBodR8^Gowe;tLWhb5ntIQEGZ-aY<^CpC;2Sh9YL5s+A0%L1z6DD$lG+EK1RjkI&4@ qEQycTE2#X%VUwGmQks)$SHuBS0y480#Q4C>$jJDNL9K`l$N>OMohTCk literal 0 HcmV?d00001 diff --git a/wizard/__pycache__/account_payment_register.cpython-312.pyc b/wizard/__pycache__/account_payment_register.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ed0400c1dabbd5f1397cc3b9b031d5e35419735 GIT binary patch literal 6165 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