# -*- coding: utf-8 -*- import datetime from odoo import models, fields, api from odoo.exceptions import ValidationError, UserError, MissingError PERIOD_SELECTION = [(str(i+1), '第%s期' % str(i+1)) for i in range(12)] def get_date_end_year(date_start): """根据开始日期计算一整年时间段的结束日期 :param date_start: 日期对象 :return: 结束日期,日期对象 """ year_end = date_start.year + 1 month_end = date_start.month if date_start.day == 29: day_end = date_start.day - 1 else: day_end = date_start.day date_end = datetime.date(year_end, month_end, day_end) - datetime.timedelta(days=1) return date_end class AccountFisclyear(models.Model): """会计年度""" _name = 'fr.account.fiscalyear' _description = '会计年度' _order = 'name desc' _sql_constraints = [('unique_year_company', 'UNIQUE (name, company_id)', '会计年度已存在!')] def _default_get_name(self): """私有方法:获取默认会计年度""" last_fiscalyear = self.search([('company_id', '=', self.env.user.company_id.id)], limit=1) if not last_fiscalyear: name = int(datetime.date.today().year) else: name = int(last_fiscalyear.name) + 1 return name def _default_get_date_start(self): """私有方法:获取会计年度默认开始日期""" last_fiscalyear = self.search([('company_id', '=', self.env.user.company_id.id)], limit=1) if not last_fiscalyear: date_start = datetime.date(datetime.date.today().year, 1, 1) else: date_start = last_fiscalyear.date_end + datetime.timedelta(days=1) return date_start # 基础字段 name = fields.Char(string='会计年度', default=_default_get_name, required=True, index=True) date_start = fields.Date(string='开始日期', required=True, default=_default_get_date_start) init_period = fields.Selection(selection=PERIOD_SELECTION, string='初始会计期间', default='1', required=True) # 关系字段 company_id = fields.Many2one('res.company', string='公司', required=True, default=lambda self: self.env.company) period_ids = fields.One2many('fr.account.period', 'fiscalyear_id', string='会计期间', readonly=True) # 计算字段 date_end = fields.Date(string='结束日期', compute='_compute_date_end', store=True) # 状态 state = fields.Selection([ ('draft', '草稿'), ('activated', '激活'), ('close', '关闭'), ], string='状态', readonly=True, copy=False, index=True, default='draft') # =================== # 公用方法 # =================== def activate_fiscalyear(self): """单实例方法:激活会计期间并添加新的总账条目""" if not self: return # 验证状态 if set(self.mapped('state')) != {'draft'}: raise ValidationError('非草稿状态的会计年度无法激活!') for rec in self: # 验证连续 last_year = rec.get_prev_fiscalyear() if last_year and last_year.state == 'draft': raise ValidationError('请先激活上一会计年度!') # 限制激活以前的年度 # activated_year = self.search([('state', '=', 'activated'), ('company_id', '=', rec.company_id.id)], limit=1) # if int(activated_year.name) > int(rec.name): # raise ValidationError('无法激活以前的会计年度!') rec.write({'state': 'activated'}) def close_fiscalyear(self): """多实例方法:关闭会计年度""" if not self: return # 操作限制 if set(self.mapped('state')) != {'activated'}: raise ValidationError('非激活状态的会计年度无法关闭!') if not set(self.mapped('period_ids').mapped('state')).issubset({'unuse', 'close'}): raise ValidationError('当期会计年度下存在未关闭的会计期间!') self.write({'state': 'close'}) def reopen_fiscalyear(self): """多实例方法:重启会计年度""" if not self: return # 操作限制 if set(self.mapped('state')) != {'close'}: raise ValidationError('非关闭状态的会计年度无法重启!') self.write({'state': 'activated'}) def get_prev_fiscalyear(self): """单实例方法:获取该会计年度的上一会计年度 :return: 会计年度对象,单实例或空对象 """ self.ensure_one() prev_name = str(int(self.name) - 1) prev_year = self.search([('name', '=', prev_name), ('company_id', '=', self.company_id.id)]) if len(prev_year) > 1: raise ValueError('错误!该公司上一会计年度不唯一。') return prev_year def get_next_fiscalyear(self): """单实例方法:获取该会计年度的下一会计年度 :return: 会计年度对象,单实例或空对象 """ self.ensure_one() next_name = str(int(self.name) + 1) next_year = self.search([('name', '=', next_name), ('company_id', '=', self.company_id.id)]) if len(next_year) > 1: raise ValueError('错误!该公司下一会计年度不唯一。') return next_year def get_period_by_num(self, num): """单实例方法:获取会计年度的指定期间号的会计期间 :return: 会计期间对象 """ self.ensure_one() period = self.mapped('period_ids').filtered(lambda x: x.num == num) return period # =================== # 继承方法 # =================== def unlink(self): if set(self.mapped('state')) != {'draft'}: raise ValidationError('无法删除非草稿状态的会计年度!') return super(AccountFisclyear, self).unlink() # =================== # 计算方法 # =================== @api.depends('date_start') def _compute_date_end(self): """多实例方法:计算会计年度结束时间""" for rec in self: if rec.date_start: rec.date_end = get_date_end_year(rec.date_start) # =================== # onchange方法 # =================== @api.onchange('date_start', 'date_end', 'name') def _generate_periods(self): """根据开始结束日期自动生成期数""" # 验证名称 self._check_name() # 计算会计期间 if self.date_start and self.date_end: self.period_ids = None months_count = 12 # 生成会计期间 for i in range(months_count): # 生成会计期间名称 num = i + 1 name = '%s年%s期' % (self.name, num) # 计算会计期间开始日期 year_start = self.date_start.year month_start = self.date_start.month + i if month_start >= 12: # 修正年月 year_start += (month_start - 1) // 12 month_start = (month_start - 1) % 12 + 1 date_start = datetime.date(year_start, month_start, 1) # 计算会计期间结束日期 year_end = year_start month_end = month_start + 1 if month_end > 12: # 修正年月 year_end += 1 month_end = 1 date_end = datetime.date(year_end, month_end, 1) - datetime.timedelta(days=1) # 计算状态 if date_start <= datetime.date.today() <= date_end: state = 'ongoing' else: state = 'open' # 创建期间 self.env['fr.account.period'].new({ 'name': name, 'num': num, 'year': int(self.name), 'date_start': date_start, 'date_end': date_end, 'state': state, 'fiscalyear_id': self.id, 'company_id': self.company_id.id }) @api.onchange('init_period', 'period_ids') def _change_periods_state(self): """多实例方法:根据初始期间更新会计期间状态""" current_date = datetime.date.today() # 更新无效期间状态 periods_unuse = self.period_ids.filtered(lambda period: period.num < int(self.init_period)) periods_unuse.update({'state': 'unuse'}) # 更新有效期间状态 periods_use = self.period_ids - periods_unuse periods_use.update({'state': 'open'}) # 更新当前期间状态 period_current = periods_use.filtered(lambda period: period.date_start < current_date < period.date_end) period_current.update({'state': 'ongoing'}) # =================== # 约束方法 # =================== @api.constrains('name') def _constrains_fiscalyear_name(self): self._check_name() @api.constrains('date_start') def _constrains_date_start(self): for rec in self: if rec.date_start.year != int(rec.name): raise ValidationError('年份与日期不匹配!') previous_fiscalyear = rec.get_prev_fiscalyear() if previous_fiscalyear and rec.date_start != previous_fiscalyear.date_end + datetime.timedelta(days=1): raise ValidationError('当前会计年度开始日期与上一会计年度结束日期不连续!') @api.constrains('init_period') def _constrains_init_period(self): for rec in self: previous_fiscalyear = rec.get_prev_fiscalyear() if previous_fiscalyear and rec.init_period != '1': raise ValidationError('当前会计年度的会计期间与上一会计年度会计期间状态不连续!') # =================== # 私有方法 # =================== def _check_name(self): for rec in self: try: year = int(rec.name) except ValueError: raise ValidationError('请输入正确的年份!') current_year = datetime.date.today().year if year < current_year - 50 or year > current_year + 50: raise ValidationError('请输入正确的年份!')