From 0545a5e27f0464243d4570c712fa48904d3fda84 Mon Sep 17 00:00:00 2001 From: git_admin Date: Fri, 1 May 2026 14:25:20 +0000 Subject: [PATCH] Tower: upload base_accounting_kit 19.0.2.3.1 (via marketplace) --- .../base_accounting_kit/models/res_partner.py | 528 ++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 addons/base_accounting_kit/models/res_partner.py diff --git a/addons/base_accounting_kit/models/res_partner.py b/addons/base_accounting_kit/models/res_partner.py new file mode 100644 index 0000000..9d8e5a0 --- /dev/null +++ b/addons/base_accounting_kit/models/res_partner.py @@ -0,0 +1,528 @@ +# -*- 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 . +# +############################################################################# +from datetime import date, timedelta +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +import base64 +import io +import json +import xlsxwriter +from odoo.exceptions import ValidationError, UserError +from odoo.tools.json import json_default + + +class ResPartner(models.Model): + """Inheriting res.partner""" + _inherit = "res.partner" + + invoice_list = fields.One2many('account.move', 'partner_id', + string="Invoice Details", + readonly=True, + domain=( + [('payment_state', '=', 'not_paid'), + ('move_type', '=', 'out_invoice')])) + total_due = fields.Monetary(compute='_compute_for_followup', store=False, + readonly=True) + next_reminder_date = fields.Date(compute='_compute_for_followup', + store=False, readonly=True) + total_overdue = fields.Monetary(compute='_compute_for_followup', + store=False, readonly=True) + followup_status = fields.Selection( + [('in_need_of_action', 'In need of action'), + ('with_overdue_invoices', 'With overdue invoices'), + ('no_action_needed', 'No action needed')], + string='Followup status', + ) + + warning_stage = fields.Float(string='Warning Amount', + help="A warning message will appear once the " + "selected customer is crossed warning " + "amount. Set its value to 0.00 to" + " disable this feature") + blocking_stage = fields.Float(string='Blocking Amount', + help="Cannot make sales once the selected " + "customer is crossed blocking amount." + "Set its value to 0.00 to disable " + "this feature") + due_amount = fields.Float(string="Total Sale", + compute="compute_due_amount") + active_limit = fields.Boolean("Active Credit Limit", default=False) + + enable_credit_limit = fields.Boolean(string="Credit Limit Enabled", + compute="_compute_enable_credit_limit") + + def _compute_for_followup(self): + """ + Compute the fields 'total_due', 'total_overdue' , 'next_reminder_date' and 'followup_status' + """ + for record in self: + total_due = 0 + total_overdue = 0 + today = fields.Date.today() + for am in record.invoice_list: + if am.company_id == self.env.company: + amount = am.amount_residual + total_due += amount + + is_overdue = today > am.invoice_date_due if am.invoice_date_due else today > am.date + if is_overdue: + total_overdue += amount or 0 + min_date = record.get_min_date() + action = record.action_after() + if min_date: + date_reminder = min_date + timedelta(days=action) + if date_reminder: + record.next_reminder_date = date_reminder + else: + date_reminder = today + record.next_reminder_date = date_reminder + if total_overdue > 0 and date_reminder > today: + followup_status = "with_overdue_invoices" + elif total_due > 0 and date_reminder <= today: + followup_status = "in_need_of_action" + else: + followup_status = "no_action_needed" + record.total_due = total_due + record.total_overdue = total_overdue + record.followup_status = followup_status + + def get_min_date(self): + """Get the minimum invoice due date from the partner's invoice list.""" + today = date.today() + for this in self: + if this.invoice_list: + min_list = this.invoice_list.mapped('invoice_date_due') + while False in min_list: + min_list.remove(False) + return min(min_list) + else: + return today + + def get_delay(self): + """Retrieve the delay information for follow-up lines associated with the company.""" + delay = """SELECT fl.id, fl.delay + FROM followup_line fl + JOIN account_followup af ON fl.followup_id = af.id + WHERE af.company_id = %s + ORDER BY fl.delay; + + """ + self._cr.execute(delay, [self.env.company.id]) + record = self._cr.dictfetchall() + + return record + + def action_after(self): + """Retrieve the delay information for follow-up lines associated with the company and return the delay value if found.""" + lines = self.env['followup.line'].search([( + 'followup_id.company_id', '=', self.env.company.id)]) + if lines: + record = self.get_delay() + for i in record: + return i['delay'] + + def compute_due_amount(self): + """Compute function to compute the due amount with the + credit and debit amount""" + for rec in self: + if not rec.id: + continue + rec.due_amount = rec.credit - rec.debit + + def _compute_enable_credit_limit(self): + """ Check credit limit is enabled in account settings """ + params = self.env['ir.config_parameter'].sudo() + customer_credit_limit = params.get_param('customer_credit_limit', + default=False) + for rec in self: + rec.enable_credit_limit = True if customer_credit_limit else False + + @api.constrains('warning_stage', 'blocking_stage') + def constrains_warning_stage(self): + """Constrains functionality used to indicate or raise an + UserError""" + if self.active_limit and self.enable_credit_limit: + if self.warning_stage >= self.blocking_stage: + if self.blocking_stage > 0: + raise UserError(_( + "Warning amount should be less than Blocking amount")) + + # customer statement + + customer_report_ids = fields.Many2many( + 'account.move', + compute='_compute_customer_report_ids', + help='Partner Invoices related to Customer') + vendor_statement_ids = fields.Many2many( + 'account.move', + compute='_compute_vendor_statement_ids', + help='Partner Bills related to Vendor') + currency_id = fields.Many2one( + 'res.currency', + default=lambda self: self.env.company.currency_id.id, + help="currency related to Customer or Vendor") + + def _compute_customer_report_ids(self): + """ For computing 'invoices' of partner """ + for rec in self: + inv_ids = self.env['account.move'].search( + [('partner_id', '=', rec.id), + ('move_type', '=', 'out_invoice'), + ('payment_state', '!=', 'paid'), + ('state', '=', 'posted')]) + rec.customer_report_ids = inv_ids + + def _compute_vendor_statement_ids(self): + """ For computing 'bills' of partner """ + for rec in self: + bills = self.env['account.move'].search( + [('partner_id', '=', rec.id), + ('move_type', '=', 'in_invoice'), + ('payment_state', '!=', 'paid'), + ('state', '=', 'posted')]) + rec.vendor_statement_ids = bills + + def main_query(self): + """ Return select query """ + query = """SELECT name , invoice_date, invoice_date_due, + amount_total_signed AS sub_total, + amount_residual_signed AS amount_due , + amount_residual AS balance + FROM account_move WHERE payment_state != 'paid' + AND state ='posted' AND partner_id= '%s' + AND company_id = '%s' """ % (self.id, self.env.company.id) + return query + + def amount_query(self): + """ Return query for calculating total amount """ + amount_query = """ SELECT SUM(amount_total_signed) AS total, + SUM(amount_residual) AS balance + FROM account_move WHERE payment_state != 'paid' + AND state ='posted' AND partner_id= '%s' + AND company_id = '%s' """ % (self.id, self.env.company.id) + return amount_query + + def action_share_pdf(self): + """ Action for sharing customer pdf report """ + if self.customer_report_ids: + main_query = self.main_query() + main_query += """ AND move_type IN ('out_invoice')""" + amount = self.amount_query() + amount += """ AND move_type IN ('out_invoice')""" + self.env.cr.execute(main_query) + main = self.env.cr.dictfetchall() + self.env.cr.execute(amount) + amount = self.env.cr.dictfetchall() + data = { + 'customer': self.display_name, + 'street': self.street, + 'street2': self.street2, + 'city': self.city, + 'state': self.state_id.name, + 'zip': self.zip, + 'my_data': main, + 'total': amount[0]['total'], + 'balance': amount[0]['balance'], + 'currency': self.currency_id.symbol, + } + report = self.env['ir.actions.report'].sudo()._render_qweb_pdf( + 'base_accounting_kit.res_partner_action', self, data=data) + data_record = base64.b64encode(report[0]) + ir_values = { + 'name': 'Statement Report', + 'type': 'binary', + 'datas': data_record, + 'mimetype': 'application/pdf', + 'res_model': 'res.partner' + } + attachment = self.env['ir.attachment'].sudo().create(ir_values) + email_values = { + 'email_to': self.email, + 'subject': 'Payment Statement Report', + 'body_html': '

Dear Mr/Miss. ' + self.name + + '

We have attached your ' + 'payment statement. Please check

' + '

Best regards,

' + self.env.user.name, + 'attachment_ids': [attachment.id], + } + mail = self.env['mail.mail'].sudo().create(email_values) + mail.send() + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'message': 'Email Sent Successfully', + 'type': 'success', + 'sticky': False + } + } + else: + raise ValidationError('There is no statement to send') + + def action_print_pdf(self): + """ Action for printing pdf report """ + if self.customer_report_ids: + main_query = self.main_query() + main_query += """ AND move_type IN ('out_invoice')""" + amount = self.amount_query() + amount += """ AND move_type IN ('out_invoice')""" + self.env.cr.execute(main_query) + main = self.env.cr.dictfetchall() + self.env.cr.execute(amount) + amount = self.env.cr.dictfetchall() + data = { + 'customer': self.display_name, + 'street': self.street, + 'street2': self.street2, + 'city': self.city, + 'state': self.state_id.name, + 'zip': self.zip, + 'my_data': main, + 'total': amount[0]['total'], + 'balance': amount[0]['balance'], + 'currency': self.currency_id.symbol, + } + return self.env.ref('base_accounting_kit.res_partner_action' + ).report_action(self, data=data) + else: + raise ValidationError('There is no statement to print') + + def action_print_xlsx(self): + """ Action for printing xlsx report of customers """ + if self.customer_report_ids: + main_query = self.main_query() + main_query += """ AND move_type IN ('out_invoice')""" + amount = self.amount_query() + amount += """ AND move_type IN ('out_invoice')""" + self.env.cr.execute(main_query) + main = self.env.cr.dictfetchall() + self.env.cr.execute(amount) + amount = self.env.cr.dictfetchall() + data = { + 'customer': self.display_name, + 'street': self.street, + 'street2': self.street2, + 'city': self.city, + 'state': self.state_id.name, + 'zip': self.zip, + 'my_data': main, + 'total': amount[0]['total'], + 'balance': amount[0]['balance'], + 'currency': self.currency_id.symbol, + } + return { + 'type': 'ir.actions.report', + 'data': { + 'model': 'res.partner', + 'options': json.dumps(data, + default=json_default), + 'output_format': 'xlsx', + 'report_name': 'Payment Statement Report' + }, + 'report_type': 'xlsx', + } + else: + raise ValidationError('There is no statement to print') + + def get_xlsx_report(self, data, response): + """ Get xlsx report data """ + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + cell_format_with_color = workbook.add_format({ + 'font_size': '14px', 'bold': True, + 'bg_color': 'yellow', 'border': 1}) + cell_format = workbook.add_format({'font_size': '14px', 'bold': True}) + txt = workbook.add_format({'font_size': '13px'}) + txt_border = workbook.add_format({'font_size': '13px', 'border': 1}) + head = workbook.add_format({'align': 'center', 'bold': True, + 'font_size': '22px'}) + sheet.merge_range('B2:Q4', 'Payment Statement Report', head) + if data['customer']: + sheet.merge_range('B7:D7', 'Customer/Supplier : ', cell_format) + sheet.merge_range('E7:H7', data['customer'], txt) + sheet.merge_range('B9:C9', 'Address : ', cell_format) + if data['street']: + sheet.merge_range('D9:F9', data['street'], txt) + if data['street2']: + sheet.merge_range('D10:F10', data['street2'], txt) + if data['city']: + sheet.merge_range('D11:F11', data['city'], txt) + if data['state']: + sheet.merge_range('D12:F12', data['state'], ) + if data['zip']: + sheet.merge_range('D13:F13', data['zip'], txt) + sheet.merge_range('B15:C15', 'Date', cell_format_with_color) + sheet.merge_range('D15:G15', 'Invoice/Bill Number', + cell_format_with_color) + sheet.merge_range('H15:I15', 'Due Date', cell_format_with_color) + sheet.merge_range('J15:L15', 'Invoices/Debit', cell_format_with_color) + sheet.merge_range('M15:O15', 'Amount Due', cell_format_with_color) + sheet.merge_range('P15:R15', 'Balance Due', cell_format_with_color) + row = 15 + column = 0 + for record in data['my_data']: + sub_total = data['currency'] + str(record['sub_total']) + amount_due = data['currency'] + str(record['amount_due']) + balance = data['currency'] + str(record['balance']) + total = data['currency'] + str(data['total']) + remain_balance = data['currency'] + str(data['balance']) + sheet.merge_range(row, column + 1, row, column + 2, + record['invoice_date'], txt_border) + sheet.merge_range(row, column + 3, row, column + 6, + record['name'], txt_border) + sheet.merge_range(row, column + 7, row, column + 8, + record['invoice_date_due'], txt_border) + sheet.merge_range(row, column + 9, row, column + 11, + sub_total, txt_border) + sheet.merge_range(row, column + 12, row, column + 14, + amount_due, txt_border) + sheet.merge_range(row, column + 15, row, column + 17, + balance, txt_border) + row = row + 1 + sheet.write(row + 2, column + 1, 'Total Amount: ', cell_format) + sheet.merge_range(row + 2, column + 3, row + 2, column + 4, + total, txt) + sheet.write(row + 4, column + 1, 'Balance Due: ', cell_format) + sheet.merge_range(row + 4, column + 3, row + 4, column + 4, + remain_balance, txt) + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def action_share_xlsx(self): + """ Action for sharing xlsx report via email """ + if self.customer_report_ids: + main_query = self.main_query() + main_query += """ AND move_type IN ('out_invoice')""" + amount = self.amount_query() + amount += """ AND move_type IN ('out_invoice')""" + self.env.cr.execute(main_query) + main = self.env.cr.dictfetchall() + self.env.cr.execute(amount) + amount = self.env.cr.dictfetchall() + data = { + 'customer': self.display_name, + 'street': self.street, + 'street2': self.street2, + 'city': self.city, + 'state': self.state_id.name, + 'zip': self.zip, + 'my_data': main, + 'total': amount[0]['total'], + 'balance': amount[0]['balance'], + 'currency': self.currency_id.symbol, + } + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + cell_format = workbook.add_format({ + 'font_size': '14px', 'bold': True}) + txt = workbook.add_format({'font_size': '13px'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '22px'}) + sheet.merge_range('B2:P4', 'Payment Statement Report', head) + date_style = workbook.add_format( + {'text_wrap': True, 'align': 'center', + 'num_format': 'yyyy-mm-dd'}) + if data['customer']: + sheet.write('B7:C7', 'Customer : ', cell_format) + sheet.merge_range('D7:G7', data['customer'], txt) + sheet.write('B9:C7', 'Address : ', cell_format) + if data['street']: + sheet.merge_range('D9:F9', data['street'], txt) + if data['street2']: + sheet.merge_range('D10:F10', data['street2'], txt) + if data['city']: + sheet.merge_range('D11:F11', data['city'], txt) + if data['state']: + sheet.merge_range('D12:F12', data['state'], txt) + if data['zip']: + sheet.merge_range('D13:F13', data['zip'], txt) + sheet.write('B15', 'Date', cell_format) + sheet.write('D15', 'Invoice/Bill Number', cell_format) + sheet.write('H15', 'Due Date', cell_format) + sheet.write('J15', 'Invoices/Debit', cell_format) + sheet.write('M15', 'Amount Due', cell_format) + sheet.write('P15', 'Balance Due', cell_format) + row = 16 + column = 0 + for record in data['my_data']: + sub_total = data['currency'] + str(record['sub_total']) + amount_due = data['currency'] + str(record['amount_due']) + balance = data['currency'] + str(record['balance']) + total = data['currency'] + str(data['total']) + remain_balance = data['currency'] + str(data['balance']) + sheet.merge_range(row, column + 1, row, column + 2, + record['invoice_date'], date_style) + sheet.merge_range(row, column + 3, row, column + 5, + record['name'], txt) + sheet.merge_range(row, column + 7, row, column + 8, + record['invoice_date_due'], date_style) + sheet.merge_range(row, column + 9, row, column + 10, + sub_total, txt) + sheet.merge_range(row, column + 12, row, column + 13, + amount_due, txt) + sheet.merge_range(row, column + 15, row, column + 16, + balance, txt) + row = row + 1 + sheet.write(row + 2, column + 1, 'Total Amount : ', cell_format) + sheet.merge_range(row + 2, column + 4, row + 2, column + 5, + total, txt) + sheet.write(row + 4, column + 1, 'Balance Due : ', cell_format) + sheet.merge_range(row + 4, column + 4, row + 4, column + 5, + remain_balance, txt) + workbook.close() + output.seek(0) + xlsx = base64.b64encode(output.read()) + output.close() + ir_values = { + 'name': "Statement Report.xlsx", + 'type': 'binary', + 'datas': xlsx, + 'store_fname': xlsx, + } + attachment = self.env['ir.attachment'].sudo().create(ir_values) + email_values = { + 'email_to': self.email, + 'subject': 'Payment Statement Report', + 'body_html': '

Dear Mr/Miss. ' + self.name + + '

We have attached your' + ' payment statement. Please check

' + '

Best regards,

' + self.env.user.name, + 'attachment_ids': [attachment.id], + } + mail = self.env['mail.mail'].sudo().create(email_values) + mail.send() + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'message': 'Email Sent Successfully', + 'type': 'success', + 'sticky': False + } + } + else: + raise ValidationError('There is no statement to send') +