diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 17:14:58 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 17:14:58 +0700 |
| commit | 1ca3b3df3421961caec3b747a364071c80f5c7da (patch) | |
| tree | 6778a1f0f3f9b4c6e26d6d87ccde16e24da6c9d6 /base_accounting_kit/models | |
| parent | b57188be371d36d96caac4b8d65a40745c0e972c (diff) | |
initial commit
Diffstat (limited to 'base_accounting_kit/models')
| -rw-r--r-- | base_accounting_kit/models/__init__.py | 38 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_account.py | 78 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_asset.py | 800 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_dashboard.py | 1622 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_followup.py | 51 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_journal.py | 72 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_move.py | 175 | ||||
| -rw-r--r-- | base_accounting_kit/models/account_payment.py | 142 | ||||
| -rw-r--r-- | base_accounting_kit/models/credit_limit.py | 250 | ||||
| -rw-r--r-- | base_accounting_kit/models/multiple_invoice.py | 49 | ||||
| -rw-r--r-- | base_accounting_kit/models/multiple_invoice_layout.py | 151 | ||||
| -rw-r--r-- | base_accounting_kit/models/payment_matching.py | 1181 | ||||
| -rw-r--r-- | base_accounting_kit/models/product_template.py | 38 | ||||
| -rw-r--r-- | base_accounting_kit/models/recurring_payments.py | 179 | ||||
| -rw-r--r-- | base_accounting_kit/models/res_config_settings.py | 44 | ||||
| -rw-r--r-- | base_accounting_kit/models/res_partner.py | 113 |
16 files changed, 4983 insertions, 0 deletions
diff --git a/base_accounting_kit/models/__init__.py b/base_accounting_kit/models/__init__.py new file mode 100644 index 0000000..8c67d7c --- /dev/null +++ b/base_accounting_kit/models/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################# +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 +from . import account_followup +from . import account_journal +from . import account_move +from . import account_payment +from . import credit_limit +from . import product_template +from . import recurring_payments +from . import res_config_settings +from . import res_partner +from . import account_dashboard +from . import payment_matching +from . import multiple_invoice +from . import multiple_invoice_layout diff --git a/base_accounting_kit/models/account_account.py b/base_accounting_kit/models/account_account.py new file mode 100644 index 0000000..2e1a320 --- /dev/null +++ b/base_accounting_kit/models/account_account.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 time +from odoo import api, models, fields, _ +from odoo.exceptions import UserError + +# Export Product +class ExportProductLine(models.Model): + _name = "export.product.line" + + reference = fields.Many2one('Reference') + value = fields.Char('Value') + +class ExportProduct(models.Model): + _name = "export.product" + + product_template_id = fields.Char('Product External ID') + product_id = fields.Char('Product ID') + product_template_name = fields.Char('Product Name') + attribute_id = fields.Char('Attribute') + value_text = fields.Char(compute="_get_value_text", string="Values in Text") + line_ids = fields.One2many('export.product.line', 'reference', 'Lines') + + @api.depends('line_ids.value') + def _get_value_text(self): + for res in self: + value_text = "" + if res.line_ids: + for line in res.line_ids: + if not value_text: + value_text += line.value + elif value_text: + value_text += "," + line.value + + res.value_text = value_text + +class CashFlow(models.Model): + _inherit = 'account.account' + + def get_cash_flow_ids(self): + 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): + 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)]}) diff --git a/base_accounting_kit/models/account_asset.py b/base_accounting_kit/models/account_asset.py new file mode 100644 index 0000000..1afdbfd --- /dev/null +++ b/base_accounting_kit/models/account_asset.py @@ -0,0 +1,800 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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.exceptions import UserError, ValidationError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from odoo.tools import float_compare, float_is_zero + + +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") + account_analytic_id = fields.Many2one('account.analytic.account', + string='Analytic Account') + account_asset_id = fields.Many2one('account.account', + string='Asset Account', required=True, + domain=[('internal_type', '=', 'other'), + ('deprecated', '=', False)], + help="Account used to record the purchase of the asset at its original price.") + account_depreciation_id = fields.Many2one('account.account', + string='Depreciation Entries: Asset Account', + required=True, domain=[ + ('internal_type', '=', 'other'), ('deprecated', '=', False)], + help="Account used in the depreciation entries, to decrease the asset value.") + account_depreciation_expense_id = fields.Many2one('account.account', + string='Depreciation Entries: Expense Account', + required=True, domain=[ + ('internal_type', '=', 'other'), ('deprecated', '=', False)], + 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) + company_id = fields.Many2one('res.company', string='Company', + required=True, default=lambda self: self.env.company) + method = fields.Selection( + [('linear', 'Linear'), ('degressive', 'Degressive')], + 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): + 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): + 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): + if self.method_time != 'number': + self.prorata = False + + +class AccountAssetAsset(models.Model): + _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, readonly=True, + states={'draft': [('readonly', False)]}) + code = fields.Char(string='Reference', size=32, readonly=True, + states={'draft': [('readonly', False)]}) + value = fields.Float(string='Gross Value', required=True, readonly=True, + digits=0, states={'draft': [('readonly', False)]}) + currency_id = fields.Many2one('res.currency', string='Currency', + required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda + self: self.env.company.currency_id.id) + company_id = fields.Many2one('res.company', string='Company', + required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda self: self.env.company) + note = fields.Text() + category_id = fields.Many2one('account.asset.category', string='Category', + required=True, change_default=True, + readonly=True, + states={'draft': [('readonly', False)]}) + date = fields.Date(string='Date', required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default=fields.Date.context_today) + state = fields.Selection( + [('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], + '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', + readonly=True, + states={'draft': [('readonly', False)]}, ) + method = fields.Selection( + [('linear', 'Linear'), ('degressive', 'Degressive')], + string='Computation Method', required=True, readonly=True, + states={'draft': [('readonly', False)]}, 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', + readonly=True, + states={'draft': [('readonly', False)]}, + default=5, + help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Number of Months in a Period', + required=True, readonly=True, default=12, + states={'draft': [('readonly', False)]}, + help="The amount of time between two depreciations, in months") + method_end = fields.Date(string='Ending Date', readonly=True, + states={'draft': [('readonly', False)]}) + method_progress_factor = fields.Float(string='Degressive Factor', + readonly=True, default=0.3, states={ + 'draft': [('readonly', False)]}) + 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, readonly=True, default='number', + states={'draft': [('readonly', False)]}, + 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', readonly=True, + states={'draft': [('readonly', False)]}, + 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', + readonly=True, states={ + 'draft': [('readonly', False)], 'open': [('readonly', False)]}) + salvage_value = fields.Float(string='Salvage Value', digits=0, + readonly=True, + states={'draft': [('readonly', False)]}, + help="It is the amount you plan to have that you cannot depreciate.") + invoice_id = fields.Many2one('account.move', string='Invoice', + states={'draft': [('readonly', False)]}, + copy=False) + type = fields.Selection(related="category_id.type", string='Type', + required=True) + + def unlink(self): + 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.model + def _cron_generate_entries(self): + self.compute_generated_entries(datetime.today()) + + @api.model + def compute_generated_entries(self, date, asset_type=None): + # 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): + 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): + 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): + 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, + '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}) + + return True + + def validate(self): + self.write({'state': 'open'}) + fields = [ + '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(fields) + 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._message_track(tracked_fields, + dict.fromkeys( + fields)) + asset.message_post(subject=_('Asset created'), + tracking_value_ids=tracking_value_ids) + + def _get_disposal_moves(self): + 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._message_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): + 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 = 'tree,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): + self.write({'state': 'draft'}) + + @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check', + 'depreciation_line_ids.amount') + def _amount_residual(self): + 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): + self.currency_id = self.company_id.currency_id.id + + @api.depends('depreciation_line_ids.move_id') + def _entry_count(self): + 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): + 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): + 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): + 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, + } + } + + @api.onchange('method_time') + def onchange_method_time(self): + if self.method_time != 'number': + self.prorata = False + + def copy_data(self, default=None): + 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): + 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() + + @api.model + def create(self, vals): + asset = super(AccountAssetAsset, + self.with_context(mail_create_nolog=True)).create(vals) + asset.sudo().compute_depreciation_board() + return asset + + def write(self, vals): + res = super(AccountAssetAsset, self).write(vals) + if 'depreciation_line_ids' not in vals and 'state' not in vals: + for rec in self: + rec.compute_depreciation_board() + return res + + def open_entries(self): + 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': 'tree,form', + 'res_model': 'account.move', + 'view_id': False, + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', move_ids)], + } + + +class AccountAssetDepreciationLine(models.Model): + _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', digits=0, + required=True) + remaining_value = fields.Float(string='Next Period Depreciation', digits=0, + 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): + for line in self: + line.move_check = bool(line.move_id) + + @api.depends('move_id.state') + def _get_move_posted_check(self): + 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): + 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: + category_id = line.asset_id.category_id + depreciation_date = self.env.context.get( + 'depreciation_date') or line.depreciation_date or fields.Date.context_today( + self) + company_currency = line.asset_id.company_id.currency_id + current_currency = line.asset_id.currency_id + amount = current_currency.with_context( + date=depreciation_date).compute(line.amount, company_currency) + 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': category_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': category_id.journal_id.id, + 'partner_id': partner.id, + 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, + 'currency_id': company_currency != current_currency and current_currency.id or False, + 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, + } + move_line_2 = { + 'name': asset_name, + 'account_id': category_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': category_id.journal_id.id, + 'partner_id': partner.id, + 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, + 'currency_id': company_currency != current_currency and current_currency.id or False, + 'amount_currency': company_currency != current_currency and line.amount or 0.0, + } + move_vals = { + 'ref': line.asset_id.code, + '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) + 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.category_id.open_asset'))).post() + return [x.id for x in created_moves] + + def create_grouped_move(self, post_move=True): + 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): + 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): + 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() diff --git a/base_accounting_kit/models/account_dashboard.py b/base_accounting_kit/models/account_dashboard.py new file mode 100644 index 0000000..e2cab9f --- /dev/null +++ b/base_accounting_kit/models/account_dashboard.py @@ -0,0 +1,1622 @@ +# -*- coding: utf-8 -*- + +import calendar +import datetime +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import models, api +from odoo.http import request + + +class DashBoard(models.Model): + _inherit = 'account.move' + + # function to getting expenses + + # function to getting income of this year + + @api.model + def get_income_this_year(self, *post): + + company_id = self.get_current_company_value() + + month_list = [] + for i in range(11, -1, -1): + l_month = datetime.now() - relativedelta(months=i) + text = format(l_month, '%B') + month_list.append(text) + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit)-sum(credit) as income ,to_char(account_move_line.date, 'Month') as month , + internal_group from account_move_line ,account_account where + account_move_line.account_id=account_account.id AND internal_group = 'income' + AND to_char(DATE(NOW()), 'YY') = to_char(account_move_line.date, 'YY') + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND %s + group by internal_group,month + ''') % (states_arg)) + record = self._cr.dictfetchall() + + self._cr.execute(('''select sum(debit)-sum(credit) as expense ,to_char(account_move_line.date, 'Month') as month , + internal_group from account_move_line ,account_account where + account_move_line.account_id=account_account.id AND internal_group = 'expense' + AND to_char(DATE(NOW()), 'YY') = to_char(account_move_line.date, 'YY') + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND %s + group by internal_group,month + ''') % (states_arg)) + + result = self._cr.dictfetchall() + records = [] + for month in month_list: + last_month_inc = list(filter(lambda m: m['month'].strip() == month, record)) + last_month_exp = list(filter(lambda m: m['month'].strip() == month, result)) + if not last_month_inc and not last_month_exp: + records.append({ + 'month': month, + 'income': 0.0, + 'expense': 0.0, + 'profit': 0.0, + }) + elif (not last_month_inc) and last_month_exp: + last_month_exp[0].update({ + 'income': 0.0, + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_exp[0].update({ + 'profit': last_month_exp[0]['income'] - last_month_exp[0]['expense'] + }) + records.append(last_month_exp[0]) + elif (not last_month_exp) and last_month_inc: + last_month_inc[0].update({ + 'expense': 0.0, + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + else: + last_month_inc[0].update({ + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'], + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + income = [] + expense = [] + month = [] + profit = [] + for rec in records: + income.append(rec['income']) + expense.append(rec['expense']) + month.append(rec['month']) + profit.append(rec['profit']) + return { + 'income': income, + 'expense': expense, + 'month': month, + 'profit': profit, + } + + # function to getting income of last year + + @api.model + def get_income_last_year(self, *post): + + company_id = self.get_current_company_value() + + month_list = [] + for i in range(11, -1, -1): + l_month = datetime.now() - relativedelta(months=i) + text = format(l_month, '%B') + month_list.append(text) + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit)-sum(credit) as income ,to_char(account_move_line.date, 'Month') as month , + internal_group from account_move_line ,account_account + where account_move_line.account_id=account_account.id AND internal_group = 'income' + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) -1 + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND %s + group by internal_group,month + ''') % (states_arg)) + record = self._cr.dictfetchall() + + self._cr.execute(('''select sum(debit)-sum(credit) as expense ,to_char(account_move_line.date, 'Month') as month , + internal_group from account_move_line , account_account where + account_move_line.account_id=account_account.id AND internal_group = 'expense' + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) -1 + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND %s + group by internal_group,month + ''') % (states_arg)) + + result = self._cr.dictfetchall() + records = [] + for month in month_list: + last_month_inc = list(filter(lambda m: m['month'].strip() == month, record)) + last_month_exp = list(filter(lambda m: m['month'].strip() == month, result)) + if not last_month_inc and not last_month_exp: + records.append({ + 'month': month, + 'income': 0.0, + 'expense': 0.0, + 'profit': 0.0, + }) + elif (not last_month_inc) and last_month_exp: + last_month_exp[0].update({ + 'income': 0.0, + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_exp[0].update({ + 'profit': last_month_exp[0]['income'] - last_month_exp[0]['expense'] + }) + records.append(last_month_exp[0]) + elif (not last_month_exp) and last_month_inc: + last_month_inc[0].update({ + 'expense': 0.0, + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + else: + last_month_inc[0].update({ + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'], + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + income = [] + expense = [] + month = [] + profit = [] + for rec in records: + income.append(rec['income']) + expense.append(rec['expense']) + month.append(rec['month']) + profit.append(rec['profit']) + return { + 'income': income, + 'expense': expense, + 'month': month, + 'profit': profit, + } + + # function to getting income of last month + + @api.model + def get_income_last_month(self, *post): + + company_id = self.get_current_company_value() + day_list = [] + now = datetime.now() + day = \ + calendar.monthrange(now.year - 1 if now.month == 1 else now.year, + now.month - 1 if not now.month == 1 else 12)[ + 1] + + for x in range(1, day + 1): + day_list.append(x) + + one_month_ago = (datetime.now() - relativedelta(months=1)).month + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit)-sum(credit) as income ,cast(to_char(account_move_line.date, 'DD')as int) + as date , internal_group from account_move_line , account_account where + Extract(month FROM account_move_line.date) in ''' + str(tuple(company_id)) + ''' + AND %s + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND account_move_line.account_id=account_account.id AND internal_group='income' + group by internal_group,date + ''') % (states_arg)) + + record = self._cr.dictfetchall() + + self._cr.execute(('''select sum(debit)-sum(credit) as expense ,cast(to_char(account_move_line.date, 'DD')as int) + as date ,internal_group from account_move_line ,account_account where + Extract(month FROM account_move_line.date) in ''' + str(tuple(company_id)) + ''' + AND %s + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND account_move_line.account_id=account_account.id AND internal_group='expense' + group by internal_group,date + ''') % (states_arg)) + result = self._cr.dictfetchall() + records = [] + for date in day_list: + last_month_inc = list(filter(lambda m: m['date'] == date, record)) + last_month_exp = list(filter(lambda m: m['date'] == date, result)) + if not last_month_inc and not last_month_exp: + records.append({ + 'date': date, + 'income': 0.0, + 'expense': 0.0, + 'profit': 0.0 + }) + elif (not last_month_inc) and last_month_exp: + last_month_exp[0].update({ + 'income': 0.0, + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_exp[0].update({ + 'profit': last_month_exp[0]['income'] - last_month_exp[0]['expense'] + }) + records.append(last_month_exp[0]) + elif (not last_month_exp) and last_month_inc: + last_month_inc[0].update({ + 'expense': 0.0, + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + else: + last_month_inc[0].update({ + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'], + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + income = [] + expense = [] + date = [] + profit = [] + for rec in records: + income.append(rec['income']) + expense.append(rec['expense']) + date.append(rec['date']) + profit.append(rec['profit']) + return { + 'income': income, + 'expense': expense, + 'date': date, + 'profit': profit + + } + + # function to getting income of this month + + @api.model + def get_income_this_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + day_list = [] + now = datetime.now() + day = calendar.monthrange(now.year, now.month)[1] + for x in range(1, day + 1): + day_list.append(x) + + self._cr.execute(('''select sum(debit)-sum(credit) as income ,cast(to_char(account_move_line.date, 'DD')as int) + as date , internal_group from account_move_line , account_account + where Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND %s + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND account_move_line.account_id=account_account.id AND internal_group='income' + group by internal_group,date + ''') % (states_arg)) + + record = self._cr.dictfetchall() + + self._cr.execute(('''select sum(debit)-sum(credit) as expense ,cast(to_char(account_move_line.date, 'DD')as int) + as date , internal_group from account_move_line , account_account where + Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND %s + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + AND account_move_line.account_id=account_account.id AND internal_group='expense' + group by internal_group,date + ''') % (states_arg)) + result = self._cr.dictfetchall() + records = [] + for date in day_list: + last_month_inc = list(filter(lambda m: m['date'] == date, record)) + last_month_exp = list(filter(lambda m: m['date'] == date, result)) + if not last_month_inc and not last_month_exp: + records.append({ + 'date': date, + 'income': 0.0, + 'expense': 0.0, + 'profit': 0.0 + }) + elif (not last_month_inc) and last_month_exp: + last_month_exp[0].update({ + 'income': 0.0, + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_exp[0].update({ + 'profit': last_month_exp[0]['income'] - last_month_exp[0]['expense'] + }) + records.append(last_month_exp[0]) + elif (not last_month_exp) and last_month_inc: + last_month_inc[0].update({ + 'expense': 0.0, + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + else: + last_month_inc[0].update({ + 'income': -1 * last_month_inc[0]['income'] if last_month_inc[0]['income'] < 1 else + last_month_inc[0]['income'], + 'expense': -1 * last_month_exp[0]['expense'] if last_month_exp[0]['expense'] < 1 else + last_month_exp[0]['expense'] + }) + last_month_inc[0].update({ + 'profit': last_month_inc[0]['income'] - last_month_inc[0]['expense'] + }) + records.append(last_month_inc[0]) + income = [] + expense = [] + date = [] + profit = [] + for rec in records: + income.append(rec['income']) + expense.append(rec['expense']) + date.append(rec['date']) + profit.append(rec['profit']) + return { + 'income': income, + 'expense': expense, + 'date': date, + 'profit': profit + + } + + # function to getting late bills + + @api.model + def get_latebills(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute((''' select res_partner.name as partner, res_partner.commercial_partner_id as res , + account_move.commercial_partner_id as parent, sum(account_move.amount_total) as amount + from account_move,res_partner where + account_move.partner_id=res_partner.id AND account_move.move_type = 'in_invoice' AND + payment_state = 'not_paid' AND + account_move.company_id in ''' + str(tuple(company_id)) + ''' AND + %s + AND account_move.commercial_partner_id=res_partner.commercial_partner_id + group by parent,partner,res + order by amount desc ''') % (states_arg)) + + record = self._cr.dictfetchall() + + bill_partner = [item['partner'] for item in record] + + bill_amount = [item['amount'] for item in record] + + amounts = sum(bill_amount[9:]) + name = bill_partner[9:] + results = [] + pre_partner = [] + + bill_amount = bill_amount[:9] + bill_amount.append(amounts) + bill_partner = bill_partner[:9] + bill_partner.append("Others") + records = { + 'bill_partner': bill_partner, + 'bill_amount': bill_amount, + 'result': results, + + } + return records + + # return record + + # function to getting over dues + + @api.model + def get_overdues(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute((''' select res_partner.name as partner, res_partner.commercial_partner_id as res, + account_move.commercial_partner_id as parent, sum(account_move.amount_total) as amount + from account_move, account_move_line, res_partner, account_account where + account_move.partner_id=res_partner.id AND account_move.move_type = 'out_invoice' + AND payment_state = 'not_paid' + AND %s + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + AND account_account.internal_type = 'payable' + AND account_move.commercial_partner_id=res_partner.commercial_partner_id + group by parent,partner,res + order by amount desc + ''') % (states_arg)) + record = self._cr.dictfetchall() + due_partner = [item['partner'] for item in record] + due_amount = [item['amount'] for item in record] + + amounts = sum(due_amount[9:]) + name = due_partner[9:] + result = [] + pre_partner = [] + + due_amount = due_amount[:9] + due_amount.append(amounts) + due_partner = due_partner[:9] + due_partner.append("Others") + records = { + 'due_partner': due_partner, + 'due_amount': due_amount, + 'result': result, + + } + return records + + @api.model + def get_overdues_this_month_and_year(self, *post): + + states_arg = "" + if post[0] != 'posted': + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + company_id = self.get_current_company_value() + if post[1] == 'this_month': + self._cr.execute((''' + select to_char(account_move.date, 'Month') as month, res_partner.name as due_partner, account_move.partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner where account_move.partner_id = res_partner.id + AND account_move.move_type = 'out_invoice' + AND payment_state = 'not_paid' + AND %s + AND Extract(month FROM account_move.invoice_date_due) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.invoice_date_due) = Extract(YEAR FROM DATE(NOW())) + AND account_move.partner_id = res_partner.commercial_partner_id + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + group by parent, due_partner, month + order by amount desc ''') % (states_arg)) + else: + self._cr.execute((''' select res_partner.name as due_partner, account_move.partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner where account_move.partner_id = res_partner.id + AND account_move.move_type = 'out_invoice' + AND payment_state = 'not_paid' + AND %s + AND Extract(YEAR FROM account_move.invoice_date_due) = Extract(YEAR FROM DATE(NOW())) + AND account_move.partner_id = res_partner.commercial_partner_id + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + + group by parent, due_partner + order by amount desc ''') % (states_arg)) + + record = self._cr.dictfetchall() + due_partner = [item['due_partner'] for item in record] + due_amount = [item['amount'] for item in record] + + amounts = sum(due_amount[9:]) + name = due_partner[9:] + result = [] + pre_partner = [] + + due_amount = due_amount[:9] + due_amount.append(amounts) + due_partner = due_partner[:9] + due_partner.append("Others") + records = { + 'due_partner': due_partner, + 'due_amount': due_amount, + 'result': result, + + } + return records + + @api.model + def get_latebillss(self, *post): + company_id = self.get_current_company_value() + + partners = self.env['res.partner'].search([('active', '=', True)]) + + states_arg = "" + if post[0] != 'posted': + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + if post[1] == 'this_month': + self._cr.execute((''' + select to_char(account_move.date, 'Month') as month, res_partner.name as bill_partner, account_move.partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner where account_move.partner_id = res_partner.id + AND account_move.move_type = 'in_invoice' + AND payment_state = 'not_paid' + AND %s + AND Extract(month FROM account_move.invoice_date_due) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.invoice_date_due) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + AND account_move.partner_id = res_partner.commercial_partner_id + group by parent, bill_partner, month + order by amount desc ''') % (states_arg)) + else: + self._cr.execute((''' select res_partner.name as bill_partner, account_move.partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner where account_move.partner_id = res_partner.id + AND account_move.move_type = 'in_invoice' + AND payment_state = 'not_paid' + AND %s + AND Extract(YEAR FROM account_move.invoice_date_due) = Extract(YEAR FROM DATE(NOW())) + AND account_move.partner_id = res_partner.commercial_partner_id + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + group by parent, bill_partner + order by amount desc ''') % (states_arg)) + + result = self._cr.dictfetchall() + bill_partner = [item['bill_partner'] for item in result] + + bill_amount = [item['amount'] for item in result] + + amounts = sum(bill_amount[9:]) + name = bill_partner[9:] + results = [] + pre_partner = [] + + bill_amount = bill_amount[:9] + bill_amount.append(amounts) + bill_partner = bill_partner[:9] + bill_partner.append("Others") + records = { + 'bill_partner': bill_partner, + 'bill_amount': bill_amount, + 'result': results, + + } + return records + + @api.model + def get_top_10_customers_month(self, *post): + record_invoice = {} + record_refund = {} + company_id = self.get_current_company_value() + states_arg = "" + if post[0] != 'posted': + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + if post[1] == 'this_month': + self._cr.execute((''' select res_partner.name as customers, account_move.commercial_partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner + where account_move.commercial_partner_id = res_partner.id + AND account_move.company_id in %s + AND account_move.move_type = 'out_invoice' + AND %s + AND Extract(month FROM account_move.invoice_date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.invoice_date) = Extract(YEAR FROM DATE(NOW())) + group by parent, customers + order by amount desc + limit 10 + ''') % (tuple(company_id), states_arg)) + record_invoice = self._cr.dictfetchall() + self._cr.execute((''' select res_partner.name as customers, account_move.commercial_partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner + where account_move.commercial_partner_id = res_partner.id + AND account_move.company_id in %s + AND account_move.move_type = 'out_refund' + AND %s + AND Extract(month FROM account_move.invoice_date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.invoice_date) = Extract(YEAR FROM DATE(NOW())) + group by parent, customers + order by amount desc + limit 10 + ''') % (tuple(company_id), states_arg)) + record_refund = self._cr.dictfetchall() + else: + one_month_ago = (datetime.now() - relativedelta(months=1)).month + self._cr.execute((''' select res_partner.name as customers, account_move.commercial_partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner + where account_move.commercial_partner_id = res_partner.id + AND account_move.company_id in %s + AND account_move.move_type = 'out_invoice' + AND %s + AND Extract(month FROM account_move.invoice_date) = ''' + str( + one_month_ago) + ''' + group by parent, customers + order by amount desc + limit 10 + ''') % (tuple(company_id), states_arg)) + record_invoice = self._cr.dictfetchall() + self._cr.execute((''' select res_partner.name as customers, account_move.commercial_partner_id as parent, + sum(account_move.amount_total) as amount from account_move, res_partner + where account_move.commercial_partner_id = res_partner.id + AND account_move.company_id in %s + AND account_move.move_type = 'out_refund' + AND %s + AND Extract(month FROM account_move.invoice_date) = ''' + str( + one_month_ago) + ''' + group by parent, customers + order by amount desc + limit 10 + ''') % (tuple(company_id), states_arg)) + record_refund = self._cr.dictfetchall() + summed = [] + for out_sum in record_invoice: + parent = out_sum['parent'] + su = out_sum['amount'] - \ + (list(filter(lambda refund: refund['parent'] == out_sum['parent'], record_refund))[0][ + 'amount'] if len( + list(filter(lambda refund: refund['parent'] == out_sum['parent'], record_refund))) > 0 else 0.0) + summed.append({ + 'customers': out_sum['customers'], + 'amount': su, + 'parent': parent + }) + return summed + + # function to get total invoice + + @api.model + def get_total_invoice(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute(('''select sum(amount_total) as customer_invoice from account_move where move_type ='out_invoice' + AND %s AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_customer = self._cr.dictfetchall() + + self._cr.execute(('''select sum(amount_total) as supplier_invoice from account_move where move_type ='in_invoice' + AND %s AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_supplier = self._cr.dictfetchall() + + self._cr.execute(('''select sum(amount_total) as credit_note from account_move where move_type ='out_refund' + AND %s AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_credit_note = self._cr.dictfetchall() + + self._cr.execute(('''select sum(amount_total) as refund from account_move where move_type ='in_refund' + AND %s AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_refund = self._cr.dictfetchall() + + customer_invoice = [item['customer_invoice'] for item in record_customer] + supplier_invoice = [item['supplier_invoice'] for item in record_supplier] + credit_note = [item['credit_note'] for item in result_credit_note] + refund = [item['refund'] for item in result_refund] + + return customer_invoice, credit_note, supplier_invoice, refund + + @api.model + def get_total_invoice_current_year(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute(('''select sum(amount_total_signed) as customer_invoice from account_move where move_type ='out_invoice' + AND %s + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_customer_current_year = self._cr.dictfetchall() + + self._cr.execute(('''select sum(-(amount_total_signed)) as supplier_invoice from account_move where move_type ='in_invoice' + AND %s + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_supplier_current_year = self._cr.dictfetchall() + result_credit_note_current_year = [{'credit_note': 0.0}] + result_refund_current_year = [{'refund': 0.0}] + self._cr.execute(('''select sum(amount_total_signed) - sum(amount_residual_signed) as customer_invoice_paid from account_move where move_type ='out_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_paid_customer_invoice_current_year = self._cr.dictfetchall() + + self._cr.execute(('''select sum(-(amount_total_signed)) - sum(-(amount_residual_signed)) as supplier_invoice_paid from account_move where move_type ='in_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_paid_supplier_invoice_current_year = self._cr.dictfetchall() + record_paid_customer_credit_current_year = [{'customer_credit_paid': 0.0}] + result_paid_supplier_refund_current_year = [{'supplier_refund_paid': 0.0}] + customer_invoice_current_year = [item['customer_invoice'] for item in record_customer_current_year] + supplier_invoice_current_year = [item['supplier_invoice'] for item in record_supplier_current_year] + + credit_note_current_year = [item['credit_note'] for item in result_credit_note_current_year] + refund_current_year = [item['refund'] for item in result_refund_current_year] + + paid_customer_invoice_current_year = [item['customer_invoice_paid'] for item in + record_paid_customer_invoice_current_year] + paid_supplier_invoice_current_year = [item['supplier_invoice_paid'] for item in + result_paid_supplier_invoice_current_year] + + paid_customer_credit_current_year = [item['customer_credit_paid'] for item in + record_paid_customer_credit_current_year] + paid_supplier_refund_current_year = [item['supplier_refund_paid'] for item in + result_paid_supplier_refund_current_year] + + return customer_invoice_current_year, credit_note_current_year, supplier_invoice_current_year, refund_current_year, paid_customer_invoice_current_year, paid_supplier_invoice_current_year, paid_customer_credit_current_year, paid_supplier_refund_current_year + + @api.model + def get_total_invoice_current_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute(('''select sum(amount_total_signed) as customer_invoice from account_move where move_type ='out_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_customer_current_month = self._cr.dictfetchall() + + self._cr.execute(('''select sum(-(amount_total_signed)) as supplier_invoice from account_move where move_type ='in_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_supplier_current_month = self._cr.dictfetchall() + result_credit_note_current_month = [{'credit_note': 0.0}] + result_refund_current_month = [{'refund': 0.0}] + self._cr.execute(('''select sum(amount_total_signed) - sum(amount_residual_signed) as customer_invoice_paid from account_move where move_type ='out_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_paid_customer_invoice_current_month = self._cr.dictfetchall() + + self._cr.execute(('''select sum(-(amount_total_signed)) - sum(-(amount_residual_signed)) as supplier_invoice_paid from account_move where move_type ='in_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_paid_supplier_invoice_current_month = self._cr.dictfetchall() + record_paid_customer_credit_current_month = [{'customer_credit_paid': 0.0}] + result_paid_supplier_refund_current_month = [{'supplier_refund_paid': 0.0}] + + customer_invoice_current_month = [item['customer_invoice'] for item in record_customer_current_month] + supplier_invoice_current_month = [item['supplier_invoice'] for item in record_supplier_current_month] + credit_note_current_month = [item['credit_note'] for item in result_credit_note_current_month] + refund_current_month = [item['refund'] for item in result_refund_current_month] + paid_customer_invoice_current_month = [item['customer_invoice_paid'] for item in + record_paid_customer_invoice_current_month] + paid_supplier_invoice_current_month = [item['supplier_invoice_paid'] for item in + result_paid_supplier_invoice_current_month] + + paid_customer_credit_current_month = [item['customer_credit_paid'] for item in + record_paid_customer_credit_current_month] + paid_supplier_refund_current_month = [item['supplier_refund_paid'] for item in + result_paid_supplier_refund_current_month] + + currency = self.get_currency() + return customer_invoice_current_month, credit_note_current_month, supplier_invoice_current_month, refund_current_month, paid_customer_invoice_current_month, paid_supplier_invoice_current_month, paid_customer_credit_current_month, paid_supplier_refund_current_month, currency + + @api.model + def get_total_invoice_this_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute(('''select sum(amount_total) from account_move where move_type = 'out_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + # function to get total invoice last month + + @api.model + def get_total_invoice_last_month(self): + + one_month_ago = (datetime.now() - relativedelta(months=1)).month + + self._cr.execute('''select sum(amount_total) from account_move where move_type = 'out_invoice' AND + account_move.state = 'posted' + AND Extract(month FROM account_move.date) = ''' + str(one_month_ago) + ''' + ''') + record = self._cr.dictfetchall() + return record + + # function to get total invoice last year + + @api.model + def get_total_invoice_last_year(self): + + self._cr.execute(''' select sum(amount_total) from account_move where move_type = 'out_invoice' + AND account_move.state = 'posted' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) - 1 + ''') + record = self._cr.dictfetchall() + return record + + # function to get total invoice this year + + @api.model + def get_total_invoice_this_year(self): + + company_id = self.get_current_company_value() + + self._cr.execute(''' select sum(amount_total) from account_move where move_type = 'out_invoice' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) AND + account_move.state = 'posted' AND + account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') + record = self._cr.dictfetchall() + return record + + # function to get unreconcile items + + @api.model + def unreconcile_items(self): + self._cr.execute(''' + select count(*) FROM account_move_line l,account_account a + where L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE ''') + record = self._cr.dictfetchall() + return record + + # function to get unreconcile items this month + + @api.model + def unreconcile_items_this_month(self, *post): + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + qry = ''' select count(*) FROM account_move_line l,account_account a + where Extract(month FROM l.date) = Extract(month FROM DATE(NOW())) AND + Extract(YEAR FROM l.date) = Extract(YEAR FROM DATE(NOW())) AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS F + AND l.''' + states_arg + ''' + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''' + + self._cr.execute((''' select count(*) FROM account_move_line l,account_account a + where Extract(month FROM l.date) = Extract(month FROM DATE(NOW())) AND + Extract(YEAR FROM l.date) = Extract(YEAR FROM DATE(NOW())) AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE + AND l.%s + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + # function to get unreconcile items last month + + @api.model + def unreconcile_items_last_month(self): + + one_month_ago = (datetime.now() - relativedelta(months=1)).month + + self._cr.execute(''' select count(*) FROM account_move_line l,account_account a + where Extract(month FROM l.date) = ''' + str(one_month_ago) + ''' AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND l.balance != 0 AND a.reconcile IS TRUE + ''') + record = self._cr.dictfetchall() + return record + + # function to get unreconcile items this year + + @api.model + def unreconcile_items_this_year(self, *post): + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute((''' select count(*) FROM account_move_line l,account_account a + where Extract(year FROM l.date) = Extract(year FROM DATE(NOW())) AND + l.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE + AND l.%s + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + @api.model + def click_expense_month(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + self._cr.execute((''' select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'expense' AND + %s + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + @api.model + def click_expense_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + self._cr.execute((''' select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'expense' AND + %s + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + @api.model + def click_total_income_month(self, *post): + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'income' + AND %s + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + @api.model + def click_total_income_year(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute((''' select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'income' + AND %s + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + @api.model + def click_profit_income_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND + %s AND + (account_account.internal_group = 'income' or + account_account.internal_group = 'expense' ) + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + profit = [row[0] for row in self._cr.fetchall()] + return profit + + @api.model + def click_profit_income_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select account_move_line.id from account_account, account_move_line where + account_move_line.account_id = account_account.id AND + %s AND + (account_account.internal_group = 'income' or + account_account.internal_group = 'expense' ) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + profit = [row[0] for row in self._cr.fetchall()] + return profit + + @api.model + def click_bill_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='in_invoice' + AND %s + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_supplier_current_year = [row[0] for row in self._cr.fetchall()] + return record_supplier_current_year + + @api.model + def click_bill_year_paid(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + + self._cr.execute(('''select account_move.id from account_move where move_type ='in_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_paid_supplier_invoice_current_year = [row[0] for row in self._cr.fetchall()] + return result_paid_supplier_invoice_current_year + + @api.model + def click_invoice_year_paid(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='out_invoice' + AND %s + AND payment_state = 'paid' + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_paid_customer_invoice_current_year = [row[0] for row in self._cr.fetchall()] + return record_paid_customer_invoice_current_year + + @api.model + def click_invoice_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='out_invoice' + AND %s + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_customer_current_year = [row[0] for row in self._cr.fetchall()] + return record_customer_current_year + + @api.model + def click_bill_month(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='in_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + bill_month = [row[0] for row in self._cr.fetchall()] + return bill_month + + @api.model + def click_bill_month_paid(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='in_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND payment_state = 'paid' + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + result_paid_supplier_invoice_current_month = [row[0] for row in self._cr.fetchall()] + return result_paid_supplier_invoice_current_month + + @api.model + def click_invoice_month_paid(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='out_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND payment_state = 'paid' + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_paid_customer_invoice_current_month = [row[0] for row in self._cr.fetchall()] + return record_paid_customer_invoice_current_month + + @api.model + def click_invoice_month(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ account_move.state in ('posted', 'draft')""" + else: + states_arg = """ account_move.state = 'posted'""" + self._cr.execute(('''select account_move.id from account_move where move_type ='out_invoice' + AND %s + AND Extract(month FROM account_move.date) = Extract(month FROM DATE(NOW())) + AND Extract(YEAR FROM account_move.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record_customer_current_month = [row[0] for row in self._cr.fetchall()] + return record_customer_current_month + + @api.model + def click_unreconcile_month(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + qry = ''' select count(*) FROM account_move_line l,account_account a + where Extract(month FROM l.date) = Extract(month FROM DATE(NOW())) AND + Extract(YEAR FROM l.date) = Extract(YEAR FROM DATE(NOW())) AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS F + AND l.''' + states_arg + ''' + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''' + + self._cr.execute((''' select l.id FROM account_move_line l,account_account a + where Extract(month FROM l.date) = Extract(month FROM DATE(NOW())) AND + Extract(YEAR FROM l.date) = Extract(YEAR FROM DATE(NOW())) AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE + AND l.%s + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + @api.model + def click_unreconcile_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + self._cr.execute((''' select l.id FROM account_move_line l,account_account a + where Extract(year FROM l.date) = Extract(year FROM DATE(NOW())) AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE + AND l.%s + AND l.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = [row[0] for row in self._cr.fetchall()] + return record + + # function to get unreconcile items last year + + @api.model + def unreconcile_items_last_year(self): + + self._cr.execute(''' select count(*) FROM account_move_line l,account_account a + where Extract(year FROM l.date) = Extract(year FROM DATE(NOW())) - 1 AND + L.account_id=a.id AND l.full_reconcile_id IS NULL AND + l.balance != 0 AND a.reconcile IS TRUE + ''') + record = self._cr.dictfetchall() + return record + + # function to get total income + + @api.model + def month_income(self): + + self._cr.execute(''' select sum(debit) as debit , sum(credit) as credit from account_move, account_account,account_move_line + where account_move.move_type = 'entry' AND account_move.state = 'posted' AND account_move_line.account_id=account_account.id AND + account_account.internal_group='income' + AND to_char(DATE(NOW()), 'MM') = to_char(account_move_line.date, 'MM') + ''') + record = self._cr.dictfetchall() + return record + + # function to get total income this month + + @api.model + def month_income_this_month(self, *post): + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit) as debit, sum(credit) as credit from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'income' + AND %s + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + @api.model + def profit_income_this_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit) - sum(credit) as profit, account_account.internal_group from account_account, account_move_line where + + account_move_line.account_id = account_account.id AND + %s AND + (account_account.internal_group = 'income' or + account_account.internal_group = 'expense' ) + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + group by internal_group + ''') % (states_arg)) + income = self._cr.dictfetchall() + profit = [item['profit'] for item in income] + internal_group = [item['internal_group'] for item in income] + net_profit = True + loss = True + if profit and profit == 0: + if (-profit[1]) > (profit[0]): + net_profit = -profit[1] - profit[0] + elif (profit[1]) > (profit[0]): + net_profit = -profit[1] - profit[0] + else: + net_profit = -profit[1] - profit[0] + + return profit + + def get_current_company_value(self): + + cookies_cids = [int(r) for r in request.httprequest.cookies.get('cids').split(",")] \ + if request.httprequest.cookies.get('cids') \ + else [request.env.user.company_id.id] + + for company_id in cookies_cids: + if company_id not in self.env.user.company_ids.ids: + cookies_cids.remove(company_id) + if not cookies_cids: + cookies_cids = [self.env.company.id] + if len(cookies_cids) == 1: + cookies_cids.append(0) + return cookies_cids + + @api.model + def profit_income_this_year(self, *post): + company_id = self.get_current_company_value() + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute(('''select sum(debit) - sum(credit) as profit, account_account.internal_group from account_account, account_move_line where + + account_move_line.account_id = account_account.id AND + %s AND + (account_account.internal_group = 'income' or + account_account.internal_group = 'expense' ) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + group by internal_group + ''') % (states_arg)) + income = self._cr.dictfetchall() + profit = [item['profit'] for item in income] + internal_group = [item['internal_group'] for item in income] + net_profit = True + loss = True + + if profit and profit == 0: + if (-profit[1]) > (profit[0]): + net_profit = -profit[1] - profit[0] + elif (profit[1]) > (profit[0]): + net_profit = -profit[1] - profit[0] + else: + net_profit = -profit[1] - profit[0] + + return profit + + # function to get total income last month + + @api.model + def month_income_last_month(self): + + one_month_ago = (datetime.now() - relativedelta(months=1)).month + + self._cr.execute(''' + select sum(debit) as debit, sum(credit) as credit from account_account, + account_move_line where + account_move_line.account_id = account_account.id + AND account_account.internal_group = 'income' AND + account_move_line.parent_state = 'posted' + AND Extract(month FROM account_move_line.date) = ''' + str(one_month_ago) + ''' + ''') + + record = self._cr.dictfetchall() + + return record + + # function to get total income this year + + @api.model + def month_income_this_year(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute((''' select sum(debit) as debit, sum(credit) as credit from account_account, account_move_line where + account_move_line.account_id = account_account.id AND account_account.internal_group = 'income' + AND %s + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + # function to get total income last year + + @api.model + def month_income_last_year(self): + + self._cr.execute(''' select sum(debit) as debit, sum(credit) as credit from account_account, account_move_line where + account_move_line.parent_state = 'posted' + AND account_move_line.account_id = account_account.id AND account_account.internal_group = 'income' + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) - 1 + ''') + record = self._cr.dictfetchall() + return record + + # function to get currency + + @api.model + def get_currency(self): + company_ids = self.get_current_company_value() + if 0 in company_ids: + company_ids.remove(0) + current_company_id = company_ids[0] + current_company = self.env['res.company'].browse(current_company_id) + default = current_company.currency_id or self.env.ref('base.main_company').currency_id + lang = self.env.user.lang + if not lang: + lang = 'en_US' + lang = lang.replace("_", '-') + currency = {'position': default.position, 'symbol': default.symbol, 'language': lang} + return currency + + # function to get total expense + + @api.model + def month_expense(self): + + self._cr.execute(''' select sum(debit) as debit , sum(credit) as credit from account_move, account_account,account_move_line + where account_move.move_type = 'entry' AND account_move.state = 'posted' AND account_move_line.account_id=account_account.id AND + account_account.internal_group='expense' + AND to_char(DATE(NOW()), 'MM') = to_char(account_move_line.date, 'MM') + ''') + record = self._cr.dictfetchall() + return record + + # function to get total expense this month + + @api.model + def month_expense_this_month(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute((''' select sum(debit) as debit, sum(credit) as credit from account_account, account_move_line where + + account_move_line.account_id = account_account.id AND account_account.internal_group = 'expense' AND + %s + AND Extract(month FROM account_move_line.date) = Extract(month FROM DATE(NOW())) + AND Extract(year FROM account_move_line.date) = Extract(year FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + + + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + # function to get total expense this year + + @api.model + def month_expense_this_year(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state in ('posted', 'draft')""" + else: + states_arg = """ parent_state = 'posted'""" + + self._cr.execute((''' select sum(debit) as debit, sum(credit) as credit from account_account, account_move_line where + + account_move_line.account_id = account_account.id AND account_account.internal_group = 'expense' AND + %s + AND Extract(YEAR FROM account_move_line.date) = Extract(YEAR FROM DATE(NOW())) + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + + + + ''') % (states_arg)) + record = self._cr.dictfetchall() + return record + + @api.model + def bank_balance(self, *post): + + company_id = self.get_current_company_value() + + states_arg = "" + if post != ('posted',): + states_arg = """ parent_state = 'posted'""" + else: + states_arg = """ parent_state in ('posted', 'draft')""" + + self._cr.execute((''' select account_account.name as name, sum(balance) as balance, + min(account_account.id) as id from account_move_line left join + account_account on account_account.id = account_move_line.account_id join + account_account_type on account_account_type.id = account_account.user_type_id + where account_account_type.name = 'Bank and Cash' + AND %s + AND account_move_line.company_id in ''' + str(tuple(company_id)) + ''' + group by account_account.name + + ''') % (states_arg)) + + record = self._cr.dictfetchall() + + banks = [item['name'] for item in record] + + banking = [item['balance'] for item in record] + + bank_ids = [item['id'] for item in record] + + records = { + 'banks': banks, + 'banking': banking, + 'bank_ids': bank_ids + + } + return records diff --git a/base_accounting_kit/models/account_followup.py b/base_accounting_kit/models/account_followup.py new file mode 100644 index 0000000..5c27125 --- /dev/null +++ b/base_accounting_kit/models/account_followup.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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): + _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) + + +class FollowupLine(models.Model): + _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") diff --git a/base_accounting_kit/models/account_journal.py b/base_accounting_kit/models/account_journal.py new file mode 100644 index 0000000..4a13eae --- /dev/null +++ b/base_accounting_kit/models/account_journal.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*-
+#############################################################################
+#
+# Cybrosys Technologies Pvt. Ltd.
+#
+# Copyright (C) 2019-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, api
+
+
+class AccountJournal(models.Model):
+ _inherit = "account.journal"
+
+ def action_open_reconcile(self):
+ if self.type in ['bank', 'cash']:
+ # Open reconciliation view for bank statements belonging to this journal
+ bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)]).mapped('line_ids')
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'bank_statement_reconciliation_view',
+ 'context': {'statement_line_ids': bank_stmt.ids, 'company_ids': self.mapped('company_id').ids},
+ }
+ 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,
+ }
+
+ @api.depends('outbound_payment_method_ids')
+ def _compute_check_printing_payment_method_selected(self):
+ for journal in self:
+ journal.check_printing_payment_method_selected = any(
+ pm.code in ['check_printing', 'pdc'] for pm in
+ journal.outbound_payment_method_ids)
+
+ @api.model
+ def _enable_pdc_on_bank_journals(self):
+ """ Enables check printing payment method and add a check
+ sequence on bank journals. Called upon module installation
+ via data file.
+ """
+ pdcin = self.env.ref('base_accounting_kit.account_payment_method_pdc_in')
+ pdcout = self.env.ref('base_accounting_kit.account_payment_method_pdc_out')
+ bank_journals = self.search([('type', '=', 'bank')])
+ for bank_journal in bank_journals:
+ bank_journal._create_check_sequence()
+ bank_journal.write({
+ 'inbound_payment_method_ids': [(4, pdcin.id, None)],
+ 'outbound_payment_method_ids': [(4, pdcout.id, None)],
+ })
diff --git a/base_accounting_kit/models/account_move.py b/base_accounting_kit/models/account_move.py new file mode 100644 index 0000000..e54920d --- /dev/null +++ b/base_accounting_kit/models/account_move.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.addons.base.models import decimal_precision as dp +from odoo.exceptions import UserError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF + + +class AccountMove(models.Model): + _inherit = 'account.move' + + asset_depreciation_ids = fields.One2many('account.asset.depreciation.line', + 'move_id', + string='Assets Depreciation Lines') + + def button_cancel(self): + 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): + self.mapped('asset_depreciation_ids').post_lines_and_close_asset() + return super(AccountMove, self).post() + + @api.model + def _refund_cleanup_lines(self, lines): + 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): + 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): + 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 + + +class AccountInvoiceLine(models.Model): + _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): + 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): + 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.onchange('asset_category_id') + def onchange_asset_category_id(self): + if self.move_id == 'out_invoice' and self.asset_category_id: + self.account_id = self.asset_category_id.account_asset_id.id + elif self.move_id == 'in_invoice' and self.asset_category_id: + self.account_id = self.asset_category_id.account_asset_id.id + + @api.onchange('product_uom_id') + def _onchange_uom_id(self): + result = super(AccountInvoiceLine, self)._onchange_uom_id() + self.onchange_asset_category_id() + return result + + @api.onchange('product_id') + def _onchange_product_id(self): + vals = super(AccountInvoiceLine, self)._onchange_product_id() + if self.product_id: + if self.move_id == 'out_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id + elif self.move_id == 'in_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id + return vals + + def _set_additional_fields(self, invoice): + 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): + return product.asset_category_id.account_asset_id or super( + AccountInvoiceLine, self).get_invoice_line_account(type, product, + fpos, company) diff --git a/base_accounting_kit/models/account_payment.py b/base_accounting_kit/models/account_payment.py new file mode 100644 index 0000000..cb0b9e9 --- /dev/null +++ b/base_accounting_kit/models/account_payment.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*-
+#############################################################################
+#
+# Cybrosys Technologies Pvt. Ltd.
+#
+# Copyright (C) 2019-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, _
+from odoo.exceptions import UserError
+
+
+class AccountRegisterPayments(models.TransientModel):
+ _inherit = "account.payment.register"
+
+ bank_reference = fields.Char(copy=False)
+ cheque_reference = fields.Char(copy=False)
+ effective_date = fields.Date('Effective Date',
+ help='Effective date of PDC', copy=False,
+ default=False)
+
+ def _prepare_payment_vals(self, invoices):
+ 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
+
+
+class AccountPayment(models.Model):
+ _inherit = "account.payment"
+
+ bank_reference = fields.Char(copy=False)
+ cheque_reference = fields.Char(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
+ self = self.filtered(lambda r:
+ r.payment_method_id.code
+ in ['check_printing', 'pdc']
+ and r.state != 'reconciled')
+ if len(self) == 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 != self[0].journal_id for payment in self):
+ raise UserError(_(
+ "In order to print multiple checks at once, they "
+ "must belong to the same bank journal."))
+
+ if not self[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 = self.search([
+ ('journal_id', '=', self[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
diff --git a/base_accounting_kit/models/credit_limit.py b/base_accounting_kit/models/credit_limit.py new file mode 100644 index 0000000..9154806 --- /dev/null +++ b/base_accounting_kit/models/credit_limit.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 +from odoo.exceptions import UserError +from odoo.tools.translate import _ + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + 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_due_amount(self): + 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): + 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")) + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + def _get_outstanding_amount(self): + for rec in self: + if not rec.id: + continue + + all_order_amount = 0 + paid_invoice_amount = 0 + outstanding_amount = 0 + + all_order_ids = self.env['sale.order'].search([ + ('state','in',['sale','done']), + ('partner_id','=',rec.partner_id.id), + ('id', '=', rec.id) + ]) + + paid_invoice_ids = self.env['account.move'].search([ + ('payment_state','=','paid'), + ('partner_id','=',rec.partner_id.id), + ('move_type','=','out_invoice') + ]) + + if all_order_ids: + for order in all_order_ids: + all_order_amount += order.amount_total + + if paid_invoice_ids: + for invoice in paid_invoice_ids: + paid_invoice_amount += invoice.amount_total + + outstanding_amount = all_order_amount - paid_invoice_amount + rec.outstanding_amount = outstanding_amount + + has_due = fields.Boolean() + is_warning = fields.Boolean() + due_amount = fields.Float(related='partner_id.due_amount') + outstanding_amount = fields.Float(compute="_get_outstanding_amount", string="Outstanding Amount") + + def compute_due_amount(self): + for rec in self: + if not rec.id: + continue + rec.due_amount = rec.credit - rec.debit + + def _action_confirm(self): + for rec in self: + """To check the selected customers due amount is exceed than + blocking stage""" + all_order_ids = [] + paid_invoice_ids = [] + all_order_amount = 0 + paid_invoice_amount = 0 + outstanding_amount = 0 + + all_order_ids = self.env['sale.order'].search([ + ('state', 'in', ['sale','done']), + ('partner_id', '=', rec.partner_id.id), + ('id', '=', rec.id) + ]) + + paid_invoice_ids = self.env['account.move'].search([ + ('payment_state','=','paid'), + ('partner_id','=',rec.partner_id.id), + ('move_type','=','out_invoice') + ]) + + if all_order_ids: + for order in all_order_ids: + print ("SO ", order.name) + all_order_amount += order.amount_total + print ("All order amount ", all_order_amount) + + if paid_invoice_ids: + for invoice in paid_invoice_ids: + paid_invoice_amount += invoice.amount_total + print ("Paid invoice amount ", paid_invoice_amount) + + outstanding_amount = all_order_amount - paid_invoice_amount + + ###### + if rec.partner_id.active_limit \ + and rec.partner_id.enable_credit_limit: + if (outstanding_amount + rec.amount_total) >= rec.partner_id.blocking_stage: + if rec.partner_id.blocking_stage != 0: + remaining_credit_limit = rec.partner_id.blocking_stage - outstanding_amount + raise UserError(_("%s is in Blocking Stage, Remaining credit limit is %s %s") % (rec.partner_id.name, rec.currency_id.symbol, remaining_credit_limit)) + + 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.outstanding_amount >= self.partner_id.warning_stage: + if self.partner_id.warning_stage != 0: + self.is_warning = True + else: + self.is_warning = False + + +class AccountMove(models.Model): + _inherit = 'account.move' + + has_due = fields.Boolean() + is_warning = fields.Boolean() + due_amount = fields.Float(related='partner_id.due_amount') + outstanding_amount = fields.Float(compute="_get_outstanding_amount", string="Outstanding Amount") + + def _get_outstanding_amount(self): + for rec in self: + if not rec.id: + continue + + all_order_amount = 0 + paid_invoice_amount = 0 + outstanding_amount = 0 + + all_order_ids = self.env['sale.order'].search([ + ('state','in',['sale','done']), + ('partner_id','=',rec.partner_id.id) + ]) + + paid_invoice_ids = self.env['account.move'].search([ + ('payment_state','=','paid'), + ('partner_id','=',rec.partner_id.id), + ('move_type','=','out_invoice') + ]) + + if all_order_ids: + for order in all_order_ids: + all_order_amount += order.amount_total + + if paid_invoice_ids: + for invoice in paid_invoice_ids: + paid_invoice_amount += invoice.amount_total + + outstanding_amount = all_order_amount - paid_invoice_amount + rec.outstanding_amount = outstanding_amount + + 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.outstanding_amount + rec.amount_total) >= rec.partner_id.blocking_stage: + if 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)) + return super(AccountMove, self).action_post() + + @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.outstanding_amount >= self.partner_id.warning_stage: + if self.partner_id.warning_stage != 0: + self.is_warning = True + else: + self.is_warning = False diff --git a/base_accounting_kit/models/multiple_invoice.py b/base_accounting_kit/models/multiple_invoice.py new file mode 100644 index 0000000..e1c99b1 --- /dev/null +++ b/base_accounting_kit/models/multiple_invoice.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class MultipleInvoice(models.Model): + """Multiple Invoice Model""" + _name = "multiple.invoice" + _order = "sequence" + + sequence = fields.Integer('Sequence No') + + copy_name = fields.Char('Invoice Copy Name') + + journal_id = fields.Many2one('account.journal', string="Journal") + + +class AccountJournal(models.Model): + """Inheriting Account Journal Model""" + _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') + + body_text_position = fields.Selection([ + ('tl', 'Top Left'), + ('tr', 'Top Right'), + ('bl', 'Bottom Left'), + ('br', 'Bottom Right'), + + ], default='tl') + + text_align = fields.Selection([ + ('right', 'Right'), + ('left', 'Left'), + ('center', 'Center'), + + ], default='right') + + layout = fields.Char(related="company_id.external_report_layout_id.key") diff --git a/base_accounting_kit/models/multiple_invoice_layout.py b/base_accounting_kit/models/multiple_invoice_layout.py new file mode 100644 index 0000000..e912642 --- /dev/null +++ b/base_accounting_kit/models/multiple_invoice_layout.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + +from odoo.modules import get_resource_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): + 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): + template_style = self.env.ref('web.styles_company_report', + raise_if_not_found=False) + if not template_style: + return b'' + + company_styles = template_style._render({ + 'company_ids': self.company_id, + }) + + 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 = get_resource_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): + if self.layout == 'web.external_layout_boxed': + new_layout = 'base_accounting_kit.boxed' + + elif self.layout == 'web.external_layout_clean': + new_layout = 'base_accounting_kit.clean' + + elif self.layout == 'web.external_layout_background': + new_layout = 'base_accounting_kit.background' + + else: + new_layout = 'base_accounting_kit.standard' + + return new_layout + + def document_layout_save(self): + # meant to be overridden + return self.env.context.get('report_action') or { + 'type': 'ir.actions.act_window_close'} diff --git a/base_accounting_kit/models/payment_matching.py b/base_accounting_kit/models/payment_matching.py new file mode 100644 index 0000000..9882fc0 --- /dev/null +++ b/base_accounting_kit/models/payment_matching.py @@ -0,0 +1,1181 @@ +# -*- coding: utf-8 -*- + +import copy +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from odoo.osv import expression +from odoo.tools.misc import formatLang, format_date, parse_date + + +class AccountReconciliation(models.AbstractModel): + _name = 'account.reconciliation.widget' + _description = 'Account Reconciliation widget' + + #################################################### + # Public + #################################################### + + @api.model + def process_bank_statement_line(self, st_line_ids, data): + """Handles data sent from the bank statement reconciliation widget + (and can otherwise serve as an old-API bridge) + + :param st_line_ids + :param list of dicts data: must contains the keys + 'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts', + whose value is the same as described in process_reconciliation + except that ids are used instead of recordsets. + :returns dict: used as a hook to add additional keys. + """ + st_lines = self.env['account.bank.statement.line'].browse(st_line_ids) + AccountMoveLine = self.env['account.move.line'] + ctx = dict(self._context, force_price_include=False) + + processed_moves = self.env['account.move'] + for st_line, datum in zip(st_lines, copy.deepcopy(data)): + payment_aml_rec = AccountMoveLine.browse(datum.get('payment_aml_ids', [])) + + for aml_dict in datum.get('counterpart_aml_dicts', []): + aml_dict['move_line'] = AccountMoveLine.browse( + aml_dict['counterpart_aml_id'] + ) + del aml_dict['counterpart_aml_id'] + + if datum.get('partner_id') is not None: + st_line.write({'partner_id': datum['partner_id']}) + + ctx['default_to_check'] = datum.get('to_check') + moves = st_line.with_context(ctx).process_reconciliation( + datum.get('counterpart_aml_dicts', []), + payment_aml_rec, + datum.get('new_aml_dicts', [])) + processed_moves = (processed_moves | moves) + return {'moves': processed_moves.ids, 'statement_line_ids': processed_moves.mapped('line_ids.statement_line_id').ids} + + @api.model + def get_move_lines_for_bank_statement_line(self, st_line_id, partner_id=None, excluded_ids=None, search_str=False, offset=0, limit=None, mode=None): + """ Returns move lines for the bank statement reconciliation widget, + formatted as a list of dicts + + :param st_line_id: ids of the statement lines + :param partner_id: optional partner id to select only the moves + line corresponding to the partner + :param excluded_ids: optional move lines ids excluded from the + result + :param search_str: optional search (can be the amout, display_name, + partner name, move line name) + :param offset: useless but kept in stable to preserve api + :param limit: number of the result to search + :param mode: 'rp' for receivable/payable or 'other' + """ + st_line = self.env['account.bank.statement.line'].browse(st_line_id) + + # Blue lines = payment on bank account not assigned to a statement yet + aml_accounts = [ + st_line.journal_id.default_account_id.id + ] + + if partner_id is None: + partner_id = st_line.partner_id.id + + domain = self._domain_move_lines_for_reconciliation(st_line, aml_accounts, partner_id, excluded_ids=excluded_ids, search_str=search_str, mode=mode) + recs_count = self.env['account.move.line'].search_count(domain) + + from_clause, where_clause, where_clause_params = self.env['account.move.line']._where_calc(domain).get_sql() + query_str = ''' + SELECT "account_move_line".id FROM {from_clause} + {where_str} + ORDER BY ("account_move_line".debit - "account_move_line".credit) = {amount} DESC, + "account_move_line".date_maturity ASC, + "account_move_line".id ASC + {limit_str} + '''.format( + from_clause=from_clause, + where_str=where_clause and (" WHERE %s" % where_clause) or '', + amount=st_line.amount, + limit_str=limit and ' LIMIT %s' or '', + ) + params = where_clause_params + (limit and [limit] or []) + self.env['account.move'].flush() + self.env['account.move.line'].flush() + self.env['account.bank.statement'].flush() + self._cr.execute(query_str, params) + res = self._cr.fetchall() + + aml_recs = self.env['account.move.line'].browse([i[0] for i in res]) + target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id + return self._prepare_move_lines(aml_recs, target_currency=target_currency, target_date=st_line.date, recs_count=recs_count) + + @api.model + def _get_bank_statement_line_partners(self, st_lines): + params = [] + + # Add the res.partner.ban's IR rules. In case partners are not shared between companies, + # identical bank accounts may exist in a company we don't have access to. + ir_rules_query = self.env['res.partner.bank']._where_calc([]) + self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + if where_clause: + where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank') + params += where_clause_params + else: + where_bank = '' + + # Add the res.partner's IR rules. In case partners are not shared between companies, + # identical partners may exist in a company we don't have access to. + ir_rules_query = self.env['res.partner']._where_calc([]) + self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + if where_clause: + where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3') + params += where_clause_params + else: + where_partner = '' + + query = ''' + SELECT + st_line.id AS id, + COALESCE(p1.id,p2.id,p3.id) AS partner_id + FROM account_bank_statement_line st_line + JOIN account_move move ON move.id = st_line.move_id + ''' + query += "LEFT JOIN res_partner_bank bank ON bank.id = move.partner_bank_id OR bank.sanitized_acc_number ILIKE regexp_replace(st_line.account_number, '\W+', '', 'g') %s\n" % (where_bank) + query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n' + query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n' + # By definition the commercial partner_id doesn't have a parent_id set + query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s AND p3.parent_id is NULL \n' % (where_partner) + query += 'WHERE st_line.id IN %s' + + params += [tuple(st_lines.ids)] + + self._cr.execute(query, params) + + result = {} + for res in self._cr.dictfetchall(): + result[res['id']] = res['partner_id'] + return result + + @api.model + def get_bank_statement_line_data(self, st_line_ids, excluded_ids=None): + """ Returns the data required to display a reconciliation widget, for + each statement line in self + + :param st_line_id: ids of the statement lines + :param excluded_ids: optional move lines ids excluded from the + result + """ + results = { + 'lines': [], + 'value_min': 0, + 'value_max': 0, + 'reconciled_aml_ids': [], + } + + if not st_line_ids: + return results + + excluded_ids = excluded_ids or [] + + # Make a search to preserve the table's order. + bank_statement_lines = self.env['account.bank.statement.line'].search([('id', 'in', st_line_ids)]) + results['value_max'] = len(bank_statement_lines) + reconcile_model = self.env['account.reconcile.model'].search([('rule_type', '!=', 'writeoff_button')]) + + # Search for missing partners when opening the reconciliation widget. + if bank_statement_lines: + partner_map = self._get_bank_statement_line_partners(bank_statement_lines) + matching_amls = reconcile_model._apply_rules(bank_statement_lines, excluded_ids=excluded_ids, partner_map=partner_map) + + # Iterate on st_lines to keep the same order in the results list. + bank_statements_left = self.env['account.bank.statement'] + for line in bank_statement_lines: + if matching_amls[line.id].get('status') == 'reconciled': + reconciled_move_lines = matching_amls[line.id].get('reconciled_lines') + results['value_min'] += 1 + results['reconciled_aml_ids'] += reconciled_move_lines and reconciled_move_lines.ids or [] + else: + aml_ids = matching_amls[line.id]['aml_ids'] + bank_statements_left += line.statement_id + target_currency = line.currency_id or line.journal_id.currency_id or line.journal_id.company_id.currency_id + + amls = aml_ids and self.env['account.move.line'].browse(aml_ids) + line_vals = { + 'st_line': self._get_statement_line(line), + 'reconciliation_proposition': aml_ids and self._prepare_move_lines(amls, target_currency=target_currency, target_date=line.date) or [], + 'model_id': matching_amls[line.id].get('model') and matching_amls[line.id]['model'].id, + 'write_off': matching_amls[line.id].get('status') == 'write_off', + } + if not line.partner_id and partner_map.get(line.id): + partner = self.env['res.partner'].browse(partner_map[line.id]) + line_vals.update({ + 'partner_id': partner.id, + 'partner_name': partner.name, + }) + results['lines'].append(line_vals) + + return results + + @api.model + def get_bank_statement_data(self, bank_statement_line_ids, srch_domain=[]): + """ Get statement lines of the specified statements or all unreconciled + statement lines and try to automatically reconcile them / find them + a partner. + Return ids of statement lines left to reconcile and other data for + the reconciliation widget. + + :param bank_statement_line_ids: ids of the bank statement lines + """ + if not bank_statement_line_ids: + return {} + domain = [['id', 'in', tuple(bank_statement_line_ids)], ('is_reconciled', '=', False)] + srch_domain + bank_statement_lines = self.env['account.bank.statement.line'].search(domain) + bank_statements = bank_statement_lines.mapped('statement_id') + + results = self.get_bank_statement_line_data(bank_statement_lines.ids) + bank_statement_lines_left = self.env['account.bank.statement.line'].browse([line['st_line']['id'] for line in results['lines']]) + bank_statements_left = bank_statement_lines_left.mapped('statement_id') + + results.update({ + 'statement_name': len(bank_statements_left) == 1 and bank_statements_left.name or False, + 'journal_id': bank_statements and bank_statements[0].journal_id.id or False, + 'notifications': [] + }) + + if len(results['lines']) < len(bank_statement_lines): + results['notifications'].append({ + 'type': 'info', + 'template': 'reconciliation.notification.reconciled', + 'reconciled_aml_ids': results['reconciled_aml_ids'], + 'nb_reconciled_lines': results['value_min'], + 'details': { + 'name': _('Journal Items'), + 'model': 'account.move.line', + 'ids': results['reconciled_aml_ids'], + } + }) + + return results + + @api.model + def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, search_str=False, offset=0, limit=None, target_currency_id=False): + """ Returns unreconciled move lines for an account or a partner+account, formatted for the manual reconciliation widget """ + + Account_move_line = self.env['account.move.line'] + Account = self.env['account.account'] + Currency = self.env['res.currency'] + + domain = self._domain_move_lines_for_manual_reconciliation(account_id, partner_id, excluded_ids, search_str) + recs_count = Account_move_line.search_count(domain) + lines = Account_move_line.search(domain, limit=limit, order="date_maturity desc, id desc") + if target_currency_id: + target_currency = Currency.browse(target_currency_id) + else: + account = Account.browse(account_id) + target_currency = account.currency_id or account.company_id.currency_id + return self._prepare_move_lines(lines, target_currency=target_currency,recs_count=recs_count) + + @api.model + def get_all_data_for_manual_reconciliation(self, partner_ids, account_ids): + """ Returns the data required for the invoices & payments matching of partners/accounts. + If an argument is None, fetch all related reconciliations. Use [] to fetch nothing. + """ + MoveLine = self.env['account.move.line'] + aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids')) + if aml_ids: + aml = MoveLine.browse(aml_ids) + account = aml[0].account_id + currency = account.currency_id or account.company_id.currency_id + return { + 'accounts': [{ + 'reconciliation_proposition': self._prepare_move_lines(aml, target_currency=currency), + 'company_id': account.company_id.id, + 'currency_id': currency.id, + 'mode': 'accounts', + 'account_id': account.id, + 'account_name': account.name, + 'account_code': account.code, + }], + 'customers': [], + 'suppliers': [], + } + # If we have specified partner_ids, don't return the list of reconciliation for specific accounts as it will + # show entries that are not reconciled with other partner. Asking for a specific partner on a specific account + # is never done. + accounts_data = [] + if not partner_ids or not any(partner_ids): + accounts_data = self.get_data_for_manual_reconciliation('account', account_ids) + return { + 'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'), + 'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'), + 'accounts': accounts_data, + } + + @api.model + def get_data_for_manual_reconciliation(self, res_type, res_ids=None, account_type=None): + """ Returns the data required for the invoices & payments matching of partners/accounts (list of dicts). + If no res_ids is passed, returns data for all partners/accounts that can be reconciled. + + :param res_type: either 'partner' or 'account' + :param res_ids: ids of the partners/accounts to reconcile, use None to fetch data indiscriminately + of the id, use [] to prevent from fetching any data at all. + :param account_type: if a partner is both customer and vendor, you can use 'payable' to reconcile + the vendor-related journal entries and 'receivable' for the customer-related entries. + """ + + Account = self.env['account.account'] + Partner = self.env['res.partner'] + + if res_ids is not None and len(res_ids) == 0: + # Note : this short-circuiting is better for performances, but also required + # since postgresql doesn't implement empty list (so 'AND id in ()' is useless) + return [] + res_ids = res_ids and tuple(res_ids) + + assert res_type in ('partner', 'account') + assert account_type in ('payable', 'receivable', None) + is_partner = res_type == 'partner' + res_alias = is_partner and 'p' or 'a' + aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids')) + all_entries = self._context.get('all_entries', False) + all_entries_query = """ + AND EXISTS ( + SELECT NULL + FROM account_move_line l + JOIN account_move move ON l.move_id = move.id + JOIN account_journal journal ON l.journal_id = journal.id + WHERE l.account_id = a.id + {inner_where} + AND l.amount_residual != 0 + AND move.state = 'posted' + ) + """.format(inner_where=is_partner and 'AND l.partner_id = p.id' or ' ') + only_dual_entries_query = """ + AND EXISTS ( + SELECT NULL + FROM account_move_line l + JOIN account_move move ON l.move_id = move.id + JOIN account_journal journal ON l.journal_id = journal.id + WHERE l.account_id = a.id + {inner_where} + AND l.amount_residual > 0 + AND move.state = 'posted' + ) + AND EXISTS ( + SELECT NULL + FROM account_move_line l + JOIN account_move move ON l.move_id = move.id + JOIN account_journal journal ON l.journal_id = journal.id + WHERE l.account_id = a.id + {inner_where} + AND l.amount_residual < 0 + AND move.state = 'posted' + ) + """.format(inner_where=is_partner and 'AND l.partner_id = p.id' or ' ') + query = (""" + SELECT {select} account_id, account_name, account_code, max_date + FROM ( + SELECT {inner_select} + a.id AS account_id, + a.name AS account_name, + a.code AS account_code, + MAX(l.write_date) AS max_date + FROM + account_move_line l + RIGHT JOIN account_account a ON (a.id = l.account_id) + RIGHT JOIN account_account_type at ON (at.id = a.user_type_id) + {inner_from} + WHERE + a.reconcile IS TRUE + AND l.full_reconcile_id is NULL + {where1} + {where2} + {where3} + AND l.company_id = {company_id} + {where4} + {where5} + GROUP BY {group_by1} a.id, a.name, a.code {group_by2} + {order_by} + ) as s + {outer_where} + """.format( + select=is_partner and "partner_id, partner_name, to_char(last_time_entries_checked, 'YYYY-MM-DD') AS last_time_entries_checked," or ' ', + inner_select=is_partner and 'p.id AS partner_id, p.name AS partner_name, p.last_time_entries_checked AS last_time_entries_checked,' or ' ', + inner_from=is_partner and 'RIGHT JOIN res_partner p ON (l.partner_id = p.id)' or ' ', + where1=is_partner and ' ' or "AND ((at.type <> 'payable' AND at.type <> 'receivable') OR l.partner_id IS NULL)", + where2=account_type and "AND at.type = %(account_type)s" or '', + where3=res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '', + company_id=self.env.company.id, + where4=aml_ids and 'AND l.id IN %(aml_ids)s' or ' ', + where5=all_entries and all_entries_query or only_dual_entries_query, + group_by1=is_partner and 'l.partner_id, p.id,' or ' ', + group_by2=is_partner and ', p.last_time_entries_checked' or ' ', + order_by=is_partner and 'ORDER BY p.last_time_entries_checked' or 'ORDER BY a.code', + outer_where=is_partner and 'WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)' or ' ', + )) + self.env['account.move.line'].flush() + self.env['account.account'].flush() + self.env.cr.execute(query, locals()) + + # Apply ir_rules by filtering out + rows = self.env.cr.dictfetchall() + ids = [x['account_id'] for x in rows] + allowed_ids = set(Account.browse(ids).ids) + rows = [row for row in rows if row['account_id'] in allowed_ids] + if is_partner: + ids = [x['partner_id'] for x in rows] + allowed_ids = set(Partner.browse(ids).ids) + rows = [row for row in rows if row['partner_id'] in allowed_ids] + + # Keep mode for future use in JS + if res_type == 'account': + mode = 'accounts' + else: + mode = 'customers' if account_type == 'receivable' else 'suppliers' + + # Fetch other data + for row in rows: + account = Account.browse(row['account_id']) + currency = account.currency_id or account.company_id.currency_id + row['currency_id'] = currency.id + partner_id = is_partner and row['partner_id'] or None + rec_prop = aml_ids and self.env['account.move.line'].browse(aml_ids) or self._get_move_line_reconciliation_proposition(account.id, partner_id) + row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency) + row['mode'] = mode + row['company_id'] = account.company_id.id + + # Return the partners with a reconciliation proposition first, since they are most likely to + # be reconciled. + return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']] + + @api.model + def process_move_lines(self, data): + """ Used to validate a batch of reconciliations in a single call + :param data: list of dicts containing: + - 'type': either 'partner' or 'account' + - 'id': id of the affected res.partner or account.account + - 'mv_line_ids': ids of existing account.move.line to reconcile + - 'new_mv_line_dicts': list of dicts containing values suitable for account_move_line.create() + """ + + Partner = self.env['res.partner'] + Account = self.env['account.account'] + + for datum in data: + if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2: + self._process_move_lines(datum['mv_line_ids'], datum['new_mv_line_dicts']) + + if datum['type'] == 'partner': + partners = Partner.browse(datum['id']) + partners.mark_as_reconciled() + + #################################################### + # Private + #################################################### + + def _str_domain_for_mv_line(self, search_str): + return [ + '|', ('account_id.code', 'ilike', search_str), + '|', ('move_id.name', 'ilike', search_str), + '|', ('move_id.ref', 'ilike', search_str), + '|', ('date_maturity', 'like', parse_date(self.env, search_str)), + '&', ('name', '!=', '/'), ('name', 'ilike', search_str) + ] + + @api.model + def _domain_move_lines(self, search_str): + """ Returns the domain from the search_str search + :param search_str: search string + """ + if not search_str: + return [] + str_domain = self._str_domain_for_mv_line(search_str) + if search_str[0] in ['-', '+']: + try: + amounts_str = search_str.split('|') + for amount_str in amounts_str: + amount = amount_str[0] == '-' and float(amount_str) or float(amount_str[1:]) + amount_domain = [ + '|', ('amount_residual', '=', amount), + '|', ('amount_residual_currency', '=', amount), + '|', (amount_str[0] == '-' and 'credit' or 'debit', '=', float(amount_str[1:])), + ('amount_currency', '=', amount), + ] + str_domain = expression.OR([str_domain, amount_domain]) + except: + pass + else: + try: + amount = float(search_str) + amount_domain = [ + '|', ('amount_residual', '=', amount), + '|', ('amount_residual_currency', '=', amount), + '|', ('amount_residual', '=', -amount), + '|', ('amount_residual_currency', '=', -amount), + '&', ('account_id.internal_type', '=', 'liquidity'), + '|', '|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount), ('amount_currency', '=', -amount), + ] + str_domain = expression.OR([str_domain, amount_domain]) + except: + pass + return str_domain + + @api.model + def _domain_move_lines_for_reconciliation(self, st_line, aml_accounts, partner_id, excluded_ids=[], search_str=False, mode='rp'): + """ Return the domain for account.move.line records which can be used for bank statement reconciliation. + + :param aml_accounts: + :param partner_id: + :param excluded_ids: + :param search_str: + :param mode: 'rp' for receivable/payable or 'other' + """ + AccountMoveLine = self.env['account.move.line'] + + #Always exclude the journal items that have been marked as 'to be checked' in a former bank statement reconciliation + to_check_excluded = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain()).ids + excluded_ids.extend(to_check_excluded) + + domain_reconciliation = [ + '&', '&', '&', + ('statement_line_id', '=', False), + ('account_id', 'in', aml_accounts), + ('payment_id', '<>', False), + ('balance', '!=', 0.0), + ] + + # default domain matching + domain_matching = [ + '&', '&', + ('reconciled', '=', False), + ('account_id.reconcile', '=', True), + ('balance', '!=', 0.0), + ] + + domain = expression.OR([domain_reconciliation, domain_matching]) + if partner_id: + domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) + if mode == 'rp': + domain = expression.AND([domain, + [('account_id.internal_type', 'in', ['receivable', 'payable', 'liquidity'])] + ]) + else: + domain = expression.AND([domain, + [('account_id.internal_type', 'not in', ['receivable', 'payable', 'liquidity'])] + ]) + + # Domain factorized for all reconciliation use cases + if search_str: + str_domain = self._domain_move_lines(search_str=search_str) + str_domain = expression.OR([ + str_domain, + [('partner_id.name', 'ilike', search_str)] + ]) + domain = expression.AND([ + domain, + str_domain + ]) + + if excluded_ids: + domain = expression.AND([ + [('id', 'not in', excluded_ids)], + domain + ]) + # filter on account.move.line having the same company as the statement line + domain = expression.AND([domain, [('company_id', '=', st_line.company_id.id)]]) + + # take only moves in valid state. Draft is accepted only when "Post At" is set + # to "Bank Reconciliation" in the associated journal + domain_post_at = [ + + ('move_id.state', 'not in', ['draft', 'cancel']), + ] + domain = expression.AND([domain, domain_post_at]) + + if st_line.company_id.account_bank_reconciliation_start: + domain = expression.AND([domain, [('date', '>=', st_line.company_id.account_bank_reconciliation_start)]]) + return domain + + @api.model + def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, search_str=False): + """ Create domain criteria that are relevant to manual reconciliation. """ + domain = [ + ('reconciled', '=', False), + ('account_id', '=', account_id), + ('move_id.state', '=', 'posted') + ] + domain = expression.AND([domain, [('balance', '!=', 0.0)]]) + if partner_id: + domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) + if excluded_ids: + domain = expression.AND([[('id', 'not in', excluded_ids)], domain]) + if search_str: + str_domain = self._domain_move_lines(search_str=search_str) + domain = expression.AND([domain, str_domain]) + # filter on account.move.line having the same company as the given account + account = self.env['account.account'].browse(account_id) + domain = expression.AND([domain, [('company_id', '=', account.company_id.id)]]) + return domain + + @api.model + def _prepare_move_lines(self, move_lines, target_currency=False, target_date=False, recs_count=0): + """ Returns move lines formatted for the manual/bank reconciliation widget + + :param move_line_ids: + :param target_currency: currency (browse) you want the move line debit/credit converted into + :param target_date: date to use for the monetary conversion + """ + context = dict(self._context or {}) + ret = [] + + for line in move_lines: + company_currency = line.company_id.currency_id + line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency + ret_line = { + 'id': line.id, + 'name': line.name and line.name != '/' and line.move_id.name != line.name and line.move_id.name + ': ' + line.name or line.move_id.name, + 'ref': line.move_id.ref or '', + # For reconciliation between statement transactions and already registered payments (eg. checks) + # NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled + 'account_id': [line.account_id.id, line.account_id.display_name], + 'already_paid': line.account_id.internal_type == 'liquidity', + 'account_code': line.account_id.code, + 'account_name': line.account_id.name, + 'account_type': line.account_id.internal_type, + 'date_maturity': format_date(self.env, line.date_maturity), + 'date': format_date(self.env, line.date), + 'journal_id': [line.journal_id.id, line.journal_id.display_name], + 'partner_id': line.partner_id.id, + 'partner_name': line.partner_id.name, + 'currency_id': line_currency.id, + } + + debit = line.debit + credit = line.credit + amount = line.amount_residual + amount_currency = line.amount_residual_currency + + # For already reconciled lines, don't use amount_residual(_currency) + if line.account_id.internal_type == 'liquidity': + amount = debit - credit + amount_currency = line.amount_currency + + target_currency = target_currency or company_currency + + # Use case: + # Let's assume that company currency is in USD and that we have the 3 following move lines + # Debit Credit Amount currency Currency + # 1) 25 0 0 NULL + # 2) 17 0 25 EUR + # 3) 33 0 25 YEN + # + # If we ask to see the information in the reconciliation widget in company currency, we want to see + # The following information + # 1) 25 USD (no currency information) + # 2) 17 USD [25 EUR] (show 25 euro in currency information, in the little bill) + # 3) 33 USD [25 YEN] (show 25 yen in currency information) + # + # If we ask to see the information in another currency than the company let's say EUR + # 1) 35 EUR [25 USD] + # 2) 25 EUR (no currency information) + # 3) 50 EUR [25 YEN] + # In that case, we have to convert the debit-credit to the currency we want and we show next to it + # the value of the amount_currency or the debit-credit if no amount currency + if target_currency == company_currency: + if line_currency == target_currency: + amount = amount + amount_currency = "" + total_amount = debit - credit + total_amount_currency = "" + else: + amount = amount + amount_currency = amount_currency + total_amount = debit - credit + total_amount_currency = line.amount_currency + + if target_currency != company_currency: + if line_currency == target_currency: + amount = amount_currency + amount_currency = "" + total_amount = line.amount_currency + total_amount_currency = "" + else: + amount_currency = line.currency_id and amount_currency or amount + company = line.account_id.company_id + date = target_date or line.date + amount = company_currency._convert(amount, target_currency, company, date) + total_amount = company_currency._convert((line.debit - line.credit), target_currency, company, date) + total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit) + + ret_line['recs_count'] = recs_count + ret_line['debit'] = amount > 0 and amount or 0 + ret_line['credit'] = amount < 0 and -amount or 0 + ret_line['amount_currency'] = amount_currency + ret_line['amount_str'] = formatLang(self.env, abs(amount), currency_obj=target_currency) + ret_line['total_amount_str'] = formatLang(self.env, abs(total_amount), currency_obj=target_currency) + ret_line['amount_currency_str'] = amount_currency and formatLang(self.env, abs(amount_currency), currency_obj=line_currency) or "" + ret_line['total_amount_currency_str'] = total_amount_currency and formatLang(self.env, abs(total_amount_currency), currency_obj=line_currency) or "" + ret.append(ret_line) + return ret + + @api.model + def _get_statement_line(self, st_line): + """ Returns the data required by the bank statement reconciliation widget to display a statement line """ + + statement_currency = st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id + if st_line.amount_currency and st_line.currency_id: + amount = st_line.amount_currency + amount_currency = st_line.amount + amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=statement_currency) + else: + amount = st_line.amount + amount_currency = amount + amount_currency_str = "" + amount_str = formatLang(self.env, abs(amount), currency_obj=st_line.currency_id or statement_currency) + + data = { + 'id': st_line.id, + 'ref': st_line.ref, + 'note': st_line.narration or "", + 'name': st_line.name, + 'date': format_date(self.env, st_line.date), + 'amount': amount, + 'amount_str': amount_str, # Amount in the statement line currency + 'currency_id': st_line.currency_id.id or statement_currency.id, + 'partner_id': st_line.partner_id.id, + 'journal_id': st_line.journal_id.id, + 'statement_id': st_line.statement_id.id, + 'account_id': [st_line.journal_id.default_account_id.id, st_line.journal_id.default_account_id.display_name], + 'account_code': st_line.journal_id.default_account_id.code, + 'account_name': st_line.journal_id.default_account_id.name, + 'partner_name': st_line.partner_id.name, + 'communication_partner_name': st_line.partner_name, + 'amount_currency_str': amount_currency_str, # Amount in the statement currency + 'amount_currency': amount_currency, # Amount in the statement currency + 'has_no_partner': not st_line.partner_id.id, + 'company_id': st_line.company_id.id, + } + if st_line.partner_id: + data['open_balance_account_id'] = amount > 0 and st_line.partner_id.property_account_receivable_id.id or st_line.partner_id.property_account_payable_id.id + + return data + + @api.model + def _get_move_line_reconciliation_proposition(self, account_id, partner_id=None): + """ Returns two lines whose amount are opposite """ + + Account_move_line = self.env['account.move.line'] + + ir_rules_query = Account_move_line._where_calc([]) + Account_move_line._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + where_str = where_clause and (" WHERE %s" % where_clause) or '' + + # Get pairs + query = """ + SELECT a.id, b.id + FROM account_move_line a, account_move_line b, + account_move move_a, account_move move_b, + account_journal journal_a, account_journal journal_b + WHERE a.id != b.id + AND move_a.id = a.move_id + AND move_a.state = 'posted' + AND move_a.journal_id = journal_a.id + AND move_b.id = b.move_id + AND move_b.journal_id = journal_b.id + AND move_b.state = 'posted' + AND a.amount_residual = -b.amount_residual + AND a.balance != 0.0 + AND b.balance != 0.0 + AND NOT a.reconciled + AND a.account_id = %s + AND (%s IS NULL AND b.account_id = %s) + AND (%s IS NULL AND NOT b.reconciled OR b.id = %s) + AND (%s is NULL OR (a.partner_id = %s AND b.partner_id = %s)) + AND a.id IN (SELECT "account_move_line".id FROM {0}) + AND b.id IN (SELECT "account_move_line".id FROM {0}) + ORDER BY a.date desc + LIMIT 1 + """.format(from_clause + where_str) + move_line_id = self.env.context.get('move_line_id') or None + params = [ + account_id, + move_line_id, account_id, + move_line_id, move_line_id, + partner_id, partner_id, partner_id, + ] + where_clause_params + where_clause_params + self.env.cr.execute(query, params) + + pairs = self.env.cr.fetchall() + + if pairs: + return Account_move_line.browse(pairs[0]) + return Account_move_line + + @api.model + def _process_move_lines(self, move_line_ids, new_mv_line_dicts): + """ Create new move lines from new_mv_line_dicts (if not empty) then call reconcile_partial on self and new move lines + + :param new_mv_line_dicts: list of dicts containing values suitable for account_move_line.create() + """ + if len(move_line_ids) < 1 or len(move_line_ids) + len(new_mv_line_dicts) < 2: + raise UserError(_('A reconciliation must involve at least 2 move lines.')) + + account_move_line = self.env['account.move.line'].browse(move_line_ids) + writeoff_lines = self.env['account.move.line'] + + # Create writeoff move lines + if len(new_mv_line_dicts) > 0: + company_currency = account_move_line[0].account_id.company_id.currency_id + same_currency = False + currencies = list(set([aml.currency_id or company_currency for aml in account_move_line])) + if len(currencies) == 1 and currencies[0] != company_currency: + same_currency = True + # We don't have to convert debit/credit to currency as all values in the reconciliation widget are displayed in company currency + # If all the lines are in the same currency, create writeoff entry with same currency also + for mv_line_dict in new_mv_line_dicts: + if not same_currency: + mv_line_dict['amount_currency'] = False + writeoff_lines += account_move_line._create_writeoff([mv_line_dict]) + + (account_move_line + writeoff_lines).reconcile() + else: + account_move_line.reconcile() + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.move.line' + + def _create_writeoff(self, writeoff_vals): + """ Create a writeoff move per journal for the account.move.lines in self. If debit/credit is not specified in vals, + the writeoff amount will be computed as the sum of amount_residual of the given recordset. + :param writeoff_vals: list of dicts containing values suitable for account_move_line.create(). The data in vals will + be processed to create bot writeoff account.move.line and their enclosing account.move. + """ + def compute_writeoff_counterpart_vals(values): + line_values = values.copy() + line_values['debit'], line_values['credit'] = line_values['credit'], line_values['debit'] + if 'amount_currency' in values: + line_values['amount_currency'] = -line_values['amount_currency'] + return line_values + # Group writeoff_vals by journals + writeoff_dict = {} + for val in writeoff_vals: + journal_id = val.get('journal_id', False) + if not writeoff_dict.get(journal_id, False): + writeoff_dict[journal_id] = [val] + else: + writeoff_dict[journal_id].append(val) + + partner_id = self.env['res.partner']._find_accounting_partner(self[0].partner_id).id + company_currency = self[0].account_id.company_id.currency_id + writeoff_currency = self[0].account_id.currency_id or company_currency + line_to_reconcile = self.env['account.move.line'] + # Iterate and create one writeoff by journal + writeoff_moves = self.env['account.move'] + for journal_id, lines in writeoff_dict.items(): + total = 0 + total_currency = 0 + writeoff_lines = [] + date = fields.Date.today() + for vals in lines: + # Check and complete vals + if 'account_id' not in vals or 'journal_id' not in vals: + raise UserError(_("It is mandatory to specify an account and a journal to create a write-off.")) + if ('debit' in vals) ^ ('credit' in vals): + raise UserError(_("Either pass both debit and credit or none.")) + if 'date' not in vals: + vals['date'] = self._context.get('date_p') or fields.Date.today() + vals['date'] = fields.Date.to_date(vals['date']) + if vals['date'] and vals['date'] < date: + date = vals['date'] + if 'name' not in vals: + vals['name'] = self._context.get('comment') or _('Write-Off') + if 'analytic_account_id' not in vals: + vals['analytic_account_id'] = self.env.context.get('analytic_id', False) + #compute the writeoff amount if not given + if 'credit' not in vals and 'debit' not in vals: + amount = sum([r.amount_residual for r in self]) + vals['credit'] = amount > 0 and amount or 0.0 + vals['debit'] = amount < 0 and abs(amount) or 0.0 + vals['partner_id'] = partner_id + total += vals['debit']-vals['credit'] + if 'amount_currency' not in vals and writeoff_currency != company_currency: + vals['currency_id'] = writeoff_currency.id + sign = 1 if vals['debit'] > 0 else -1 + vals['amount_currency'] = sign * abs(sum([r.amount_residual_currency for r in self])) + total_currency += vals['amount_currency'] + + writeoff_lines.append(compute_writeoff_counterpart_vals(vals)) + + # Create balance line + writeoff_lines.append({ + 'name': _('Write-Off'), + 'debit': total > 0 and total or 0.0, + 'credit': total < 0 and -total or 0.0, + 'amount_currency': total_currency, + 'currency_id': total_currency and writeoff_currency.id or False, + 'journal_id': journal_id, + 'account_id': self[0].account_id.id, + 'partner_id': partner_id + }) + + # Create the move + writeoff_move = self.env['account.move'].create({ + 'journal_id': journal_id, + 'date': date, + 'state': 'draft', + 'line_ids': [(0, 0, line) for line in writeoff_lines], + }) + writeoff_moves += writeoff_move + line_to_reconcile += writeoff_move.line_ids.filtered(lambda r: r.account_id == self[0].account_id).sorted(key='id')[-1:] + + #post all the writeoff moves at once + if writeoff_moves: + writeoff_moves.action_post() + + # Return the writeoff move.line which is to be reconciled + return line_to_reconcile + + +class AccountBankStatement(models.Model): + + _inherit = "account.bank.statement" + + accounting_date = fields.Date(string="Accounting Date", + help="If set, the accounting entries created during the bank statement reconciliation process will be created at this date.\n" + "This is useful if the accounting period in which the entries should normally be booked is already closed.", + states={'open': [('readonly', False)]}, readonly=True) + + def action_bank_reconcile_bank_statements(self): + self.ensure_one() + bank_stmt_lines = self.mapped('line_ids') + return { + 'type': 'ir.actions.client', + 'tag': 'bank_statement_reconciliation_view', + 'context': {'statement_line_ids': bank_stmt_lines.ids, 'company_ids': self.mapped('company_id').ids}, + } + + +class AccountBankStatementLine(models.Model): + + _inherit = "account.bank.statement.line" + + move_name = fields.Char(string='Journal Entry Name', readonly=True, + default=False, copy=False, + help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.") + + def process_reconciliation(self, counterpart_aml_dicts=None, payment_aml_rec=None, new_aml_dicts=None): + """Match statement lines with existing payments (eg. checks) and/or + payables/receivables (eg. invoices and credit notes) and/or new move + lines (eg. write-offs). + If any new journal item needs to be created (via new_aml_dicts or + counterpart_aml_dicts), a new journal entry will be created and will + contain those items, as well as a journal item for the bank statement + line. + Finally, mark the statement line as reconciled by putting the matched + moves ids in the column journal_entry_ids. + + :param self: browse collection of records that are supposed to have no + accounting entries already linked. + :param (list of dicts) counterpart_aml_dicts: move lines to create to + reconcile with existing payables/receivables. + The expected keys are : + - 'name' + - 'debit' + - 'credit' + - 'move_line' + # The move line to reconcile (partially if specified + # debit/credit is lower than move line's credit/debit) + + :param (list of recordsets) payment_aml_rec: recordset move lines + representing existing payments (which are already fully reconciled) + + :param (list of dicts) new_aml_dicts: move lines to create. The expected + keys are : + - 'name' + - 'debit' + - 'credit' + - 'account_id' + - (optional) 'tax_ids' + - (optional) Other account.move.line fields like analytic_account_id + or analytics_id + - (optional) 'reconcile_model_id' + + :returns: The journal entries with which the transaction was matched. + If there was at least an entry in counterpart_aml_dicts or + new_aml_dicts, this list contains the move created by the + reconciliation, containing entries for the statement.line (1), the + counterpart move lines (0..*) and the new move lines (0..*). + """ + payable_account_type = self.env.ref("account.data_account_type_payable") + receivable_account_type = self.env.ref("account.data_account_type_receivable") + suspense_moves_mode = self._context.get("suspense_moves_mode") + counterpart_aml_dicts = counterpart_aml_dicts or [] + payment_aml_rec = payment_aml_rec or self.env["account.move.line"] + new_aml_dicts = new_aml_dicts or [] + + aml_obj = self.env["account.move.line"] + + company_currency = self.journal_id.company_id.currency_id + statement_currency = self.journal_id.currency_id or company_currency + + counterpart_moves = self.env["account.move"] + + # Check and prepare received data + if any(rec.statement_id for rec in payment_aml_rec): + raise UserError(_("A selected move line was already reconciled.")) + for aml_dict in counterpart_aml_dicts: + if aml_dict["move_line"].reconciled and not suspense_moves_mode: + raise UserError(_("A selected move line was already reconciled.")) + if isinstance(aml_dict["move_line"], int): + aml_dict["move_line"] = aml_obj.browse(aml_dict["move_line"]) + + account_types = self.env["account.account.type"] + for aml_dict in counterpart_aml_dicts + new_aml_dicts: + if aml_dict.get("tax_ids") and isinstance(aml_dict["tax_ids"][0], int): + # Transform the value in the format required for One2many and + # Many2many fields + aml_dict["tax_ids"] = [(4, id, None) for id in aml_dict["tax_ids"]] + + user_type_id = ( + self.env["account.account"] + .browse(aml_dict.get("account_id")) + .user_type_id + ) + if ( + user_type_id in [payable_account_type, receivable_account_type] + and user_type_id not in account_types + ): + account_types |= user_type_id + # Fully reconciled moves are just linked to the bank statement + total = self.amount + currency = self.currency_id or statement_currency + for aml_rec in payment_aml_rec: + balance = ( + aml_rec.amount_currency if aml_rec.currency_id else aml_rec.balance + ) + aml_currency = aml_rec.currency_id or aml_rec.company_currency_id + total -= aml_currency._convert( + balance, currency, aml_rec.company_id, aml_rec.date + ) + aml_rec.with_context(check_move_validity=False).write({"statement_line_id": self.id}) + counterpart_moves = counterpart_moves | aml_rec.move_id + # Update + if aml_rec.payment_id and aml_rec.move_id.state == "draft": + # In case the journal is set to only post payments when performing bank + #reconciliation, we modify its date and post it. + aml_rec.move_id.date = self.date + aml_rec.payment_id.payment_date = self.date + aml_rec.move_id.action_post() + # We check the paid status of the invoices reconciled with this payment + for invoice in aml_rec.payment_id.reconciled_invoice_ids: + self._check_invoice_state(invoice) + + # Create move line(s). Either matching an existing journal entry (eg. invoice), in which + # case we reconcile the existing and the new move lines together, or being a write-off. + if counterpart_aml_dicts or new_aml_dicts: + aml_obj = self.env["account.move.line"] + self.move_id.line_ids.with_context(force_delete=True).unlink() + liquidity_aml_dict = self._prepare_liquidity_move_line_vals() + aml_obj.with_context(check_move_validity=False).create(liquidity_aml_dict) + + self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1 + counterpart_moves = counterpart_moves | self.move_id + + # Complete dicts to create both counterpart move lines and write-offs + to_create = counterpart_aml_dicts + new_aml_dicts + date = self.date or fields.Date.today() + for aml_dict in to_create: + aml_dict["move_id"] = self.move_id.id + aml_dict["partner_id"] = self.partner_id.id + aml_dict["statement_line_id"] = self.id + self._prepare_move_line_for_currency(aml_dict, date) + + # Create write-offs + for aml_dict in new_aml_dicts: + aml_obj.with_context(check_move_validity=False).create(aml_dict) + + # Create counterpart move lines and reconcile them + aml_to_reconcile = [] + for aml_dict in counterpart_aml_dicts: + if not aml_dict["move_line"].statement_line_id: + aml_dict["move_line"].write({"statement_line_id": self.id}) + if aml_dict["move_line"].partner_id.id: + aml_dict["partner_id"] = aml_dict["move_line"].partner_id.id + aml_dict["account_id"] = aml_dict["move_line"].account_id.id + + counterpart_move_line = aml_dict.pop("move_line") + new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict) + + aml_to_reconcile.append((new_aml, counterpart_move_line)) + + # Post to allow reconcile + if self.move_id.state == 'draft': + self.move_id.with_context(skip_account_move_synchronization=True).action_post() + + # Reconcile new lines with counterpart + for new_aml, counterpart_move_line in aml_to_reconcile: + (new_aml | counterpart_move_line).reconcile() + + self._check_invoice_state(counterpart_move_line.move_id) + + # Needs to be called manually as lines were created 1 by 1 + self.move_id.update_lines_tax_exigibility() + if self.move_id.state == 'draft': + self.move_id.with_context(skip_account_move_synchronization=True).action_post() + # record the move name on the statement line to be able to retrieve + # it in case of unreconciliation + self.write({"move_name": self.move_id.name}) + + elif self.move_name: + raise UserError(_('Operation not allowed. Since your statement line already received a number (%s), you cannot reconcile it entirely with existing journal entries otherwise it would make a gap in the numbering. You should book an entry and make a regular revert of it in case you want to cancel it.')% (self.move_name)) + + # create the res.partner.bank if needed + if self.account_number and self.partner_id and not self.bank_account_id: + # Search bank account without partner to handle the case the res.partner.bank already exists but is set + # on a different partner. + self.partner_bank_id = self._find_or_create_bank_account() + + counterpart_moves._check_balanced() + return counterpart_moves + + def _prepare_move_line_for_currency(self, aml_dict, date): + self.ensure_one() + company_currency = self.journal_id.company_id.currency_id + statement_currency = self.journal_id.currency_id or company_currency + st_line_currency = self.currency_id or statement_currency + st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False + company = self.company_id + + if st_line_currency.id != company_currency.id: + aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit'] + aml_dict['currency_id'] = st_line_currency.id + if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: + # Statement is in company currency but the transaction is in foreign currency + aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate) + aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate) + elif self.currency_id and st_line_currency_rate: + # Statement is in foreign currency and the transaction is in another one + aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date) + aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date) + else: + # Statement is in foreign currency and no extra currency is given for the transaction + aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date) + aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date) + elif statement_currency.id != company_currency.id: + # Statement is in foreign currency but the transaction is in company currency + prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency + aml_dict['amount_currency'] = prorata_factor * self.amount + aml_dict['currency_id'] = statement_currency.id + + def _check_invoice_state(self, invoice): + if invoice.is_invoice(include_receipts=True): + invoice._compute_amount() + + +class ResCompany(models.Model): + _inherit = "res.company" + + account_bank_reconciliation_start = fields.Date(string="Bank Reconciliation Threshold", help="""The bank reconciliation widget won't ask to reconcile payments older than this date. + This is useful if you install accounting after having used invoicing for some time and + don't want to reconcile all the past payments with bank statements.""") diff --git a/base_accounting_kit/models/product_template.py b/base_accounting_kit/models/product_template.py new file mode 100644 index 0000000..f5f5b5c --- /dev/null +++ b/base_accounting_kit/models/product_template.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 ProductTemplate(models.Model): + _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): + 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 diff --git a/base_accounting_kit/models/recurring_payments.py b/base_accounting_kit/models/recurring_payments.py new file mode 100644 index 0000000..ba2ba7b --- /dev/null +++ b/base_accounting_kit/models/recurring_payments.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 models, fields, api, _ +from odoo.exceptions import UserError + + +class FilterRecurringEntries(models.Model): + _inherit = 'account.move' + + recurring_ref = fields.Char() + + +class RecurringPayments(models.Model): + _name = 'account.recurring.payments' + _description = 'Accounting Recurring Payment' + + def _get_next_schedule(self): + 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('Name') + debit_account = fields.Many2one('account.account', 'Debit Account', + required=True, + domain="['|', ('company_id', '=', False), " + "('company_id', '=', company_id)]") + credit_account = fields.Many2one('account.account', 'Credit Account', + required=True, + domain="['|', ('company_id', '=', False), " + "('company_id', '=', company_id)]") + 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): + 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): + 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() + + + class GetAllRecurringEntries(models.TransientModel): + _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') + + diff --git a/base_accounting_kit/models/res_config_settings.py b/base_accounting_kit/models/res_config_settings.py new file mode 100644 index 0000000..675671d --- /dev/null +++ b/base_accounting_kit/models/res_config_settings.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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): + _inherit = 'res.config.settings' + + customer_credit_limit = fields.Boolean(string="Customer Credit Limit") + + @api.model + def get_values(self): + 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): + super(ResConfigSettings, self).set_values() + self.env['ir.config_parameter'].sudo().set_param( + "customer_credit_limit", + self.customer_credit_limit) diff --git a/base_accounting_kit/models/res_partner.py b/base_accounting_kit/models/res_partner.py new file mode 100644 index 0000000..52d3e67 --- /dev/null +++ b/base_accounting_kit/models/res_partner.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2019-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 fields, models + + +class ResPartner(models.Model): + _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', + ) + + 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): + 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): + delay = """select id,delay from followup_line where followup_id = + (select id from account_followup where company_id = %s) + order by delay limit 1""" + self._cr.execute(delay, [self.env.company.id]) + record = self._cr.dictfetchall() + + return record + + + def action_after(self): + 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'] |
