Files
gzth/yuthon_property_approval/models/property_lease_renewal.py
T
2026-09-02 12:01:32 +08:00

535 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, exceptions, _
from datetime import datetime, timedelta, date
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError
import calendar
class PropertyLeaseRenewal(models.Model):
_name = 'property.lease.renewal'
_description = '租赁变更审批'
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin', 'soong.workflow.mixin']
_rec_name = 'name'
name = fields.Char(string='标题')
user_id = fields.Many2one('res.users', string="申请人", default=lambda self: self.env.user)
department_id = fields.Many2one(related='user_id.department_id', string="部门")
company_id = fields.Many2one(related='user_id.company_id', string="公司")
code = fields.Char(string="编号",
default=lambda self: self.env['ir.sequence'].next_by_code('property_lease_renewal_code'))
renewal_type = fields.Selection([('early_checkout', '合同终止'), ('lease', '承租方变更'), ('extend', '延长租期')], string='变更类型')
urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度',
default='normal')
is_chapter = fields.Selection([('yes', '是'), ('no', '否')], string="是否用章")
type_stamp_ids = fields.Many2many('seals.type.setting', string='用章类型', domain="['&', ('start_date', '<=', context_today()), '|', ('end_date', '=', False), ('end_date', '>=', context_today())]")
def _get_default_name_title(self):
return self._description if self._description else self._name
name_title = fields.Char(default=_get_default_name_title)
self_users_ids = fields.Many2many('res.users', string="当前经办人", default=lambda self: [(6, 0, [self.env.user.id])])
# 原合同信息
lessee_id = fields.Many2one(related="contract_id.lessee_id", string='原承租方')
hide = fields.Boolean(string="隐藏")
display_name = fields.Char(string='原承租方')
contract_id = fields.Many2one('property.lease.contract', string='租赁合同')
monthly_rent = fields.Float(related="contract_id.rent", string='租金')
cycle_dict = fields.Selection(related="contract_id.cycle_dict", string='租金支付周期')
original_start_date = fields.Date(related="contract_id.term_start_time", string='原租赁期开始时间')
original_end_date = fields.Date(related="contract_id.term_end_time", string='原租赁期结束时间')
property_name = fields.Char(related="contract_id.property_name", string='物业信息')
# 变更信息
new_lessee_id = fields.Many2one("property.tenant.information", string="承租方", store=True)
contact = fields.Char(string='联系人')
phone = fields.Char(string='联系电话')
card = fields.Char(string='身份证号')
effective_date = fields.Date(string='生效日期', required=True)
entity_type = fields.Selection([('person', '个人'), ('company', '企业')], string='承租方类型')
reason = fields.Text(string='变更原因')
fee = fields.Float(string='变更费用')
document_ids = fields.Many2many('ir.attachment', string="变更附件")
early_explain = fields.Text(string="退租说明")
early_reason = fields.Selection([('normal', '正常退租'), ('agree', '协商一致'), ('settle', '违约结清'), ('other', '其他')], string="退租原因")
early_quitting_date = fields.Date(string="退租日期")
active = fields.Boolean('Active', default=True, tracking=True)
work_end = fields.Boolean(string="结束")
deposit = fields.Float(string="原押金")
refund_amount = fields.Float(string="退款金额")
deposit_treatment = fields.Selection([('refund', '全额退押'), ('part_refund', '部分退租'), ('no_refund', '全额没收')], string="押金处理方式", default='refund')
total_receivables = fields.Float(string="应收金额", compute="_compute_total_receivables")
forfeit = fields.Float(string="没收金额")
receivables_ids = fields.One2many('early.receivables.line', 'renewal_id', string='应收明细')
# 延长租期
actual_end_date = fields.Date(string='实际结束日期')
breakdown_ids = fields.One2many('property.rent.breakdown.line', 'renewal_id', string='租金明细')
def _get_tenant_by_contact(self, contact):
"""按联系人或姓名获取承租方信息"""
if not contact:
return False
return self.env['property.tenant.information'].search([
'|', ('contact', '=', contact), ('name', '=', contact)
], limit=1)
def _get_employee_by_contact(self, contact):
"""按联系人姓名匹配员工,拿身份证号等信息"""
if not contact:
return False
return self.env['hr.employee'].search([('name', '=', contact)], limit=1)
@api.onchange('new_lessee_id')
def _onchange_new_lessee_id_fill_contact(self):
"""选择新承租方时,不再自动带出联系人/电话/身份证号,由联系人字段驱动。"""
# 需求调整:不做自动填充,保留用户手动输入的联系人
return
@api.onchange('refund_amount')
def _onchange_forfeit(self):
for rec in self:
rec.forfeit = rec.deposit - rec.refund_amount
@api.onchange('contact')
def _onchange_contact_fill_info(self):
"""填写联系人后,按员工信息带出身份证号与联系电话;不再从承租方带出。"""
for rec in self:
if not rec.contact:
continue
employee = rec._get_employee_by_contact(rec.contact)
if employee:
rec.phone = employee.mobile_phone or False
rec.card = employee.identification_id or False
else:
# 未找到员工时,清空以避免沿用旧值,由用户手填
rec.phone = False
rec.card = False
@api.depends('receivables_ids', 'receivables_ids.original_amount', 'receivables_ids.reduction_amount',
'receivables_ids.void')
def _compute_total_receivables(self):
for renewal in self:
valid_receivable_lines = renewal.receivables_ids.filtered(lambda line: not line.void and line.is_reconciliation == False)
total = sum(line.original_amount - line.reduction_amount for line in valid_receivable_lines)
renewal.total_receivables = total
def action_print_lease_renewal(self):
"""打印租赁变更审批"""
self.ensure_one()
report = self.env.ref('yuthon_property_approval.action_property_lease_renewal_report')
return report.report_action(self)
def end_approval(self):
for rec in self:
if rec.renewal_type == 'extend':
# 延长租期:修改原合同实际结束日期,并将租金明细写入原合同
if rec.actual_end_date:
rec.contract_id.write({
'actual_end_time': rec.actual_end_date
})
if rec.breakdown_ids:
for line in rec.breakdown_ids:
self.env['property.rent.breakdown.line'].create({
'breakdown_id': rec.contract_id.id,
'unit_price': line.unit_price,
'monthly_amount': line.monthly_amount,
'start_data': line.start_data,
'end_data': line.end_data,
'month_number': line.month_number,
'period_rent': line.period_rent,
})
elif rec.renewal_type == 'lease':
origin_contract = rec.contract_id
contract_actual_end = origin_contract.actual_end_time if origin_contract else None
effective_date = fields.Date.to_date(rec.effective_date)
last_day = calendar.monthrange(effective_date.year, effective_date.month)[1]
month_end_date = date(effective_date.year, effective_date.month, last_day)
contract_actual_end_date = fields.Date.to_date(contract_actual_end) if contract_actual_end else None
if not contract_actual_end_date or contract_actual_end_date >= month_end_date:
rec.hide = True
origin_contract = rec.contract_id
original_lessee_id = origin_contract.lessee_id.id if origin_contract.lessee_id else False
version_str = origin_contract.version.lstrip('V')
main_version = version_str.split('.')[0]
new_main_version = int(main_version) + 1
new_version = f"V{new_main_version}.0"
# 删除原承租方的物业合同信息
contract_to_unlink = origin_contract.lessee_id.property_contract_ids.filtered(
lambda line: line.lease_contract_id.id == rec.contract_id.id
)
if contract_to_unlink:
contract_to_unlink.unlink()
# 新增新承租方的物业合同信息
property_contract_list = []
origin_contract.write({
'version': new_version,
'lessee_id': rec.new_lessee_id.id
})
origin_contract.is_lessee = True
self.env['lessee.alteration.line'].create({
'contract_id': origin_contract.id,
'lessee_id': original_lessee_id,
'start_date': origin_contract.actual_start_time,
'end_date': rec.effective_date - relativedelta(days=1),
})
origin_contract._compute_contract_count()
property_contract_list.append([0, 0, {
'tenant_id': rec.new_lessee_id.id,
'lease_contract_id': rec.contract_id.id,
'rent': rec.contract_id.rent,
}])
rec.new_lessee_id.write({'property_contract_ids': property_contract_list})
# 新增一条更名应收账单
management_type_id = rec.env["yuthon.management.type"].search([('name', '=', '更名费')])
if not management_type_id:
management_type_id = rec.env["yuthon.management.type"].create({'name': '更名费'})
if rec.fee > 0:
self.env['property.accounts.receivable'].create({
'contract_id': origin_contract.id,
'original_amount': rec.fee,
'management_type_id': management_type_id.id,
'due_date': rec.effective_date,
})
elif rec.renewal_type == 'early_checkout':
workflow_record_id = self.env["soong.workflow.record"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id)])
lessor_id = rec.contract_id.lessor_id
company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1)
if lessor_id.referred in ['天河投资', '天投城建']:
company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1)
elif lessor_id.referred in ['天河物管']:
company_id = self.env['res.company'].browse(3)
elif lessor_id.referred in ['天河粮储']:
company_id = self.env['res.company'].search([('name', '=', '天河粮储')], limit=1)
# 取本审批最后一条流程日志的操作时间,作为账单 due_date
operation_log = self.env['soong.workflow.log'].search([
('model', '=', self._name),
('res_id', '=', rec.id),
], order='lase_click_date DESC, id DESC', limit=1)
operation_date = operation_log.lase_click_date.date() if operation_log.lase_click_date else fields.Date.today()
# 非押金退款明细(除押金外的退款,原逻辑保留)
if rec.receivables_ids:
refund_lines = rec.receivables_ids.filtered(lambda l: l.refund_amount > 0 and l.management_type_id.name != '押金')
bills_line_list = []
if refund_lines:
for refund_line in refund_lines:
bills_line_list.append({
'lessor_id': rec.contract_id.lessor_id.id,
'due_date': operation_date,
'management_type_id': refund_line.management_type_id.id,
'amount': refund_line.refund_amount,
'contract_id': rec.contract_id.id,
'receivables_id': refund_line.id,
'remake': rec.early_explain
})
if bills_line_list:
self.env['property.bills.list'].create(bills_line_list)
# 押金处理(根据押金处理方式分三种,先判断去重再创建)
deposit_management_type = self.env['yuthon.management.type'].search(
[('name', '=', '押金')], limit=1)
if rec.deposit_treatment == 'refund':
# 全额退押金:仅创建 bills.list,金额 = 原押金 deposit
if rec.deposit > 0:
exist_bill = self.env['property.bills.list'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', deposit_management_type.id),
('amount', '=', rec.deposit),
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not exist_bill:
self.env['property.bills.list'].create({
'lessor_id': rec.contract_id.lessor_id.id,
'due_date': operation_date,
'management_type_id': deposit_management_type.id,
'amount': rec.deposit,
'contract_id': rec.contract_id.id,
'remake': rec.early_explain,
'workflow_record_id': workflow_record_id.id,
})
elif rec.deposit_treatment == 'part_refund':
# 部分退租:bills.list 金额 = refund_amount;bank.statements 和 receivable 金额 = forfeit
if rec.refund_amount > 0:
exist_bill = self.env['property.bills.list'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', deposit_management_type.id),
('amount', '=', rec.refund_amount),
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not exist_bill:
self.env['property.bills.list'].create({
'lessor_id': rec.contract_id.lessor_id.id,
'due_date': operation_date,
'management_type_id': deposit_management_type.id,
'amount': rec.refund_amount,
'contract_id': rec.contract_id.id,
'remake': rec.early_explain,
'workflow_record_id': workflow_record_id.id,
})
if rec.forfeit > 0:
start_time = datetime.combine(rec.early_quitting_date, datetime.min.time().replace(hour=8, minute=00))
existing_bank_statement = self.env['property.bank.statements'].sudo().search([
('start_time', '=', start_time),
('original_amount', '=', rec.forfeit),
('name_account', '=', rec.lessee_id.name),
('remake', '=', rec.early_explain),
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not existing_bank_statement:
bank_accounts = self.env['property.bank.line'].search([
('tenant_id', '=', rec.lessee_id.id)
]).mapped('bank')
bank_accounts = [acc for acc in bank_accounts if acc][0]
rec.contract_id.write({
'actual_end_time': rec.early_quitting_date
})
bank_id = self.env['property.bank.statements'].sudo().create({
'start_time': start_time,
'name': bank_accounts,
'original_amount': rec.forfeit,
'name_account': rec.lessee_id.name,
'remake': rec.early_explain,
'lending_direction': 'loan',
'workflow_record_id': workflow_record_id.id,
'company_id': company_id.id,
'your_own_unit': lessor_id.referred,
'your_own_account': lessor_id.rent_account,
})
bank_id._compute_property_employee_id()
management_type_id = self.env['yuthon.management.type'].search([
('name', '=', '违约金')
], limit=1)
exist_receivable = self.env['property.accounts.receivable'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', management_type_id.id),
('due_date', '=', rec.early_quitting_date),
('original_amount', '=', rec.forfeit),
], limit=1)
if not exist_receivable:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'management_type_id': management_type_id.id,
'due_date': rec.early_quitting_date,
'original_amount': rec.forfeit,
'remark': rec.early_explain,
})
deposit_id = self.env['property.accounts.receivable'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id.name', '=', '押金'),
], limit=1)
if deposit_id:
deposit_id.write({
'workflow_record_id': workflow_record_id.id
})
elif rec.deposit_treatment == 'no_refund':
# 全额没收:bills.list、bank.statements、receivable 金额均为 deposit
if rec.deposit > 0:
exist_bill = self.env['property.bills.list'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', deposit_management_type.id),
('amount', '=', rec.deposit),
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not exist_bill:
bill = self.env['property.bills.list'].create({
'lessor_id': rec.contract_id.lessor_id.id,
'due_date': operation_date,
'management_type_id': deposit_management_type.id,
'amount': rec.deposit,
'contract_id': rec.contract_id.id,
'remake': rec.early_explain,
'workflow_record_id': workflow_record_id.id,
})
else:
bill = exist_bill
start_time = datetime.combine(rec.early_quitting_date, datetime.min.time().replace(hour=8, minute=00))
existing_bank_statement = self.env['property.bank.statements'].sudo().search([
('start_time', '=', start_time),
('original_amount', '=', rec.deposit),
('name_account', '=', rec.lessee_id.name),
('remake', '=', rec.early_explain),
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not existing_bank_statement:
bank_accounts = self.env['property.bank.line'].search([
('tenant_id', '=', rec.lessee_id.id)
]).mapped('bank')
bank_accounts = [acc for acc in bank_accounts if acc][0]
rec.contract_id.write({
'actual_end_time': rec.early_quitting_date
})
bank_id = self.env['property.bank.statements'].sudo().create({
'start_time': start_time,
'name': bank_accounts,
'original_amount': rec.deposit,
'name_account': rec.lessee_id.name,
'remake': rec.early_explain,
'lending_direction': 'loan',
'workflow_record_id': workflow_record_id.id,
'company_id': company_id.id,
'your_own_unit': lessor_id.referred,
'your_own_account': lessor_id.rent_account,
})
bank_id._compute_property_employee_id()
else:
bank_id = existing_bank_statement
management_type_id = self.env['yuthon.management.type'].search([
('name', '=', '违约金')
], limit=1)
exist_receivable = self.env['property.accounts.receivable'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', management_type_id.id),
('due_date', '=', rec.early_quitting_date),
('original_amount', '=', rec.deposit),
], limit=1)
if not exist_receivable:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'management_type_id': management_type_id.id,
'due_date': rec.early_quitting_date,
'original_amount': rec.deposit,
'remark': rec.early_explain,
})
# ===== 触发 action_pay + button_reconciliation 的真实逻辑 =====
# 说明:action_pay / button_reconciliation 按钮本身只是返回向导弹窗的方法,
# 真正的业务在各自 wizard 的 confirm / 对账逻辑里。这里不再手工 write 子集字段,
# 而是直接调用 property.bills.wizard 的 _do_pay()(与弹窗 confirm 共用同一份支付逻辑),
# 以及复现银行流水对账的核心效果,保证与人工点“核销/对账”完全一致。
deposit_receivable = self.env['property.accounts.receivable'].search([
('contract_id', '=', rec.contract_id.id),
('management_type_id', '=', deposit_management_type.id),
], limit=1)
# 1) 支付效果(action_pay):调用支付弹窗的真实逻辑 —— 实付日期=应付日期、标记已付、
# 回写押金应收单实付日期与支付状态、结清合同。全额没收场景下跳过 confirm 的未对账校验。
if bill and not bill.actual_date:
bill.amount_actual = bill.amount
pay_wizard = self.env['property.bills.wizard'].create({
'bills_list_id': bill.id,
'actual_date': bill.due_date,
'receivables_id': deposit_receivable.id if deposit_receivable else False,
})
pay_wizard._do_pay()
# 2) 对账效果(button_reconciliation):bank.statements 与押金应收对账
if bank_id and bank_id.bank_state != 'reconciled':
bank_id.bank_state = 'reconciled'
bank_id.yes_amount = bank_id.original_amount
bank_id.reconciliation_time = fields.Datetime.now() - timedelta(hours=8)
# 自动核销违约金
if exist_receivable:
exist_receivable.write({
'has_date': bill.due_date if bill else operation_date,
'is_reconciliation': True,
'payout_date': operation_date,
'delivered1': rec.deposit,
'deposit_state': 'paid',
'reconciliation_time': fields.Datetime.now(),
'bank_id': bank_id.id,
})
self.env['receivable.modify.line'].create({
'bank_id': bank_id.id,
'not_collected': rec.deposit,
'reconciliation_date': operation_date,
'receivable_id': exist_receivable.id,
'is_write': True,
'operation_date': fields.Date.today(),
'is_penalty': True,
'remake1': rec.early_explain,
'amount': rec.deposit,
})
rec.contract_id.contact_state = 'settle'
rec.contract_id.compute_is_has()
# 原有作废/减免逻辑保留
for receivable_id in rec.receivables_ids:
if receivable_id.void:
receivable_id.receivable_id.write({
'is_invalid': True,
'void_date': operation_date
})
if receivable_id.reduction_amount:
receivable_id.receivable_id.discount_amount = receivable_id.reduction_amount
receivable_id.receivable_id.discount_date = operation_date
@api.onchange('early_quitting_date', 'contract_id')
def _onchange_early_quitting_date(self):
self.receivables_ids = [(5, 0, 0)]
receivable_ids = self.env['property.accounts.receivable'].search([
('due_date', '<=', self.early_quitting_date),
('contract_id', '=', self.contract_id.id),
])
deposit_receivables = self.env['property.accounts.receivable']
normal_receivables = self.env['property.accounts.receivable']
for rec in receivable_ids:
if rec.management_type_id and rec.management_type_id.name == '押金':
deposit_receivables |= rec
else:
normal_receivables |= rec
if deposit_receivables:
self.deposit = sum(rec.original_amount for rec in deposit_receivables)
receivable_list = []
for rec in normal_receivables:
receivable_list.append([0, 0, {
'receivable_id': rec.id,
'is_reconciliation': rec.is_reconciliation,
}])
self.receivables_ids = receivable_list
@api.onchange('contract_id', 'renewal_type')
def _onchange_name(self):
if self.contract_id and self.renewal_type:
renewal_type_text = dict(self._fields['renewal_type'].selection).get(self.renewal_type, '')
self.name = f"{self.contract_id.name} + {renewal_type_text}"
elif self.contract_id:
self.name = self.contract_id.property_name
else:
self.name = False
class PropertyEarlyQuitting(models.Model):
_name = 'property.early.quitting'
class EarlyReceivablesLine(models.Model):
_name = 'early.receivables.line'
_description = '应收明细'
renewal_id = fields.Many2one('property.lease.renewal', string='关联续租审批')
receivable_id = fields.Many2one('property.accounts.receivable', string='应收账单')
management_type_id = fields.Many2one(related='receivable_id.management_type_id', string='类型')
is_reconciliation = fields.Boolean(string='对账')
due_date = fields.Date(related='receivable_id.due_date', string='应收日期')
original_amount = fields.Float(string='金额', compute='_compute_original_amount')
reduction_amount = fields.Float(string='减免金额')
refund_amount = fields.Float(string='退款金额')
void = fields.Boolean(string='作废', default=False)
@api.depends('receivable_id')
def _compute_original_amount(self):
for line in self:
line.original_amount = line.receivable_id.original_amount - line.receivable_id.discount_amount