diff --git a/yuthon_property/models/property_accounts_receivable.py b/yuthon_property/models/property_accounts_receivable.py index d3e97884a..49a5f0e5b 100644 --- a/yuthon_property/models/property_accounts_receivable.py +++ b/yuthon_property/models/property_accounts_receivable.py @@ -126,9 +126,8 @@ class PropertyAccountsReceivable(models.Model): is_ended = True is_forfeited_deposit = False if contract: - renewal = self.env['property.lease.renewal'].search([ + renewal = self.env['property.tenant.checkout'].search([ ('contract_id', '=', contract.id), - ('renewal_type', '=', 'early_checkout'), ('work_end', '=', True), ], limit=1) if renewal: @@ -234,9 +233,8 @@ class PropertyAccountsReceivable(models.Model): penalty_start_date = date(next_year, next_month, 1) contract = record.contract_id - renewal = self.env['property.lease.renewal'].search([ + renewal = self.env['property.tenant.checkout'].search([ ('contract_id', '=', contract.id), - ('renewal_type', '=', 'early_checkout'), ('work_end', '=', True), ], limit=1) diff --git a/yuthon_property_approval/__manifest__.py b/yuthon_property_approval/__manifest__.py index f44ff9408..cca04bccd 100644 --- a/yuthon_property_approval/__manifest__.py +++ b/yuthon_property_approval/__manifest__.py @@ -80,6 +80,7 @@ 'views/property_evaluate_application_views.xml', 'views/contract_accounts_receivable_viwes.xml', 'views/inherit_yuthon_property_views.xml', + 'views/receivable_unpaid_views.xml', 'views/property_project_works_viwes.xml', 'views/property_sporadic_works_views.xml', 'views/property_service_approval_views.xml', diff --git a/yuthon_property_approval/models/__init__.py b/yuthon_property_approval/models/__init__.py index 53b3834c6..6fc314ed3 100644 --- a/yuthon_property_approval/models/__init__.py +++ b/yuthon_property_approval/models/__init__.py @@ -68,3 +68,4 @@ from . import yuthon_property_task from . import property_tenant_checkout from . import property_tenant_checkout_inspection from . import property_tenant_checkout_fee +from . import property_accounts_receivable_summary diff --git a/yuthon_property_approval/models/property_lease_contract.py b/yuthon_property_approval/models/property_lease_contract.py index e0bcf37a2..3a0a3f61a 100644 --- a/yuthon_property_approval/models/property_lease_contract.py +++ b/yuthon_property_approval/models/property_lease_contract.py @@ -783,13 +783,10 @@ class PropertyLeaseContract(models.Model): def action_open_contract(self): self.ensure_one() - action = self.env["ir.actions.actions"]._for_xml_id("yuthon_property_approval.property_accounts_receivable_action") + action = self.env["ir.actions.actions"]._for_xml_id( + "yuthon_property_approval.property_accounts_receivable_contract_action") action.update({ 'domain': [('contract_id', '=', self.id)], - 'context': { - 'search_default_group_by_management_type_id': 1, - 'group_by': 'management_type_id' - } }) return action diff --git a/yuthon_property_approval/models/property_lease_renewal.py b/yuthon_property_approval/models/property_lease_renewal.py index e6fa40c7f..40816f226 100644 --- a/yuthon_property_approval/models/property_lease_renewal.py +++ b/yuthon_property_approval/models/property_lease_renewal.py @@ -20,7 +20,7 @@ class PropertyLeaseRenewal(models.Model): 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='变更类型') + renewal_type = fields.Selection([('lease', '承租方变更'), ('extend', '延长租期')], string='变更类型') urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度', default='normal') is_chapter = fields.Selection([('yes', '是'), ('no', '否')], string="是否用章") @@ -53,20 +53,10 @@ class PropertyLeaseRenewal(models.Model): 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='实际结束日期') @@ -92,11 +82,6 @@ class PropertyLeaseRenewal(models.Model): # 需求调整:不做自动填充,保留用户手动输入的联系人 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): """填写联系人后,按员工信息带出身份证号与联系电话;不再从承租方带出。""" @@ -114,14 +99,6 @@ class PropertyLeaseRenewal(models.Model): 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() @@ -201,305 +178,6 @@ class PropertyLeaseRenewal(models.Model): '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') - filtered_accounts = [acc for acc in bank_accounts if acc] - bank_accounts = filtered_accounts[0] if filtered_accounts else None - 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') - filtered_accounts = [acc for acc in bank_accounts if acc] - bank_accounts = filtered_accounts[0] if filtered_accounts else None - 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): @@ -511,15 +189,11 @@ class PropertyLeaseRenewal(models.Model): 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='关联续租审批') + checkout_id = fields.Many2one('property.tenant.checkout', string='关联退租表', ondelete='cascade') 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='对账') @@ -534,3 +208,4 @@ class EarlyReceivablesLine(models.Model): for line in self: line.original_amount = line.receivable_id.original_amount - line.receivable_id.discount_amount + diff --git a/yuthon_property_approval/models/property_tenant_checkout.py b/yuthon_property_approval/models/property_tenant_checkout.py index c1da1b939..f36b8da07 100644 --- a/yuthon_property_approval/models/property_tenant_checkout.py +++ b/yuthon_property_approval/models/property_tenant_checkout.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime, timedelta, date +import calendar + from odoo import models, fields, api from odoo.exceptions import UserError @@ -33,7 +36,7 @@ class PropertyTenantCheckout(models.Model): 'checkout_id', 'attachment_id', string="附件") # 附件 # ===== 关联租赁合同 ===== - contract_id = fields.Many2one('property.lease.contract', string='关联租赁合同') # 从合同列表「退租」按钮发起时自动带入 + contract_id = fields.Many2one('property.lease.contract', string='租赁合同') # 从合同列表「退租」按钮发起时自动带入 # ===== 合同信息(关联后自动带出,只读参考)===== tenant_unit = fields.Char(string='租户单元') # 如:4F东边01单元(关联合同后自动带出,可改) @@ -46,11 +49,12 @@ class PropertyTenantCheckout(models.Model): rent_address = fields.Char(string='租赁地址') # 租赁地址(关联合同后自动带出,可改) # ===== 节点一:发起申请 ===== - checkout_deadline = fields.Date(string='退租截止日期') # 退租截止日期 - contract_handle_type = fields.Selection([ - ('checkout', '退租'), ('renew', '续租'), ('transfer', '转租'), ('other', '其他') - ], string='合同办理方式', default='checkout') # 合同办理方式 - checkout_reason = fields.Text(string='退租原因') # 退租原因 + # 退租原因(同时兼任「合同办理方式」):驱动终止逻辑与表单显隐;early_quitting_date 为终止统一日期(退租截止日期与退租日期统一使用此字段) + early_reason = fields.Selection([ + ('normal', '正常退租'), ('agree', '协商一致'), ('settle', '违约结清'), ('other', '其他') + ], string='退租原因', default='normal') + early_quitting_date = fields.Date(string='退租日期') # 实际退租日期(终止逻辑统一使用此字段) + early_explain = fields.Text(string='退租说明') # 退租说明(进入终止/押金处理逻辑) other_note = fields.Text(string='其它(补充说明)') # 其它补充说明 # ===== 节点二:工程组检查与确认 ===== @@ -70,8 +74,8 @@ class PropertyTenantCheckout(models.Model): fee_ids = fields.One2many('property.tenant.checkout.fee', 'checkout_id', string='应收费用明细') # 应收费用明细 total_fee = fields.Float(string='应收合计', compute='_compute_total_fee', store=True) # 应收费用合计(自动求和) deposit_treatment = fields.Selection([ - ('full', '退还全额'), ('part', '部分退还'), ('none', '不退还') - ], string='保证金处理方式') # 保证金处理方式 + ('refund', '全额退押'), ('part_refund', '部分退租'), ('no_refund', '全额没收') + ], string='保证金处理方式') # 保证金处理方式(与租赁变更审批·合同终止逻辑一致) settlement_sign = fields.Binary(string='结算复核签字') # 结算复核签字(PNG) # ===== 节点五:财务处理 ===== @@ -80,6 +84,13 @@ class PropertyTenantCheckout(models.Model): finance_person_in_charge = fields.Char(string='财务部负责人') # 财务部负责人 finance_date = fields.Date(string='财务日期') # 财务部日期 + # ===== 合同终止相关(从租赁变更审批·合同终止迁移)===== + deposit = fields.Float(string='原押金') # 原押金(关联合同押金类应收汇总,onchange 带出) + refund_amount = fields.Float(string='退款金额') # 退款金额(部分退押时填写) + forfeit = fields.Float(string='没收金额') # 没收金额(= 原押金 - 退款金额) + total_receivables = fields.Float(string='应收金额', compute='_compute_total_receivables', store=True) # 应收金额(部分退押时汇总) + receivables_ids = fields.One2many('early.receivables.line', 'checkout_id', string='应收明细') # 截至退租日的应收账单(可作废/减免/退款) + @api.depends('fee_ids', 'fee_ids.amount') def _compute_total_fee(self): """应收合计 = 所有费用明细金额之和""" @@ -96,11 +107,336 @@ class PropertyTenantCheckout(models.Model): @api.onchange('contract_id') def _onchange_contract_id(self): - """关联合同后自动带出公司名称/物业信息/租赁地址(可再手工修改);其余信息以 related 只读展示""" + """关联合同后自动带出公司名称/物业信息/租赁地址(可再手工修改);其余信息以 related 只读展示。 + 正常退租时,自动带出合同结束日期作为退租日期。""" if self.contract_id: self.tenant_unit = self.contract_id.property_name # 租户单元=合同物业信息 # 租赁地址=合同物业明细首行对应的物业地址名称(无物业明细则为空) self.rent_address = self.contract_id.lease_ids[:1].address_id.name + # 正常退租:退租日期默认取合同结束日期(可再手工修改) + if self.early_reason == 'normal' and self.contract_id.term_end_time: + self.early_quitting_date = self.contract_id.term_end_time + + @api.depends('receivables_ids', 'receivables_ids.original_amount', 'receivables_ids.reduction_amount', + 'receivables_ids.void') + def _compute_total_receivables(self): + """应收金额 = 未被作废且未对账的应收明细(原金额 - 减免金额)之和""" + for rec in self: + valid_lines = rec.receivables_ids.filtered(lambda line: not line.void and not line.is_reconciliation) + rec.total_receivables = sum(line.original_amount - line.reduction_amount for line in valid_lines) + + @api.onchange('refund_amount') + def _onchange_forfeit(self): + """部分退押时:没收金额 = 原押金 - 退款金额""" + for rec in self: + rec.forfeit = rec.deposit - rec.refund_amount + + @api.onchange('early_quitting_date', 'contract_id') + def _onchange_early_quitting_date(self): + """退租日期/合同变化:拉取截至退租日的应收账单生成应收明细,并汇总原押金""" + self.receivables_ids = [(5, 0, 0)] + if not self.early_quitting_date or not self.contract_id: + self.deposit = 0 + return + 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 + + def end_approval(self): + """合同终止终审逻辑(从租赁变更审批·合同终止 early_checkout 分支迁移)。 + 统一以 early_quitting_date 作为终止日期;按 early_reason 区分退租方式由前端/配置决定, + 此处对所有退租场景统一处理:押金处理 + 应收作废/减免 + 银行流水与对账。""" + for rec in self: + contract = rec.contract_id + if not contract: + continue + workflow_record_id = self.env["soong.workflow.record"].search( + [("res_model", "=", self._name), ("res_id", "=", self.id)]) + lessor_id = contract.lessor_id + 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) + else: + 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 and 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': contract.lessor_id.id, + 'due_date': operation_date, + 'management_type_id': refund_line.management_type_id.id, + 'amount': refund_line.refund_amount, + 'contract_id': contract.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', '=', contract.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': contract.lessor_id.id, + 'due_date': operation_date, + 'management_type_id': deposit_management_type.id, + 'amount': rec.deposit, + 'contract_id': contract.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 + forfeit = rec.deposit - rec.refund_amount + if rec.refund_amount > 0: + exist_bill = self.env['property.bills.list'].search([ + ('contract_id', '=', contract.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': contract.lessor_id.id, + 'due_date': operation_date, + 'management_type_id': deposit_management_type.id, + 'amount': rec.refund_amount, + 'contract_id': contract.id, + 'remake': rec.early_explain, + 'workflow_record_id': workflow_record_id.id, + }) + if 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', '=', 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') + filtered_accounts = [acc for acc in bank_accounts if acc] + bank_accounts = filtered_accounts[0] if filtered_accounts else None + contract.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': 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', '=', contract.id), + ('management_type_id', '=', management_type_id.id), + ('due_date', '=', rec.early_quitting_date), + ('original_amount', '=', forfeit), + ], limit=1) + if not exist_receivable: + self.env['property.accounts.receivable'].create({ + 'contract_id': contract.id, + 'management_type_id': management_type_id.id, + 'due_date': rec.early_quitting_date, + 'original_amount': forfeit, + 'remark': rec.early_explain, + }) + deposit_id = self.env['property.accounts.receivable'].search([ + ('contract_id', '=', contract.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', '=', contract.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': contract.lessor_id.id, + 'due_date': operation_date, + 'management_type_id': deposit_management_type.id, + 'amount': rec.deposit, + 'contract_id': contract.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') + filtered_accounts = [acc for acc in bank_accounts if acc] + bank_accounts = filtered_accounts[0] if filtered_accounts else None + contract.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', '=', contract.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': contract.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 的真实逻辑 ===== + deposit_receivable = self.env['property.accounts.receivable'].search([ + ('contract_id', '=', contract.id), + ('management_type_id', '=', deposit_management_type.id), + ], limit=1) + + # 1) 支付效果(action_pay):调用支付弹窗的真实逻辑 + 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, + }) + contract.contact_state = 'settle' + contract.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 def action_print_tenant_checkout(self): """打印租户退租表""" diff --git a/yuthon_property_approval/report/property_lease_renewal_template.xml b/yuthon_property_approval/report/property_lease_renewal_template.xml index d7c1afbbd..496883cdd 100644 --- a/yuthon_property_approval/report/property_lease_renewal_template.xml +++ b/yuthon_property_approval/report/property_lease_renewal_template.xml @@ -44,8 +44,7 @@