flectra/addons/account/models/company.py

355 lines
19 KiB
Python

# -*- coding: utf-8 -*-
from datetime import timedelta, datetime
import calendar
from flectra import fields, models, api, _
from flectra.exceptions import ValidationError
from flectra.exceptions import UserError
from flectra.tools.float_utils import float_round, float_is_zero
class ResCompany(models.Model):
_inherit = "res.company"
#TODO check all the options/fields are in the views (settings + company form view)
fiscalyear_last_day = fields.Integer(default=31, required=True)
fiscalyear_last_month = fields.Selection([(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], default=12, required=True)
period_lock_date = fields.Date(string="Lock Date for Non-Advisers", help="Only users with the 'Adviser' role can edit accounts prior to and inclusive of this date. Use it for period locking inside an open fiscal year, for example.")
fiscalyear_lock_date = fields.Date(string="Lock Date", help="No users, including Advisers, can edit accounts prior to and inclusive of this date. Use it for fiscal year locking for example.")
transfer_account_id = fields.Many2one('account.account',
domain=lambda self: [('reconcile', '=', True), ('user_type_id.id', '=', self.env.ref('account.data_account_type_current_assets').id), ('deprecated', '=', False)], string="Inter-Banks Transfer Account", help="Intermediary account used when moving money from a liquidity account to another")
expects_chart_of_accounts = fields.Boolean(string='Expects a Chart of Accounts', default=True)
chart_template_id = fields.Many2one('account.chart.template', help='The chart template for the company (if any)')
bank_account_code_prefix = fields.Char(string='Prefix of the bank accounts', oldname="bank_account_code_char")
cash_account_code_prefix = fields.Char(string='Prefix of the cash accounts')
accounts_code_digits = fields.Integer(string='Number of digits in an account code')
tax_cash_basis_journal_id = fields.Many2one('account.journal', string="Cash Basis Journal")
tax_calculation_rounding_method = fields.Selection([
('round_per_line', 'Round per Line'),
('round_globally', 'Round Globally'),
], default='round_per_line', string='Tax Calculation Rounding Method')
currency_exchange_journal_id = fields.Many2one('account.journal', string="Exchange Gain or Loss Journal", domain=[('type', '=', 'general')])
income_currency_exchange_account_id = fields.Many2one('account.account', related='currency_exchange_journal_id.default_credit_account_id',
string="Gain Exchange Rate Account", domain="[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', id)]")
expense_currency_exchange_account_id = fields.Many2one('account.account', related='currency_exchange_journal_id.default_debit_account_id',
string="Loss Exchange Rate Account", domain="[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', id)]")
anglo_saxon_accounting = fields.Boolean(string="Use anglo-saxon accounting")
property_stock_account_input_categ_id = fields.Many2one('account.account', string="Input Account for Stock Valuation", oldname="property_stock_account_input_categ")
property_stock_account_output_categ_id = fields.Many2one('account.account', string="Output Account for Stock Valuation", oldname="property_stock_account_output_categ")
property_stock_valuation_account_id = fields.Many2one('account.account', string="Account Template for Stock Valuation")
bank_journal_ids = fields.One2many('account.journal', 'company_id', domain=[('type', '=', 'bank')], string='Bank Journals')
overdue_msg = fields.Text(string='Overdue Payments Message', translate=True,
default=lambda s: _('''Dear Sir/Madam,
Our records indicate that some payments on your account are still due. Please find details below.
If the amount has already been paid, please disregard this notice. Otherwise, please forward us the total amount stated below.
If you have any queries regarding your account, Please contact us.
Thank you in advance for your cooperation.
Best Regards,'''))
tax_exigibility = fields.Boolean(string='Use Cash Basis')
#Fields of the setup step for opening move
account_opening_move_id = fields.Many2one(string='Opening Journal Entry', comodel_name='account.move', help="The journal entry containing the initial balance of all this company's accounts.")
account_opening_journal_id = fields.Many2one(string='Opening Journal', comodel_name='account.journal', related='account_opening_move_id.journal_id', help="Journal where the opening entry of this company's accounting has been posted.")
account_opening_date = fields.Date(string='Opening Date', related='account_opening_move_id.date', help="Date at which the opening entry of this company's accounting has been posted.")
#Fields marking the completion of a setup step
account_setup_company_data_done = fields.Boolean(string='Company Setup Marked As Done', help="Technical field holding the status of the company setup step.")
account_setup_bank_data_done = fields.Boolean('Bank Setup Marked As Done', help="Technical field holding the status of the bank setup step.")
account_setup_fy_data_done = fields.Boolean('Financial Year Setup Marked As Done', help="Technical field holding the status of the financial year setup step.")
account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.")
account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.")
@api.model
def _verify_fiscalyear_last_day(self, company_id, last_day, last_month):
company = self.browse(company_id)
last_day = last_day or (company and company.fiscalyear_last_day) or 31
last_month = last_month or (company and company.fiscalyear_last_month) or 12
current_year = datetime.now().year
last_day_of_month = calendar.monthrange(current_year, last_month)[1]
return last_day > last_day_of_month and last_day_of_month or last_day
@api.multi
def compute_fiscalyear_dates(self, date):
""" Computes the start and end dates of the fiscalyear where the given 'date' belongs to
@param date: a datetime object
@returns: a dictionary with date_from and date_to
"""
self = self[0]
last_month = self.fiscalyear_last_month
last_day = self.fiscalyear_last_day
if (date.month < last_month or (date.month == last_month and date.day <= last_day)):
date = date.replace(month=last_month, day=last_day)
else:
if last_month == 2 and last_day == 29 and (date.year + 1) % 4 != 0:
date = date.replace(month=last_month, day=28, year=date.year + 1)
else:
date = date.replace(month=last_month, day=last_day, year=date.year + 1)
date_to = date
date_from = date + timedelta(days=1)
if date_from.month == 2 and date_from.day == 29:
date_from = date_from.replace(day=28, year=date_from.year - 1)
else:
date_from = date_from.replace(year=date_from.year - 1)
return {'date_from': date_from, 'date_to': date_to}
def get_new_account_code(self, current_code, old_prefix, new_prefix, digits):
return new_prefix + current_code.replace(old_prefix, '', 1).lstrip('0').rjust(digits-len(new_prefix), '0')
def reflect_code_prefix_change(self, old_code, new_code, digits):
accounts = self.env['account.account'].search([('code', 'like', old_code), ('internal_type', '=', 'liquidity'),
('company_id', '=', self.id)], order='code asc')
for account in accounts:
if account.code.startswith(old_code):
account.write({'code': self.get_new_account_code(account.code, old_code, new_code, digits)})
def reflect_code_digits_change(self, digits):
accounts = self.env['account.account'].search([('company_id', '=', self.id)], order='code asc')
for account in accounts:
account.write({'code': account.code.rstrip('0').ljust(digits, '0')})
@api.multi
def _validate_fiscalyear_lock(self, values):
if values.get('fiscalyear_lock_date'):
nb_draft_entries = self.env['account.move'].search([
('company_id', 'in', [c.id for c in self]),
('state', '=', 'draft'),
('date', '<=', values['fiscalyear_lock_date'])])
if nb_draft_entries:
raise ValidationError(_('There are still unposted entries in the period you want to lock. You should either post or delete them.'))
@api.multi
def write(self, values):
#restrict the closing of FY if there are still unposted entries
self._validate_fiscalyear_lock(values)
# Reflect the change on accounts
for company in self:
digits = values.get('accounts_code_digits') or company.accounts_code_digits
if values.get('bank_account_code_prefix') or values.get('accounts_code_digits'):
new_bank_code = values.get('bank_account_code_prefix') or company.bank_account_code_prefix
company.reflect_code_prefix_change(company.bank_account_code_prefix, new_bank_code, digits)
if values.get('cash_account_code_prefix') or values.get('accounts_code_digits'):
new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code, digits)
if values.get('accounts_code_digits'):
company.reflect_code_digits_change(digits)
return super(ResCompany, self).write(values)
@api.model
def setting_init_company_action(self):
""" Called by the 'Company Data' button of the setup bar."""
company = self.env.user.company_id
view_id = self.env.ref('account.setup_view_company_form').id
return {'type': 'ir.actions.act_window',
'name': _('Company Data'),
'res_model': 'res.company',
'target': 'new',
'view_mode': 'form',
'res_id': company.id,
'views': [[view_id, 'form']],
}
@api.model
def setting_init_bank_account_action(self):
""" Called by the 'Bank Accounts' button of the setup bar."""
company = self.env.user.company_id
view_id = self.env.ref('account.setup_bank_journal_form').id
res = {
'type': 'ir.actions.act_window',
'name': _('Bank Account'),
'view_mode': 'form',
'res_model': 'account.journal',
'target': 'new',
'views': [[view_id, 'form']],
}
# If some bank journal already exists, we open it in the form, so the user can edit it.
# Otherwise, we just open the form in creation mode.
bank_journal = self.env['account.journal'].search([('company_id','=', company.id), ('type','=','bank')], limit=1)
if bank_journal:
res['res_id'] = bank_journal.id
else:
res['context'] = {'default_type': 'bank'}
return res
@api.model
def setting_init_fiscal_year_action(self):
""" Called by the 'Fiscal Year Opening' button of the setup bar."""
company = self.env.user.company_id
company.create_op_move_if_non_existant()
new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id})
view_id = self.env.ref('account.setup_financial_year_opening_form').id
return {
'type': 'ir.actions.act_window',
'name': _('Fiscal Year'),
'view_mode': 'form',
'res_model': 'account.financial.year.op',
'target': 'new',
'res_id': new_wizard.id,
'views': [[view_id, 'form']],
}
@api.model
def setting_chart_of_accounts_action(self):
""" Called by the 'Chart of Accounts' button of the setup bar."""
company = self.env.user.company_id
company.account_setup_coa_done = True
# If an opening move has already been posted, we open the tree view showing all the accounts
if company.opening_move_posted():
return 'account.action_account_form'
# Otherwise, we create the opening move
company.create_op_move_if_non_existant()
# Then, we open will open a custom tree view allowing to edit opening balances of the account
view_id = self.env.ref('account.init_accounts_tree').id
# Hide the current year earnings account as it is automatically computed
domain = [('user_type_id', '!=', self.env.ref('account.data_unaffected_earnings').id), ('company_id','=', company.id)]
return {
'type': 'ir.actions.act_window',
'name': _('Chart of Accounts'),
'res_model': 'account.account',
'view_mode': 'tree',
'search_view_id': self.env.ref('account.view_account_search').id,
'views': [[view_id, 'list']],
'domain': domain,
}
@api.model
def setting_opening_move_action(self):
""" Called by the 'Initial Balances' button of the setup bar."""
company = self.env.user.company_id
# If the opening move has already been posted, we open its form view
if company.opening_move_posted():
form_view_id = self.env.ref('account.setup_posted_move_form').id
return {
'type': 'ir.actions.act_window',
'name': _('Initial Balances'),
'view_mode': 'form',
'res_model': 'account.move',
'target': 'new',
'res_id': company.account_opening_move_id.id,
'views': [[form_view_id, 'form']],
}
# Otherwise, we open a custom wizard to post it.
company.create_op_move_if_non_existant()
new_wizard = self.env['account.opening'].create({'company_id': company.id})
view_id = self.env.ref('account.setup_opening_move_wizard_form').id
return {
'type': 'ir.actions.act_window',
'name': _('Initial Balances'),
'view_mode': 'form',
'res_model': 'account.opening',
'target': 'new',
'res_id': new_wizard.id,
'views': [[view_id, 'form']],
'context': {'check_move_validity': False},
}
@api.model
def setting_hide_setup_bar(self):
""" Called by the cross button of the setup bar, to close it."""
self.env.user.company_id.account_setup_bar_closed = True
@api.model
def create_op_move_if_non_existant(self):
""" Creates an empty opening move in 'draft' state for the current company
if there wasn't already one defined. For this, the function needs at least
one journal of type 'general' to exist (required by account.move).
"""
self.ensure_one()
if not self.account_opening_move_id:
default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1)
if not default_journal:
raise UserError(_("No miscellaneous journal could be found. Please create one before proceeding."))
self.account_opening_move_id = self.env['account.move'].create({
'name': _('Opening Journal Entry'),
'company_id': self.id,
'journal_id': default_journal.id,
})
def mark_company_setup_as_done_action(self):
""" Marks the 'company' setup step as completed."""
self.account_setup_company_data_done = True
def unmark_company_setup_as_done_action(self):
""" Marks the 'company' setup step as uncompleted."""
self.account_setup_company_data_done = False
def opening_move_posted(self):
""" Returns true if this company has an opening account move and this move is posted."""
return bool(self.account_opening_move_id) and self.account_opening_move_id.state == 'posted'
def get_unaffected_earnings_account(self):
""" Returns the unaffected earnings account for this company, creating one
if none has yet been defined.
"""
unaffected_earnings_type = self.env.ref("account.data_unaffected_earnings")
account = self.env['account.account'].search([('company_id', '=', self.id),
('user_type_id', '=', unaffected_earnings_type.id)])
if account:
return account[0]
return self.env['account.account'].create({
'code': '999999',
'name': _('Undistributed Profits/Losses'),
'user_type_id': unaffected_earnings_type.id,
'company_id': self.id,
})
def get_opening_move_differences(self, opening_move_lines):
currency = self.currency_id
balancing_move_line = opening_move_lines.filtered(lambda x: x.account_id == self.get_unaffected_earnings_account())
debits_sum = credits_sum = 0.0
for line in opening_move_lines:
if line != balancing_move_line:
#skip the autobalancing move line
debits_sum += line.debit
credits_sum += line.credit
difference = abs(debits_sum - credits_sum)
debit_diff = (debits_sum > credits_sum) and float_round(difference, precision_rounding=currency.rounding) or 0.0
credit_diff = (debits_sum < credits_sum) and float_round(difference, precision_rounding=currency.rounding) or 0.0
return debit_diff, credit_diff
def _auto_balance_opening_move(self):
""" Checks the opening_move of this company. If it has not been posted yet
and is unbalanced, balances it with a automatic account.move.line in the
current year earnings account.
"""
if self.account_opening_move_id and self.account_opening_move_id.state == 'draft':
debit_diff, credit_diff = self.get_opening_move_differences(self.account_opening_move_id.line_ids)
currency = self.currency_id
balancing_move_line = self.account_opening_move_id.line_ids.filtered(lambda x: x.account_id == self.get_unaffected_earnings_account())
if float_is_zero(debit_diff + credit_diff, precision_rounding=currency.rounding):
if balancing_move_line:
# zero difference and existing line : delete the line
balancing_move_line.unlink()
else:
if balancing_move_line:
# Non-zero difference and existing line : edit the line
balancing_move_line.write({'debit': credit_diff, 'credit': debit_diff})
else:
# Non-zero difference and no existing line : create a new line
balancing_account = self.get_unaffected_earnings_account()
self.env['account.move.line'].create({
'name': _('Automatic Balancing Line'),
'move_id': self.account_opening_move_id.id,
'account_id': balancing_account.id,
'debit': credit_diff,
'credit': debit_diff,
})