first commit
This commit is contained in:
commit
bd8af2382a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
__pycache__/
|
||||
15
README.md
Normal file
15
README.md
Normal 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
4
__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
18
__manifest__.py
Normal file
18
__manifest__.py
Normal 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
14
data/ir_cron.xml
Normal 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
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import account_move
|
||||
from . import account_journal
|
||||
210
models/account_journal.py
Normal file
210
models/account_journal.py
Normal 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
12
models/account_move.py
Normal 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.'
|
||||
)
|
||||
2
security/ir.model.access.csv
Normal file
2
security/ir.model.access.csv
Normal 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
|
||||
|
18
views/account_journal_views.xml
Normal file
18
views/account_journal_views.xml
Normal 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
2
wizard/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import account_cash_centralization_wizard
|
||||
51
wizard/account_cash_centralization_wizard.py
Normal file
51
wizard/account_cash_centralization_wizard.py
Normal 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)
|
||||
25
wizard/account_cash_centralization_wizard_views.xml
Normal file
25
wizard/account_cash_centralization_wizard_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user