diff --git a/addons/base_accounting_kit/report/report_aged_partner.py b/addons/base_accounting_kit/report/report_aged_partner.py new file mode 100644 index 0000000..ca96076 --- /dev/null +++ b/addons/base_accounting_kit/report/report_aged_partner.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies() +# Author: Cybrosys Techno Solutions() +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################# +import time +from datetime import datetime +from dateutil.relativedelta import relativedelta +from odoo import api, models, _ +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + + +class ReportAgedPartnerBalance(models.AbstractModel): + _name = 'report.base_accounting_kit.report_agedpartnerbalance' + _description = 'Aged Partner Balance Report' + + def _get_partner_move_lines(self, account_type, date_from, target_move, + period_length): + # This method can receive the context key 'include_nullified_amount' {Boolean} + # Do an invoice and a payment and unreconcile. The amount will be nullified + # By default, the partner wouldn't appear in this report. + # The context key allow it to appear + # In case of a period_length of 30 days as of 2019-02-08, we want the following periods: + # Name Stop Start + # 1 - 30 : 2019-02-07 - 2019-01-09 + # 31 - 60 : 2019-01-08 - 2018-12-10 + # 61 - 90 : 2018-12-09 - 2018-11-10 + # 91 - 120 : 2018-11-09 - 2018-10-11 + # +120 : 2018-10-10 + periods = {} + start = datetime.strptime(date_from, "%Y-%m-%d") + date_from = datetime.strptime(date_from, "%Y-%m-%d").date() + for i in range(5)[::-1]: + stop = start - relativedelta(days=period_length) + period_name = str((5 - (i + 1)) * period_length + 1) + '-' + str( + (5 - i) * period_length) + period_stop = (start - relativedelta(days=1)).strftime('%Y-%m-%d') + if i == 0: + period_name = '+' + str(4 * period_length) + periods[str(i)] = { + 'name': period_name, + 'stop': period_stop, + 'start': (i != 0 and stop.strftime('%Y-%m-%d') or False), + } + start = stop + + res = [] + total = [] + cr = self.env.cr + user_company = self.env.company + user_currency = user_company.currency_id + ResCurrency = self.env['res.currency'].with_context(date=date_from) + company_ids = self._context.get('company_ids') or [user_company.id] + move_state = ['draft', 'posted'] + if target_move == 'posted': + move_state = ['posted'] + arg_list = (tuple(move_state), tuple(account_type)) + # build the reconciliation clause to see what partner needs to be printed + reconciliation_clause = '(l.reconciled IS FALSE)' + cr.execute( + 'SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where max_date > %s', + (date_from,)) + reconciled_after_date = [] + + for row in cr.fetchall(): + reconciled_after_date += [row[0], row[1]] + + if reconciled_after_date: + reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)' + arg_list += (tuple(reconciled_after_date),) + arg_list += (date_from, tuple(company_ids)) + query = ''' + SELECT DISTINCT l.partner_id, UPPER(res_partner.name) + FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am + WHERE (l.account_id = account_account.id) + AND (l.move_id = am.id) + AND (am.state IN %s) + AND (account_account.account_type IN %s) + AND ''' + reconciliation_clause + ''' + AND (l.date <= %s) + AND l.company_id IN %s + ORDER BY UPPER(res_partner.name)''' + cr.execute(query, arg_list) + + partners = cr.dictfetchall() + # put a total of 0 + for i in range(7): + total.append(0) + + # Build a string like (1,2,3) for easy use in SQL query + partner_ids = [partner['partner_id'] for partner in partners if + partner['partner_id']] + lines = dict( + (partner['partner_id'] or False, []) for partner in partners) + if not partner_ids: + return [], [], {} + + # This dictionary will store the not due amount of all partners + undue_amounts = {} + query = '''SELECT l.id + FROM account_move_line AS l, account_account, account_move am + WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) + AND (am.state IN %s) + AND (account_account.account_type IN %s) + AND (COALESCE(l.date_maturity,l.date) >= %s)\ + AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) + AND (l.date <= %s) + AND l.company_id IN %s''' + cr.execute(query, ( + tuple(move_state), tuple(account_type), date_from, + tuple(partner_ids), date_from, tuple(company_ids))) + aml_ids = cr.fetchall() + aml_ids = aml_ids and [x[0] for x in aml_ids] or [] + for line in self.env['account.move.line'].browse(aml_ids): + partner_id = line.partner_id.id or False + if partner_id not in undue_amounts: + undue_amounts[partner_id] = 0.0 + line_amount = ResCurrency._get_conversion_rate(line.company_id.currency_id, + user_currency, line.balance) + if user_currency.is_zero(line_amount): + continue + for partial_line in line.matched_debit_ids: + if partial_line.max_date <= date_from: + line_amount += ResCurrency._get_conversion_rate( + partial_line.company_id.currency_id, user_currency, + partial_line.amount) + for partial_line in line.matched_credit_ids: + if partial_line.max_date <= date_from: + line_amount -= ResCurrency._get_conversion_rate( + partial_line.company_id.currency_id, user_currency, + partial_line.amount) + if not self.env.company.currency_id.is_zero(line_amount): + undue_amounts[partner_id] += line_amount + lines[partner_id].append({ + 'line': line, + 'amount': line_amount, + 'period': 6, + }) + # Use one query per period and store results in history (a list variable) + # Each history will contain: history[1] = {'': } + history = [] + for i in range(5): + args_list = ( + tuple(move_state), tuple(account_type), tuple(partner_ids),) + dates_query = '(COALESCE(l.date_maturity,l.date)' + + if periods[str(i)]['start'] and periods[str(i)]['stop']: + dates_query += ' BETWEEN %s AND %s)' + args_list += ( + periods[str(i)]['start'], periods[str(i)]['stop']) + elif periods[str(i)]['start']: + dates_query += ' >= %s)' + args_list += (periods[str(i)]['start'],) + else: + dates_query += ' <= %s)' + args_list += (periods[str(i)]['stop'],) + args_list += (date_from, tuple(company_ids)) + query = '''SELECT l.id + FROM account_move_line AS l, account_account, account_move am + WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) + AND (am.state IN %s) + AND (account_account.account_type IN %s) + AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) + AND ''' + dates_query + ''' + AND (l.date <= %s) + AND l.company_id IN %s''' + cr.execute(query, args_list) + partners_amount = {} + aml_ids = cr.fetchall() + aml_ids = aml_ids and [x[0] for x in aml_ids] or [] + for line in self.env['account.move.line'].browse(aml_ids): + partner_id = line.partner_id.id or False + if partner_id not in partners_amount: + partners_amount[partner_id] = 0.0 + line_amount = ResCurrency._get_conversion_rate(line.company_id.currency_id, + user_currency, line.balance) + if user_currency.is_zero(line_amount): + continue + for partial_line in line.matched_debit_ids: + if partial_line.max_date <= date_from: + line_amount += ResCurrency._get_conversion_rate( + partial_line.company_id.currency_id, user_currency, + partial_line.amount) + for partial_line in line.matched_credit_ids: + if partial_line.max_date <= date_from: + line_amount -= ResCurrency._get_conversion_rate( + partial_line.company_id.currency_id, user_currency, + partial_line.amount) + + if not self.env.company.currency_id.is_zero( + line_amount): + partners_amount[partner_id] += line_amount + lines[partner_id].append({ + 'line': line, + 'amount': line_amount, + 'period': i + 1, + }) + history.append(partners_amount) + for partner in partners: + if partner['partner_id'] is None: + partner['partner_id'] = False + at_least_one_amount = False + values = {} + undue_amt = 0.0 + if partner[ + 'partner_id'] in undue_amounts: # Making sure this partner actually was found by the query + undue_amt = undue_amounts[partner['partner_id']] + + total[6] = total[6] + undue_amt + values['direction'] = undue_amt + if not float_is_zero(values['direction'], + precision_rounding=self.env.company.currency_id.rounding): + at_least_one_amount = True + + for i in range(5): + during = False + if partner['partner_id'] in history[i]: + during = [history[i][partner['partner_id']]] + # Adding counter + total[(i)] = total[(i)] + (during and during[0] or 0) + values[str(i)] = during and during[0] or 0.0 + if not float_is_zero(values[str(i)], + precision_rounding=self.env.company.currency_id.rounding): + at_least_one_amount = True + values['total'] = sum( + [values['direction']] + [values[str(i)] for i in range(5)]) + ## Add for total + total[(i + 1)] += values['total'] + values['partner_id'] = partner['partner_id'] + if partner['partner_id']: + browsed_partner = self.env['res.partner'].browse( + partner['partner_id']) + values['name'] = browsed_partner.name and len( + browsed_partner.name) >= 45 and browsed_partner.name[ + 0:40] + '...' or browsed_partner.name + values['trust'] = browsed_partner.trust + else: + values['name'] = _('Unknown Partner') + values['trust'] = False + + if at_least_one_amount or ( + self._context.get('include_nullified_amount') and lines[ + partner['partner_id']]): + res.append(values) + return res, total, lines + + @api.model + def _get_report_values(self, docids, data=None): + if not data.get('form') or not self.env.context.get( + 'active_model') or not self.env.context.get('active_id'): + raise UserError( + _("Form content is missing, this report cannot be printed.")) + + total = [] + model = self.env.context.get('active_model') + docs = self.env[model].browse(self.env.context.get('active_id')) + + target_move = data['form'].get('target_move', 'all') + date_from = data['form'].get('date_from', time.strftime('%Y-%m-%d')) + + if data['form']['result_selection'] == 'customer': + account_type = ['asset_receivable'] + elif data['form']['result_selection'] == 'supplier': + account_type = ['liability_payable'] + else: + account_type = ['liability_payable', 'asset_receivable'] + + movelines, total, dummy = self._get_partner_move_lines(account_type, + date_from, + target_move, + data['form']['period_length']) + return { + 'doc_ids': self.ids, + 'doc_model': model, + 'data': data['form'], + 'docs': docs, + 'time': time, + 'get_partner_lines': movelines, + 'get_direction': total, + }