You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
23 KiB
527 lines
23 KiB
# -*- coding: utf-8 -*-
|
|
#############################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
#############################################################################
|
|
from datetime import date, timedelta
|
|
from odoo import api, fields, models, _
|
|
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")
|
|
|
|
# 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_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
|
|
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': '<p>Dear <strong> Mr/Miss. ' + self.name +
|
|
'</strong> </p> <p> We have attached your '
|
|
'payment statement. Please check </p> '
|
|
'<p>Best regards, </p> <p> ' + 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': '<p>Dear <strong> Mr/Miss. ' + self.name +
|
|
'</strong> </p> <p> We have attached your'
|
|
' payment statement. Please check </p> '
|
|
'<p>Best regards, </p> <p> ' + 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')
|
|
|