You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

512 lines
16 KiB
Python

8 months ago
# -*- coding: utf-8 -*-
from collections import defaultdict
import datetime
from odoo import models, fields, api
from odoo.exceptions import ValidationError, MissingError, UserError
class AccountPeriod(models.Model):
"""会计期间"""
_name = 'fr.account.period'
_description = '会计期间'
_sql_constraints = [('unique_year_num_company', 'UNIQUE (year, num, company_id)', '会计期间已存在!')]
# 基础字段
name = fields.Char(string='会计期间', required=True)
num = fields.Integer(string='期间号', required=True, index=True)
date_start = fields.Date(string='开始日期', required=True)
date_end = fields.Date(string='结束日期', required=True)
# 关系字段
fiscalyear_id = fields.Many2one('fr.account.fiscalyear', string='会计年度', required=True, ondelete='cascade')
unpost_move_ids = fields.One2many('account.move', 'fr_period_id', string='未过账凭证', domain=[('state', '!=', 'posted')])
# 关联字段
company_id = fields.Many2one(related='fiscalyear_id.company_id', string='公司', store=True)
# 计算字段
year = fields.Integer(compute='_compute_year', string='期间年度', store=True, index=True)
# 状态
state = fields.Selection([
('unuse', '无效'),
('open', '开启'),
('close', '关闭'),
('ongoing', '当前'),
], string='状态', copy=False, index=True, default='open')
# ===================
# 公用方法
# ===================
def get_prev_period(self):
"""单实例方法:获取当前期间的上一会计期间
:return: 会计期间对象单实例或空对象
"""
self.ensure_one()
# 获取上一会计期间
if self.num != 1:
prev_period = self.get_prev_period_in_fiscalyear()
else:
prev_fiscalyear = self.fiscalyear_id.get_prev_fiscalyear()
if prev_fiscalyear:
prev_period = prev_fiscalyear.get_period_by_num(12)
else:
prev_period = self.env['fr.account.period']
return prev_period
def get_next_period(self):
"""单实例方法:获取当前期间的下一会计期间
:return: 会计期间对象单实例或空对象
"""
self.ensure_one()
# 获取下一会计期间
if self.num != 12:
next_period = self.get_next_period_in_fiscalyear()
else:
next_fiscalyear = self.fiscalyear_id.get_next_fiscalyear()
if next_fiscalyear:
next_period = next_fiscalyear.get_period_by_num(12)
else:
next_period = self.env['fr.account.period']
return next_period
def get_prev_period_in_fiscalyear(self):
"""单实例方法:获取当前期间该年度的上一会计期间
:return: 会计期间对象单实例或空对象
"""
self.ensure_one()
prev_period = self.fiscalyear_id.get_period_by_num(self.num - 1)
return prev_period
def get_next_period_in_fiscalyear(self):
"""单实例方法:获取当前期间该年度的下一会计期间
:return: 会计期间对象单实例或空对象
"""
self.ensure_one()
next_period = self.fiscalyear_id.get_period_by_num(self.num + 1)
return next_period
# ===================
# 公用方法
# ===================
def check_carry_over(self):
"""单实例方法:检查是否存在未结转损益科目余额"""
self.ensure_one()
balance_data = self._fetch_balance_data(self.company_id.id, self.date_end)
accounts = self.env['account.account'].search([
('company_id', '=', self.company_id.id), ('fr_as_leaf', '=', True), ('internal_group', 'in', ['income', 'expense'])])
for account in accounts:
balance = balance_data[account.id]
if balance != 0:
return False
return True
def execute_carry_over(self, account_id, journal_id):
"""单实例方法:结转当前期间对应的损益科目余额
损益结转逻辑收入本来的贷方变为借方费用类本来的借方值变为贷方本年利润值=借方-贷方值
使得借贷平衡具体参考下列截图暂不考虑负数21_7_20 负数结转问题一解决
"""
self.ensure_one()
move_lines = []
balance_total = 0
debit_sum = 0
credit_sum = 0
balance_data = self._fetch_balance_data(self.company_id.id, self.date_end)
accounts = self.env['account.account'].search([
('company_id', '=', self.company_id.id), ('fr_as_leaf', '=', True), ('internal_group', 'in', ['income', 'expense'])])
# 结转收入, 收入类的贷方,变为借方。
for account in accounts.filtered(lambda acc: acc.internal_group == 'income'):
balance = balance_data[account.id]
if balance != 0:
balance_total += balance
# 注释内容为7_20
# if balance < 0:
debit_sum += balance
move_lines.append((0, 0, {
'name': '期间损益结转-结转收入',
'account_id': account.id,
'debit': -balance,
'credit': 0,
}))
# else:
# debit_sum += balance
# move_lines.append((0, 0, {
# 'name': '期间损益结转-结转收入',
# 'account_id': account.id,
# 'debit': balance,
# 'credit': 0,
# }))
# 结转费用 费用类本来的借方值变为贷方
for account in accounts.filtered(lambda acc: acc.internal_group == 'expense'):
balance = balance_data[account.id]
if balance != 0:
balance_total += balance
# if balance > 0:
credit_sum += balance
move_lines.append((0, 0, {
'name': '期间损益结转-结转费用',
'account_id': account.id,
'debit': 0,
'credit': balance,
}))
# else:
# credit_sum += -balance
# move_lines.append((0, 0, {
# 'name': '期间损益结转-结转费用',
# 'account_id': account.id,
# 'debit': 0,
# 'credit': -balance,
# }))
# move_lines.append((0, 0, {
# 'name': '期间损益结转-结转费用',
# 'account_id': account.id,
# 'debit': 0,
# 'credit': - balance,
# }))
# 添加结转科目行7-20本年利润计算。自带凭证明细行为负数结转问题
if balance_total > 0:
move_lines.append((0, 0, {
'name': '期间损益结转-本年利润',
'account_id': account_id,
'debit': balance_total,
'credit': 0,
}))
elif balance_total < 0:
move_lines.append((0, 0, {
'name': '期间损益结转-本年利润',
'account_id': account_id,
'debit': 0,
'credit': - balance_total,
}))
# 添加结转科目行
# if balance_total > 0:
# move_lines.append((0, 0, {
# 'name': '期间损益结转-本年利润',
# 'account_id': account_id,
# 'debit': - (debit_sum - credit_sum),
# 'credit': 0,
# }))
# elif balance_total < 0:
# move_lines.append((0, 0, {
# 'name': '期间损益结转-本年利润',
# 'account_id': account_id,
# 'debit': 0,
# 'credit': debit_sum - credit_sum,
# }))
#创建凭证
if len(move_lines) > 0:
move = self.env['account.move'].create({
'journal_id': journal_id,
'ref': '期间损益结转',
'date': self.date_end,
'line_ids': move_lines,
'state': 'draft',
})
# 凭证审核过账
# move.voucher_approved_posted()
# 此处跟之前损益结转 生成凭证,判断科目 必填 方法冲突。需要修改单独,执行新的审核过账方法
move.voucher_approved_posted_execute_carry()
return move
def check_move_num(self):
"""单实例方法:检查期间凭证编号"""
self.ensure_one()
check_res = False
moves = self.env['account.move'].search([('fr_period_id', '=', self.id), ('state', '!=', 'draft')])
str_nums = moves.filtered(lambda move: move.num and move.num != '/').mapped('num')
if not moves:
check_res = True
elif len(str_nums) == len(moves):
nums = set([int(str_num.split('-')[-1]) for str_num in str_nums])
if max(nums) == len(moves):
check_res = True
return check_res
def reset_move_num(self):
"""【凭证编号日期重排】单实例方法:重置期间凭证编号"""
self.ensure_one()
# moves = self.env['account.move'].search([('fr_period_id', '=', self.id), ('state', '!=', 'draft')])
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)])
# 重置序列号
sequence = self.env['ir.sequence'].search([('code', '=', 'move.sequence'),
('company_id', '=', self.company_id.id)])
sub_sequence = sequence.date_range_ids.filtered \
(lambda x: x.date_from == self.date_start and x.date_to == self.date_end)
if sub_sequence:
sub_sequence.unlink()
# 重置凭证编号
moves.write({'num': '/'})
moves._generate_move_num(moves)
return True
def reset_move_num_approved(self):
"""【凭证编号审核日期重排】 单实例方法:根据审批日期重置凭证编号"""
self.ensure_one()
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)])
# 重置序列号
sequence = self.env['ir.sequence'].search([('code', '=', 'move.sequence.approved'),
('company_id', '=', self.company_id.id)])
sub_sequence = sequence.date_range_ids.filtered \
(lambda x: x.date_from == self.date_start and x.date_to == self.date_end)
if sub_sequence:
sub_sequence.unlink()
moves._generate_move_num_approved(moves)
return True
def reset_move_num_fill(self, move_id):
"""单实例方法:填补凭证编号"""
self.ensure_one()
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)])
# 重置序列号
sequence = self.env['ir.sequence'].search([('code', '=', 'move.sequence'),
('company_id', '=', self.company_id.id)])
sub_sequence = sequence.date_range_ids.filtered \
(lambda x: x.date_from == self.date_start and x.date_to == self.date_end)
if not sub_sequence:
# 创建子序列
self.env['ir.sequence.date_range'].sudo().create({
'date_from': self.date_start,
'date_to': self.date_end,
'sequence_id': sequence.id,
'number_next_actual': len(moves) + 1
})
moves._generate_move_num_fill(moves, move_id, sub_sequence)
return True
# 手动凭证补号操作。
def reset_move_num_manual(self, move_id):
"""单实例方法:手动凭证补号操作"""
self.ensure_one()
# 获取当前期间全部凭证,并且进行编号排序
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)]).sorted(key=lambda m: m.num)
# 获取编号最后一位,截取数字。
last_moves = moves[-1].num[2:]
# 没有的编号列表
not_move_list = []
# 获取没有的编号
for move_x in range(1, (int(last_moves) + 1)):
move_num = '记-' + str(move_x).zfill(4)
if move_num not in moves.mapped('num'):
not_move_list.append(move_num)
return not_move_list
def reset_move_num_next(self):
"""【凭证编号重排】单实例方法:凭证编号重排"""
self.ensure_one()
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)])
# 检查序列号是否存在
sequence = self.env['ir.sequence'].search([('code', '=', 'move.sequence'),
('company_id', '=', self.company_id.id)])
sub_sequence = sequence.date_range_ids.filtered \
(lambda x: x.date_from == self.date_start and x.date_to == self.date_end)
# 如果没有子序列,则创建子序列。
if not sub_sequence:
self.env['ir.sequence.date_range'].sudo().create({
'date_from': self.date_start,
'date_to': self.date_end,
'sequence_id': sequence.id,
'number_next_actual': len(moves) + 1
})
moves._generate_move_num_next(moves, sub_sequence)
return True
# 系统编号重排
def reset_move_name_next(self):
"""单实例方法:系统编号重排"""
self.ensure_one()
moves = self.env['account.move'].search([('fr_period_id', '=', self.id)])
# 先重置所有凭证的序列号
for i, move in enumerate(moves):
move.name = '/' + str(i)
# 重置序列号
sequence = self.env['ir.sequence'].search([('code', '=', 'move.sequence.sys.number'),
('company_id', '=', self.company_id.id)])
if sequence:
sequence.unlink()
sequence = self.env['ir.sequence'].create({
'name': '%s系统编号序列(重置)' % self.company_id.name,
'code': 'move.sequence.sys.number',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(year)s/%(month)s/',
'use_date_range': False,
'date_range_type': 'month',
'company_id': self.company_id.id,
})
moves._generate_move_name_next(moves, sequence)
return True
def carry_forward(self):
"""单实例方法:期末月结,关闭会计期间"""
self.ensure_one()
if self.fiscalyear_id.state == 'draft':
raise ValidationError('期间所属会计年度尚未激活,无法进行期末月结!')
# 无效期间无法月结
if self.state == 'unuse':
raise ValidationError('无法月结无效的期间!')
# 当前期间无法月结
elif self.state == 'ongoing':
raise ValidationError('无法月结进行中的期间!')
# 已结账期间无法月结
elif self.state == 'close':
raise ValidationError('无法月结已结账的期间!')
else:
# 上一期间未结账无法月结
last_period = self.get_prev_period()
if last_period.state == 'open' or last_period.state == 'ongoing':
raise ValidationError('在当前期间之前有未结账的期间,请先处理上一期间!')
# 存在未过账凭证
if len(self.unpost_move_ids) > 0:
raise ValidationError('期间存在未过账凭证,无法结账!')
# 凭证编号不连续
if self.check_move_num() is False:
raise ValidationError('期间凭证编号不连续,无法结账!')
# 存在未结转的损益科目
if self.check_carry_over() is False:
raise ValidationError('期间存在未结转的损益科目,无法结账!')
# 更改会计期间状态
self.write({'state': 'close'})
# 关闭会计年度
if self.num == 12:
self.fiscalyear_id.close_fiscalyear()
def carry_forward_cancel(self):
"""单实例方法:取消月结,开启会计期间"""
self.ensure_one()
if self.fiscalyear_id.state == 'draft':
raise ValidationError('期间所属会计年度尚未激活,无法进行取消月结!')
# 无效期间无法取消月结
if self.state == 'unuse':
raise ValidationError('无法取消月结无效的期间!')
# 当前期间无法月结
elif self.state == 'ongoing':
raise ValidationError('无法取消月结进行中的期间!')
# 未结账期间无法取消月结
elif self.state == 'open':
raise ValidationError('无法取消月结未结账的期间!')
else:
# 下一期间已结账无法取消月结
next_period = self.get_next_period()
if next_period.state == 'close':
raise ValidationError('在当前期间之后有已结账的期间,请先处理下一期间!')
# 更新会计期间状态
self.write({'state': 'open'})
# 关闭会计年度
if self.num == 12:
self.fiscalyear_id.reopen_fiscalyear()
# ===================
# 定时任务
# ===================
@api.model
def refresh_current_period(self):
"""模型方法:根据当前时间更新会计期间状态(用于定时任务)"""
current_date = datetime.date.today()
periods_current_old = self.search([('state', '=', 'ongoing')])
if periods_current_old:
periods_current_old.write({'state': 'open'})
periods_current_new = self.search([('date_start', '<=', current_date), ('date_end', '>=', current_date)])
if periods_current_new:
periods_current_new.write({'state': 'ongoing'})
# ===================
# 计算方法
# ===================
@api.depends('fiscalyear_id', 'fiscalyear_id.name')
def _compute_year(self):
"""计算方法:计算期间年份"""
for rec in self:
rec.year = int(rec.fiscalyear_id.name)
# ===================
# 私有方法
# ===================
def _fetch_balance_data(self, company_id, date_end):
# 期末余额预读取
self.env.cr.execute(f"""
SELECT
account_id,
sum( balance ) AS balance
FROM
account_move_line
WHERE
company_id = {company_id} and date <= '{date_end}' and fr_state = 'posted'
GROUP BY
account_id
""")
balance_data = defaultdict(float)
for res in self.env.cr.fetchall():
balance_data[res[0]] = res[1]
return balance_data