diff --git a/modules/account/__init__.py b/modules/account/__init__.py
index d3f280d..3e8d0f2 100755
--- a/modules/account/__init__.py
+++ b/modules/account/__init__.py
@@ -30,6 +30,8 @@ def register():
account.GeneralLedgerAccountParty,
account.GeneralLedgerLine,
account.GeneralLedgerLineContext,
+ account.GeneralLedgerDetail,
+ account.GeneralLedgerDetailContext,
account.BalanceSheetContext,
account.BalanceSheetComparisionContext,
account.IncomeStatementContext,
diff --git a/modules/account/account.py b/modules/account/account.py
index 27a7177..f24a9ac 100755
--- a/modules/account/account.py
+++ b/modules/account/account.py
@@ -7,7 +7,7 @@ from decimal import Decimal
from itertools import zip_longest
from dateutil.relativedelta import relativedelta
-from sql import Column, Literal, Null, Window
+from sql import Column, Literal, Null, Union, Window
from sql.aggregate import Count, Max, Min, Sum
from sql.conditionals import Case, Coalesce
@@ -2583,6 +2583,444 @@ class GeneralLedgerLineContext(GeneralLedgerAccountContext):
return super().get_context(fields_names=fields_names)
+class GeneralLedgerDetailContext(GeneralLedgerAccountContext):
+ 'General Ledger Detail Context'
+ __name__ = 'account.general_ledger.detail.context'
+
+ account = fields.Many2One(
+ 'account.account', "Account",
+ domain=[
+ ('company', '=', Eval('company')),
+ ('type', '!=', None),
+ ('closed', '!=', True),
+ ],
+ depends=['company'])
+ account_from = fields.Char("Account From")
+ account_to = fields.Char("Account To")
+ currency = fields.Many2One('currency.currency', "Transaction Currency")
+ party = fields.Many2One(
+ 'party.party', "Party",
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['company'])
+ document_number = fields.Char("Document Number")
+
+ @classmethod
+ def default_account(cls):
+ return Transaction().context.get('account')
+
+ @classmethod
+ def default_account_from(cls):
+ return Transaction().context.get('account_from')
+
+ @classmethod
+ def default_account_to(cls):
+ return Transaction().context.get('account_to')
+
+ @classmethod
+ def default_currency(cls):
+ return Transaction().context.get('currency')
+
+ @classmethod
+ def default_party(cls):
+ return Transaction().context.get('party')
+
+ @classmethod
+ def default_document_number(cls):
+ return Transaction().context.get('document_number')
+
+ @classmethod
+ def get_context(cls, fields_names=None):
+ fields_names = fields_names.copy() if fields_names is not None else []
+ fields_names += [
+ 'account', 'account_from', 'account_to', 'currency', 'party',
+ 'document_number']
+ return super().get_context(fields_names=fields_names)
+
+
+class GeneralLedgerDetail(DescriptionOriginMixin, ModelSQL, ModelView):
+ 'General Ledger Detail'
+ __name__ = 'account.general_ledger.detail'
+
+ row_type = fields.Selection([
+ ('opening', "Opening"),
+ ('movement', "Movement"),
+ ('closing', "Closing"),
+ ], "Row Type", sort=False)
+ row_sequence = fields.Integer("Row Sequence")
+
+ company = fields.Many2One('company.company', "Company")
+ account = fields.Many2One('account.account', "Account")
+ account_code = fields.Char("Account Code")
+ account_name = fields.Char("Account Name")
+ transaction_currency = fields.Many2One(
+ 'currency.currency', "Transaction Currency")
+ base_currency = fields.Many2One('currency.currency', "Base Currency")
+
+ posting_date = fields.Date("Posting Date")
+ journal = fields.Many2One('account.journal', "Journal")
+ journal_entry_number = fields.Char("Journal Entry Number")
+ document_number = fields.Char("Document Number")
+ voucher_number = fields.Char("Voucher Number")
+ document_type = fields.Char("Document Type")
+ document_date = fields.Date("Document Date")
+ supplier_invoice_number = fields.Char("Supplier Invoice Number")
+ posting_status = fields.Selection([
+ ('draft', "Draft"),
+ ('posted', "Posted"),
+ ], "Posting Status", sort=False)
+
+ party = fields.Many2One('party.party', "Party")
+ counterparty = fields.Function(
+ fields.Char("Counterparty"), 'get_counterparty')
+ move = fields.Many2One('account.move', "Move")
+ origin = fields.Reference('Origin', selection='get_origin')
+ description = fields.Char("Description")
+ reference = fields.Char("Reference")
+
+ debit_base_currency = Monetary(
+ "Debit Base Currency", currency='base_currency',
+ digits='base_currency')
+ credit_base_currency = Monetary(
+ "Credit Base Currency", currency='base_currency',
+ digits='base_currency')
+ balance_base_currency = Monetary(
+ "Balance Base Currency", currency='base_currency',
+ digits='base_currency')
+ running_balance_base_currency = Monetary(
+ "Running Balance Base Currency", currency='base_currency',
+ digits='base_currency')
+
+ debit_transaction_currency = Monetary(
+ "Debit Transaction Currency", currency='transaction_currency',
+ digits='transaction_currency')
+ credit_transaction_currency = Monetary(
+ "Credit Transaction Currency", currency='transaction_currency',
+ digits='transaction_currency')
+ balance_transaction_currency = Monetary(
+ "Balance Transaction Currency", currency='transaction_currency',
+ digits='transaction_currency')
+ running_balance_transaction_currency = Monetary(
+ "Running Balance Transaction Currency",
+ currency='transaction_currency', digits='transaction_currency')
+
+ payable_qty = fields.Numeric("Payable Qty")
+ uom = fields.Char("UoM")
+
+ entered_by = fields.Many2One('res.user', "Entered By")
+ entered_date = fields.Date("Entered Date")
+ modified_by = fields.Many2One('res.user', "Modified By")
+ modified_date = fields.Date("Modified Date")
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._order = [
+ ('account_code', 'ASC'),
+ ('transaction_currency', 'ASC'),
+ ('row_sequence', 'ASC'),
+ ('posting_date', 'ASC'),
+ ('journal_entry_number', 'ASC'),
+ ('id', 'ASC'),
+ ]
+
+ @classmethod
+ def _get_period_ids(cls, name, context):
+ pool = Pool()
+ Period = pool.get('account.period')
+
+ period = None
+ if name == 'opening':
+ period_ids = []
+ if context.get('start_period'):
+ period = Period(context['start_period'])
+ elif name == 'closing':
+ period_ids = []
+ if context.get('end_period'):
+ period = Period(context['end_period'])
+ else:
+ periods = Period.search([
+ ('fiscalyear', '=', context.get('fiscalyear')),
+ ('type', '=', 'standard'),
+ ],
+ order=[('start_date', 'DESC')], limit=1)
+ if periods:
+ period, = periods
+ else:
+ start_period_ids = set(cls._get_period_ids('opening', context))
+ end_period_ids = set(cls._get_period_ids('closing', context))
+ return list(end_period_ids.difference(start_period_ids))
+
+ if period:
+ if name == 'opening':
+ date_clause = ('end_date', '<=', period.start_date)
+ else:
+ date_clause = [
+ ('end_date', '<=', period.end_date),
+ ('start_date', '<', period.end_date),
+ ]
+ periods = Period.search([
+ ('fiscalyear', '=', context.get('fiscalyear')),
+ date_clause,
+ ])
+ if period.start_date == period.end_date:
+ periods.append(period)
+ if periods:
+ period_ids = [p.id for p in periods]
+ if name == 'closing':
+ period_ids.append(period.id)
+ return period_ids
+
+ @classmethod
+ def _get_dates(cls, name, context):
+ if name == 'opening':
+ to_date = context.get('from_date') or datetime.date.min
+ if to_date:
+ try:
+ to_date -= datetime.timedelta(days=1)
+ except OverflowError:
+ pass
+ return None, to_date
+ elif name == 'closing':
+ return None, context.get('to_date')
+ return context.get('from_date'), context.get('to_date')
+
+ @classmethod
+ def _query_context(cls, name, context):
+ if context.get('start_period') or context.get('end_period'):
+ return {
+ 'periods': cls._get_period_ids(name, context),
+ 'from_date': None,
+ 'to_date': None,
+ }
+ elif context.get('from_date') or context.get('to_date'):
+ from_date, to_date = cls._get_dates(name, context)
+ return {
+ 'periods': None,
+ 'from_date': from_date,
+ 'to_date': to_date,
+ }
+ else:
+ if name == 'opening':
+ return {
+ 'periods': [],
+ 'from_date': None,
+ 'to_date': None,
+ }
+ return {
+ 'periods': None,
+ 'from_date': None,
+ 'to_date': None,
+ }
+
+ @classmethod
+ def _extra_where(cls, line, move, account, tx_currency, context):
+ where = (
+ (move.company == context.get('company'))
+ & (account.type != Null)
+ & (account.closed != Literal(True)))
+ if context.get('account'):
+ where &= line.account == context['account']
+ if context.get('account_from'):
+ where &= account.code >= context['account_from']
+ if context.get('account_to'):
+ where &= account.code <= context['account_to']
+ if context.get('currency'):
+ where &= tx_currency == context['currency']
+ if context.get('party'):
+ where &= line.party == context['party']
+ if context.get('document_number'):
+ value = '%%%s%%' % context['document_number']
+ where &= (
+ move.number.ilike(value)
+ | move.post_number.ilike(value)
+ | move.ext_ref.ilike(value))
+ return where
+
+ @classmethod
+ def _tables(cls):
+ pool = Pool()
+ Line = pool.get('account.move.line')
+ Move = pool.get('account.move')
+ Account = pool.get('account.account')
+ Company = pool.get('company.company')
+ line = Line.__table__()
+ move = Move.__table__()
+ account = Account.__table__()
+ company = Company.__table__()
+ return Line, line, move, account, company
+
+ @classmethod
+ def _line_query(cls, name, context):
+ Line, line, move, account, company = cls._tables()
+ tx_currency = Coalesce(line.second_currency, company.currency)
+ base_amount = Coalesce(line.debit, 0) - Coalesce(line.credit, 0)
+ second_amount = Coalesce(line.amount_second_currency, 0)
+ tx_amount = Case(
+ (line.second_currency != Null, second_amount),
+ else_=base_amount)
+ debit_tx = Case(
+ (line.second_currency == Null, Coalesce(line.debit, 0)),
+ (second_amount > 0, second_amount),
+ else_=Literal(0))
+ credit_tx = Case(
+ (line.second_currency == Null, Coalesce(line.credit, 0)),
+ (second_amount < 0, -second_amount),
+ else_=Literal(0))
+
+ with Transaction().set_context(
+ context, **cls._query_context(name, context)):
+ line_query, _ = Line.query_get(line)
+ where = line_query & cls._extra_where(
+ line, move, account, tx_currency, context)
+
+ window = Window(
+ [line.account, tx_currency],
+ order_by=[move.date.asc, move.number.asc, line.id.asc])
+
+ return (line.join(move, condition=line.move == move.id)
+ .join(account, condition=line.account == account.id)
+ .join(company, condition=move.company == company.id)
+ .select(
+ (line.id * 3).as_('id'),
+ line.create_uid.as_('create_uid'),
+ line.create_date.as_('create_date'),
+ line.write_uid.as_('write_uid'),
+ line.write_date.as_('write_date'),
+ Literal('movement').as_('row_type'),
+ Literal(1).as_('row_sequence'),
+ move.company.as_('company'),
+ line.account.as_('account'),
+ account.code.as_('account_code'),
+ account.name.as_('account_name'),
+ tx_currency.as_('transaction_currency'),
+ company.currency.as_('base_currency'),
+ move.date.as_('posting_date'),
+ move.journal.as_('journal'),
+ Coalesce(move.post_number, move.number).as_(
+ 'journal_entry_number'),
+ Coalesce(move.ext_ref, move.number).as_('document_number'),
+ move.number.as_('voucher_number'),
+ move.origin.as_('document_type'),
+ move.date.as_('document_date'),
+ Literal(None).as_('supplier_invoice_number'),
+ move.state.as_('posting_status'),
+ line.party.as_('party'),
+ line.move.as_('move'),
+ line.origin.as_('origin'),
+ line.description.as_('description'),
+ Coalesce(move.ext_ref, move.description).as_('reference'),
+ line.debit.as_('debit_base_currency'),
+ line.credit.as_('credit_base_currency'),
+ base_amount.as_('balance_base_currency'),
+ Sum(base_amount, window=window).as_(
+ 'running_balance_base_currency'),
+ debit_tx.as_('debit_transaction_currency'),
+ credit_tx.as_('credit_transaction_currency'),
+ tx_amount.as_('balance_transaction_currency'),
+ Sum(tx_amount, window=window).as_(
+ 'running_balance_transaction_currency'),
+ Literal(None).as_('payable_qty'),
+ Literal(None).as_('uom'),
+ line.create_uid.as_('entered_by'),
+ line.create_date.as_('entered_date'),
+ line.write_uid.as_('modified_by'),
+ line.write_date.as_('modified_date'),
+ where=where))
+
+ @classmethod
+ def _summary_query(cls, name, context):
+ Line, line, move, account, company = cls._tables()
+ tx_currency = Coalesce(line.second_currency, company.currency)
+ base_amount = Coalesce(line.debit, 0) - Coalesce(line.credit, 0)
+ tx_amount = Case(
+ (line.second_currency != Null,
+ Coalesce(line.amount_second_currency, 0)),
+ else_=base_amount)
+ row_sequence = 0 if name == 'opening' else 2
+ row_id = (
+ (Min(line.id) * 3 - 1) if name == 'opening'
+ else (Min(line.id) * 3 - 2))
+
+ with Transaction().set_context(
+ context, **cls._query_context(name, context)):
+ line_query, _ = Line.query_get(line)
+ where = line_query & cls._extra_where(
+ line, move, account, tx_currency, context)
+
+ return (line.join(move, condition=line.move == move.id)
+ .join(account, condition=line.account == account.id)
+ .join(company, condition=move.company == company.id)
+ .select(
+ row_id.as_('id'),
+ Literal(0).as_('create_uid'),
+ Max(line.create_date).as_('create_date'),
+ Literal(0).as_('write_uid'),
+ Max(line.write_date).as_('write_date'),
+ Literal(name).as_('row_type'),
+ Literal(row_sequence).as_('row_sequence'),
+ move.company.as_('company'),
+ line.account.as_('account'),
+ account.code.as_('account_code'),
+ account.name.as_('account_name'),
+ tx_currency.as_('transaction_currency'),
+ company.currency.as_('base_currency'),
+ Literal(None).as_('posting_date'),
+ Literal(None).as_('journal'),
+ Literal(None).as_('journal_entry_number'),
+ Literal(None).as_('document_number'),
+ Literal(None).as_('voucher_number'),
+ Literal(None).as_('document_type'),
+ Literal(None).as_('document_date'),
+ Literal(None).as_('supplier_invoice_number'),
+ Literal(None).as_('posting_status'),
+ Literal(None).as_('party'),
+ Literal(None).as_('move'),
+ Literal(None).as_('origin'),
+ Literal(name.title()).as_('description'),
+ Literal(None).as_('reference'),
+ Literal(0).as_('debit_base_currency'),
+ Literal(0).as_('credit_base_currency'),
+ Sum(base_amount).as_('balance_base_currency'),
+ Sum(base_amount).as_('running_balance_base_currency'),
+ Literal(0).as_('debit_transaction_currency'),
+ Literal(0).as_('credit_transaction_currency'),
+ Sum(tx_amount).as_('balance_transaction_currency'),
+ Sum(tx_amount).as_('running_balance_transaction_currency'),
+ Literal(None).as_('payable_qty'),
+ Literal(None).as_('uom'),
+ Literal(None).as_('entered_by'),
+ Literal(None).as_('entered_date'),
+ Literal(None).as_('modified_by'),
+ Literal(None).as_('modified_date'),
+ where=where,
+ group_by=[
+ move.company, line.account, account.code, account.name,
+ tx_currency, company.currency],
+ having=Sum(base_amount) != 0))
+
+ @classmethod
+ def table_query(cls):
+ pool = Pool()
+ DetailContext = pool.get('account.general_ledger.detail.context')
+ context = DetailContext.get_context()
+ return Union(
+ cls._summary_query('opening', context),
+ cls._line_query('movement', context),
+ cls._summary_query('closing', context))
+
+ @classmethod
+ def get_origin(cls):
+ Line = Pool().get('account.move.line')
+ return Line.get_origin()
+
+ def get_counterparty(self, name):
+ if self.party:
+ return self.party.rec_name
+ return None
+
+
class GeneralLedger(Report):
__name__ = 'account.general_ledger'
diff --git a/modules/account/account.xml b/modules/account/account.xml
index e7c2e11..fd22196 100755
--- a/modules/account/account.xml
+++ b/modules/account/account.xml
@@ -806,6 +806,58 @@ this repository contains the full copyright notices and license terms. -->
+
+ account.general_ledger.detail
+ tree
+ general_ledger_detail_list
+
+
+
+ General Ledger Detail
+ account.general_ledger.detail
+ account.general_ledger.detail.context
+
+
+
+
+
+
+
+
+
+ account.general_ledger.detail
+
+
+
+
+
+
+ account.general_ledger.detail
+
+
+
+
+
+
+
+
+ User in companies
+ account.general_ledger.detail
+
+
+
+
+
+
+
account.general_ledger.account.context
form
@@ -817,6 +869,11 @@ this repository contains the full copyright notices and license terms. -->
general_ledger_line_context_form
+
+ account.general_ledger.detail.context
+ form
+ general_ledger_detail_context_form
+
account.account.type
diff --git a/modules/account/tests/test_module.py b/modules/account/tests/test_module.py
index b496535..28da799 100755
--- a/modules/account/tests/test_module.py
+++ b/modules/account/tests/test_module.py
@@ -2097,5 +2097,52 @@ class AccountTestCase(
self.assertEqual(account.parent, new_account)
+ @with_transaction()
+ def test_general_ledger_detail_models_are_registered(self):
+ "Test General Ledger Detail models are registered with expected fields"
+ pool = Pool()
+ Detail = pool.get('account.general_ledger.detail')
+ DetailContext = pool.get('account.general_ledger.detail.context')
+
+ for name in [
+ 'row_type', 'account_code', 'transaction_currency',
+ 'journal_entry_number', 'document_number',
+ 'debit_base_currency', 'credit_base_currency',
+ 'running_balance_base_currency',
+ 'debit_transaction_currency',
+ 'credit_transaction_currency',
+ 'running_balance_transaction_currency']:
+ self.assertIn(name, Detail._fields)
+
+ for name in [
+ 'account', 'account_from', 'account_to', 'currency', 'party',
+ 'document_number']:
+ self.assertIn(name, DetailContext._fields)
+
+ @with_transaction()
+ def test_general_ledger_detail_date_contexts(self):
+ "Test General Ledger Detail opening and movement date contexts"
+ Detail = Pool().get('account.general_ledger.detail')
+ context = {
+ 'from_date': datetime.date(2026, 1, 10),
+ 'to_date': datetime.date(2026, 1, 31),
+ }
+
+ self.assertEqual(Detail._query_context('opening', context), {
+ 'periods': None,
+ 'from_date': None,
+ 'to_date': datetime.date(2026, 1, 9),
+ })
+ self.assertEqual(Detail._query_context('movement', context), {
+ 'periods': None,
+ 'from_date': datetime.date(2026, 1, 10),
+ 'to_date': datetime.date(2026, 1, 31),
+ })
+ self.assertEqual(Detail._query_context('closing', context), {
+ 'periods': None,
+ 'from_date': None,
+ 'to_date': datetime.date(2026, 1, 31),
+ })
+
del ModuleTestCase
diff --git a/modules/account/view/general_ledger_detail_context_form.xml b/modules/account/view/general_ledger_detail_context_form.xml
new file mode 100644
index 0000000..0f13d20
--- /dev/null
+++ b/modules/account/view/general_ledger_detail_context_form.xml
@@ -0,0 +1,41 @@
+
+
+
diff --git a/modules/account/view/general_ledger_detail_list.xml b/modules/account/view/general_ledger_detail_list.xml
new file mode 100644
index 0000000..fbb6f34
--- /dev/null
+++ b/modules/account/view/general_ledger_detail_list.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+