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.

1201 lines
54 KiB
Python

8 months ago
# -*- coding: utf-8 -*-
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# QQ:570165989
# Author'wangshuai'
# Date2020/11/25 0025
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
import datetime, decimal
from datetime import date
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, MissingError, UserError
from .utils import money2chinese
BALANCE_DIRECTION = [('debit', ''), ('credit', ''), ('balance', '')]
class AccountMove(models.Model):
"""记账凭证:原生的日记账分录扩展"""
_name = 'account.move'
_inherit = ['account.move', 'mail.thread', 'mail.activity.mixin']
_description = '记账凭证'
# 基础字段
num = fields.Char(string='凭证编号', copy=False, default='/', readonly=True)
date = fields.Date(string='凭证日期')
fr_type = fields.Selection([
('auto', '自动'),
('manual', '手工')
], default='auto', string='来源类型')
fr_money_format = fields.Char(string='金额大写', compute='_compute_fr_money_format')
# 时间字段
fr_create_date = fields.Datetime(string='制单日期', copy=False, default=fields.Datetime.now)
# 关于此处时间问题如果发现时间不是当前时间不会变化。需要重启一下服务。后发现重启服务器也没用。需要去除时间default 的括号
fr_create_today = fields.Date(string='制单日期', copy=False, default=fields.Date.today)
fr_approved_date = fields.Datetime(string='审核日期', copy=False)
fr_posted_date = fields.Datetime(string='过账日期', copy=False)
# 关系字段
fr_create_uid = fields.Many2one('res.users', string='制单人', copy=False, default=lambda x: x.env.user.id)
fr_approved_uid = fields.Many2one('res.users', string='审核人', copy=False)
fr_posted_uid = fields.Many2one('res.users', string='过账人', copy=False)
fr_template_id = fields.Many2one('account.move.template', string='凭证模板', copy=False)
fr_seller_id = fields.Many2one('res.partner', string='销售客户')
# 关联字段
fr_period_state = fields.Selection(related='fr_period_id.state', string='期间状态', store=True)
fr_fiscalyear_id = fields.Many2one(related='fr_period_id.fiscalyear_id', string='会计年度', store=True)
# 计算字段
fr_period_id = fields.Many2one('fr.account.period', compute='_compute_account_period', string='会计期间', store=True, index=True)
fr_attachcount = fields.Integer(string='附件数', compute='_compute_attachment_number')
fr_modify = fields.Boolean(string='修改', default=False)
fr_credentials = fields.Char(string='自有凭证号')
# 状态
state = fields.Selection([
('draft', '草稿'),
('approved', '已审核'),
('posted', '已过账'),
('cancel', '已取消'),
('invalid', '作废'),
], string='状态', readonly=True, copy=False, index=True, track_visibility='onchange', track_sequence=3, default='draft')
# ===================
# odoo14没有找到的字段和方法
# 字段amount dummy_account_idtax_type_domainauto_reverse
# 方法_amount_compute _onchange_date
# ===================
amount = fields.Monetary(compute='_amount_compute', store=True)
dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False,
readonly=True)
tax_type_domain = fields.Char(store=False,
help='Technical field used to have a dynamic taxes domain on the form view.')
auto_reverse = fields.Boolean(string='Reverse Automatically', default=False, help='If this checkbox is ticked, this entry will be automatically reversed at the reversal date you defined.')
reverse_date = fields.Date(string='Reversal Date', help='Date of the reverse accounting entry.')
reverse_entry_id = fields.Many2one('account.move', String="Reverse entry", store=True, readonly=True)
tax_type_domain = fields.Char(store=False,
help='Technical field used to have a dynamic taxes domain on the form view.')
@api.onchange('journal_id')
def _onchange_journal_id(self):
self.tax_type_domain = self.journal_id.type if self.journal_id.type in ('sale', 'purchase') else None
@api.depends('line_ids.debit', 'line_ids.credit')
def _amount_compute(self):
for move in self:
total = 0.0
for line in move.line_ids:
total += line.debit
move.amount = total
def changer_move_delete(self):
"""查询3-4-5月凭证对应的应收单做取消操作"""
company_id = self.env.user.company_id.id
all_moves = self.env['account.move'].search([('date', '>=', '2020-03-01'), ('date', '<=', '2020-05-31'),
('state', '=', 'draft'), ('company_id', '=', company_id)])
for move in all_moves:
if move.line_ids:
all_move_line = move.line_ids.mapped('id')
for move_line in all_move_line:
if move_line:
self.env['account.move.line'].search([('id', '=', move_line)]).invoice_id.action_invoice_cancel()
# ===================
# 公用方法
# ===================
def voucher_number_fill(self):
"""凭证补号"""
for period in self.mapped('fr_period_id'):
period.reset_move_num_fill(self)
# 手动凭证补号
def voucher_number_manual(self):
"""手动凭证补号"""
# 获取当前期间
move_not_list = self.fr_period_id.reset_move_num_manual(self)
if len(move_not_list) > 0:
return {
'name': '手动补号',
'view_mode': 'form',
'view_type': 'form',
'res_model': 'move.manual.no',
'type': 'ir.actions.act_window',
'views': [[self.env.ref('account_ledger.MoveManualNoWizardViewForm').id, 'form']],
'target': 'new',
'context': {
'default_move_id': self.id,
'default_account_move_not_list': move_not_list,
}
}
else:
raise ValidationError('凭证号码连续,没有可以填补的号码。')
def voucher_approved(self):
"""多实例方法:凭证审核"""
# 操作限制
if set(self.mapped('state')) != {'draft'}:
raise ValidationError('非草稿状态的记账凭证无法进行审核!')
# 新增凭证审核检查科目 判断 分析账户- 分析标签必填
for order in self:
for line in order.line_ids:
if line.account_id.label_bool:
if len(line.analytic_tag_ids) <= 0:
raise ValidationError('对应科目分析标签必填,请点击【快捷添加标签/账户】按钮,'
'快捷添加记录ID:' + str(order) + "科目" + str(line.account_id.name)
+ str(line.account_id))
if line.account_id.analysis_bool:
if len(line.analytic_account_id) <= 0:
raise ValidationError('对应科目分析账户必填,请点击【快捷添加标签/账户】按钮,'
'快捷添加记录ID:' + str(order) + "科目" + str(line.account_id.name)
+ str(line.account_id))
# 更新状态和过账信息
return self.write({
'fr_approved_uid': self.env.user.id,
'fr_approved_date': datetime.datetime.now(),
'state': 'approved'
})
def voucher_approved_execute_carry(self):
"""凭证审核,损益结转单独方法执行"""
# 操作限制
if set(self.mapped('state')) != {'draft'}:
raise ValidationError('非草稿状态的记账凭证无法进行审核!')
# 更新状态和过账信息
return self.write({
'fr_approved_uid': self.env.user.id,
'fr_approved_date': datetime.datetime.now(),
'state': 'approved'
})
def voucher_approved_cancel(self):
"""多实例方法:取消审核"""
if not self:
return True
# 操作限制
if set(self.mapped('state')) != {'approved'}:
raise ValidationError('非已审核状态的记账凭证无法取消审核!')
# 更新状态及清除过账信息
return self.write({
'fr_approved_uid': None,
'fr_approved_date': None,
'state': 'draft',
'posted_before': False,
})
def voucher_posted(self):
"""多实例方法:凭证过账"""
# 操作限制
if set(self.mapped('state')) != {'approved'}:
raise ValidationError('非已审核状态的日记账凭证无法进行过账!')
# 过账分录
self.post()
# 修改过账人
return self.write({
'fr_posted_uid': self.env.user.id,
})
def voucher_posted_execute_carry(self):
"""损益结转 凭证过账方法"""
# 操作限制
if set(self.mapped('state')) != {'approved'}:
raise ValidationError('非已审核状态的日记账凭证无法进行过账!')
# 过账分录
self.post()
# 修改过账人
return self.write({
'fr_posted_uid': self.env.user.id,
})
def voucher_posted_cancel(self):
"""多实例方法:撤销过账"""
if not self:
return True
# 操作限制
if set(self.mapped('state')) != {'posted'}:
raise ValidationError('非已过账状态的记账凭证无法撤销过账!')
if 'close' in set(self.mapped('fr_period_id').mapped('state')):
raise ValidationError('已关闭会计期间内的凭证无法撤销过账!')
# 移除分析行
self.mapped('line_ids.analytic_line_ids').unlink()
# 更新状态及清除过账信息
return self.write({
'fr_posted_uid': None,
'fr_posted_date': None,
'state': 'approved'
})
def voucher_posted_invalid(self):
"""多实例方法:凭证作废"""
if not self:
return True
if set(self.mapped('state')) == {'posted'}:
self.voucher_posted_cancel()
if set(self.mapped('state')) == {'approved'}:
self.voucher_approved_cancel()
# 更新状态及清除过账信息
return self.write({
'fr_posted_uid': None,
'fr_posted_date': None,
'state': 'invalid'
})
def voucher_posted_draft(self):
"""多实例方法:重置为草稿"""
if not self:
return True
# 更新状态及清除过账信息
return self.write({
'fr_posted_uid': None,
'fr_posted_date': None,
'state': 'draft'
})
def voucher_approved_posted(self):
"""多实例方法:审核并过账"""
self.voucher_approved()
self.voucher_posted()
def voucher_approved_posted_execute_carry(self):
"""多实例方法:损益科目结转审核过账方法"""
self.voucher_approved_execute_carry()
self.voucher_posted_execute_carry()
def reset_num(self):
"""【凭证编号日期重排】多实例方法:凭证编号整理"""
for period in self.mapped('fr_period_id'):
period.reset_move_num()
def reset_num_approved(self):
"""【凭证编号审核日期重排】多实例方法:凭证编号整理根据审批时间整理"""
for period in self.mapped('fr_period_id'):
period.reset_move_num_approved()
def reset_num_next(self):
"""【凭证编号重排】多实例方法:凭证编号整向前补号,服务器动作,凭证编号重排"""
for period in self.mapped('fr_period_id'):
# 【凭证编号重排】
period.reset_move_num_next()
# 系统编号重排
def reset_name_next(self):
for period in self.mapped('fr_period_id'):
period.reset_move_name_next()
@property
def has_name(self):
"""是否已有系统编号"""
self.ensure_one()
if not self.name or self.name == '/':
return False
else:
return True
@property
def has_num(self):
"""是否已有凭证编号"""
self.ensure_one()
if not self.num or self.num == '/':
return False
else:
return True
def check_debit_credit(self, value=False):
"""
检查借贷方是否相平,新增检查凭证明细行 借贷方不为 0 操作以及借贷方相平
:return:
"""
"""
for order in self:
# 借方
debit = []
# 贷方
credit = []
for line in order.line_ids:
if line.account_id:
if line.debit == 0 and line.credit == 0:
raise ValidationError('借贷方不允许同时为0请检查')
if line.debit:
debit.append(line.debit)
if line.credit:
credit.append(line.credit)
# # 检查科目必填问题
# if not line.account_id.partner_bool:
# line.write({
# 'partner_id': None,
# })
#
# # 如果科目中没有选择 标签和账户,那么将标签和账户 设置为空
# if not line.account_id.analysis_bool:
# line.write({
# 'analytic_account_id': None,
# })
# if not line.account_id.label_bool:
# line.write({
# 'analytic_tag_ids': None,
# })
if round(sum(debit), 2) != round(sum(credit), 2):
raise ValidationError('借贷方不相平请检查【order_ID:' + str(order.id) + '')
"""
for order in self:
for line in order.line_ids:
if line.account_id:
if line.debit == 0 and line.credit == 0:
raise ValidationError('借贷方不允许同时为0请检查')
if not value:
for order in self:
sum_debit = sum(order.line_ids.mapped('debit'))
sum_credit = sum(order.line_ids.mapped('credit'))
# 借贷总和不相等的时候直接不允许保存
if round(decimal.Decimal(sum_debit), 2) != round(decimal.Decimal(sum_credit), 2):
raise ValidationError('借贷方不相平请检查【order_ID:' + str(order.id) + '')
else:
for order in value:
line_ids = order.get('line_ids', False)
if line_ids:
# 借方
debit = []
# 贷方
credit = []
for line in line_ids:
if line[2].get('debit'):
debit.append(line[2].get('debit'))
if line[2].get('credit'):
credit.append(line[2].get('credit'))
if round(sum(debit), 2) != round(sum(credit), 2):
raise ValidationError('借贷方不相平,请检查!')
# ===================
# 继承方法
# ===================
@api.model_create_multi
def create(self, vals_list):
self.check_debit_credit(value=vals_list)
moves = super(AccountMove, self).create(vals_list)
# 直接创建已审核/已过帐凭证为其生成凭证编号
# self._generate_move_name_num(moves.filtered(lambda move: move.state in ['approved', 'posted']))
# 审核信息
moves.filtered(lambda rec: not rec.fr_approved_uid and not rec.fr_approved_date and rec.state in ['approved', 'posted']).write({
'fr_approved_uid': self.env.ref('base.user_admin').id,
'fr_approved_date': datetime.datetime.now(),
})
# 添加过账信息
moves.filtered(lambda rec: not rec.fr_posted_uid and not rec.fr_posted_date and rec.state == 'posted').write({
'fr_posted_uid': self.env.ref('base.user_admin').id,
'fr_posted_date': datetime.datetime.now(),
})
# 生成凭证编号和系统编号
if moves:
self._generate_move_name_num(moves)
# 检查借贷方问题
# moves.check_debit_credit()
return moves
# 不允许修改
def write(self, vals):
if 'line_ids' in vals or 'date' in vals:
if 'date' in vals:
for order in self:
order_date = vals.get('date')
if order_date:
# 查询修改日期凭证编号
sequence_num = self.env['ir.sequence'].search([('code', '=', 'move.sequence'),
('company_id', '=', order.company_id.id)])
# 该公司无系统编号则创建
if not sequence_num:
sequence_num = self.env['ir.sequence'].create({
'name': '%s凭证序列' % order.company_id.name,
'code': 'move.sequence',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(range_year)s/%(range_month)s/',
'use_date_range': True,
'date_range_type': 'month',
'company_id': order.company_id.id,
})
# 获取序列号并更新凭证编号
next_sequence = sequence_num.with_context(ir_sequence_date=order_date).next_by_id()
if next_sequence:
vals['num'] = '记-' + next_sequence.split('/')[-1]
# order.num = '记-' + next_sequence.split('/')[-1]
# 查询系统编号
sequence_name = self.env['ir.sequence'].search([('code', '=', 'move.sequence.constant'),
('company_id', '=', self.company_id.id)])
# 该公司无系统编号则创建
if not sequence_name:
sequence_name = self.env['ir.sequence'].create({
'name': '%s凭证序列(固定)' % order.company_id.name,
'code': 'move.sequence.constant',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(range_year)s/%(range_month)s/',
'use_date_range': True,
'date_range_type': 'month',
'company_id': order.company_id.id,
})
# 获取系统编号并更新系统编号
new_name = sequence_name.with_context(ir_sequence_date=order_date).next_by_id()
vals['name'] = new_name
# res = super(AccountMove, self).write(vals)
# return res
if 'line_ids' in vals:
res = super(AccountMove, self.with_context(check_move_validity=False)).write(vals)
self.assert_balanced()
# 这里在银行对账功能上,会出现两次修改明细行问题。第一次检查借贷是向平的。第二次检查,会有参数 to_check 参数、此处会
# 出现修改问题 ,需要判断。如果带参数。那么就不进行借贷方检查。
# 检查借贷方是否平
if 'to_check' not in vals:
self.check_debit_credit()
return res
else:
res = super(AccountMove, self).write(vals)
return res
# 此处修改,凭证日期,讲凭证编号,于系统编号进行修改
else:
res = super(AccountMove, self).write(vals)
return res
def unlink(self):
for rec in self:
if rec.state != 'draft':
raise ValidationError('无法删除非草稿状态的记账凭证!请先取消审核。')
# for rec in self:
# if rec.num:
# raise ValidationError('凭证号已经生成,不能删除该凭证')
return super(AccountMove, self).unlink()
@api.model
def default_get(self, fields_list):
defaults = super(AccountMove, self).default_get(fields_list)
# 采购日记账-自动变成期初问题:
# journal = self.env['account.journal'].search([('name', 'ilike', '期初'), ('type', '=', 'general')], limit=1)
# defaults.update({'journal_id': journal.id})
return defaults
@api.depends('num', 'state')
def name_get(self):
result = []
for move in self:
if move.name != '/' or (move.num and move.num != '/'):
name = move.num
else:
name = '* ' + str(move.id)
result.append((move.id, name))
return result
#==================================
#odoo14 没有方法
#assert_balancedcheck_lock_date_post_validate
#==================================
def assert_balanced(self):
if not self.ids:
return True
prec = self.env.user.company_id.currency_id.decimal_places
all = self.env['account.move.line'].search([('move_id', 'in', self.ids)])
if len(all)>1:
self._cr.execute("""\
SELECT move_id
FROM account_move_line
WHERE move_id in %s
GROUP BY move_id
HAVING abs(sum(debit) - sum(credit)) > %s
""", (tuple(self.ids), 10 ** (-max(5, prec))))
if len(self._cr.fetchall()) != 0:
raise UserError("无法保存借贷不平衡的凭证。")
return True
def _check_lock_date(self):
for move in self:
lock_date = max(move.company_id.period_lock_date or date.min, move.company_id.fiscalyear_lock_date or date.min)
if self.user_has_groups('account.group_account_manager'):
lock_date = move.company_id.fiscalyear_lock_date
if move.date <= (lock_date or date.min):
if self.user_has_groups('account.group_account_manager'):
message = _("You cannot add/modify entries prior to and inclusive of the lock date %s") % (lock_date)
else:
message = _("You cannot add/modify entries prior to and inclusive of the lock date %s. Check the company settings or ask someone with the 'Adviser' role") % (lock_date)
raise UserError(message)
return True
def _post_validate(self):
for move in self:
if move.line_ids:
if not all([x.company_id.id == move.company_id.id for x in move.line_ids]):
raise UserError(_("Cannot create moves for different companies."))
self.assert_balanced()
return self._check_lock_date()
def post(self, invoice=False):
"""多实例方法:原生过账方法继承修改,添加验证条件"""
# 验证分录行的科目是否作为父级科目及是否初始化
accounts = self.mapped('line_ids.account_id')
accounts_not_as_leaf = accounts.filtered(lambda account: account.fr_as_leaf is False)
accounts_unitialized = accounts.filtered(lambda account: account.state == 'uninitialized')
if accounts_not_as_leaf or accounts_unitialized:
raise ValidationError('分录中存在以下科目无法记账:\n\n非末级科目:%s\n未初始化的科目:%s'
% (','.join([rec_name for rec_id, rec_name in accounts_not_as_leaf.name_get()]),
','.join([rec_name for rec_id, rec_name in accounts_unitialized.name_get()])))
# 过账验证
self._post_validate()
# 未生成生成系统编号和凭证编号
self._generate_move_name_num(self, invoice)
# Create the analytic lines in batch is faster as it leads to less cache invalidation.
self.mapped('line_ids').create_analytic_lines()
for move in self:
if move == move.company_id.account_opening_move_id and not move.company_id.account_bank_reconciliation_start:
# For opening moves, we set the reconciliation date threshold
# to the move's date if it wasn't already set (we don't want
# to have to reconcile all the older payments -made before
# installing Accounting- with bank statements)
move.company_id.account_bank_reconciliation_start = move.date
# 添加审核信息
self.filtered(lambda rec: not rec.fr_approved_uid and not rec.fr_approved_date).write({
'fr_approved_uid': self.env.ref('base.user_admin').id,
'fr_approved_date': datetime.datetime.now(),
})
# 更新过账信息
self.write({
'state': 'posted',
'fr_posted_uid': self.env.ref('base.user_admin').id,
'fr_posted_date': datetime.datetime.now(),
})
# ===================
# 视图方法
# ===================
def action_get_attachment_view(self):
"""单实例方法:查看全部附件"""
self.ensure_one()
res = self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
res['domain'] = [('res_model', '=', 'account.move'), ('res_id', 'in', self.ids)]
res['context'] = {'default_res_model': 'account.move', 'default_res_id': self.id}
return res
# # ===================
# # 计算方法
# # ===================
@api.depends('date')
def _compute_account_period(self):
"""多实例方法:计算会计期间"""
for rec in self:
all=self.env['fr.account.period'].search([])
if all:
period = self.env['fr.account.period'].search(
[('date_end', '>=', rec.date), ('date_start', '<=', rec.date), ('company_id', '=', self.env.company.id)])
if not period:
raise ValidationError('凭证日期对应的会计期间不存在!检查日期的会计年度是否已创建。')
elif len(period) > 1:
raise ValidationError('凭证日期的对应会计期间不唯一!')
else:
rec.fr_period_id = period
def _compute_attachment_number(self):
"""多实例方法:计算附件数量"""
attachment_data = self.env['ir.attachment'].read_group(
[('res_model', '=', 'account.move'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id'])
attachment = dict((data['res_id'], data['res_id_count']) for data in attachment_data)
for rec in self:
rec.fr_attachcount = attachment.get(rec.id, 0)
@api.depends('amount')
def _compute_fr_money_format(self):
"""多实例方法:计算金额大写"""
for rec in self:
rec.fr_money_format = money2chinese(rec.amount)
# ===================
# onchange方法
# ===================
@api.onchange('fr_template_id')
def _apply_template(self):
"""单实例方法:使用凭证模板"""
if self.fr_template_id:
# 带入日记账
self.journal_id = self.fr_template_id.journal_id
# 带入凭证行
for line_id in self.fr_template_id.line_ids:
self.env['account.move.line'].new({
'name': line_id.name,
'account_id': line_id.account_id.id,
'move_id': self.id
})
# ===================
# 约束方法
# ===================
@api.constrains('date')
def _constrains_date(self):
for rec in self:
period = self.env['fr.account.period'].search(
[('date_end', '>=', rec.date), ('date_start', '<=', rec.date), ('company_id', '=', rec.company_id.id)])
if not period:
raise ValidationError('凭证日期的对应会计期间不存在!检查日期的会计年度是否已创建。')
elif len(period) > 1:
raise ValidationError('凭证日期的对应会计期间不唯一!')
if period.fiscalyear_id.state == 'draft':
raise ValidationError('无法在未激活的会计年度中创建凭证,请检查会计年度是否已激活!')
elif period.state == 'unuse':
raise ValidationError('无法在无效的会计期间中创建凭证,请检查凭证日期!')
elif period.state == 'close':
raise ValidationError('无法在已关闭的会计期间中创建凭证,请检查凭证日期!')
# 这个编号为判断系统编号
# @api.constrains('name')
# def _constrains_name(self):
# for rec in self:
# moves = self.search([('name', '=', rec.name), ('name', '!=', '/')])
# if len(moves) > 1:
# raise ValidationError(f'凭证系统编号{rec.name}已存在!')
# @api.constrains('line_ids')
# def _constrains_line_ids(self):
# """验证凭证行不能为空"""
# for rec in self:
# if len(rec.line_ids) < 1:
# raise MissingError('凭证行不能为空!')
# ===================
# 私有方法
# ===================
def _generate_move_name_num(self, moves, invoice=False):
# 系统编号生成
self._generate_move_name(moves, invoice)
# 凭证编号生成
self._generate_move_num(moves)
def _generate_move_name(self, moves, invoice=False):
"""多实例方法:系统编号生成"""
for move in moves.filtered(lambda rec: rec.has_name is False):
if invoice and invoice.move_name and invoice.move_name != '/':
new_name = invoice.move_name
else:
# 获取该公司系统编号生成
sequence = self.env['ir.sequence'].search(
[('code', '=', 'move.sequence.constant'), ('company_id', '=', move.company_id.id)])
# 该公司无系统编号则创建
if not sequence:
sequence = self.env['ir.sequence'].create({
'name': '%s凭证序列(固定)' % move.company_id.name,
'code': 'move.sequence.constant',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(range_year)s/%(range_month)s/',
'use_date_range': True,
'date_range_type': 'month',
'company_id': move.company_id.id,
})
# 获取系统编号并更新系统编号
new_name = sequence.with_context(ir_sequence_date=move.date).next_by_id()
if self.env['account.move'].search([('name', '=', new_name)]):
new_name = sequence.with_context(ir_sequence_date=move.date).next_by_id()
move.name = new_name
def _generate_move_num(self, moves):
"""多实例方法:凭证编号生成"""
for move in moves.filtered(lambda rec: rec.has_num is False).sorted(reverse=True):
# 获取该公司凭证序列
sequence = self.env['ir.sequence'].search(
[('code', '=', 'move.sequence'), ('company_id', '=', move.company_id.id)])
# 该公司无凭证序列则创建
if not sequence:
sequence = self.env['ir.sequence'].create({
'name': '%s凭证序列' % move.company_id.name,
'code': 'move.sequence',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(year)s/%(month)s/',
'use_date_range': True,
'date_range_type': 'month',
'company_id': move.company_id.id,
})
# 获取序列号并更新凭证编号
next_sequence = sequence.with_context(ir_sequence_date=move.date).next_by_id()
if next_sequence:
move.num = '记-' + next_sequence.split('/')[-1]
def _generate_move_num_approved(self, moves):
"""【凭证编号审核日期重排】多实例方法:凭证编号生成, 审核日期 """
# 循环找到没有审核日期的凭证
move_error = []
for date_move in moves:
if not date_move.fr_approved_date:
move_error.append(date_move.num)
if len(move_error) > 0:
raise MissingError("凭证号:" + str(move_error) + "没有审核日期。请重新审核!")
# 按照审核时间进行排序、生成凭证编号
# 重置凭证编号
moves.write({'num': '/'})
moves_sorted = (moves.filtered(lambda rec: rec.has_num is False).sorted(key=lambda m: m.fr_approved_date))
for move in moves_sorted:
# 获取该公司凭证序列
sequence = self.env['ir.sequence'].search(
[('code', '=', 'move.sequence.approved'), ('company_id', '=', move.company_id.id)])
# 该公司无凭证序列则创建
if not sequence:
sequence = self.env['ir.sequence'].create({
'name': '%s凭证序列审核时间整理' % move.company_id.name,
'code': 'move.sequence.approved',
'implementation': 'no_gap',
'padding': 4,
'prefix': '%(year)s/%(month)s/',
'use_date_range': True,
'date_range_type': 'month',
'company_id': move.company_id.id,
})
# 获取序列号并更新凭证编号
next_sequence = sequence.next_by_id()
if next_sequence:
move.num = '记-' + next_sequence.split('/')[-1]
def _generate_move_num_fill(self, moves, move_id, sub_sequence):
"""多实例方法:单凭证号填补"""
moves_sorted = moves.sorted(key=lambda m: m.num)
n = 0
for i, move in enumerate(moves_sorted):
if int(str(move.num)[-4:]) != i+1:
if n == 0:
# 回填序号记录中,下一个序号的值
sub_sequence.number_next = int(str(move_id.num)[-4:])
# 填补空缺号码
move_id.num = '记-' + str(i+1).zfill(4)
n = n + 1
def _generate_move_num_next(self, moves, sub_sequence):
"""【凭证编号重排】多实例方法:多凭证号填补移位"""
moves_sorted = moves.sorted(key=lambda m: m.num)
for i, move in enumerate(moves_sorted):
if int(str(move.num)[-4:]) != i + 1:
# 回填序号记录中,下一个序号的值
sub_sequence.number_next = int(i + 2)
# 填补空缺号码
move.num = '记-' + str(i + 1).zfill(4)
else:
# 如果没有满足要求的编号,则重置到最新编号
sub_sequence.number_next = int(i + 2)
# 系统编号重排
def _generate_move_name_next(self, moves, sequence):
"""多实例方法:系统编号重排"""
moves_sorted = moves.sorted(key=lambda m: m.num)
move_date = None
for i, move in enumerate(moves_sorted):
next_sequence = sequence.with_context(ir_sequence_date=move.date).next_by_id()
move.name = next_sequence
move_date = move.date
# 修改原始的序列号
sequence_old = self.env['ir.sequence'].search([('code', '=', 'move.sequence.constant'),
('company_id', '=', self.company_id.id)])
for line in sequence_old.date_range_ids:
if move_date >= line.date_from and move_date <= line.date_to:
line.number_next = sequence.number_next
line.number_next_actual = sequence.number_next_actual
class AccountMoveLine(models.Model):
"""记账凭证行:原生的日记账分录项目扩展"""
_inherit = 'account.move.line'
_description = '记账凭证行'
_order = "date desc, move_id_num desc, id"
# 基础字段
sequence = fields.Integer(string='序列')
# 关系字段
fr_cash_flow_id = fields.Many2one('account.cash.flow', string='现金流量项目')
# 关联字段
fr_period_id = fields.Many2one(related='move_id.fr_period_id', string='会计期间', store=True, index=True)
fr_fiscalyear_id = fields.Many2one(related='move_id.fr_period_id.fiscalyear_id', string='会计年度', store=True)
fr_state = fields.Selection(related='move_id.state', string='状态', store=True)
fr_period_state = fields.Selection(related='move_id.fr_period_id.state', string='期间状态', store=True)
fr_journal_type = fields.Selection(related='journal_id.type', string='日记账类型', store=True)
# 计算字段
fr_in_debit = fields.Boolean(string='借方', compute='_compute_fr_in_debit', store=True)
# 科目明细账余额和方向字段:性能开销较高,非明细账视图谨慎使用
fr_balance_end = fields.Monetary(string='余额', compute='_compute_fr_balance_direction_end')
fr_direction_end = fields.Selection(BALANCE_DIRECTION, string='方向', compute='_compute_fr_balance_direction_end')
# 发生额
fr_amount_incurred = fields.Monetary(string='发生额', compute='_compute_fr_balance_direction_end')
partner_bool = fields.Boolean(related='account_id.partner_bool', store=True)
cash_bool = fields.Boolean(related='account_id.cash_bool', store=True)
analysis_bool = fields.Boolean(related='account_id.analysis_bool', store=True)
label_bool = fields.Boolean(related='account_id.label_bool', store=True)
fr_pool_balance = fields.Float('客户余额', compute="_compute_fr_pool_balance", store=True, help="方便计算客户的应收余额")
# 财务总账相关修改
################################################
account_balance_id = fields.Char(string="余额表id")
invoice_id = fields.Many2one('account.invoice', oldname="invoice")
narration = fields.Text(string='Internal Note')
#odoo12升级odoo14缺少字段tax_line_grouping_key
tax_line_grouping_key = fields.Char(store=False, string='Old Taxes', help="Technical field used to store the old values of fields used to compute tax lines (in account.move form view) between the moment the user changed it and the moment the ORM reflects that change in its one2many")
# 2021-7-14升级
move_id_num = fields.Char(string='凭证编号', related='move_id.num', store=True, index=True)
# 添加明细行,默认摘要字段为上一个明细行的摘要内容
@api.onchange('move_id')
def onchange_name_update(self):
if self.move_id:
# 此操作,除了实现添加明细行,默认摘要字段为上一个明细行的摘要内容,
# 还实现,添加凭证明细行,第二行,默认填写上一行借贷方,且与上一行借贷方相反
# 处理方式,正数金额走原生方法,使得第二行凭证明细,自动反向填写。负数凭证,第二行不自动填写金额。需手动填写。
"""
if (len(self.move_id.line_ids)) > 1:
first_line = self.move_id.line_ids[-2]
self.update({
'name': first_line.name,
})
if first_line.debit < 0:
self.update({
'credit': first_line.balance,
'balance': first_line.balance,
'debit': 0,
})
if first_line.credit < 0:
self.update({
'credit': 0,
'balance': first_line.balance,
'debit': - first_line.balance,
})
"""
if (len(self.move_id.line_ids)) > 1:
first_line = self.move_id.line_ids[-2]
sum_balance = sum((self.move_id.line_ids - self).mapped('balance'))
sum_credit = sum((self.move_id.line_ids - self).mapped('credit'))
sum_debit = sum((self.move_id.line_ids - self).mapped('debit'))
self.update({
'name': first_line.name,
'balance': sum_balance,
})
if sum_credit > sum_debit:
self.update({
'credit': 0,
'debit': sum_credit - sum_debit
})
if sum_debit > sum_credit:
self.update({
'credit': sum_debit - sum_credit,
'debit': 0
})
if sum_credit == sum_debit:
self.update({
'credit': 0,
'debit': 0
})
# 创建凭证明细行时根据明细行内容判断是否要带出现金流量项目。需要重新create方法
# @api.model_create_multi
# def create(self, vals_list):
# for line in vals_list:
# if line.get('account_id') and not line.get('fr_cash_flow_id'):
# account_id = self.env['account.account'].search([('id', '=', line['account_id'])], limit=1)
# if account_id and account_id.cash_bool:
# account_cash_flow = account_id.fr_cash_flow_id
# line['fr_cash_flow_id'] = account_cash_flow.id
# move_line = super(AccountMoveLine, self).create(vals_list)
# return move_line
# 此处修改系统原生问题, 可实现凭证明细行借贷为负数值。
_sql_constraints = [
(
'check_credit_debit',
'CHECK(1=1)',
'Wrong credit or debit value in accounting entry !'
)
]
# 此方法系统原生方法,不允许凭证明细行有负数。修改此方法,使得凭证明细行可以为负数
# 此处修改为负数时不会执行第二行金额自动填写第二行自动填写方法def onchange_name_update(self):
# 处理方式,正数金额走原生方法,使得第二行凭证明细,自动反向填写。负数凭证,第二行不自动填写金额。需手动填写。
@api.onchange('debit')
def _onchange_debit(self):
if self.debit:
if self.debit > 0:
self.credit = 0.0
self._onchange_balance()
# 此方法系统原生方法,不允许凭证明细行有负数。修改此方法,使得凭证明细行可以为负数
# 此处修改为负数时不会执行第二行金额自动填写第二行自动填写方法def onchange_name_update(self):
# 处理方式,正数金额走原生方法,使得第二行凭证明细,自动反向填写。负数凭证,第二行不自动填写金额。需手动填写。
@api.onchange('credit')
def _onchange_credit(self):
if self.credit:
if self.credit > 0:
self.debit = 0.0
self._onchange_balance()
# 重写account_move_line检查科目方法
def _check_reconcile_validity(self):
# Perform all checks on lines
company_ids = set()
all_accounts = []
for line in self:
company_ids.add(line.company_id.id)
all_accounts.append(line.account_id)
if (line.matched_debit_ids or line.matched_credit_ids) and line.reconciled:
raise UserError(_('You are trying to reconcile some entries that are already reconciled.'))
if len(company_ids) > 1:
raise UserError(_('To reconcile the entries company should be the same for all entries.'))
if len(set(all_accounts)) > 1:
pass
# raise UserError(_('Entries are not from the same account.'))
if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
raise UserError(_(
'Account %s (%s) does not allow reconciliation. First change the configuration of this account to allow it.') % (
all_accounts[0].name, all_accounts[0].code))
# 财务总账余额表查询
def action_move_line_list_balance(self, account_id, company_id, partner_id, date_start, date_end):
domain = [('account_id', '=', account_id), ('date', '>=', date_start), ('date', '<=', date_end),
('partner_id', '=', partner_id), ('company_id', '=', company_id)]
account_move_line = self.search(domain)
if account_move_line:
return {
'type': 'ir.actions.act_window',
'res_model': 'account.move.line',
'name': '科目明细',
'views': [[False, 'list'], [False, 'form']],
'domain': domain,
'context': {'create': False, }
}
else:
raise UserError("没有找到相关科目信息")
###############################################
@api.depends('credit', 'debit')
def _compute_fr_pool_balance(self):
for order in self:
order.fr_pool_balance = order.credit - order.debit
# ===================
# 公用方法
# ===================
def get_invisible_columns(self):
invisible_columns = {}
for column in self.env['account.move.line.column'].search([]):
if column.property_active is False:
invisible_columns.update({column.code: True})
return invisible_columns
# ===================
# 继承方法
# ===================
# @api.model_cr
def init(self):
""" This method is called after :meth:`~._auto_init`, and may be
overridden to create or modify a model's database schema.
"""
# 移除原生借贷方金额不为负约束
self.env.cr.execute("""
ALTER TABLE account_move_line DROP CONSTRAINT IF EXISTS account_move_line_credit_debit2;
""")
return super(AccountMoveLine, self).init()
@api.model
def default_get(self, fields_list):
defaults = super(AccountMoveLine, self).default_get(fields_list)
if 'line_ids' not in self._context:
return defaults
# lines = self.move_id.resolve_2many_commands('line_ids', self._context['line_ids'], fields=['name'])
#
# # 自动带入上一行摘要
# if lines:
# name = lines[-1].get('name', False)
# defaults.update({'name': name})
return defaults
def _update_check(self):
# 科目日记账项目迁移时不作限制
if self._context.get('migrate', None) is True:
return True
else:
return super(AccountMoveLine, self)._update_check()
# ===================
# 计算方法
# ===================
@api.depends('balance')
def _compute_fr_in_debit(self):
for rec in self:
if rec.balance > 0:
rec.fr_in_debit = True
else:
rec.fr_in_debit = False
def _compute_fr_balance_direction_end(self):
# 明细账查询方法。
# FIXME: 优化性能
for rec in self:
self.env.cr.execute(f"""
select sum(balance) as balance
from account_move_line
where account_id = {rec.account_id.id} and
(date < '{rec.date}' or (date = '{rec.date}' and id <= {rec.id}))
""")
balance_end = self.env.cr.fetchone()[0]
if balance_end > 0:
direction_end = 'debit'
# rec.
elif balance_end < 0:
balance_end = - balance_end
direction_end = 'credit'
else:
direction_end = 'balance'
rec.fr_balance_end = balance_end
rec.fr_direction_end = direction_end
# 发生额 处理
if rec.balance > 0:
rec.fr_amount_incurred = rec.balance
elif rec.balance < 0:
rec.fr_amount_incurred = - rec.balance
else:
rec.fr_amount_incurred = 0
# ===================
# onchange方法
# ===================
@api.onchange('account_id')
def _onchange_account_id(self):
self.analytic_tag_ids = self.analytic_tag_ids.filtered(
lambda x: x.category_id in self.account_id.fr_category_ids)
# ===================
# 约束方法
# ===================
@api.constrains('account_id')
def _constrains_account_id(self):
for rec in self:
if rec.account_id:
# 验证公司
if rec.account_id.company_id != rec.company_id:
raise ValidationError('科目 “%s”公司%s 与凭证公司%s属性不一致!' % (rec.account_id.name_get()[0][1],
rec.account_id.company_id.name,rec.company_id.name))
if rec.account_id.fr_as_leaf is False:
raise ValidationError('科目 “%s” 作为非末级科目无法记账!' % rec.account_id.name_get()[0][1])
if rec.account_id.state == 'uninitialized':
raise ValidationError('科目 “%s” 未初始化无法记账!' % rec.account_id.name_get()[0][1])
if rec.account_id.deprecated is True or rec.account_id.state == 'deprecated':
raise ValidationError('科目 “%s” 已封存无法记账!' % rec.account_id.name_get()[0][1])
# 判断 科目 缺少必填项目
# @api.constrains('account_id', 'partner_id', 'fr_state')
# def _constrains_partner_id(self):
# for rec in self:
# if rec.account_id.internal_type in ['receivable',
# 'payable'] and not rec.partner_id and rec.fr_state != 'draft':
# print("111111111111111111", rec.partner_id)
# print("111111111111111111", rec.fr_state)
# raise ValidationError(f'凭证{rec.move_id}的日记账项目中应收应付类科目【{rec.account_id.code + rec.account_id.fr_full_name}】缺少业务伙伴!')
# 判断 科目 缺少必填项目
# @api.constrains('account_id', 'fr_cash_flow_id', 'fr_state')
# def _constrains_cash_flow_id(self):
# for rec in self:
# if rec.account_id.internal_type == 'liquidity' and not rec.fr_cash_flow_id and rec.fr_state != 'draft':
# raise ValidationError(f'凭证{rec.move_id}的日记账项目中现金银行类科目【{rec.account_id.code + rec.account_id.fr_full_name}】缺少现金流量项目!')
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
res = super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
return res
class ValidateAccountMove(models.TransientModel):
_inherit = "validate.account.move"
_description = "Validate Account Move"
def validate_move(self):
context = dict(self._context or {})
moves = self.env['account.move'].browse(context.get('active_ids'))
move_to_post = moves.filtered(lambda move: move.state in ['draft', 'approved'])
move_to_post.post()
return {'type': 'ir.actions.act_window_close'}
class AccountMoveLineColumn(models.Model):
_name = "account.move.line.column"
_description = "凭证行字段"
name = fields.Char(string='名称', required=True)
code = fields.Char(string='代码', required=True)
property_active = fields.Boolean(string="启用", company_dependent=True)