first commit

This commit is contained in:
Suherdy Yacob 2026-06-09 17:03:33 +07:00
commit bd8af2382a
13 changed files with 378 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.pyc
*.pyo
*~
__pycache__/

15
README.md Normal file
View File

@ -0,0 +1,15 @@
Account Shared Bank Cash Auto Entry
===================================
This module automatically creates and posts cash centralization entries between branch companies and the parent company based on the net cash movement of the previous day.
Key Features:
* Run automatically every day at 10:00 AM WIB (03:00 AM UTC).
* Calculates net cash balance of the default cash account (111103) from the previous day.
* Accounts for vendor payments (from the vendor_payment_misc_auto_entry module) and other cash movements.
* Automatically converts currencies if the parent and branch company currencies are different.
* Generates and posts the corresponding intercompany journal entries in both companies.
Configuration:
* Relies on the configuration set in the parent module "account_shared_bank_cash".
* Configure the Parent Company, Parent Journal, and Intercompany accounts on the branch company's cash journal.

4
__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

18
__manifest__.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
{
'name': 'Account Shared Bank Cash Auto Entry',
'version': '1.0',
'category': 'Accounting',
'summary': 'Automatically generate cash centralization journal entries next day at 10:00 AM WIB',
'author': 'Suherdy Yacob',
'depends': ['account_shared_bank_cash'],
'data': [
'security/ir.model.access.csv',
'wizard/account_cash_centralization_wizard_views.xml',
'views/account_journal_views.xml',
'data/ir_cron.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

14
data/ir_cron.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="ir_cron_cash_intercompany_centralization" model="ir.cron">
<field name="name">Account Cash Shared Centralization Auto Entry</field>
<field name="model_id" ref="account.model_account_journal"/>
<field name="state">code</field>
<field name="code">model._cron_create_cash_intercompany_entries()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 03:00:00')"/>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
</record>
</odoo>

3
models/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_move
from . import account_journal

210
models/account_journal.py Normal file
View File

@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
import logging
import pytz
from datetime import datetime, timedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class AccountJournal(models.Model):
_inherit = 'account.journal'
@api.model
def _cron_create_cash_intercompany_entries(self, target_date=None, journal_ids=None):
"""Cron action to automatically generate cash centralization entries.
Runs daily at 10:00 AM WIB (03:00 AM UTC) to process the previous day's net cash movements.
"""
# Determine the target date (previous day in WIB by default)
if not target_date:
jakarta_tz = pytz.timezone('Asia/Jakarta')
now_jakarta = datetime.now(jakarta_tz)
prev_day = (now_jakarta - timedelta(days=1)).date()
elif isinstance(target_date, str):
prev_day = fields.Date.from_string(target_date)
else:
prev_day = target_date
_logger.info("Starting cash intercompany centralization for date %s", prev_day)
# Search for all cash journals that have centralization configured
domain = [
('type', '=', 'cash'),
('is_centralized', '=', True)
]
if journal_ids:
domain.append(('id', 'in', journal_ids))
journals = self.sudo().search(domain)
results = []
for journal in journals:
# Check basic configuration requirements
parent_company = journal.parent_company_id
parent_journal = journal.parent_journal_id
branch_rk_account = journal.branch_intercompany_account_id
parent_rk_account = journal.parent_intercompany_account_id
if not parent_company or not parent_journal or not branch_rk_account or not parent_rk_account:
msg = _("Centralization settings are incomplete (Parent Company, Parent Journal, or RK accounts not configured).")
_logger.warning("Skipping cash journal %s (%s): %s", journal.name, journal.company_id.name, msg)
results.append((journal.id, 'incomplete', msg))
continue
# Ensure default cash account is present
branch_cash_account = journal.default_account_id
if not branch_cash_account:
msg = _("Journal has no default account configured.")
_logger.warning("Skipping cash journal %s (%s): %s", journal.name, journal.company_id.name, msg)
results.append((journal.id, 'no_account', msg))
continue
# Check for existing intercompany transfer entries for this day to prevent duplication
existing_move = self.env['account.move'].sudo().search([
('journal_id', '=', journal.id),
('date', '=', prev_day),
('is_cash_intercompany_transfer', '=', True),
('state', '!=', 'cancel')
], limit=1)
if existing_move:
msg = _("Centralization already run for this date. (Move: %s)") % existing_move.name
_logger.info("Skipping cash journal %s (%s): %s", journal.name, journal.company_id.name, msg)
results.append((journal.id, 'already_run', msg))
continue
# Find all posted journal items on the branch cash account for the target date,
# excluding centralization transfers we generate.
lines = self.env['account.move.line'].sudo().search([
('company_id', '=', journal.company_id.id),
('account_id', '=', branch_cash_account.id),
('date', '=', prev_day),
('parent_state', '=', 'posted'),
('move_id.is_cash_intercompany_transfer', '=', False)
])
# Calculate net movement (debit - credit)
debit_sum = sum(lines.mapped('debit'))
credit_sum = sum(lines.mapped('credit'))
net_balance = debit_sum - credit_sum
_logger.info(
"Journal %s (%s) net movement on %s: Debits=%s, Credits=%s, Net=%s",
journal.name, journal.company_id.name, prev_day, debit_sum, credit_sum, net_balance
)
if net_balance <= 0.0:
msg = _("No positive net cash movement (Net Balance: %s).") % net_balance
_logger.info("Skipping journal %s on %s: %s", journal.name, prev_day, msg)
results.append((journal.id, 'no_balance', msg))
continue
# Identify the parent cash account ( Kas Besar Hasil Penjualan - 111103)
# Search by code 111103 with the parent company's context
parent_cash_account = self.env['account.account'].sudo().with_company(parent_company).search([
('code', '=', '111103'),
('company_ids', 'in', [parent_company.id])
], limit=1)
if not parent_cash_account:
parent_cash_account = parent_journal.default_account_id
if not parent_cash_account:
msg = _("Parent cash account (111103 or default account of parent journal) not found in company %s.") % parent_company.name
_logger.error("Skipping journal %s: %s", journal.name, msg)
results.append((journal.id, 'no_parent_account', msg))
continue
# Handle currency conversion if parent and branch currencies differ
parent_currency = parent_company.currency_id
branch_currency = journal.company_id.currency_id
if branch_currency != parent_currency:
amount_parent_curr = branch_currency._convert(
net_balance, parent_currency, parent_company, prev_day
)
else:
amount_parent_curr = net_balance
# 1. Create the Branch company journal entry
branch_line_ids = [
(0, 0, {
'name': _("Cash Centralization - %s") % prev_day,
'account_id': branch_rk_account.id,
'debit': net_balance,
'credit': 0.0,
'company_id': journal.company_id.id,
'display_type': 'product',
}),
(0, 0, {
'name': _("Cash Centralization - %s") % prev_day,
'account_id': branch_cash_account.id,
'debit': 0.0,
'credit': net_balance,
'company_id': journal.company_id.id,
'display_type': 'product',
})
]
# 2. Create the Parent company journal entry
parent_line_ids = [
(0, 0, {
'name': _("Cash Centralization: %s - %s") % (journal.company_id.name, prev_day),
'account_id': parent_cash_account.id,
'debit': amount_parent_curr,
'credit': 0.0,
'partner_id': journal.company_id.partner_id.id,
'company_id': parent_company.id,
'display_type': 'product',
}),
(0, 0, {
'name': _("Cash Centralization: %s - %s") % (journal.company_id.name, prev_day),
'account_id': parent_rk_account.id,
'debit': 0.0,
'credit': amount_parent_curr,
'partner_id': journal.company_id.partner_id.id,
'company_id': parent_company.id,
'display_type': 'product',
})
]
try:
# Create and post branch entry
branch_move = self.env['account.move'].sudo().with_company(journal.company_id).create({
'move_type': 'entry',
'date': prev_day,
'company_id': journal.company_id.id,
'journal_id': journal.id,
'ref': f"Cash Centralization - {prev_day}",
'is_cash_intercompany_transfer': True,
'line_ids': branch_line_ids,
})
branch_move.action_post()
# Create and post parent entry
parent_move = self.env['account.move'].sudo().with_company(parent_company).create({
'move_type': 'entry',
'date': prev_day,
'company_id': parent_company.id,
'journal_id': parent_journal.id,
'partner_id': journal.company_id.partner_id.id,
'ref': f"Cash Centralization - {prev_day} ({journal.company_id.name})",
'is_cash_intercompany_transfer': True,
'line_ids': parent_line_ids,
})
parent_move.action_post()
msg = _("Successfully created branch entry %s and parent entry %s.") % (branch_move.name, parent_move.name)
_logger.info("Successfully centralized cash for %s: %s", journal.name, msg)
results.append((journal.id, 'success', msg))
except Exception as e:
msg = str(e)
_logger.error(
"Failed to create cash centralization entries for journal %s on %s: %s",
journal.name, prev_day, msg
)
results.append((journal.id, 'error', msg))
return results

12
models/account_move.py Normal file
View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class AccountMove(models.Model):
_inherit = 'account.move'
is_cash_intercompany_transfer = fields.Boolean(
string='Is Cash Intercompany Transfer',
copy=False,
default=False,
help='Indicates if this journal entry was automatically created for cash intercompany centralization.'
)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_cash_centralization_wizard_user,account.cash.centralization.wizard.user,model_account_cash_centralization_wizard,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_cash_centralization_wizard_user account.cash.centralization.wizard.user model_account_cash_centralization_wizard account.group_account_user 1 1 1 1

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_journal_form_inherit_auto_entry" model="ir.ui.view">
<field name="name">account.journal.form.inherit.auto.entry</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account_shared_bank_cash.view_account_journal_form_inherit_centralized"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='centralized_payment']/group" position="after">
<group invisible="type != 'cash'">
<button name="%(action_account_cash_centralization_wizard)d"
type="action"
string="Run Centralization Manually"
class="btn-primary"/>
</group>
</xpath>
</field>
</record>
</odoo>

2
wizard/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import account_cash_centralization_wizard

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AccountCashCentralizationWizard(models.TransientModel):
_name = 'account.cash.centralization.wizard'
_description = 'Run Cash Centralization Manually'
date = fields.Date(
string='Target Date',
required=True,
default=lambda self: fields.Date.context_today(self)
)
def action_run_centralization(self):
self.ensure_one()
active_id = self.env.context.get('active_id')
if not active_id:
raise UserError(_("No active journal found in context."))
journal = self.env['account.journal'].browse(active_id)
if journal.type != 'cash' or not journal.is_centralized:
raise UserError(_("This action is only supported for centralized cash journals."))
# Call the centralization method for this specific journal and date
results = self.env['account.journal']._cron_create_cash_intercompany_entries(
target_date=self.date,
journal_ids=[journal.id]
)
if not results:
raise UserError(_("No results returned. Please check the logs or ensure centralization configuration is correct."))
journal_id, status, message = results[0]
if status == 'success':
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': message,
'type': 'success',
'sticky': False,
'next': {
'type': 'ir.actions.client',
'tag': 'reload',
},
}
}
else:
raise UserError(message)

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_cash_centralization_wizard_form" model="ir.ui.view">
<field name="name">account.cash.centralization.wizard.form</field>
<field name="model">account.cash.centralization.wizard</field>
<field name="arch" type="xml">
<form string="Run Cash Centralization Manually">
<group>
<field name="date"/>
</group>
<footer>
<button string="Run Centralization" name="action_run_centralization" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
<record id="action_account_cash_centralization_wizard" model="ir.actions.act_window">
<field name="name">Run Cash Centralization Manually</field>
<field name="res_model">account.cash.centralization.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>