GL report

This commit is contained in:
2026-05-18 20:23:25 +02:00
parent 80b2fed34b
commit 8ff380e229
6 changed files with 626 additions and 1 deletions

View File

@@ -30,6 +30,8 @@ def register():
account.GeneralLedgerAccountParty,
account.GeneralLedgerLine,
account.GeneralLedgerLineContext,
account.GeneralLedgerDetail,
account.GeneralLedgerDetailContext,
account.BalanceSheetContext,
account.BalanceSheetComparisionContext,
account.IncomeStatementContext,

View File

@@ -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'

View File

@@ -806,6 +806,58 @@ this repository contains the full copyright notices and license terms. -->
<field name="rule_group" ref="rule_group_general_ledger_line_companies"/>
</record>
<record model="ir.ui.view" id="general_ledger_detail_view_list">
<field name="model">account.general_ledger.detail</field>
<field name="type">tree</field>
<field name="name">general_ledger_detail_list</field>
</record>
<record model="ir.action.act_window" id="act_general_ledger_detail_form">
<field name="name">General Ledger Detail</field>
<field name="res_model">account.general_ledger.detail</field>
<field name="context_model">account.general_ledger.detail.context</field>
</record>
<record model="ir.action.act_window.view"
id="act_general_ledger_detail_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="general_ledger_detail_view_list"/>
<field name="act_window" ref="act_general_ledger_detail_form"/>
</record>
<menuitem
name="General Ledger Detail"
parent="menu_reporting"
action="act_general_ledger_detail_form"
sequence="11"
id="menu_general_ledger_detail"/>
<record model="ir.model.access" id="access_general_ledger_detail">
<field name="model">account.general_ledger.detail</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_general_ledger_detail_account">
<field name="model">account.general_ledger.detail</field>
<field name="group" ref="group_account"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.rule.group" id="rule_group_general_ledger_detail_companies">
<field name="name">User in companies</field>
<field name="model">account.general_ledger.detail</field>
<field name="global_p" eval="True"/>
</record>
<record model="ir.rule" id="rule_general_ledger_detail_companies">
<field name="domain"
eval="[('company', 'in', Eval('companies', []))]"
pyson="1"/>
<field name="rule_group" ref="rule_group_general_ledger_detail_companies"/>
</record>
<record model="ir.ui.view" id="general_ledger_account_context_view_form">
<field name="model">account.general_ledger.account.context</field>
<field name="type">form</field>
@@ -817,6 +869,11 @@ this repository contains the full copyright notices and license terms. -->
<field name="inherit" ref="general_ledger_account_context_view_form"/>
<field name="name">general_ledger_line_context_form</field>
</record>
<record model="ir.ui.view" id="general_ledger_detail_context_view_form">
<field name="model">account.general_ledger.detail.context</field>
<field name="type">form</field>
<field name="name">general_ledger_detail_context_form</field>
</record>
<record model="ir.ui.view" id="account_balance_sheet_view_tree">
<field name="model">account.account.type</field>

View File

@@ -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

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="fiscalyear"/>
<field name="fiscalyear"/>
<label name="company"/>
<field name="company"/>
<label name="journal"/>
<field name="journal" widget="selection"/>
<group col="-1" colspan="2" id="checkboxes">
<label name="posted"/>
<field name="posted"/>
</group>
<label name="start_period"/>
<field name="start_period"/>
<label name="end_period"/>
<field name="end_period"/>
<label name="from_date"/>
<field name="from_date"/>
<label name="to_date"/>
<field name="to_date"/>
<label name="account"/>
<field name="account"/>
<label name="currency"/>
<field name="currency"/>
<label name="account_from"/>
<field name="account_from"/>
<label name="account_to"/>
<field name="account_to"/>
<label name="party"/>
<field name="party"/>
<label name="document_number"/>
<field name="document_number"/>
</form>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="row_type"/>
<field name="account_code"/>
<field name="account_name" expand="1"/>
<field name="transaction_currency"/>
<field name="posting_date"/>
<field name="journal"/>
<field name="journal_entry_number"/>
<field name="document_number"/>
<field name="voucher_number" optional="1"/>
<field name="document_type" optional="1"/>
<field name="document_date" optional="1"/>
<field name="supplier_invoice_number" optional="1"/>
<field name="posting_status" optional="1"/>
<field name="party" optional="1"/>
<field name="counterparty" optional="1"/>
<field name="description" expand="1" optional="1"/>
<field name="reference" optional="1"/>
<field name="origin" optional="1"/>
<field name="debit_base_currency" sum="1"/>
<field name="credit_base_currency" sum="1"/>
<field name="balance_base_currency" sum="1"/>
<field name="running_balance_base_currency" optional="1"/>
<field name="debit_transaction_currency" sum="1" optional="1"/>
<field name="credit_transaction_currency" sum="1" optional="1"/>
<field name="balance_transaction_currency" sum="1" optional="1"/>
<field name="running_balance_transaction_currency" optional="1"/>
<field name="payable_qty" sum="1" optional="1"/>
<field name="uom" optional="1"/>
<field name="entered_by" optional="1"/>
<field name="entered_date" optional="1"/>
<field name="modified_by" optional="1"/>
<field name="modified_date" optional="1"/>
<field name="move" optional="1"/>
<field name="company" optional="1"/>
<field name="base_currency" optional="1"/>
</tree>