# -*- 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