first push message
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 . import account_account
|
||||
from . import account_asset_asset
|
||||
from . import account_asset_category
|
||||
from . import account_asset_depreciation_line
|
||||
from . import account_bank_statement_line
|
||||
from . import account_followup
|
||||
from . import account_journal
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
from . import account_payment_method
|
||||
from . import account_recurring_entries_line
|
||||
from . import account_report
|
||||
from . import followup_line
|
||||
from . import multiple_invoice
|
||||
from . import multiple_invoice_layout
|
||||
from . import product_template
|
||||
from . import recurring_payments
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import sale_order
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
from odoo.tools.misc import get_lang
|
||||
|
||||
|
||||
class CashFlow(models.Model):
|
||||
"""Inherits the account.account model to add additional functionality and
|
||||
fields to the account"""
|
||||
_inherit = 'account.account'
|
||||
|
||||
def get_cash_flow_ids(self):
|
||||
"""Returns a list of cashflows for the account"""
|
||||
cash_flow_id = self.env.ref('base_accounting_kit.account_financial_report_cash_flow0')
|
||||
if cash_flow_id:
|
||||
return [('parent_id.id', '=', cash_flow_id.id)]
|
||||
|
||||
cash_flow_type = fields.Many2one('account.financial.report',
|
||||
string="Cash Flow type",
|
||||
domain=get_cash_flow_ids)
|
||||
|
||||
@api.onchange('cash_flow_type')
|
||||
def onchange_cash_flow_type(self):
|
||||
"""Onchange the cash flow type of the account that will be updating
|
||||
the account_ids values"""
|
||||
for rec in self.cash_flow_type:
|
||||
# update new record
|
||||
rec.write({
|
||||
'account_ids': [(4, self._origin.id)]
|
||||
})
|
||||
if self._origin.cash_flow_type.ids:
|
||||
for rec in self._origin.cash_flow_type:
|
||||
# remove old record
|
||||
rec.write({'account_ids': [(3, self._origin.id)]})
|
||||
|
||||
|
||||
class AccountCommonJournalReport(models.TransientModel):
|
||||
"""Model used for creating the common journal report"""
|
||||
_name = 'account.common.journal.report'
|
||||
_description = 'Common Journal Report'
|
||||
_inherit = "account.report"
|
||||
|
||||
section_main_report_ids = fields.Many2many(string="Section Of",
|
||||
comodel_name='account.report',
|
||||
relation="account_common_journal_report_section_rel",
|
||||
column1="sub_report_id",
|
||||
column2="main_report_id")
|
||||
section_report_ids = fields.Many2many(string="Sections",
|
||||
comodel_name='account.report',
|
||||
relation="account_common_journal_report_section_rel",
|
||||
column1="main_report_id",
|
||||
column2="sub_report_id")
|
||||
amount_currency = fields.Boolean(
|
||||
'With Currency',
|
||||
help="Print Report with the currency column if the currency differs "
|
||||
"from the company currency.")
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
required=True, readonly=True,
|
||||
default=lambda self: self.env.company)
|
||||
date_from = fields.Date(string='Start Date')
|
||||
date_to = fields.Date(string='End Date')
|
||||
target_move = fields.Selection([('posted', 'All Posted Entries'),
|
||||
('all', 'All Entries'),
|
||||
], string='Target Moves',
|
||||
required=True, default='posted')
|
||||
|
||||
def pre_print_report(self, data):
|
||||
"""Pre-print the given data and that updates the amount
|
||||
amount_currency value"""
|
||||
data['form'].update({'amount_currency': self.amount_currency})
|
||||
return data
|
||||
|
||||
def check_report(self):
|
||||
"""Function to check if the report comes active models and related values"""
|
||||
self.ensure_one()
|
||||
data = {}
|
||||
data['ids'] = self.env.context.get('active_ids', [])
|
||||
data['model'] = self.env.context.get('active_model', 'ir.ui.menu')
|
||||
data['form'] = self.read(['date_from', 'date_to', 'journal_ids', 'target_move', 'company_id'])[0]
|
||||
used_context = self._build_contexts(data)
|
||||
data['form']['used_context'] = dict(used_context, lang=get_lang(self.env).code)
|
||||
return self.with_context(discard_logo_check=True)._print_report(data)
|
||||
|
||||
def _build_contexts(self, data):
|
||||
"""Builds the context information for the given data"""
|
||||
result = {}
|
||||
result['journal_ids'] = 'journal_ids' in data['form'] and data['form']['journal_ids'] or False
|
||||
result['state'] = 'target_move' in data['form'] and data['form']['target_move'] or ''
|
||||
result['date_from'] = data['form']['date_from'] or False
|
||||
result['date_to'] = data['form']['date_to'] or False
|
||||
result['strict_range'] = True if result['date_from'] else False
|
||||
result['company_id'] = data['form']['company_id'][0] or False
|
||||
return result
|
||||
@@ -0,0 +1,623 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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/>.
|
||||
#
|
||||
#############################################################################
|
||||
import calendar
|
||||
from datetime import date, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.fields import Date
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, float_is_zero
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class AccountAssetAsset(models.Model):
|
||||
"""
|
||||
Model for managing assets with depreciation functionality
|
||||
"""
|
||||
_name = 'account.asset.asset'
|
||||
_description = 'Asset/Revenue Recognition'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
entry_count = fields.Integer(compute='_entry_count',
|
||||
string='# Asset Entries')
|
||||
name = fields.Char(string='Asset Name', required=True)
|
||||
code = fields.Char(string='Reference', size=32)
|
||||
value = fields.Float(string='Gross Value', required=True,
|
||||
digits=0)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency',
|
||||
required=True,
|
||||
default=lambda self: self.env.company.currency_id.id)
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company)
|
||||
note = fields.Text()
|
||||
category_id = fields.Many2one('account.asset.category', string='Asset Model',
|
||||
required=False, change_default=True
|
||||
)
|
||||
date = fields.Date(string='Date', required=True,
|
||||
default=fields.Date.context_today)
|
||||
state = fields.Selection(
|
||||
[('draft', 'Draft'), ('open', 'Running'), ('close', 'Close'),('cancelled','Cancelled')],
|
||||
'Status', required=True, copy=False, default='draft',
|
||||
help="When an asset is created, the status is 'Draft'.\n"
|
||||
"If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n"
|
||||
"You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.")
|
||||
active = fields.Boolean(default=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Partner')
|
||||
method = fields.Selection(
|
||||
[('linear', 'Straight Line'), ('degressive', 'Declining')],
|
||||
string='Computation Method', required=True,default='linear',
|
||||
help="Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n"
|
||||
" * Degressive: Calculated on basis of: Residual Value * Degressive Factor")
|
||||
method_number = fields.Integer(string='Number of Depreciations',
|
||||
default=5,
|
||||
help="The number of depreciation's needed to depreciate your asset")
|
||||
method_period = fields.Integer(string='Number of Months in a Period',
|
||||
required=True, default=12,
|
||||
help="The amount of time between two depreciation's, in months")
|
||||
method_end = fields.Date(string='Ending Date')
|
||||
method_progress_factor = fields.Float(string='Degressive Factor',
|
||||
default=0.3,)
|
||||
value_residual = fields.Float(compute='_amount_residual',
|
||||
digits=0, string='Residual Value')
|
||||
method_time = fields.Selection(
|
||||
[('number', 'Number of Entries'), ('end', 'Ending Date')],
|
||||
string='Time Method', required=True, default='number',
|
||||
help="Choose the method to use to compute the dates and number of entries.\n"
|
||||
" * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n"
|
||||
" * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.")
|
||||
prorata = fields.Boolean(string='Prorata Temporis',
|
||||
help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year')
|
||||
depreciation_line_ids = fields.One2many('account.asset.depreciation.line',
|
||||
'asset_id',
|
||||
string='Depreciation Lines',
|
||||
)
|
||||
salvage_value = fields.Float(string='Salvage Value', digits=0,
|
||||
|
||||
help="It is the amount you plan to have that you cannot depreciate.")
|
||||
invoice_id = fields.Many2one('account.move', string='Invoice',
|
||||
copy=False)
|
||||
type = fields.Selection([('sale', 'Sale: Revenue Recognition'),
|
||||
('purchase', 'Purchase: Asset')], required=True, index=True, default='purchase')
|
||||
|
||||
|
||||
#asset category
|
||||
account_analytic_id = fields.Many2one('account.analytic.account',
|
||||
string='Analytic Account',
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
account_asset_id = fields.Many2one('account.account',
|
||||
string='Asset Account', required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=', 'liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=', 'liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used to record the purchase of the asset at its original price.")
|
||||
account_depreciation_id = fields.Many2one(
|
||||
'account.account', string='Depreciation Account',
|
||||
required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=', 'liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=', 'liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used in the depreciation entries, to decrease the asset value.")
|
||||
account_depreciation_expense_id = fields.Many2one(
|
||||
'account.account', string='Expense Account',
|
||||
required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=','liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=','liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used in the periodical entries, to record a part of the asset as expense.")
|
||||
journal_id = fields.Many2one('account.journal', string='Journal',
|
||||
required=True)
|
||||
open_asset = fields.Boolean(string='Auto-confirm Assets',
|
||||
help="Check this if you want to automatically confirm the assets of this category when created by invoices.")
|
||||
group_entries = fields.Boolean(string='Group Journal Entries',
|
||||
help="Check this if you want to group the generated entries by categories.")
|
||||
|
||||
def unlink(self):
|
||||
""" Prevents deletion of assets in 'open' or 'close' state or with posted depreciation entries."""
|
||||
for asset in self:
|
||||
if asset.state in ['open', 'close']:
|
||||
raise UserError(
|
||||
_('You cannot delete a document is in %s state.') % (
|
||||
asset.state,))
|
||||
for depreciation_line in asset.depreciation_line_ids:
|
||||
if depreciation_line.move_id:
|
||||
raise UserError(_(
|
||||
'You cannot delete a document that contains posted entries.'))
|
||||
return super(AccountAssetAsset, self).unlink()
|
||||
|
||||
def _get_last_depreciation_date(self):
|
||||
"""
|
||||
@param id: ids of a account.asset.asset objects
|
||||
@return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset
|
||||
"""
|
||||
self.env.cr.execute("""
|
||||
SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date
|
||||
FROM account_asset_asset a
|
||||
LEFT JOIN account_asset_depreciation_line rel ON (rel.asset_id = a.id)
|
||||
LEFT JOIN account_move m ON (rel.move_id = m.id)
|
||||
WHERE a.id IN %s
|
||||
GROUP BY a.id, m.date """, (tuple(self.ids),))
|
||||
result = dict(self.env.cr.fetchall())
|
||||
return result
|
||||
|
||||
@api.onchange('category_id')
|
||||
def gross_value(self):
|
||||
"""Update the 'value' field based on the 'price' of the selected 'category_id'."""
|
||||
self.value = self.category_id.price
|
||||
@api.onchange('method')
|
||||
def onchange_method(self):
|
||||
if self.depreciation_line_ids:
|
||||
self.depreciation_line_ids = [(fields.Command.clear())]
|
||||
|
||||
|
||||
@api.model
|
||||
def compute_generated_entries(self, date, asset_type=None):
|
||||
"""Compute generated entries for assets based on the provided date and asset type."""
|
||||
# Entries generated : one by grouped category and one by asset from ungrouped category
|
||||
created_move_ids = []
|
||||
type_domain = []
|
||||
if asset_type:
|
||||
type_domain = [('type', '=', asset_type)]
|
||||
|
||||
ungrouped_assets = self.env['account.asset.asset'].search(
|
||||
type_domain + [('state', '=', 'open'),
|
||||
('category_id.group_entries', '=', False)])
|
||||
created_move_ids += ungrouped_assets._compute_entries(date,
|
||||
group_entries=False)
|
||||
|
||||
for grouped_category in self.env['account.asset.category'].search(
|
||||
type_domain + [('group_entries', '=', True)]):
|
||||
assets = self.env['account.asset.asset'].search(
|
||||
[('state', '=', 'open'),
|
||||
('category_id', '=', grouped_category.id)])
|
||||
created_move_ids += assets._compute_entries(date,
|
||||
group_entries=True)
|
||||
return created_move_ids
|
||||
|
||||
def _compute_board_amount(self, sequence, residual_amount, amount_to_depr,
|
||||
undone_dotation_number,
|
||||
posted_depreciation_line_ids, total_days,
|
||||
depreciation_date):
|
||||
"""Compute the depreciation amount for a specific sequence in the asset's depreciation schedule."""
|
||||
amount = 0
|
||||
if sequence == undone_dotation_number:
|
||||
amount = residual_amount
|
||||
else:
|
||||
if self.method == 'linear':
|
||||
amount = amount_to_depr / (undone_dotation_number - len(
|
||||
posted_depreciation_line_ids))
|
||||
if self.prorata:
|
||||
amount = amount_to_depr / self.method_number
|
||||
if sequence == 1:
|
||||
if self.method_period % 12 != 0:
|
||||
date = datetime.strptime(str(self.date),
|
||||
'%Y-%m-%d')
|
||||
month_days = \
|
||||
calendar.monthrange(date.year, date.month)[1]
|
||||
days = month_days - date.day + 1
|
||||
amount = (
|
||||
amount_to_depr / self.method_number) / month_days * days
|
||||
else:
|
||||
days = (self.company_id.compute_fiscalyear_dates(
|
||||
depreciation_date)[
|
||||
'date_to'] - depreciation_date).days + 1
|
||||
amount = (
|
||||
amount_to_depr / self.method_number) / total_days * days
|
||||
elif self.method == 'degressive':
|
||||
amount = residual_amount * self.method_progress_factor
|
||||
if self.prorata:
|
||||
if sequence == 1:
|
||||
if self.method_period % 12 != 0:
|
||||
date = datetime.strptime(str(self.date),
|
||||
'%Y-%m-%d')
|
||||
month_days = \
|
||||
calendar.monthrange(date.year, date.month)[1]
|
||||
days = month_days - date.day + 1
|
||||
amount = (
|
||||
residual_amount * self.method_progress_factor) / month_days * days
|
||||
else:
|
||||
days = (self.company_id.compute_fiscalyear_dates(
|
||||
depreciation_date)[
|
||||
'date_to'] - depreciation_date).days + 1
|
||||
amount = (
|
||||
residual_amount * self.method_progress_factor) / total_days * days
|
||||
return amount
|
||||
|
||||
def _compute_board_undone_dotation_nb(self, depreciation_date, total_days):
|
||||
"""Compute the number of remaining depreciations for an asset based on the depreciation date and total days."""
|
||||
undone_dotation_number = self.method_number
|
||||
if self.method_time == 'end':
|
||||
end_date = datetime.strptime(str(self.method_end), DF).date()
|
||||
undone_dotation_number = 0
|
||||
while depreciation_date <= end_date:
|
||||
depreciation_date = date(depreciation_date.year,
|
||||
depreciation_date.month,
|
||||
depreciation_date.day) + relativedelta(
|
||||
months=+self.method_period)
|
||||
undone_dotation_number += 1
|
||||
if self.prorata:
|
||||
undone_dotation_number += 1
|
||||
return undone_dotation_number
|
||||
|
||||
def compute_depreciation_board(self):
|
||||
"""
|
||||
Compute the depreciation schedule for the asset based on its current state and parameters.
|
||||
This method calculates the depreciation amount for each period and generates depreciation entries accordingly.
|
||||
"""
|
||||
self.ensure_one()
|
||||
posted_depreciation_line_ids = self.depreciation_line_ids.filtered(
|
||||
lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date)
|
||||
unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(
|
||||
lambda x: not x.move_check)
|
||||
|
||||
# Remove old unposted depreciation lines. We cannot use unlink() with One2many field
|
||||
commands = [(2, line_id.id, False) for line_id in
|
||||
unposted_depreciation_line_ids]
|
||||
|
||||
if self.value_residual != 0.0:
|
||||
amount_to_depr = residual_amount = self.value_residual
|
||||
if self.prorata:
|
||||
# if we already have some previous validated entries, starting date is last entry + method perio
|
||||
if posted_depreciation_line_ids and \
|
||||
posted_depreciation_line_ids[-1].depreciation_date:
|
||||
last_depreciation_date = datetime.strptime(
|
||||
posted_depreciation_line_ids[-1].depreciation_date,
|
||||
DF).date()
|
||||
depreciation_date = last_depreciation_date + relativedelta(
|
||||
months=+self.method_period)
|
||||
else:
|
||||
depreciation_date = datetime.strptime(
|
||||
str(self._get_last_depreciation_date()[self.id]),
|
||||
DF).date()
|
||||
else:
|
||||
# depreciation_date = 1st of January of purchase year if annual valuation, 1st of
|
||||
# purchase month in other cases
|
||||
if self.method_period >= 12:
|
||||
if self.company_id.fiscalyear_last_month:
|
||||
asset_date = date(year=int(self.date.year),
|
||||
month=int(
|
||||
self.company_id.fiscalyear_last_month),
|
||||
day=int(
|
||||
self.company_id.fiscalyear_last_day)) + relativedelta(
|
||||
days=1) + \
|
||||
relativedelta(year=int(
|
||||
self.date.year)) # e.g. 2018-12-31 +1 -> 2019
|
||||
else:
|
||||
asset_date = datetime.strptime(
|
||||
str(self.date)[:4] + '-01-01', DF).date()
|
||||
else:
|
||||
asset_date = datetime.strptime(str(self.date)[:7] + '-01',
|
||||
DF).date()
|
||||
# if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
|
||||
if posted_depreciation_line_ids and \
|
||||
posted_depreciation_line_ids[-1].depreciation_date:
|
||||
last_depreciation_date = datetime.strptime(str(
|
||||
posted_depreciation_line_ids[-1].depreciation_date),
|
||||
DF).date()
|
||||
depreciation_date = last_depreciation_date + relativedelta(
|
||||
months=+self.method_period)
|
||||
else:
|
||||
depreciation_date = asset_date
|
||||
day = depreciation_date.day
|
||||
month = depreciation_date.month
|
||||
year = depreciation_date.year
|
||||
total_days = (year % 4) and 365 or 366
|
||||
|
||||
undone_dotation_number = self._compute_board_undone_dotation_nb(
|
||||
depreciation_date, total_days)
|
||||
|
||||
for x in range(len(posted_depreciation_line_ids),
|
||||
undone_dotation_number):
|
||||
sequence = x + 1
|
||||
amount = self._compute_board_amount(sequence, residual_amount,
|
||||
amount_to_depr,
|
||||
undone_dotation_number,
|
||||
posted_depreciation_line_ids,
|
||||
total_days,
|
||||
depreciation_date)
|
||||
|
||||
amount = self.currency_id.round(amount)
|
||||
if float_is_zero(amount,
|
||||
precision_rounding=self.currency_id.rounding):
|
||||
continue
|
||||
residual_amount -= amount
|
||||
vals = {
|
||||
'amount': amount,
|
||||
'asset_id': self.id,
|
||||
'sequence': sequence,
|
||||
'name': (self.code or '') + '/' + str(sequence),
|
||||
'remaining_value': residual_amount if residual_amount >= 0 else 0.0,
|
||||
'depreciated_value': self.value - (
|
||||
self.salvage_value + residual_amount),
|
||||
'depreciation_date': depreciation_date.strftime(DF),
|
||||
}
|
||||
commands.append((0, False, vals))
|
||||
# Considering Depr. Period as months
|
||||
depreciation_date = date(year, month, day) + relativedelta(
|
||||
months=+self.method_period)
|
||||
day = depreciation_date.day
|
||||
month = depreciation_date.month
|
||||
year = depreciation_date.year
|
||||
|
||||
self.write({'depreciation_line_ids': commands})
|
||||
last_depr_date = None
|
||||
if self.depreciation_line_ids:
|
||||
last_depr_date = max(self.depreciation_line_ids.mapped('depreciation_date'))
|
||||
if last_depr_date:
|
||||
self._compute_entries(date=last_depr_date)
|
||||
return True
|
||||
|
||||
def validate(self):
|
||||
"""Update the state to 'open' and track specific fields based on the asset's method."""
|
||||
self.write({'state': 'open'})
|
||||
field = [
|
||||
'method',
|
||||
'method_number',
|
||||
'method_period',
|
||||
'method_end',
|
||||
'method_progress_factor',
|
||||
'method_time',
|
||||
'salvage_value',
|
||||
'invoice_id',
|
||||
]
|
||||
ref_tracked_fields = self.env['account.asset.asset'].fields_get(field)
|
||||
if not self.depreciation_line_ids:
|
||||
self.compute_depreciation_board()
|
||||
for asset in self:
|
||||
tracked_fields = ref_tracked_fields.copy()
|
||||
if asset.method == 'linear':
|
||||
del (tracked_fields['method_progress_factor'])
|
||||
if asset.method_time != 'end':
|
||||
del (tracked_fields['method_end'])
|
||||
else:
|
||||
del (tracked_fields['method_number'])
|
||||
dummy, tracking_value_ids = asset._mail_track(tracked_fields,
|
||||
dict.fromkeys(
|
||||
field))
|
||||
asset.message_post(subject=_('Asset created'),
|
||||
tracking_value_ids=tracking_value_ids)
|
||||
|
||||
today_date = fields.Date.context_today(self)
|
||||
|
||||
# Split lines based on depreciation_date
|
||||
draft_lines = asset.depreciation_line_ids.filtered(lambda l: l.move_id and l.move_id.state == 'draft')
|
||||
|
||||
#Post only entries before today
|
||||
lines_to_post_now = draft_lines.filtered(lambda l: l.depreciation_date < today_date)
|
||||
moves_to_post_now = lines_to_post_now.mapped('move_id')
|
||||
if moves_to_post_now:
|
||||
moves_to_post_now._post()
|
||||
|
||||
#Set auto_post='at_date' for entries today or later
|
||||
future_lines = draft_lines.filtered(lambda l: l.depreciation_date >= today_date)
|
||||
future_moves = future_lines.mapped('move_id')
|
||||
if future_moves:
|
||||
future_moves.write({'auto_post': 'at_date'})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_disposal_moves(self):
|
||||
"""Get the disposal moves for the asset."""
|
||||
move_ids = []
|
||||
for asset in self:
|
||||
unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(
|
||||
lambda x: not x.move_check)
|
||||
if unposted_depreciation_line_ids:
|
||||
old_values = {
|
||||
'method_end': asset.method_end,
|
||||
'method_number': asset.method_number,
|
||||
}
|
||||
|
||||
# Remove all unposted depr. lines
|
||||
commands = [(2, line_id.id, False) for line_id in
|
||||
unposted_depreciation_line_ids]
|
||||
|
||||
# Create a new depr. line with the residual amount and post it
|
||||
sequence = len(asset.depreciation_line_ids) - len(
|
||||
unposted_depreciation_line_ids) + 1
|
||||
today = datetime.today().strftime(DF)
|
||||
vals = {
|
||||
'amount': asset.value_residual,
|
||||
'asset_id': asset.id,
|
||||
'sequence': sequence,
|
||||
'name': (asset.code or '') + '/' + str(sequence),
|
||||
'remaining_value': 0,
|
||||
'depreciated_value': asset.value - asset.salvage_value,
|
||||
# the asset is completely depreciated
|
||||
'depreciation_date': today,
|
||||
}
|
||||
commands.append((0, False, vals))
|
||||
asset.write(
|
||||
{'depreciation_line_ids': commands, 'method_end': today,
|
||||
'method_number': sequence})
|
||||
tracked_fields = self.env['account.asset.asset'].fields_get(
|
||||
['method_number', 'method_end'])
|
||||
changes, tracking_value_ids = asset._mail_track(
|
||||
tracked_fields, old_values)
|
||||
|
||||
if changes:
|
||||
asset.message_post(subject=_(
|
||||
'Asset sold or disposed. Accounting entry awaiting for validation.'),
|
||||
tracking_value_ids=tracking_value_ids)
|
||||
move_ids += asset.depreciation_line_ids[-1].create_move(
|
||||
post_move=False)
|
||||
|
||||
return move_ids
|
||||
|
||||
def set_to_close(self):
|
||||
"""Set the asset to close state by creating disposal moves and returning an action window to view the move(s)."""
|
||||
move_ids = self._get_disposal_moves()
|
||||
if move_ids:
|
||||
name = _('Disposal Move')
|
||||
view_mode = 'form'
|
||||
if len(move_ids) > 1:
|
||||
name = _('Disposal Moves')
|
||||
view_mode = 'list,form'
|
||||
return {
|
||||
'name': name,
|
||||
'view_mode': view_mode,
|
||||
'res_model': 'account.move',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'current',
|
||||
'res_id': move_ids[0],
|
||||
}
|
||||
# Fallback, as if we just clicked on the smartbutton
|
||||
return self.open_entries()
|
||||
|
||||
def set_to_draft(self):
|
||||
"""Set the asset's state to 'draft'."""
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
@api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check',
|
||||
'depreciation_line_ids.amount')
|
||||
def _amount_residual(self):
|
||||
"""Compute the residual value of the asset based on the total depreciation amount."""
|
||||
for record in self:
|
||||
total_amount = 0.0
|
||||
for line in record.depreciation_line_ids:
|
||||
if line.move_check:
|
||||
total_amount += line.amount
|
||||
record.value_residual = record.value - total_amount - record.salvage_value
|
||||
|
||||
@api.onchange('company_id')
|
||||
def onchange_company_id(self):
|
||||
"""Update the 'currency_id' field based on the selected 'company_id'."""
|
||||
self.currency_id = self.company_id.currency_id.id
|
||||
|
||||
@api.depends('depreciation_line_ids.move_id')
|
||||
def _entry_count(self):
|
||||
"""Compute the number of entries related to the asset based on the depreciation lines."""
|
||||
for asset in self:
|
||||
res = self.env['account.asset.depreciation.line'].search_count(
|
||||
[('asset_id', '=', asset.id), ('move_id', '!=', False)])
|
||||
asset.entry_count = res or 0
|
||||
|
||||
@api.constrains('prorata', 'method_time')
|
||||
def _check_prorata(self):
|
||||
"""Check if prorata temporis can be applied for the given asset based on the 'prorata' and 'method_time' fields."""
|
||||
if self.prorata and self.method_time != 'number':
|
||||
raise ValidationError(_(
|
||||
'Prorata temporis can be applied only for time method "number of depreciations".'))
|
||||
|
||||
@api.onchange('category_id')
|
||||
def onchange_category_id(self):
|
||||
"""Update the fields of the asset based on the selected 'category_id'."""
|
||||
vals = self.onchange_category_id_values(self.category_id.id)
|
||||
# We cannot use 'write' on an object that doesn't exist yet
|
||||
if vals:
|
||||
for k, v in vals['value'].items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def onchange_category_id_values(self, category_id):
|
||||
"""Update the fields of the asset based on the selected 'category_id'."""
|
||||
if category_id:
|
||||
category = self.env['account.asset.category'].browse(category_id)
|
||||
return {
|
||||
'value': {
|
||||
'method': category.method,
|
||||
'method_number': category.method_number,
|
||||
'method_time': category.method_time,
|
||||
'method_period': category.method_period,
|
||||
'method_progress_factor': category.method_progress_factor,
|
||||
'method_end': category.method_end,
|
||||
'prorata': category.prorata,
|
||||
'journal_id':category.journal_id.id,
|
||||
'account_asset_id':category.account_asset_id.id,
|
||||
'account_depreciation_id':category.account_depreciation_id.id,
|
||||
'account_depreciation_expense_id':category.account_depreciation_expense_id.id,
|
||||
'account_analytic_id':category.account_analytic_id.id
|
||||
}
|
||||
}
|
||||
|
||||
@api.onchange('method_time')
|
||||
def onchange_method_time(self):
|
||||
"""Update the 'prorata' field based on the selected 'method_time' value."""
|
||||
if self.method_time != 'number':
|
||||
self.prorata = False
|
||||
|
||||
def copy_data(self, default=None):
|
||||
"""Copies the data of the current record with the option to override default values."""
|
||||
if default is None:
|
||||
default = {}
|
||||
default['name'] = self.name + _(' (copy)')
|
||||
return super(AccountAssetAsset, self).copy_data(default)
|
||||
|
||||
def _compute_entries(self, date, group_entries=False):
|
||||
"""Compute depreciation entries for the given date."""
|
||||
depreciation_ids = self.env['account.asset.depreciation.line'].search([
|
||||
('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
|
||||
('move_check', '=', False)])
|
||||
if group_entries:
|
||||
return depreciation_ids.create_grouped_move()
|
||||
return depreciation_ids.create_move()
|
||||
|
||||
def open_entries(self):
|
||||
"""Return a dictionary to open journal entries related to the asset."""
|
||||
move_ids = []
|
||||
for asset in self:
|
||||
for depreciation_line in asset.depreciation_line_ids:
|
||||
if depreciation_line.move_id:
|
||||
move_ids.append(depreciation_line.move_id.id)
|
||||
return {
|
||||
'name': _('Journal Entries'),
|
||||
'view_mode': 'list,form',
|
||||
'res_model': 'account.move',
|
||||
'views': [(self.env.ref('account.view_move_tree').id, 'list'), (False, 'form')],
|
||||
'view_id': False,
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('id', 'in', move_ids)],
|
||||
}
|
||||
|
||||
def action_save_model(self):
|
||||
return{
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Asset Model'),
|
||||
'res_model': 'account.asset.category',
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'context': {'default_price': self.value,
|
||||
'default_method_time':self.method_time,
|
||||
'default_method_end':self.method_end,
|
||||
'default_method_number':self.method_number,
|
||||
'default_method_period':self.method_period,
|
||||
'default_method':self.method,
|
||||
'default_company_id':self.company_id.id,
|
||||
'default_method_progress_factor':self.method_progress_factor,
|
||||
'default_prorata':self.prorata,
|
||||
'default_group_entries':self.group_entries,
|
||||
'default_open_asset':self.open_asset,
|
||||
'default_account_analytic_id':self.account_analytic_id.id,
|
||||
'default_account_depreciation_expense_id':self.account_depreciation_expense_id.id,
|
||||
'default_account_depreciation_id':self.account_depreciation_id.id,
|
||||
'default_account_asset_id':self.account_asset_id.id,
|
||||
'default_journal_id':self.journal_id.id,
|
||||
'default_asset_id': self.id,
|
||||
}
|
||||
}
|
||||
|
||||
def action_cancel_assets(self):
|
||||
for asset in self:
|
||||
for move in asset.depreciation_line_ids.mapped('move_id'):
|
||||
if move.state == 'posted':
|
||||
# Force to draft
|
||||
move.button_draft() # or move.state = 'draft' if button_draft is restricted
|
||||
move.unlink()
|
||||
|
||||
# Delete all depreciation lines
|
||||
asset.depreciation_line_ids.unlink()
|
||||
|
||||
# Reset state
|
||||
asset.state = 'cancelled'
|
||||
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountAssetCategory(models.Model):
|
||||
_name = 'account.asset.category'
|
||||
_description = 'Asset category'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(required=True, index=True, string="Asset Type")
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company)
|
||||
price = fields.Monetary(string='Price', required=True)
|
||||
currency_id = fields.Many2one("res.currency",
|
||||
default=lambda self: self.env[
|
||||
'res.currency'].search(
|
||||
[('name', '=', 'USD')]).id,
|
||||
readonly=True, hide=True)
|
||||
account_analytic_id = fields.Many2one('account.analytic.account',
|
||||
string='Analytic Account',
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
account_asset_id = fields.Many2one('account.account',
|
||||
string='Asset Account', required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=', 'liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=', 'liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used to record the purchase of the asset at its original price.")
|
||||
account_depreciation_id = fields.Many2one(
|
||||
'account.account', string='Depreciation Account',
|
||||
required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=', 'liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=', 'liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used in the depreciation entries, to decrease the asset value.")
|
||||
account_depreciation_expense_id = fields.Many2one(
|
||||
'account.account', string='Expense Account',
|
||||
required=True,
|
||||
domain="[('account_type', '!=', 'asset_receivable'),('account_type', '!=','liability_payable'),('account_type', '!=', 'asset_cash'),('account_type', '!=','liability_credit_card'),('active', '=', True)]",
|
||||
help="Account used in the periodical entries, to record a part of the asset as expense.")
|
||||
journal_id = fields.Many2one('account.journal', string='Journal',
|
||||
required=True)
|
||||
method = fields.Selection(
|
||||
[('linear', 'Straight Line'), ('degressive', 'Declining')],
|
||||
string='Computation Method', required=True, default='linear',
|
||||
help="Choose the method to use to compute the amount of depreciation lines.\n"
|
||||
" * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n"
|
||||
" * Degressive: Calculated on basis of: Residual Value * Degressive Factor")
|
||||
method_number = fields.Integer(string='Number of Depreciations', default=5,
|
||||
help="The number of depreciations needed to depreciate your asset")
|
||||
method_period = fields.Integer(string='Period Length', default=1,
|
||||
help="State here the time between 2 depreciations, in months",
|
||||
required=True)
|
||||
method_progress_factor = fields.Float('Degressive Factor', default=0.3)
|
||||
method_time = fields.Selection(
|
||||
[('number', 'Number of Entries'), ('end', 'Ending Date')],
|
||||
string='Time Method', required=True, default='number',
|
||||
help="Choose the method to use to compute the dates and number of entries.\n"
|
||||
" * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n"
|
||||
" * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.")
|
||||
method_end = fields.Date('Ending date')
|
||||
prorata = fields.Boolean(string='Prorata Temporis',
|
||||
help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first of January')
|
||||
open_asset = fields.Boolean(string='Auto-confirm Assets',
|
||||
help="Check this if you want to automatically confirm the assets of this category when created by invoices.")
|
||||
group_entries = fields.Boolean(string='Group Journal Entries',
|
||||
help="Check this if you want to group the generated entries by categories.")
|
||||
type = fields.Selection([('sale', 'Sale: Revenue Recognition'),
|
||||
('purchase', 'Purchase: Asset')], required=True,
|
||||
index=True, default='purchase')
|
||||
|
||||
@api.onchange('account_asset_id')
|
||||
def onchange_account_asset(self):
|
||||
"""Onchange method triggered when the 'account_asset_id' field is modified.
|
||||
Updates 'account_depreciation_id' or 'account_depreciation_expense_id' based on the 'type' field value."""
|
||||
if self.type == "purchase":
|
||||
self.account_depreciation_id = self.account_asset_id
|
||||
elif self.type == "sale":
|
||||
self.account_depreciation_expense_id = self.account_asset_id
|
||||
|
||||
@api.onchange('type')
|
||||
def onchange_type(self):
|
||||
"""Update the 'prorata' and 'method_period' fields based on the value of the 'type' field."""
|
||||
if self.type == 'sale':
|
||||
self.prorata = True
|
||||
self.method_period = 1
|
||||
else:
|
||||
self.method_period = 12
|
||||
|
||||
@api.onchange('method_time')
|
||||
def _onchange_method_time(self):
|
||||
"""Update the 'prorata' field based on the value of the 'method_time' field.
|
||||
Set 'prorata' to False if 'method_time' is not equal to 'number'."""
|
||||
if self.method_time != 'number':
|
||||
self.prorata = False
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
record = super().create(vals)
|
||||
asset_id = self.env.context.get('default_asset_id')
|
||||
if asset_id:
|
||||
asset = self.env['account.asset.asset'].browse(asset_id)
|
||||
asset.category_id = record.id
|
||||
return record
|
||||
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class AccountAssetDepreciationLine(models.Model):
|
||||
"""Model for managing asset depreciation lines in the accounting system."""
|
||||
_name = 'account.asset.depreciation.line'
|
||||
_description = 'Asset depreciation line'
|
||||
|
||||
name = fields.Char(string='Depreciation Name', required=True, index=True)
|
||||
sequence = fields.Integer(required=True)
|
||||
asset_id = fields.Many2one('account.asset.asset', string='Asset',
|
||||
required=True, ondelete='cascade')
|
||||
parent_state = fields.Selection(related='asset_id.state',
|
||||
string='State of Asset')
|
||||
amount = fields.Float(string='Current Depreciation',
|
||||
required=True)
|
||||
remaining_value = fields.Float(string='Next Period Depreciation',
|
||||
required=True)
|
||||
depreciated_value = fields.Float(string='Cumulative Depreciation',
|
||||
required=True)
|
||||
depreciation_date = fields.Date('Depreciation Date', index=True)
|
||||
move_id = fields.Many2one('account.move', string='Depreciation Entry')
|
||||
move_check = fields.Boolean(compute='_get_move_check', string='Linked',
|
||||
store=True)
|
||||
move_posted_check = fields.Boolean(compute='_get_move_posted_check',
|
||||
string='Posted', store=True)
|
||||
|
||||
@api.depends('move_id')
|
||||
def _get_move_check(self):
|
||||
"""Compute the 'move_check' field based on the presence of 'move_id'
|
||||
for each record in the 'AccountAssetDepreciationLine' class."""
|
||||
for line in self:
|
||||
line.move_check = bool(line.move_id)
|
||||
|
||||
@api.depends('move_id.state')
|
||||
def _get_move_posted_check(self):
|
||||
"""Compute the 'move_posted_check' field based on the state of 'move_id'
|
||||
for each record in the 'AccountAssetDepreciationLine' class."""
|
||||
for line in self:
|
||||
line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False
|
||||
|
||||
def create_move(self, post_move=True):
|
||||
"""Create accounting moves for asset depreciation lines."""
|
||||
created_moves = self.env['account.move']
|
||||
prec = self.env['decimal.precision'].precision_get('Account')
|
||||
if self.mapped('move_id'):
|
||||
raise UserError(_(
|
||||
'This depreciation is already linked to a journal entry! Please post or delete it.'))
|
||||
for line in self:
|
||||
asset_id = line.asset_id
|
||||
depreciation_date = self.env.context.get(
|
||||
'depreciation_date') or line.depreciation_date or fields.Date.context_today(
|
||||
self)
|
||||
company_currency = asset_id.company_id.currency_id
|
||||
current_currency = asset_id.currency_id
|
||||
amount = current_currency._convert(line.amount, company_currency,
|
||||
line.asset_id.company_id,
|
||||
depreciation_date)
|
||||
asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids))
|
||||
partner = self.env['res.partner']._find_accounting_partner(line.asset_id.partner_id)
|
||||
move_line_1 = {
|
||||
'name': asset_name,
|
||||
'account_id': asset_id.account_depreciation_id.id,
|
||||
'debit': 0.0 if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else -amount,
|
||||
'credit': amount if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else 0.0,
|
||||
'journal_id': asset_id.journal_id.id,
|
||||
'partner_id': partner.id,
|
||||
'currency_id': company_currency != current_currency and current_currency.id or company_currency.id,
|
||||
'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0,
|
||||
}
|
||||
move_line_2 = {
|
||||
'name': asset_name,
|
||||
'account_id': asset_id.account_depreciation_expense_id.id,
|
||||
'credit': 0.0 if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else -amount,
|
||||
'debit': amount if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else 0.0,
|
||||
'journal_id': asset_id.journal_id.id,
|
||||
'partner_id': partner.id,
|
||||
'currency_id': company_currency != current_currency and current_currency.id or company_currency.id,
|
||||
'amount_currency': company_currency != current_currency and line.amount or 0.0,
|
||||
}
|
||||
line_ids = [(0, 0, {
|
||||
'account_id': asset_id.account_depreciation_id.id,
|
||||
'partner_id': partner.id,
|
||||
'credit': amount if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else 0.0,
|
||||
}), (0, 0, {
|
||||
'account_id': asset_id.account_depreciation_expense_id.id,
|
||||
'partner_id': partner.id,
|
||||
'debit': amount if float_compare(amount, 0.0,
|
||||
precision_digits=prec) > 0 else 0.0,
|
||||
})]
|
||||
move = self.env['account.move'].create({
|
||||
'ref': line.asset_id.code,
|
||||
'date': depreciation_date or False,
|
||||
'journal_id': asset_id.journal_id.id,
|
||||
'line_ids': line_ids,
|
||||
})
|
||||
for move_line in move.line_ids:
|
||||
if move_line.account_id.id == move_line_1['account_id']:
|
||||
move_line.write({'credit': move_line_1['credit'],
|
||||
'debit': move_line_1['debit']})
|
||||
elif move_line.account_id.id == move_line_2['account_id']:
|
||||
move_line.write({'debit': move_line_2['debit'],
|
||||
'credit': move_line_2['credit']})
|
||||
if move.line_ids.filtered(
|
||||
lambda x: x.name == 'Automatic Balancing Line'):
|
||||
move.line_ids.filtered(
|
||||
lambda x: x.name == 'Automatic Balancing Line').unlink()
|
||||
line.write({'move_id': move.id, 'move_check': True})
|
||||
created_moves |= move
|
||||
|
||||
if post_move and created_moves:
|
||||
created_moves.filtered(lambda m: any(
|
||||
m.asset_depreciation_ids.mapped(
|
||||
'asset_id.open_asset'))).post()
|
||||
return [x.id for x in created_moves]
|
||||
|
||||
def create_grouped_move(self, post_move=True):
|
||||
"""Create a grouped accounting move for asset depreciation lines."""
|
||||
if not self.exists():
|
||||
return []
|
||||
created_moves = self.env['account.move']
|
||||
category_id = self[
|
||||
0].asset_id.category_id # we can suppose that all lines have the same category
|
||||
depreciation_date = self.env.context.get(
|
||||
'depreciation_date') or fields.Date.context_today(self)
|
||||
amount = 0.0
|
||||
for line in self:
|
||||
# Sum amount of all depreciation lines
|
||||
company_currency = line.asset_id.company_id.currency_id
|
||||
current_currency = line.asset_id.currency_id
|
||||
amount += current_currency.compute(line.amount, company_currency)
|
||||
|
||||
name = category_id.name + _(' (grouped)')
|
||||
move_line_1 = {
|
||||
'name': name,
|
||||
'account_id': category_id.account_depreciation_id.id,
|
||||
'debit': 0.0,
|
||||
'credit': amount,
|
||||
'journal_id': category_id.journal_id.id,
|
||||
'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False,
|
||||
}
|
||||
move_line_2 = {
|
||||
'name': name,
|
||||
'account_id': category_id.account_depreciation_expense_id.id,
|
||||
'credit': 0.0,
|
||||
'debit': amount,
|
||||
'journal_id': category_id.journal_id.id,
|
||||
'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False,
|
||||
}
|
||||
move_vals = {
|
||||
'ref': category_id.name,
|
||||
'date': depreciation_date or False,
|
||||
'journal_id': category_id.journal_id.id,
|
||||
'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
|
||||
}
|
||||
move = self.env['account.move'].create(move_vals)
|
||||
self.write({'move_id': move.id, 'move_check': True})
|
||||
created_moves |= move
|
||||
|
||||
if post_move and created_moves:
|
||||
self.post_lines_and_close_asset()
|
||||
created_moves.post()
|
||||
return [x.id for x in created_moves]
|
||||
|
||||
def post_lines_and_close_asset(self):
|
||||
# we re-evaluate the assets to determine whether we can close them
|
||||
# `message_post` invalidates the (whole) cache
|
||||
# preprocess the assets and lines in which a message should be posted,
|
||||
# and then post in batch will prevent the re-fetch of the same data over and over.
|
||||
assets_to_close = self.env['account.asset.asset']
|
||||
for line in self:
|
||||
asset = line.asset_id
|
||||
if asset.currency_id.is_zero(asset.value_residual):
|
||||
assets_to_close |= asset
|
||||
self.log_message_when_posted()
|
||||
assets_to_close.write({'state': 'close'})
|
||||
for asset in assets_to_close:
|
||||
asset.message_post(body=_("Document closed."))
|
||||
|
||||
def log_message_when_posted(self):
|
||||
"""Format and post messages for asset depreciation lines that are posted."""
|
||||
def _format_message(message_description, tracked_values):
|
||||
message = ''
|
||||
if message_description:
|
||||
message = '<span>%s</span>' % message_description
|
||||
for name, values in tracked_values.items():
|
||||
message += '<div> • <b>%s</b>: ' % name
|
||||
message += '%s</div>' % values
|
||||
return message
|
||||
|
||||
# `message_post` invalidates the (whole) cache
|
||||
# preprocess the assets in which messages should be posted,
|
||||
# and then post in batch will prevent the re-fetch of the same data over and over.
|
||||
assets_to_post = {}
|
||||
for line in self:
|
||||
if line.move_id and line.move_id.state == 'draft':
|
||||
partner_name = line.asset_id.partner_id.name
|
||||
currency_name = line.asset_id.currency_id.name
|
||||
msg_values = {_('Currency'): currency_name,
|
||||
_('Amount'): line.amount}
|
||||
if partner_name:
|
||||
msg_values[_('Partner')] = partner_name
|
||||
msg = _format_message(_('Depreciation line posted.'),
|
||||
msg_values)
|
||||
assets_to_post.setdefault(line.asset_id, []).append(msg)
|
||||
for asset, messages in assets_to_post.items():
|
||||
for msg in messages:
|
||||
asset.message_post(body=msg)
|
||||
|
||||
# def unlink(self):
|
||||
# """Check if the depreciation line is linked to a posted move before deletion."""
|
||||
# for record in self:
|
||||
# if record.move_check:
|
||||
# if record.asset_id.category_id.type == 'purchase':
|
||||
# msg = _("You cannot delete posted depreciation lines.")
|
||||
# else:
|
||||
# msg = _("You cannot delete posted installment lines.")
|
||||
# raise UserError(msg)
|
||||
# return super(AccountAssetDepreciationLine, self).unlink()
|
||||
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
"""Update the 'rowdata' field for the specified record."""
|
||||
_name = 'account.bank.statement.line'
|
||||
_inherit = ['account.bank.statement.line', 'mail.thread',
|
||||
'mail.activity.mixin', 'analytic.mixin']
|
||||
|
||||
lines_widget = fields.Char(string="Lines Widget")
|
||||
account_id = fields.Many2one('account.account', string='Account')
|
||||
tax_ids = fields.Many2many('account.tax')
|
||||
form_name = fields.Char()
|
||||
form_balance = fields.Monetary(currency_field='currency_id')
|
||||
rowdata = fields.Json(string="RowData")
|
||||
matchRowdata = fields.Json(string="MatchRowData")
|
||||
record_id = fields.Integer()
|
||||
company_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id', readonly=True,
|
||||
)
|
||||
bank_state = fields.Selection(selection=[('invalid', 'Invalid'),
|
||||
('valid', 'Valid'),
|
||||
('reconciled', 'Reconciled')],
|
||||
compute='_compute_state', store=True)
|
||||
reconcile_models_widget = fields.Char()
|
||||
lines_widget_json = fields.Json(store=True)
|
||||
|
||||
@api.model
|
||||
def update_rowdata(self, record_id):
|
||||
"""Update the 'rowdata' field for the specified record."""
|
||||
request.session['record_id'] = record_id
|
||||
|
||||
@api.model
|
||||
def update_match_row_data(self, resId):
|
||||
"""Update the match row data for a specific record identified by the given resId."""
|
||||
request.session['resId'] = resId
|
||||
move_record = self.env['account.move.line'].browse(resId)
|
||||
move_record_values = {
|
||||
'id': move_record.id,
|
||||
'account_id': move_record.account_id.id,
|
||||
'account_name': move_record.account_id.name,
|
||||
'account_code': move_record.account_id.code,
|
||||
'partner_id': move_record.partner_id,
|
||||
'partner_name': move_record.partner_id.name,
|
||||
'date': move_record.date,
|
||||
'move_id': move_record.move_id,
|
||||
'move_name': move_record.move_id.name,
|
||||
'name': move_record.name,
|
||||
'amount_residual_currency': move_record.amount_residual_currency,
|
||||
'amount_residual': move_record.amount_residual,
|
||||
'currency_id': move_record.currency_id.id,
|
||||
'currency_symbol': move_record.currency_id.symbol
|
||||
}
|
||||
return move_record_values
|
||||
|
||||
def button_validation(self, async_action=False):
|
||||
"""Ensure the current recordset holds a single record and mark it as reconciled."""
|
||||
self.ensure_one()
|
||||
self.is_reconciled = True
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
def button_reset(self):
|
||||
"""Reset the current bank statement line if it is in a 'reconciled' state."""
|
||||
self.ensure_one()
|
||||
if self.bank_state == 'reconciled':
|
||||
self.action_undo_reconciliation()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
def button_to_check(self, async_action=True):
|
||||
"""Ensure the current recordset holds a single record, validate the bank
|
||||
state, and mark the move as 'to check'."""
|
||||
self.ensure_one()
|
||||
if self.bank_state == 'valid':
|
||||
self.button_validation(async_action=async_action)
|
||||
self.move_id.to_check = True
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
def button_set_as_checked(self):
|
||||
"""Mark the associated move as 'not to check' by setting 'to_check' to False."""
|
||||
self.ensure_one()
|
||||
self.move_id.to_check = False
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_statement_line(self, record_id):
|
||||
"""Retrieve and format bank statement line details based on the provided record ID."""
|
||||
statement_line_records = self.env[
|
||||
'account.bank.statement.line'].search_read([('id', '=', record_id)])
|
||||
result_list = []
|
||||
for record in statement_line_records:
|
||||
move_id = record.get('move_id', False)
|
||||
partner_id = record.get('partner_id', False)
|
||||
date = record.get('date', False)
|
||||
amount = record.get('amount', False)
|
||||
currency_id = record.get('currency_id', False)
|
||||
payment_ref = record.get("payment_ref", False)
|
||||
bank_state = record.get("bank_state", False)
|
||||
id = record.get("id", False)
|
||||
if move_id:
|
||||
move_record = self.env['account.move.line'].search(
|
||||
[('move_id', '=', move_id[0])], limit=1)
|
||||
currency_symbol = self.env['res.currency'].browse(
|
||||
currency_id[0])
|
||||
account_id = move_record.account_id
|
||||
date_str = date.strftime('%Y-%m-%d') if date else None
|
||||
result_list.append({
|
||||
'id': id,
|
||||
'move_id': move_id,
|
||||
'partner_id': partner_id,
|
||||
'account_id': account_id.id,
|
||||
'account_name': account_id.name,
|
||||
'account_code': account_id.code,
|
||||
'date': date_str,
|
||||
'amount': amount,
|
||||
'currency_symbol': currency_symbol.symbol,
|
||||
'payment_ref': payment_ref,
|
||||
'bank_state': bank_state,
|
||||
})
|
||||
# Update the account_id for the current record
|
||||
self.env['account.bank.statement.line'].browse(
|
||||
record['id']).write({'account_id': account_id.id})
|
||||
return result_list
|
||||
|
||||
@api.depends('account_id')
|
||||
def _compute_state(self):
|
||||
"""Compute the state of bank transactions based on the account's
|
||||
reconciliation status and journal settings."""
|
||||
for record in self:
|
||||
if record.is_reconciled:
|
||||
record.bank_state = 'reconciled'
|
||||
else:
|
||||
suspense_account = record.journal_id.suspense_account_id
|
||||
if suspense_account in record.account_id:
|
||||
record.bank_state = 'invalid'
|
||||
else:
|
||||
record.bank_state = 'valid'
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models
|
||||
|
||||
|
||||
class Followup(models.Model):
|
||||
"""Model for managing account follow-ups."""
|
||||
_name = 'account.followup'
|
||||
_description = 'Account Follow-up'
|
||||
_rec_name = 'name'
|
||||
|
||||
followup_line_ids = fields.One2many('followup.line', 'followup_id',
|
||||
'Follow-up', copy=True)
|
||||
company_id = fields.Many2one('res.company', 'Company',
|
||||
default=lambda self: self.env.company)
|
||||
name = fields.Char(related='company_id.name', readonly=True)
|
||||
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models, _
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
"""Module inherited for adding the reconcile method in the account
|
||||
journal"""
|
||||
_inherit = "account.journal"
|
||||
|
||||
multiple_invoice_ids = fields.One2many('multiple.invoice',
|
||||
'journal_id',
|
||||
string='Multiple Invoice')
|
||||
multiple_invoice_type = fields.Selection(
|
||||
[('text', 'Text'), ('watermark', 'Watermark')], required=True,
|
||||
default='text', string="Display Type")
|
||||
text_position = fields.Selection([
|
||||
('header', 'Header'),
|
||||
('footer', 'Footer'),
|
||||
('body', 'Document Body')
|
||||
], required=True, default='header', string='Text Position')
|
||||
body_text_position = fields.Selection([
|
||||
('tl', 'Top Left'),
|
||||
('tr', 'Top Right'),
|
||||
('bl', 'Bottom Left'),
|
||||
('br', 'Bottom Right'),
|
||||
], default='tl', string='Body Text Position')
|
||||
text_align = fields.Selection([
|
||||
('right', 'Right'),
|
||||
('left', 'Left'),
|
||||
('center', 'Center'),
|
||||
], default='right', string='Center Align Text Position')
|
||||
layout = fields.Char(string="Layout",
|
||||
related="company_id.external_report_layout_id.key")
|
||||
|
||||
def action_open_reconcile(self):
|
||||
"""Open the reconciliation view based on the type of the account journal."""
|
||||
self.ensure_one()
|
||||
if self.type in ('bank', 'cash'):
|
||||
views = [
|
||||
(self.env.ref(
|
||||
'base_accounting_kit.account_bank_statement_line_view_kanban').id,
|
||||
'kanban'),
|
||||
(self.env.ref(
|
||||
'base_accounting_kit.account_bank_statement_line_view_tree').id,
|
||||
'list'), # Include tree view
|
||||
]
|
||||
context = {
|
||||
'default_journal_id': self.id,
|
||||
'search_default_journal_id': self.id,
|
||||
}
|
||||
kanban_first = True
|
||||
name = None
|
||||
extra_domain = None
|
||||
return {
|
||||
'name': name or _("Bank Reconciliation"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.bank.statement.line',
|
||||
'context': context,
|
||||
'search_view_id': [
|
||||
self.env.ref(
|
||||
'base_accounting_kit.account_bank_statement_line_view_search').id,
|
||||
'search'],
|
||||
'view_mode': 'kanban,list' if kanban_first else 'list,kanban',
|
||||
'views': views if kanban_first else views[::-1],
|
||||
'domain': [('state', '!=', 'cancel')] + (extra_domain or []),
|
||||
'help': _("""
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Nothing to do here!
|
||||
</p>
|
||||
<p>
|
||||
No transactions matching your filters were found.
|
||||
</p>
|
||||
"""),
|
||||
}
|
||||
else:
|
||||
# Open reconciliation view for customers/suppliers
|
||||
action_context = {'show_mode_selector': False,
|
||||
'company_ids': self.mapped('company_id').ids}
|
||||
if self.type == 'sale':
|
||||
action_context.update({'mode': 'customers'})
|
||||
elif self.type == 'purchase':
|
||||
action_context.update({'mode': 'suppliers'})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'manual_reconciliation_view',
|
||||
'context': action_context,
|
||||
}
|
||||
|
||||
def action_import_wizard(self):
|
||||
"""Function to open wizard"""
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'import.bank.statement',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_journal_id': self.id,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
"""Inherits from the account.move model for adding the depreciation
|
||||
field to the account"""
|
||||
_inherit = 'account.move'
|
||||
|
||||
has_due = fields.Boolean(string='Has due')
|
||||
is_warning = fields.Boolean(string='Is warning')
|
||||
due_amount = fields.Float(string="Due Amount",
|
||||
related='partner_id.due_amount')
|
||||
recurring_ref = fields.Char(string='Recurring Ref')
|
||||
asset_depreciation_ids = fields.One2many('account.asset.depreciation.line',
|
||||
'move_id',
|
||||
string='Assets Depreciation Lines')
|
||||
to_check = fields.Boolean(string='To Check', tracking=True,
|
||||
help="If this checkbox is ticked, it means that the user was not sure of all the related "
|
||||
"information at the time of the creation of the move and that the move needs to be "
|
||||
"checked again.",
|
||||
)
|
||||
|
||||
def button_cancel(self):
|
||||
"""Button action to cancel the transfer"""
|
||||
for move in self:
|
||||
for line in move.asset_depreciation_ids:
|
||||
line.move_posted_check = False
|
||||
return super(AccountMove, self).button_cancel()
|
||||
|
||||
def post(self):
|
||||
"""Supering the post method to mapped the asset depreciation records"""
|
||||
self.mapped('asset_depreciation_ids').post_lines_and_close_asset()
|
||||
return super(AccountMove, self).action_post()
|
||||
|
||||
@api.model
|
||||
def _refund_cleanup_lines(self, lines):
|
||||
"""Supering the refund cleanup lines to check the asset category """
|
||||
result = super(AccountMove, self)._refund_cleanup_lines(lines)
|
||||
for i, line in enumerate(lines):
|
||||
for name, field in line._fields.items():
|
||||
if name == 'asset_category_id':
|
||||
result[i][2][name] = False
|
||||
break
|
||||
return result
|
||||
|
||||
def action_cancel(self):
|
||||
"""Action perform to cancel the asset record"""
|
||||
res = super(AccountMove, self).action_cancel()
|
||||
self.env['account.asset.asset'].sudo().search(
|
||||
[('invoice_id', 'in', self.ids)]).write({'active': False})
|
||||
return res
|
||||
|
||||
def action_post(self):
|
||||
"""To check the selected customers due amount is exceed than blocking stage"""
|
||||
pay_type = ['out_invoice', 'out_refund', 'out_receipt']
|
||||
for rec in self:
|
||||
if rec.partner_id.active_limit and rec.move_type in pay_type \
|
||||
and rec.partner_id.enable_credit_limit:
|
||||
if rec.due_amount >= rec.partner_id.blocking_stage and rec.partner_id.blocking_stage != 0:
|
||||
raise UserError(_(
|
||||
"%s is in Blocking Stage and "
|
||||
"has a due amount of %s %s to pay") % (
|
||||
rec.partner_id.name, rec.due_amount,
|
||||
rec.currency_id.symbol))
|
||||
|
||||
result = super(AccountMove, self).action_post()
|
||||
for inv in self:
|
||||
context = dict(self.env.context)
|
||||
# Within the context of an invoice,
|
||||
# this default value is for the type of the invoice, not the type
|
||||
# of the asset. This has to be cleaned from the context before
|
||||
# creating the asset,otherwise it tries to create the asset with
|
||||
# the type of the invoice.
|
||||
context.pop('default_type', None)
|
||||
inv.invoice_line_ids.with_context(context).asset_create()
|
||||
return result
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def check_due(self):
|
||||
"""To show the due amount and warning stage"""
|
||||
if self.partner_id and self.partner_id.due_amount > 0 \
|
||||
and self.partner_id.active_limit \
|
||||
and self.partner_id.enable_credit_limit:
|
||||
self.has_due = True
|
||||
else:
|
||||
self.has_due = False
|
||||
if self.partner_id and self.partner_id.active_limit \
|
||||
and self.partner_id.enable_credit_limit:
|
||||
if self.due_amount >= self.partner_id.warning_stage:
|
||||
if self.partner_id.warning_stage != 0:
|
||||
self.is_warning = True
|
||||
else:
|
||||
self.is_warning = False
|
||||
@@ -0,0 +1,211 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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/>.
|
||||
#
|
||||
#############################################################################
|
||||
import ast
|
||||
from datetime import datetime
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
"""Define a model for account invoice lines with fields related to assets and their management."""
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
asset_category_id = fields.Many2one('account.asset.category',
|
||||
string='Asset Category')
|
||||
asset_start_date = fields.Date(string='Asset Start Date',
|
||||
compute='_get_asset_date', readonly=True,
|
||||
store=True)
|
||||
asset_end_date = fields.Date(string='Asset End Date',
|
||||
compute='_get_asset_date', readonly=True,
|
||||
store=True)
|
||||
asset_mrr = fields.Float(string='Monthly Recurring Revenue',
|
||||
compute='_get_asset_date',
|
||||
readonly=True, digits='Account',
|
||||
store=True)
|
||||
|
||||
@api.depends('asset_category_id', 'move_id.invoice_date')
|
||||
def _get_asset_date(self):
|
||||
"""Returns the asset_start_date and the asset_end_date of the Asset"""
|
||||
for record in self:
|
||||
record.asset_mrr = 0
|
||||
record.asset_start_date = False
|
||||
record.asset_end_date = False
|
||||
cat = record.asset_category_id
|
||||
if cat:
|
||||
if cat.method_number == 0 or cat.method_period == 0:
|
||||
raise UserError(_(
|
||||
'The number of depreciations or the period length of '
|
||||
'your asset category cannot be null.'))
|
||||
months = cat.method_number * cat.method_period
|
||||
if record.move_id in ['out_invoice', 'out_refund']:
|
||||
record.asset_mrr = record.price_subtotal_signed / months
|
||||
if record.move_id.invoice_date:
|
||||
start_date = datetime.strptime(
|
||||
str(record.move_id.invoice_date), DF).replace(day=1)
|
||||
end_date = (start_date + relativedelta(months=months,
|
||||
days=-1))
|
||||
record.asset_start_date = start_date.strftime(DF)
|
||||
record.asset_end_date = end_date.strftime(DF)
|
||||
|
||||
def asset_create(self):
|
||||
"""Create function for the asset and its associated properties"""
|
||||
for record in self:
|
||||
if record.asset_category_id:
|
||||
vals = {
|
||||
'name': record.name,
|
||||
'code': record.move_id.name or False,
|
||||
'category_id': record.asset_category_id.id,
|
||||
'value': record.price_subtotal,
|
||||
'partner_id': record.partner_id.id,
|
||||
'company_id': record.move_id.company_id.id,
|
||||
'currency_id': record.move_id.company_currency_id.id,
|
||||
'date': record.move_id.invoice_date,
|
||||
'invoice_id': record.move_id.id,
|
||||
}
|
||||
changed_vals = record.env[
|
||||
'account.asset.asset'].onchange_category_id_values(
|
||||
vals['category_id'])
|
||||
vals.update(changed_vals['value'])
|
||||
asset = record.env['account.asset.asset'].create(vals)
|
||||
if record.asset_category_id.open_asset:
|
||||
asset.validate()
|
||||
return True
|
||||
|
||||
@api.depends('asset_category_id')
|
||||
def onchange_asset_category_id(self):
|
||||
"""On change function based on the category and its updates the
|
||||
account status"""
|
||||
if self.move_id.move_type == 'out_invoice' and self.asset_category_id:
|
||||
self.account_id = self.asset_category_id.account_asset_id.id
|
||||
elif self.move_id.move_type == 'in_invoice' and self.asset_category_id:
|
||||
self.account_id = self.asset_category_id.account_asset_id.id
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_uom_id(self):
|
||||
"""Onchange function for product that's call the UOM compute function
|
||||
and the asset category function"""
|
||||
result = super(AccountInvoiceLine, self)._compute_product_uom_id()
|
||||
self.onchange_asset_category_id()
|
||||
return result
|
||||
|
||||
@api.depends('product_id')
|
||||
def _onchange_product_id(self):
|
||||
"""Onchange product values and it's associated with the move types"""
|
||||
vals = super(AccountInvoiceLine, self)._compute_price_unit()
|
||||
if self.product_id:
|
||||
if self.move_id.move_type == 'out_invoice':
|
||||
self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id
|
||||
elif self.move_id.move_type == 'in_invoice':
|
||||
self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id
|
||||
return vals
|
||||
|
||||
def _set_additional_fields(self, invoice):
|
||||
"""The function adds additional fields that based on the invoice
|
||||
move types"""
|
||||
if not self.asset_category_id:
|
||||
if invoice.type == 'out_invoice':
|
||||
self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id
|
||||
elif invoice.type == 'in_invoice':
|
||||
self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
|
||||
self.onchange_asset_category_id()
|
||||
super(AccountInvoiceLine, self)._set_additional_fields(invoice)
|
||||
|
||||
def get_invoice_line_account(self, type, product, fpos, company):
|
||||
""""It returns the invoice line and callback"""
|
||||
return product.asset_category_id.account_asset_id or super(
|
||||
AccountInvoiceLine, self).get_invoice_line_account(type, product,
|
||||
fpos, company)
|
||||
|
||||
@api.model
|
||||
def _query_get(self, domain=None):
|
||||
"""Used to add domain constraints to the query"""
|
||||
self.check_access_rights('read')
|
||||
|
||||
context = dict(self._context or {})
|
||||
domain = domain or []
|
||||
if not isinstance(domain, (list, tuple)):
|
||||
domain = ast.literal_eval(domain)
|
||||
|
||||
date_field = 'date'
|
||||
if context.get('aged_balance'):
|
||||
date_field = 'date_maturity'
|
||||
if context.get('date_to'):
|
||||
domain += [(date_field, '<=', context['date_to'])]
|
||||
if context.get('date_from'):
|
||||
if not context.get('strict_range'):
|
||||
domain += ['|', (date_field, '>=', context['date_from']),
|
||||
('account_id.include_initial_balance', '=', True)]
|
||||
elif context.get('initial_bal'):
|
||||
domain += [(date_field, '<', context['date_from'])]
|
||||
else:
|
||||
domain += [(date_field, '>=', context['date_from'])]
|
||||
|
||||
if context.get('journal_ids'):
|
||||
domain += [('journal_id', 'in', context['journal_ids'])]
|
||||
|
||||
state = context.get('state')
|
||||
if state and state.lower() != 'all':
|
||||
domain += [('parent_state', '=', state)]
|
||||
|
||||
if context.get('company_id'):
|
||||
domain += [('company_id', '=', context['company_id'])]
|
||||
elif context.get('allowed_company_ids'):
|
||||
domain += [('company_id', 'in', self.env.companies.ids)]
|
||||
else:
|
||||
domain += [('company_id', '=', self.env.company.id)]
|
||||
|
||||
if context.get('reconcile_date'):
|
||||
domain += ['|', ('reconciled', '=', False), '|',
|
||||
('matched_debit_ids.max_date', '>', context['reconcile_date']),
|
||||
('matched_credit_ids.max_date', '>', context['reconcile_date'])]
|
||||
|
||||
if context.get('account_tag_ids'):
|
||||
domain += [('account_id.tag_ids', 'in', context['account_tag_ids'].ids)]
|
||||
|
||||
if context.get('account_ids'):
|
||||
domain += [('account_id', 'in', context['account_ids'].ids)]
|
||||
|
||||
if context.get('analytic_tag_ids'):
|
||||
domain += [('analytic_tag_ids', 'in', context['analytic_tag_ids'].ids)]
|
||||
|
||||
if context.get('analytic_account_ids'):
|
||||
domain += [('analytic_account_id', 'in', context['analytic_account_ids'].ids)]
|
||||
|
||||
if context.get('partner_ids'):
|
||||
domain += [('partner_id', 'in', context['partner_ids'].ids)]
|
||||
|
||||
if context.get('partner_categories'):
|
||||
domain += [('partner_id.category_id', 'in', context['partner_categories'].ids)]
|
||||
|
||||
where_clause = ""
|
||||
where_clause_params = []
|
||||
tables = ''
|
||||
if domain:
|
||||
domain.append(('display_type', 'not in', ('line_section', 'line_note')))
|
||||
domain.append(('parent_state', '!=', 'cancel'))
|
||||
query = self._search(domain, bypass_access=True)
|
||||
tables, from_params = query.from_clause
|
||||
where_clause, where_params = query.where_clause
|
||||
where_clause_params = from_params + where_params
|
||||
return tables, where_clause, where_clause_params
|
||||
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountRegisterPayments(models.TransientModel):
|
||||
"""Inherits the account.payment.register model to add the new
|
||||
fields and functions"""
|
||||
_inherit = "account.payment.register"
|
||||
|
||||
bank_reference = fields.Char(string="Bank Reference", copy=False)
|
||||
cheque_reference = fields.Char(string="Cheque Reference", copy=False)
|
||||
effective_date = fields.Date('Effective Date',
|
||||
help='Effective date of PDC', copy=False,
|
||||
default=False)
|
||||
|
||||
def _prepare_payment_vals(self, invoices):
|
||||
"""Its prepare the payment values for the invoice and update
|
||||
the MultiPayment"""
|
||||
res = super(AccountRegisterPayments, self)._prepare_payment_vals(
|
||||
invoices)
|
||||
# Check payment method is Check or PDC
|
||||
check_pdc_ids = self.env['account.payment.method'].search(
|
||||
[('code', 'in', ['pdc', 'check_printing'])])
|
||||
if self.payment_method_id.id in check_pdc_ids.ids:
|
||||
currency_id = self.env['res.currency'].browse(res['currency_id'])
|
||||
journal_id = self.env['account.journal'].browse(res['journal_id'])
|
||||
# Updating values in case of Multi payments
|
||||
res.update({
|
||||
'bank_reference': self.bank_reference,
|
||||
'cheque_reference': self.cheque_reference,
|
||||
'check_manual_sequencing': journal_id.check_manual_sequencing,
|
||||
'effective_date': self.effective_date,
|
||||
'check_amount_in_words': currency_id.amount_to_text(
|
||||
res['amount']),
|
||||
})
|
||||
return res
|
||||
|
||||
def _create_payment_vals_from_wizard(self, batch_result):
|
||||
"""It super the wizard action of the create payment values and update
|
||||
the bank and cheque values"""
|
||||
res = super(AccountRegisterPayments,
|
||||
self)._create_payment_vals_from_wizard(
|
||||
batch_result)
|
||||
if self.effective_date:
|
||||
res.update({
|
||||
'bank_reference': self.bank_reference,
|
||||
'cheque_reference': self.cheque_reference,
|
||||
'effective_date': self.effective_date,
|
||||
})
|
||||
return res
|
||||
|
||||
def _create_payment_vals_from_batch(self, batch_result):
|
||||
"""It super the batch action of the create payment values and update
|
||||
the bank and cheque values"""
|
||||
res = super(AccountRegisterPayments,
|
||||
self)._create_payment_vals_from_batch(
|
||||
batch_result)
|
||||
if self.effective_date:
|
||||
res.update({
|
||||
'bank_reference': self.bank_reference,
|
||||
'cheque_reference': self.cheque_reference,
|
||||
'effective_date': self.effective_date,
|
||||
})
|
||||
return res
|
||||
|
||||
def _create_payments(self):
|
||||
"""USed to create a list of payments and update the bank and
|
||||
cheque reference"""
|
||||
payments = super(AccountRegisterPayments, self)._create_payments()
|
||||
|
||||
for payment in payments:
|
||||
payment.write({
|
||||
'bank_reference': self.bank_reference,
|
||||
'cheque_reference': self.cheque_reference
|
||||
})
|
||||
return payments
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
"""It inherits the account.payment model for adding new fields
|
||||
and functions"""
|
||||
_inherit = "account.payment"
|
||||
|
||||
bank_reference = fields.Char(string="Bank Reference", copy=False)
|
||||
cheque_reference = fields.Char(string="Cheque Reference",copy=False)
|
||||
effective_date = fields.Date('Effective Date',
|
||||
help='Effective date of PDC', copy=False,
|
||||
default=False)
|
||||
|
||||
def open_payment_matching_screen(self):
|
||||
"""Open reconciliation view for customers/suppliers"""
|
||||
move_line_id = False
|
||||
for move_line in self.line_ids:
|
||||
if move_line.account_id.reconcile:
|
||||
move_line_id = move_line.id
|
||||
break
|
||||
if not self.partner_id:
|
||||
raise UserError(_("Payments without a customer can't be matched"))
|
||||
action_context = {'company_ids': [self.company_id.id], 'partner_ids': [
|
||||
self.partner_id.commercial_partner_id.id]}
|
||||
if self.partner_type == 'customer':
|
||||
action_context.update({'mode': 'customers'})
|
||||
elif self.partner_type == 'supplier':
|
||||
action_context.update({'mode': 'suppliers'})
|
||||
if move_line_id:
|
||||
action_context.update({'move_line_id': move_line_id})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'manual_reconciliation_view',
|
||||
'context': action_context,
|
||||
}
|
||||
|
||||
def print_checks(self):
|
||||
""" Check that the recordset is valid, set the payments state to
|
||||
sent and call print_checks() """
|
||||
# Since this method can be called via a client_action_multi, we
|
||||
# need to make sure the received records are what we expect
|
||||
selfs = self.filtered(lambda r:
|
||||
r.payment_method_id.code
|
||||
in ['check_printing', 'pdc']
|
||||
and r.state != 'reconciled')
|
||||
if len(selfs) == 0:
|
||||
raise UserError(_(
|
||||
"Payments to print as a checks must have 'Check' "
|
||||
"or 'PDC' selected as payment method and "
|
||||
"not have already been reconciled"))
|
||||
if any(payment.journal_id != selfs[0].journal_id for payment in selfs):
|
||||
raise UserError(_(
|
||||
"In order to print multiple checks at once, they "
|
||||
"must belong to the same bank journal."))
|
||||
|
||||
if not selfs[0].journal_id.check_manual_sequencing:
|
||||
# The wizard asks for the number printed on the first
|
||||
# pre-printed check so payments are attributed the
|
||||
# number of the check the'll be printed on.
|
||||
last_printed_check = selfs.search([
|
||||
('journal_id', '=', selfs[0].journal_id.id),
|
||||
('check_number', '!=', "0")], order="check_number desc",
|
||||
limit=1)
|
||||
next_check_number = last_printed_check and int(
|
||||
last_printed_check.check_number) + 1 or 1
|
||||
return {
|
||||
'name': _('Print Pre-numbered Checks'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'print.prenumbered.checks',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'payment_ids': self.ids,
|
||||
'default_next_check_number': next_check_number,
|
||||
}
|
||||
}
|
||||
else:
|
||||
self.filtered(lambda r: r.state == 'draft').post()
|
||||
self.write({'state': 'sent'})
|
||||
return self.do_print_checks()
|
||||
|
||||
def _prepare_payment_moves(self):
|
||||
""" supered function to set effective date """
|
||||
res = super(AccountPayment, self)._prepare_payment_moves()
|
||||
inbound_pdc_id = self.env.ref(
|
||||
'base_accounting_kit.account_payment_method_pdc_in').id
|
||||
outbound_pdc_id = self.env.ref(
|
||||
'base_accounting_kit.account_payment_method_pdc_out').id
|
||||
if self.payment_method_id.id == inbound_pdc_id or \
|
||||
self.payment_method_id.id == outbound_pdc_id \
|
||||
and self.effective_date:
|
||||
res[0]['date'] = self.effective_date
|
||||
for line in res[0]['line_ids']:
|
||||
line[2]['date_maturity'] = self.effective_date
|
||||
return res
|
||||
|
||||
def mark_as_sent(self):
|
||||
"""Updates the is_move_sent value of the payment model"""
|
||||
self.write({'is_sent': True})
|
||||
|
||||
def unmark_as_sent(self):
|
||||
"""Updates the is_move_sent value of the payment model"""
|
||||
self.write({'is_sent': False})
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, models
|
||||
|
||||
|
||||
class AccountPaymentMethod(models.Model):
|
||||
"""The class inherits the account payment method for supering the
|
||||
_get_payment_method_information function"""
|
||||
_inherit = "account.payment.method"
|
||||
|
||||
@api.model
|
||||
def _get_payment_method_information(self):
|
||||
"""Super the function to update the pdc values"""
|
||||
res = super()._get_payment_method_information()
|
||||
res['pdc'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]}
|
||||
return res
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models
|
||||
|
||||
|
||||
class GetAllRecurringEntries(models.TransientModel):
|
||||
"""Model for managing account recurring entries lines."""
|
||||
_name = 'account.recurring.entries.line'
|
||||
_description = 'Account Recurring Entries Line'
|
||||
|
||||
date = fields.Date('Date')
|
||||
template_name = fields.Char('Name')
|
||||
amount = fields.Float('Amount')
|
||||
tmpl_id = fields.Many2one('account.recurring.payments', string='id')
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
from odoo.tools import get_lang
|
||||
|
||||
|
||||
class AccountCommonReport(models.Model):
|
||||
"""Inherits the Account report model to add special fields and functions"""
|
||||
_inherit = "account.report"
|
||||
_description = "Account Common Report"
|
||||
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
required=True, readonly=True,
|
||||
default=lambda self: self.env.company)
|
||||
journal_ids = fields.Many2many(
|
||||
comodel_name='account.journal',
|
||||
string='Journals',
|
||||
required=True,
|
||||
default=lambda self: self.env['account.journal'].search([('company_id', '=', self.company_id.id)]),
|
||||
domain="[('company_id', '=', company_id)]")
|
||||
date_from = fields.Date(string='Start Date')
|
||||
date_to = fields.Date(string='End Date')
|
||||
target_move = fields.Selection([('posted', 'All Posted Entries'),
|
||||
('all', 'All Entries'),
|
||||
], string='Target Moves',
|
||||
required=True, default='posted')
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_company_id(self):
|
||||
"""Onchange function based on the company and updated the journals"""
|
||||
if self.company_id:
|
||||
self.journal_ids = self.env['account.journal'].search(
|
||||
[('company_id', '=', self.company_id.id)])
|
||||
else:
|
||||
self.journal_ids = self.env['account.journal'].search([])
|
||||
|
||||
def _build_contexts(self, data):
|
||||
"""Builds the context information for the given data"""
|
||||
result = {}
|
||||
result['journal_ids'] = 'journal_ids' in data['form'] and data['form']['journal_ids'] or False
|
||||
result['state'] = 'target_move' in data['form'] and data['form']['target_move'] or ''
|
||||
result['date_from'] = data['form']['date_from'] or False
|
||||
result['date_to'] = data['form']['date_to'] or False
|
||||
result['strict_range'] = True if result['date_from'] else False
|
||||
result['company_id'] = data['form']['company_id'][0] or False
|
||||
return result
|
||||
|
||||
def _print_report(self, data):
|
||||
"""Raise an error if the report comes checked """
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_report(self):
|
||||
"""Function to check if the report comes active models and related
|
||||
values"""
|
||||
self.ensure_one()
|
||||
data = {}
|
||||
data['ids'] = self.env.context.get('active_ids', [])
|
||||
data['model'] = self.env.context.get('active_model', 'ir.ui.menu')
|
||||
data['form'] = self.read(['date_from', 'date_to', 'journal_ids', 'target_move', 'company_id'])[0]
|
||||
used_context = self._build_contexts(data)
|
||||
data['form']['used_context'] = dict(used_context, lang=get_lang(self.env).code)
|
||||
return self.with_context(discard_logo_check=True)._print_report(data)
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models
|
||||
|
||||
|
||||
class FollowupLine(models.Model):
|
||||
"""Model for defining follow-up criteria including the action name, sequence order, due days, and related follow-ups."""
|
||||
_name = 'followup.line'
|
||||
_description = 'Follow-up Criteria'
|
||||
_order = 'delay'
|
||||
|
||||
name = fields.Char('Follow-Up Action', required=True, translate=True)
|
||||
sequence = fields.Integer(
|
||||
help="Gives the sequence order when displaying a list of follow-up lines.")
|
||||
delay = fields.Integer('Due Days', required=True,
|
||||
help="The number of days after the due date of the invoice"
|
||||
" to wait before sending the reminder."
|
||||
" Could be negative if you want to send a polite alert beforehand.")
|
||||
followup_id = fields.Many2one('account.followup', 'Follow Ups',
|
||||
ondelete="cascade")
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models
|
||||
|
||||
|
||||
class MultipleInvoice(models.Model):
|
||||
"""Multiple Invoice Model"""
|
||||
_name = "multiple.invoice"
|
||||
_description = 'Multiple Invoice'
|
||||
_order = "sequence"
|
||||
|
||||
sequence = fields.Integer(string='Sequence No')
|
||||
copy_name = fields.Char(string='Invoice Copy Name')
|
||||
journal_id = fields.Many2one('account.journal', string="Journal")
|
||||
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
from odoo.tools.misc import file_path
|
||||
|
||||
try:
|
||||
import sass as libsass
|
||||
except ImportError:
|
||||
libsass = None
|
||||
|
||||
|
||||
class MultipleInvoiceLayout(models.TransientModel):
|
||||
"""
|
||||
Customise the invoice copy document layout and display a live preview
|
||||
"""
|
||||
_name = 'multiple.invoice.layout'
|
||||
_description = 'Multiple Invoice Document Layout'
|
||||
|
||||
def _get_default_journal(self):
|
||||
"""The default function to return the journal for the invoice"""
|
||||
return self.env['account.journal'].search(
|
||||
[('id', '=', self.env.context.get('active_id'))]).id
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', default=lambda self: self.env.company, required=True)
|
||||
layout = fields.Char(related="company_id.external_report_layout_id.key")
|
||||
journal_id = fields.Many2one('account.journal', string='Journal',
|
||||
required=True, default=_get_default_journal)
|
||||
multiple_invoice_type = fields.Selection(
|
||||
related='journal_id.multiple_invoice_type', readonly=False,
|
||||
required=True)
|
||||
text_position = fields.Selection(related='journal_id.text_position',
|
||||
readonly=False, required=True,
|
||||
default='header')
|
||||
body_text_position = fields.Selection(
|
||||
related='journal_id.body_text_position',
|
||||
readonly=False)
|
||||
text_align = fields.Selection(
|
||||
related='journal_id.text_align',
|
||||
readonly=False)
|
||||
preview = fields.Html(compute='_compute_preview',
|
||||
sanitize=False,
|
||||
sanitize_tags=False,
|
||||
sanitize_attributes=False,
|
||||
sanitize_style=False,
|
||||
sanitize_form=False,
|
||||
strip_style=False,
|
||||
strip_classes=False)
|
||||
|
||||
@api.depends('multiple_invoice_type', 'text_position', 'body_text_position',
|
||||
'text_align')
|
||||
def _compute_preview(self):
|
||||
""" compute a qweb based preview to display on the wizard """
|
||||
|
||||
styles = self._get_asset_style()
|
||||
|
||||
for wizard in self:
|
||||
if wizard.company_id:
|
||||
preview_css = self._get_css_for_preview(styles, wizard.id)
|
||||
layout = self._get_layout_for_preview()
|
||||
ir_ui_view = wizard.env['ir.ui.view']
|
||||
wizard.preview = ir_ui_view._render_template(
|
||||
'base_accounting_kit.multiple_invoice_wizard_preview',
|
||||
{'company': wizard.company_id, 'preview_css': preview_css,
|
||||
'layout': layout,
|
||||
'mi_type': self.multiple_invoice_type,
|
||||
'txt_position': self.text_position,
|
||||
'body_txt_position': self.body_text_position,
|
||||
'txt_align': self.text_align,
|
||||
'mi': self.env.ref(
|
||||
'base_accounting_kit.multiple_invoice_sample_name')
|
||||
})
|
||||
else:
|
||||
wizard.preview = False
|
||||
|
||||
def _get_asset_style(self):
|
||||
"""Used to set the asset style"""
|
||||
company_styles = self.env['ir.qweb']._render(
|
||||
'web.styles_company_report', {
|
||||
'company_ids': self.company_id,
|
||||
}, raise_if_not_found=False)
|
||||
return company_styles
|
||||
|
||||
@api.model
|
||||
def _get_css_for_preview(self, scss, new_id):
|
||||
"""
|
||||
Compile the scss into css.
|
||||
"""
|
||||
css_code = self._compile_scss(scss)
|
||||
return css_code
|
||||
|
||||
@api.model
|
||||
def _compile_scss(self, scss_source):
|
||||
"""
|
||||
This code will compile valid scss into css.
|
||||
Parameters are the same from odoo/addons/base/models/assetsbundle.py
|
||||
Simply copied and adapted slightly
|
||||
"""
|
||||
|
||||
# No scss ? still valid, returns empty css
|
||||
if not scss_source.strip():
|
||||
return ""
|
||||
|
||||
precision = 8
|
||||
output_style = 'expanded'
|
||||
bootstrap_path = file_path('web', 'static', 'lib', 'bootstrap',
|
||||
'scss')
|
||||
try:
|
||||
return libsass.compile(
|
||||
string=scss_source,
|
||||
include_paths=[
|
||||
bootstrap_path,
|
||||
],
|
||||
output_style=output_style,
|
||||
precision=precision,
|
||||
)
|
||||
except libsass.CompileError as e:
|
||||
raise libsass.CompileError(e.args[0])
|
||||
|
||||
def _get_layout_for_preview(self):
|
||||
"""Returns the layout Preview for the accounting module"""
|
||||
if self.layout == 'web.external_layout_boxed':
|
||||
new_layout = 'base_accounting_kit.boxed'
|
||||
|
||||
elif self.layout == 'web.external_layout_bold':
|
||||
new_layout = 'base_accounting_kit.bold'
|
||||
|
||||
elif self.layout == 'web.external_layout_striped':
|
||||
new_layout = 'base_accounting_kit.striped'
|
||||
|
||||
else:
|
||||
new_layout = 'base_accounting_kit.standard'
|
||||
|
||||
return new_layout
|
||||
|
||||
def document_layout_save(self):
|
||||
"""meant to be overridden document_layout_save"""
|
||||
return self.env.context.get('report_action') or {
|
||||
'type': 'ir.actions.act_window_close'}
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
"""Inherited the model for adding new fields and functions"""
|
||||
_inherit = 'product.template'
|
||||
|
||||
asset_category_id = fields.Many2one('account.asset.category',
|
||||
string='Asset Type',
|
||||
company_dependent=True,
|
||||
ondelete="restrict")
|
||||
deferred_revenue_category_id = fields.Many2one('account.asset.category',
|
||||
string='Deferred Revenue Type',
|
||||
company_dependent=True,
|
||||
ondelete="restrict")
|
||||
|
||||
def _get_asset_accounts(self):
|
||||
"""Override method to customize asset accounts based on asset and deferred revenue categories."""
|
||||
res = super(ProductTemplate, self)._get_asset_accounts()
|
||||
if self.asset_category_id:
|
||||
res['stock_input'] = self.property_account_expense_id
|
||||
if self.deferred_revenue_category_id:
|
||||
res['stock_output'] = self.property_account_income_id
|
||||
return res
|
||||
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class RecurringPayments(models.Model):
|
||||
"""Created the module for recurring payments"""
|
||||
_name = 'account.recurring.payments'
|
||||
_description = 'Accounting Recurring Payment'
|
||||
|
||||
def _get_next_schedule(self):
|
||||
"""Function for adding the schedule process"""
|
||||
if self.date:
|
||||
recurr_dates = []
|
||||
today = datetime.today()
|
||||
start_date = datetime.strptime(str(self.date), '%Y-%m-%d')
|
||||
while start_date <= today:
|
||||
recurr_dates.append(str(start_date.date()))
|
||||
if self.recurring_period == 'days':
|
||||
start_date += relativedelta(days=self.recurring_interval)
|
||||
elif self.recurring_period == 'weeks':
|
||||
start_date += relativedelta(weeks=self.recurring_interval)
|
||||
elif self.recurring_period == 'months':
|
||||
start_date += relativedelta(months=self.recurring_interval)
|
||||
else:
|
||||
start_date += relativedelta(years=self.recurring_interval)
|
||||
self.next_date = start_date.date()
|
||||
|
||||
name = fields.Char(string='Name')
|
||||
debit_account = fields.Many2one('account.account', 'Debit Account',
|
||||
required=True)
|
||||
credit_account = fields.Many2one('account.account', 'Credit Account',
|
||||
required=True)
|
||||
journal_id = fields.Many2one('account.journal', 'Journal', required=True)
|
||||
analytic_account_id = fields.Many2one('account.analytic.account',
|
||||
'Analytic Account')
|
||||
date = fields.Date('Starting Date', required=True, default=date.today())
|
||||
next_date = fields.Date('Next Schedule', compute=_get_next_schedule,
|
||||
readonly=True, copy=False)
|
||||
recurring_period = fields.Selection(selection=[('days', 'Days'),
|
||||
('weeks', 'Weeks'),
|
||||
('months', 'Months'),
|
||||
('years', 'Years')],
|
||||
store=True, required=True)
|
||||
amount = fields.Float('Amount')
|
||||
description = fields.Text('Description')
|
||||
state = fields.Selection(selection=[('draft', 'Draft'),
|
||||
('running', 'Running')],
|
||||
default='draft', string='Status')
|
||||
journal_state = fields.Selection(selection=[('draft', 'Unposted'),
|
||||
('posted', 'Posted')],
|
||||
required=True, default='draft',
|
||||
string='Generate Journal As')
|
||||
recurring_interval = fields.Integer('Recurring Interval', default=1)
|
||||
partner_id = fields.Many2one('res.partner', 'Partner')
|
||||
pay_time = fields.Selection(selection=[('pay_now', 'Pay Directly'),
|
||||
('pay_later', 'Pay Later')],
|
||||
store=True, required=True)
|
||||
company_id = fields.Many2one('res.company',
|
||||
default=lambda l: l.env.company.id)
|
||||
recurring_lines = fields.One2many('account.recurring.entries.line', 'tmpl_id')
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def onchange_partner_id(self):
|
||||
"""Onchange partner field for updating the credit account value"""
|
||||
if self.partner_id.property_account_receivable_id:
|
||||
self.credit_account = self.partner_id.property_account_payable_id
|
||||
|
||||
@api.model
|
||||
def _cron_generate_entries(self):
|
||||
"""Generate recurring entries based on the defined schedule
|
||||
and create corresponding accounting moves."""
|
||||
data = self.env['account.recurring.payments'].search(
|
||||
[('state', '=', 'running')])
|
||||
entries = self.env['account.move'].search(
|
||||
[('recurring_ref', '!=', False)])
|
||||
journal_dates = []
|
||||
journal_codes = []
|
||||
remaining_dates = []
|
||||
for entry in entries:
|
||||
journal_dates.append(str(entry.date))
|
||||
if entry.recurring_ref:
|
||||
journal_codes.append(str(entry.recurring_ref))
|
||||
today = datetime.today()
|
||||
for line in data:
|
||||
if line.date:
|
||||
recurr_dates = []
|
||||
start_date = datetime.strptime(str(line.date), '%Y-%m-%d')
|
||||
while start_date <= today:
|
||||
recurr_dates.append(str(start_date.date()))
|
||||
if line.recurring_period == 'days':
|
||||
start_date += relativedelta(
|
||||
days=line.recurring_interval)
|
||||
elif line.recurring_period == 'weeks':
|
||||
start_date += relativedelta(
|
||||
weeks=line.recurring_interval)
|
||||
elif line.recurring_period == 'months':
|
||||
start_date += relativedelta(
|
||||
months=line.recurring_interval)
|
||||
else:
|
||||
start_date += relativedelta(
|
||||
years=line.recurring_interval)
|
||||
for rec in recurr_dates:
|
||||
recurr_code = str(line.id) + '/' + str(rec)
|
||||
if recurr_code not in journal_codes:
|
||||
remaining_dates.append({
|
||||
'date': rec,
|
||||
'template_name': line.name,
|
||||
'amount': line.amount,
|
||||
'tmpl_id': line.id,
|
||||
})
|
||||
child_ids = self.recurring_lines.create(remaining_dates)
|
||||
for line in child_ids:
|
||||
tmpl_id = line.tmpl_id
|
||||
recurr_code = str(tmpl_id.id) + '/' + str(line.date)
|
||||
line_ids = [(0, 0, {
|
||||
'account_id': tmpl_id.credit_account.id,
|
||||
'partner_id': tmpl_id.partner_id.id,
|
||||
'credit': line.amount,
|
||||
# 'analytic_account_id': tmpl_id.analytic_account_id.id,
|
||||
}), (0, 0, {
|
||||
'account_id': tmpl_id.debit_account.id,
|
||||
'partner_id': tmpl_id.partner_id.id,
|
||||
'debit': line.amount,
|
||||
# 'analytic_account_id': tmpl_id.analytic_account_id.id,
|
||||
})]
|
||||
vals = {
|
||||
'date': line.date,
|
||||
'recurring_ref': recurr_code,
|
||||
'company_id': self.env.company.id,
|
||||
'journal_id': tmpl_id.journal_id.id,
|
||||
'ref': line.template_name,
|
||||
'narration': 'Recurring entry',
|
||||
'line_ids': line_ids
|
||||
}
|
||||
move_id = self.env['account.move'].create(vals)
|
||||
if tmpl_id.journal_state == 'posted':
|
||||
move_id.post()
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 datetime
|
||||
import calendar
|
||||
from odoo import models, api, _
|
||||
from odoo.exceptions import RedirectWarning
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
"""Model for inheriting res_company."""
|
||||
_inherit = "res.company"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Ensure fiscal year day does not exceed the maximum valid day for the selected month during record creation."""
|
||||
for vals in vals_list:
|
||||
if 'fiscalyear_last_month' in vals and 'fiscalyear_last_day' in vals:
|
||||
month = vals.get('fiscalyear_last_month')
|
||||
day = vals.get('fiscalyear_last_day')
|
||||
if month and day:
|
||||
if vals.account_opening_date:
|
||||
year = vals.account_opening_date.year
|
||||
else:
|
||||
year = datetime.now().year
|
||||
max_day = calendar.monthrange(year, int(month))[1]
|
||||
if int(day) > max_day:
|
||||
vals['fiscalyear_last_day'] = max_day
|
||||
return super(ResCompany, self).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
"""Auto-correct fiscal year day to a valid value when month or day is updated to prevent invalid calendar dates."""
|
||||
if 'fiscalyear_last_month' in vals or 'fiscalyear_last_day' in vals:
|
||||
month = vals.get('fiscalyear_last_month')
|
||||
day = vals.get('fiscalyear_last_day')
|
||||
if month:
|
||||
if self.account_opening_date:
|
||||
year = self.account_opening_date.year
|
||||
else:
|
||||
year = datetime.now().year
|
||||
max_day = calendar.monthrange(year, int(month))[1]
|
||||
if not day:
|
||||
if any(company.fiscalyear_last_day > max_day for company in self):
|
||||
vals['fiscalyear_last_day'] = max_day
|
||||
elif int(day) > max_day:
|
||||
vals['fiscalyear_last_day'] = max_day
|
||||
|
||||
return super(ResCompany, self).write(vals)
|
||||
|
||||
def _validate_locks(self, values):
|
||||
"""Validate the hard lock date by checking for unposted entries and unreconciled bank statement lines."""
|
||||
if values.get('hard_lock_date'):
|
||||
draft_entries = self.env['account.move'].search([
|
||||
('company_id', 'in', self.ids),
|
||||
('state', '=', 'draft'),
|
||||
('date', '<=', values['hard_lock_date'])])
|
||||
if draft_entries:
|
||||
error_msg = _('There are still unposted entries in the '
|
||||
'period you want to lock. You should either post '
|
||||
'or delete them.')
|
||||
action_error = {
|
||||
'view_mode': 'list',
|
||||
'name': 'Unposted Entries',
|
||||
'res_model': 'account.move',
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('id', 'in', draft_entries.ids)],
|
||||
'search_view_id': [self.env.ref('account.view_account_move_filter').id, 'search'],
|
||||
'views': [[self.env.ref('account.view_move_tree').id, 'list'], [self.env.ref('account.view_move_form').id, 'form']],
|
||||
}
|
||||
raise RedirectWarning(error_msg, action_error, _('Show unposted entries'))
|
||||
|
||||
unreconciled_statement_lines = self.env['account.bank.statement.line'].search([
|
||||
('company_id', 'in', self.ids),
|
||||
('is_reconciled', '=', False),
|
||||
('date', '<=', values['hard_lock_date']),
|
||||
('move_id.state', 'in', ('draft', 'posted')),
|
||||
])
|
||||
if unreconciled_statement_lines:
|
||||
error_msg = _("There are still unreconciled bank statement lines in the period you want to lock."
|
||||
"You should either reconcile or delete them.")
|
||||
action_error = {
|
||||
'view_mode': 'kanban',
|
||||
'name': 'Unreconciled Transactions',
|
||||
'res_model': 'account.bank.statement.line',
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('id', 'in', unreconciled_statement_lines.ids)],
|
||||
'views': [[self.env.ref(
|
||||
'base_accounting_kit.account_bank_statement_line_view_kanban').id,
|
||||
'kanban']]
|
||||
}
|
||||
raise RedirectWarning(error_msg, action_error, _('Show Unreconciled Bank Statement Lines'))
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import models, fields, api
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
"""Defines a model for configuration settings with additional fields for
|
||||
managing customer credit limit and Anglo-Saxon accounting settings."""
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
customer_credit_limit = fields.Boolean(string="Customer Credit Limit")
|
||||
|
||||
use_anglo_saxon_accounting = fields.Boolean(string="Use Anglo-Saxon accounting", readonly=False,
|
||||
related='company_id.anglo_saxon_accounting')
|
||||
fiscalyear_last_day = fields.Integer(
|
||||
related='company_id.fiscalyear_last_day', readonly=False
|
||||
)
|
||||
fiscalyear_last_month = fields.Selection(
|
||||
related='company_id.fiscalyear_last_month', readonly=False
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
"""Retrieve the values for configuration settings including the
|
||||
customer credit limit from the database parameters. """
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
params = self.env['ir.config_parameter'].sudo()
|
||||
customer_credit_limit = params.get_param('customer_credit_limit',
|
||||
default=False)
|
||||
res.update(customer_credit_limit=customer_credit_limit)
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
"""Set the customer credit limit value in the database parameters using superuser access."""
|
||||
super(ResConfigSettings, self).set_values()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
"customer_credit_limit",
|
||||
self.customer_credit_limit)
|
||||
|
||||
@api.model
|
||||
def get_view_id(self):
|
||||
"""Retrieve the ID of the view for bank reconciliation widget form."""
|
||||
view_id = self.env['ir.model.data']._xmlid_to_res_id(
|
||||
'base_accounting_kit.view_bank_reconcile_widget_form')
|
||||
return view_id
|
||||
@@ -0,0 +1,528 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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, _
|
||||
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': '<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')
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#############################################################################
|
||||
#
|
||||
# Cybrosys Technologies Pvt. Ltd.
|
||||
#
|
||||
# Copyright (C) 2025-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 odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
"""The Class inherits the sale.order model for adding the new
|
||||
fields and functions"""
|
||||
_inherit = 'sale.order'
|
||||
|
||||
has_due = fields.Boolean(string='Has due')
|
||||
is_warning = fields.Boolean(string='Is warning')
|
||||
due_amount = fields.Float(string='Due Amount',
|
||||
related='partner_id.due_amount')
|
||||
|
||||
def _action_confirm(self):
|
||||
"""To check the selected customers due amount is exceed than
|
||||
blocking stage"""
|
||||
if self.partner_id.active_limit \
|
||||
and self.partner_id.enable_credit_limit:
|
||||
if self.due_amount >= self.partner_id.blocking_stage:
|
||||
if self.partner_id.blocking_stage != 0:
|
||||
raise UserError(_(
|
||||
"%s is in Blocking Stage and "
|
||||
"has a due amount of %s %s to pay") % (
|
||||
self.partner_id.name, self.due_amount,
|
||||
self.currency_id.symbol))
|
||||
return super(SaleOrder, self)._action_confirm()
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def check_due(self):
|
||||
"""To show the due amount and warning stage"""
|
||||
if self.partner_id and self.partner_id.due_amount > 0 \
|
||||
and self.partner_id.active_limit \
|
||||
and self.partner_id.enable_credit_limit:
|
||||
self.has_due = True
|
||||
else:
|
||||
self.has_due = False
|
||||
if self.partner_id and self.partner_id.active_limit\
|
||||
and self.partner_id.enable_credit_limit:
|
||||
if self.due_amount >= self.partner_id.warning_stage:
|
||||
if self.partner_id.warning_stage != 0:
|
||||
self.is_warning = True
|
||||
else:
|
||||
self.is_warning = False
|
||||
Reference in New Issue
Block a user