|  | # Copyright 2016 上海开阖软件有限公司 (http://www.osbzr.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
# 字段只读状态
READONLY_STATES = {
    'done': [('readonly', True)],
    'cancel': [('readonly', True)],
}
class Voucher(models.Model):
    '''新建凭证'''
    _name = 'voucher'
    _inherit = ['mail.thread']
    _order = 'period_id, name desc'
    _description = '会计凭证'
    @api.model
    def _default_voucher_date(self):
        return self._default_voucher_date_impl()
    @api.model
    def _default_voucher_date_impl(self):
        ''' 获得默认的凭证创建日期 '''
        voucher_setting = self.env['ir.default']._get(
            'finance.config.settings', 'defaul_voucher_date')
        now_date = fields.Date.context_today(self)
        if voucher_setting == 'last' and self.search([], limit=1):
            create_date = self.search([], limit=1).date
        else:
            create_date = now_date
        return create_date
    @api.model
    def _select_objects(self):
        models = self.env['ir.model'].sudo().search([])
        return [(model.model, model.name) for model in models]
    @api.depends('date')
    def _compute_period_id(self):
        for v in self:
            v.period_id = self.env['finance.period'].get_period(v.date)
    document_word_id = fields.Many2one(
        'document.word', '凭证字', ondelete='restrict', required=True,
        default=lambda self: self.env.ref('finance.document_word_1'))
    date = fields.Date('凭证日期', required=True, default=_default_voucher_date,
                       tracking=True, help='本张凭证创建的时间', copy=False)
    name = fields.Char('凭证号', tracking=True, copy=False)
    att_count = fields.Integer(
        '附单据', default=1, help='原始凭证的张数')
    period_id = fields.Many2one(
        'finance.period',
        '会计期间',
        compute='_compute_period_id',
        ondelete='restrict',
        store=True,
        help='本张凭证发生日期对应的,会计期间')
    line_ids = fields.One2many(
        'voucher.line',
        'voucher_id',
        '凭证明细',
        copy=True,)
    amount_text = fields.Float('总计', compute='_compute_amount', store=True,
                               tracking=True, digits='Amount', help='凭证金额')
    state = fields.Selection([('draft', '草稿'),
                              ('done', '已确认'),
                              ('cancel', '已作废')], '状态', default='draft',
                             index=True,
                             tracking=True, help='凭证所属状态!')
    is_checkout = fields.Boolean('结账凭证', help='是否是结账凭证')
    is_init = fields.Boolean('是否初始化凭证', help='是否是初始化凭证')
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        change_default=True,
        default=lambda self: self.env.company)
    ref = fields.Reference(string='前置单据',
                           selection='_select_objects')
    brief = fields.Char('摘要', related="line_ids.name", store=True)
    details = fields.Html('明细', compute='_compute_details')
    @api.depends('line_ids')
    def _compute_details(self):
        for v in self:
            vl = {'col': ['往来单位', '科目', '借方', '贷方'], 'val': []}
            for line in v.line_ids:
                vl['val'].append([line.partner_id.name or '', line.account_id.name, line.debit, line.credit])
            v.details = v.company_id._get_html_table(vl)
    def voucher_done(self):
        """
        确认 凭证按钮 所调用的方法
        :return: 主要是把 凭证的 state改变
        """
        for v in self:
            if v.state == 'done':
                raise UserError('凭证%s已经确认,请不要重复确认!' % v.name)
            if v.date < self.env.company.start_date:
                raise UserError('凭证日期不可早于启用日期')
            if v.period_id.is_closed:
                raise UserError('该会计期间已结账!不能确认')
            if not v.line_ids:
                raise ValidationError('请输入凭证行')
            for line in v.line_ids:
                if line.debit + line.credit == 0:
                    raise ValidationError(
                        '单行凭证行 %s 借和贷不能同时为0' % line.account_id.name)
                if line.debit * line.credit != 0:
                    raise ValidationError(
                        '单行凭证行不能同时输入借和贷\n 摘要为%s的凭证行 借方为:%s 贷方为:%s' %
                        (line.name, line.debit, line.credit))
            debit_sum = sum([line.debit for line in v.line_ids])
            credit_sum = sum([line.credit for line in v.line_ids])
            precision = self.env['decimal.precision'].precision_get('Amount')
            debit_sum = round(debit_sum, precision)
            credit_sum = round(credit_sum, precision)
            if debit_sum != credit_sum:
                raise ValidationError('借贷方不平,无法确认!\n 借方合计:%s 贷方合计:%s' %
                                      (debit_sum, credit_sum))
            v.state = 'done'
            if v.is_checkout:   # 月结凭证不做反转
                return True
            for line in v.line_ids:
                if line.account_id.costs_types == 'out' and line.credit:
                    # 费用类科目只能在借方记账,比如银行利息收入
                    line.debit = -line.credit
                    line.credit = 0
                if line.account_id.costs_types == 'in' and line.debit:
                    # 收入类科目只能在贷方记账,比如退款给客户的情况
                    line.credit = -line.debit
                    line.debit = 0
    def voucher_can_be_draft(self):
        for v in self:
            if v.ref:
                raise UserError('不能撤销确认由其他单据生成的凭证!')
        self.voucher_draft()
    def voucher_draft(self):
        for v in self:
            if v.state == 'draft':
                raise UserError('凭证%s已经撤销确认,请不要重复撤销!' % v.name)
            if v.period_id.is_closed:
                raise UserError('%s期 会计期间已结账!不能撤销确认' % v.period_id.name)
            v.state = 'draft'
    @api.depends('line_ids')
    def _compute_amount(self):
        for v in self:
            v.amount_text = str(sum([line.debit for line in v.line_ids]))
    # 重载write 方法
    def write(self, vals):
        for order in self:  # 还需要进一步优化
            if self.env.context.get('call_module', False) == "checkout_wizard":
                return super().write(vals)
            if order.period_id.is_closed is True:
                raise UserError('%s期 会计期间已结账,凭证不能再修改!' % order.period_id.name)
            return super().write(vals)
class VoucherLine(models.Model):
    '''凭证明细'''
    _name = 'voucher.line'
    _description = '会计凭证明细'
    @api.model
    def _default_get(self, data):
        ''' 给明细行摘要、借方金额、贷方金额字段赋默认值 '''
        move_obj = self.env['voucher']
        total = 0.0
        context = self._context
        # odoo16没这个函数
        # if context.get('line_ids'):
        # for move_line_dict in move_obj.resolve_2many_commands(
        #     'line_ids', context.get('line_ids')):
        #     data['name'] = data.get('name') or move_line_dict.get('name')
        #     total += move_line_dict.get('debit', 0.0) - \
        #         move_line_dict.get('credit', 0.0)
        # data['debit'] = total < 0 and -total or 0.0
        # data['credit'] = total > 0 and total or 0.0
        return data
    @api.model
    def default_get(self, fields):
        ''' 创建记录时,根据字段的 default 值和该方法给字段的赋值 来给 记录上的字段赋默认值 '''
        fields_data = super(VoucherLine, self).default_get(fields)
        data = self._default_get(fields_data)
        # 判断 data key是否在 fields 里,如果不在则删除该 key。程序员开发用
        for f in list(data.keys()):
            if f not in fields:
                del data[f]
        return data
    voucher_id = fields.Many2one('voucher', '对应凭证', ondelete='cascade')
    name = fields.Char('摘要', required=True, help='描述本条凭证行的缘由')
    account_id = fields.Many2one(
        'finance.account', '会计科目',
        ondelete='restrict',
        required=True,
        domain="[('account_type','=','normal')]")
    debit = fields.Float('借方金额', digits='Amount',
                         help='每条凭证行中只能记录借方金额或者贷方金额中的一个,\
                               一张凭证中所有的凭证行的借方余额,必须等于贷方余额。')
    credit = fields.Float('贷方金额', digits='Amount',
                          help='每条凭证行中只能记录借方金额或者贷方金额中的一个,\
                                一张凭证中所有的凭证行的借方余额,必须等于贷方余额。')
    partner_id = fields.Many2one(
        'partner', '往来单位', ondelete='restrict', help='凭证行的对应的往来单位')
    currency_amount = fields.Float('外币金额', digits='Amount')
    currency_id = fields.Many2one('res.currency', '外币币别', ondelete='restrict')
    rate_silent = fields.Float('汇率', digits=0)
    period_id = fields.Many2one(
        related='voucher_id.period_id', string='凭证期间', store=True)
    goods_qty = fields.Float('数量',
                             digits='Quantity')
    goods_id = fields.Many2one('goods', '商品', ondelete='restrict')
    auxiliary_id = fields.Many2one(
        'auxiliary.financing', '辅助核算', help='辅助核算是对账务处理的一种补充,即实现更广泛的账务处理,\
        以适应企业管理和决策的需要.辅助核算一般通过核算项目来实现', ondelete='restrict')
    date = fields.Date(compute='_compute_voucher_date',
                       store=True, string='凭证日期')
    state = fields.Selection(
        [('draft', '草稿'), ('done', '已确认'), ('cancel', '已作废')],
        compute='_compute_voucher_state',
        index=True,
        store=True, string='状态')
    init_obj = fields.Char('初始化对象', help='描述本条凭证行由哪个单证生成而来')
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        change_default=True,
        default=lambda self: self.env.company)
    @api.depends('voucher_id.date')
    def _compute_voucher_date(self):
        for vl in self:
            vl.date = vl.voucher_id.date
    @api.depends('voucher_id.state')
    def _compute_voucher_state(self):
        for vl in self:
            vl.state = vl.voucher_id.state
    @api.onchange('account_id')
    def onchange_account_id(self):
        self.currency_id = self.account_id.currency_id
        self.rate_silent = self.account_id.currency_id.rate
        res = {
            'domain': {
                'partner_id': [('name', '=', False)],
                'goods_id': [('name', '=', False)],
                'auxiliary_id': [('name', '=', False)]}}
        if not self.account_id or not self.account_id.auxiliary_financing:
            return res
        if self.account_id.auxiliary_financing == 'customer':
            res['domain']['partner_id'] = [('c_category_id', '!=', False)]
        elif self.account_id.auxiliary_financing == 'supplier':
            res['domain']['partner_id'] = [('s_category_id', '!=', False)]
        elif self.account_id.auxiliary_financing == 'goods':
            res['domain']['goods_id'] = []
        else:
            res['domain']['auxiliary_id'] = [
                ('type', '=', self.account_id.auxiliary_financing)]
        return res
    def view_document(self):
        self.ensure_one()
        return {
            'name': '凭证',
            'view_mode': 'form',
            'res_model': 'voucher',
            'res_id': self.voucher_id.id,
            'type': 'ir.actions.act_window',
        }
    @api.constrains('account_id')
    def _check_account_id(self):
        for record in self:
            if record.account_id.account_type == 'view':
                raise UserError(_('不能往视图科目 %s-%s 记账,只能往下级科目记账!') % (self.account_id.code,self.account_id.name))
    def check_restricted_account(self):
        prohibit_account_debit_ids = self.env['finance.account'].search(
            [('restricted_debit', '=', True)])
        prohibit_account_credit_ids = self.env['finance.account'].search(
            [('restricted_credit', '=', True)])
        account_ids = []
        account = self.account_id
        account_ids.append(account)
        while account.parent_id:
            account_ids.append(account.parent_id)
            account = account.parent_id
        inner_account_debit = [
            acc for acc in account_ids if acc in prohibit_account_debit_ids]
        inner_account_credit = [
            acc for acc in account_ids if acc in prohibit_account_credit_ids]
        if self.debit and not self.credit and inner_account_debit:
            raise UserError(
                '借方禁止科目: %s-%s \n\n 提示:%s ' % (
                    self.account_id.code,
                    self.account_id.name,
                    inner_account_debit[0].restricted_debit_msg))
        if not self.debit and self.credit and inner_account_credit:
            raise UserError(
                '贷方禁止科目: %s-%s \n\n 提示:%s ' % (
                    self.account_id.code,
                    self.account_id.name,
                    inner_account_credit[0].restrict_credit_msg))
    @api.model_create_multi
    def create(self, values):
        """
            Create a new record for a model VoucherLine
            @param values: provides a data for new record
            @return: returns a id of new record
        """
        result = super(VoucherLine, self).create(values)
        if not self.env.context.get('entry_manual', False):
            return result
        for r in result:
            r.check_restricted_account()
        return result
    def write(self, values):
        """
            Update all record(s) in recordset, with new value comes as {values}
            return True on success, False otherwise
            @param values: dict of new values to be set
            @return: True on success, False otherwise
        """
        result = super(VoucherLine, self).write(values)
        if not self.env.context.get('entry_manual', False):
            return result
        for record in self:
            record.check_restricted_account()
        return result
class DocumentWord(models.Model):
    '''凭证字'''
    _name = 'document.word'
    _description = '凭证字'
    name = fields.Char('凭证字')
    print_title = fields.Char('打印标题', help='凭证在打印时的显示的标题')
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        change_default=True,
        default=lambda self: self.env.company)
class ChangeVoucherName(models.Model):
    ''' 修改凭证编号 '''
    _name = 'change.voucher.name'
    _description = '月末凭证变更记录'
    period_id = fields.Many2one('finance.period', '会计期间')
    before_voucher_name = fields.Char('以前凭证号')
    after_voucher_name = fields.Char('更新后凭证号')
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        change_default=True,
        default=lambda self: self.env.company)
 |