From 2729c83a6de0ae6179e6572eba72de2577c5691a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Tue, 21 Apr 2026 15:41:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E7=A5=A8=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_property/__manifest__.py | 1 + .../models/property_accounts_receivable.py | 186 ++--------------- yuthon_property/security/ir.model.access.csv | 3 +- yuthon_property/wizard/__init__.py | 1 + .../wizard/batch_invoice_export_wizard.py | 193 ++++++++++++++++++ .../batch_invoice_export_wizard_views.xml | 20 ++ 6 files changed, 229 insertions(+), 175 deletions(-) create mode 100644 yuthon_property/wizard/batch_invoice_export_wizard.py create mode 100644 yuthon_property/wizard/batch_invoice_export_wizard_views.xml diff --git a/yuthon_property/__manifest__.py b/yuthon_property/__manifest__.py index 0205354e8..1aee4363f 100644 --- a/yuthon_property/__manifest__.py +++ b/yuthon_property/__manifest__.py @@ -71,6 +71,7 @@ 'wizard/property_statements_wizard_views.xml', 'wizard/utility_meter_wizard_views.xml', 'wizard/tenant_import_wizard_views.xml', + 'wizard/batch_invoice_export_wizard_views.xml', 'views/menuitem.xml', ], 'assets': { diff --git a/yuthon_property/models/property_accounts_receivable.py b/yuthon_property/models/property_accounts_receivable.py index 820399878..8308e3f65 100644 --- a/yuthon_property/models/property_accounts_receivable.py +++ b/yuthon_property/models/property_accounts_receivable.py @@ -286,7 +286,7 @@ class PropertyAccountsReceivable(models.Model): } def export_batch_invoice(self): - """导出批量开票数据到新Excel模板""" + """打开批量开票导出弹窗""" receivable_ids = self._context.get('active_ids', []) if not receivable_ids: return { @@ -300,183 +300,21 @@ class PropertyAccountsReceivable(models.Model): } } - receivable_records = self.env['property.accounts.receivable'].browse(receivable_ids) - - # 加载新模板文件 - template_path = get_module_resource('yuthon_property', 'static', 'tmpl', 'piliangkaipiao (3).xlsx') - workbook = load_workbook(template_path) - - # 指定写入"原始数据"sheet - sheet = workbook['原始数据'] - - # 统计导出数据中最常见的年月(用于B2单元格) - year_month_list = [] - for record in receivable_records: - if record.due_date: - year_month_list.append((record.due_date.year, record.due_date.month)) - - if year_month_list: - # 取出现次数最多的年月 - from collections import Counter - most_common_ym = Counter(year_month_list).most_common(1)[0][0] - title_year, title_month = most_common_ym - else: - # 如果没有应收日期,使用当前年月 - now = datetime.now() - title_year, title_month = now.year, now.month - - # 填充B2单元格:年月 - sheet['B2'] = f'{title_year}年{title_month}月' - - # 统计导出数据中最常见的公司(用于C2单元格) - company_list = [] - for record in receivable_records: - if record.contract_id and record.contract_id.company_id: - company_list.append(record.contract_id.company_id.name) - - if company_list: - from collections import Counter - most_common_company = Counter(company_list).most_common(1)[0][0] - else: - most_common_company = '' - - # 填充C2单元格:所属公司 - sheet['C2'] = most_common_company - - # 从第4行开始写入数据(第1-2行是标题,第3行是表头) - row = 4 - - # 获取模板样式行(使用第4行作为参考,因为第3行是表头) - template_row_idx = 4 - if sheet.max_row >= template_row_idx: - template_styles = {} - for col in range(1, 11): # A到J列(10列) - cell = sheet.cell(row=template_row_idx, column=col) - template_styles[col] = { - 'font': copy(cell.font), - 'fill': copy(cell.fill), - 'border': copy(cell.border), - 'alignment': copy(cell.alignment), - } - else: - template_styles = {} - - for index, record in enumerate(receivable_records, start=1): - # 先解除当前行的合并单元格(如果有) - merged_cells_to_unmerge = [] - for merged_range in sheet.merged_cells.ranges: - if merged_range.min_row <= row <= merged_range.max_row: - merged_cells_to_unmerge.append(str(merged_range)) - for merged_range_str in merged_cells_to_unmerge: - sheet.unmerge_cells(merged_range_str) - - # 先为所有列应用模板样式并统一居中对齐 - for col in range(1, 11): # A到J列 - cell = sheet.cell(row=row, column=col) - if col in template_styles: - cell.font = copy(template_styles[col]['font']) - cell.fill = copy(template_styles[col]['fill']) - cell.border = copy(template_styles[col]['border']) - # 统一设置居中对齐 - cell.alignment = Alignment(horizontal='center', vertical='center') - - # A列:序号(从1开始) - sheet.cell(row=row, column=1, value=index) - - # B列:应收租期(due_date ~ due_date_end) - date_range = '' - if record.due_date and record.due_date_end: - date_range = f"{record.due_date.strftime('%Y-%m-%d')} ~ {record.due_date_end.strftime('%Y-%m-%d')}" - elif record.due_date: - date_range = record.due_date.strftime('%Y-%m-%d') - sheet.cell(row=row, column=2, value=date_range) - - # C列:金额(original_amount) - sheet.cell(row=row, column=3, value=record.original_amount or 0) - - # D列:物业名称(property_name) - sheet.cell(row=row, column=4, value=record.property_name or '') - - # E列:合同编号(contract_code,从合同获取) - contract_code = '' - if record.contract_id: - contract_code = record.contract_id.contract_code or '' - sheet.cell(row=row, column=5, value=contract_code) - - # F列:承租方名称(lessee_id) - sheet.cell(row=row, column=6, value=record.lessee_id.name if record.lessee_id else '') - - # G列:统一社会信用代码/纳税人识别号(从invoicing.information.line获取) - tax_number = '' - if record.lessee_id and record.lessee_id.invoicing_ids: - # 取第一条有效记录的税号 - for inv_line in record.lessee_id.invoicing_ids: - if inv_line.tax_number: - tax_number = inv_line.tax_number - break - sheet.cell(row=row, column=7, value=tax_number) - - # H列:产权证(title_information,从合同获取) - title_info = '' - if record.contract_id: - title_info = record.contract_id.title_information or '' - sheet.cell(row=row, column=8, value=title_info) - - # I列:发票类型(从invoicing.information.line的invoicing_type_id获取) - invoicing_type = '' - if record.lessee_id and record.lessee_id.invoicing_ids: - # 取第一条有效记录的发票类型 - for inv_line in record.lessee_id.invoicing_ids: - if inv_line.invoicing_type_id: - invoicing_type = inv_line.invoicing_type_id.name or '' - break - sheet.cell(row=row, column=9, value=invoicing_type) - - # J列:税率(从合同的tax_rate获取,使用小数格式,如0.05) - tax_rate_value = 0.0 - if record.contract_id and record.contract_id.tax_rate: - # 将税率名称(如"0.05"或"5%")转换为小数 - tax_rate_name = record.contract_id.tax_rate.name or '0' - try: - # 如果是百分比格式(如"5%"),转换为小数 - if '%' in tax_rate_name: - tax_rate_value = float(tax_rate_name.replace('%', '')) / 100 - else: - # 直接是小数格式(如"0.05") - tax_rate_value = float(tax_rate_name) - except (ValueError, TypeError): - tax_rate_value = 0.0 - sheet.cell(row=row, column=10, value=tax_rate_value) - - # 设置最小行高 - if not sheet.row_dimensions[row].height or sheet.row_dimensions[row].height < 30: - sheet.row_dimensions[row].height = 30 - - row += 1 - - # 保存到内存 - output = io.BytesIO() - workbook.save(output) - output.seek(0) - excel_data = base64.b64encode(output.read()) - output.close() - - # 创建附件 - filename = f'批量开票_{datetime.now().strftime("%Y%m%d")}.xlsx' - attachment = self.env['ir.attachment'].create({ - 'name': filename, - 'type': 'binary', - 'datas': excel_data, - 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + wizard = self.env['batch.invoice.export.wizard'].create({ + 'active_ids': str(receivable_ids), }) - - # 返回下载链接 return { - 'type': 'ir.actions.act_url', - 'url': f'/web/content/{attachment.id}?download=true', - 'target': 'self', + 'type': 'ir.actions.act_window', + 'res_model': 'batch.invoice.export.wizard', + 'res_id': wizard.id, + 'name': '批量开票导出', + 'view_mode': 'form', + 'view_type': 'form', + 'views': [[False, 'form']], + 'target': 'new', } + def export_kingdee_receivable(self): """导出应收单金蝶数据到Excel模板""" # 获取勾选的应收账单记录 diff --git a/yuthon_property/security/ir.model.access.csv b/yuthon_property/security/ir.model.access.csv index df618b3e0..10fa872c0 100644 --- a/yuthon_property/security/ir.model.access.csv +++ b/yuthon_property/security/ir.model.access.csv @@ -103,4 +103,5 @@ access_yuthon_epitope_values, yuthon.epitope.values,model_yuthon_epitope_values, access_task_push_wizard,task.push.wizard,model_task_push_wizard,base.group_user,1,1,1,1 access_utility_meter_wizard,utility.meter.wizard,model_utility_meter_wizard,base.group_user,1,1,1,1 access_invoicing_information_line,invoicing.information.line,model_invoicing_information_line,base.group_user,1,1,1,1 -access_tenant_import_wizard,tenant.import.wizard,model_tenant_import_wizard,base.group_user,1,1,1,1 \ No newline at end of file +access_tenant_import_wizard,tenant.import.wizard,model_tenant_import_wizard,base.group_user,1,1,1,1 +access_batch_invoice_export_wizard,batch.invoice.export.wizard,model_batch_invoice_export_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/yuthon_property/wizard/__init__.py b/yuthon_property/wizard/__init__.py index 2a5319f28..00c30aee9 100644 --- a/yuthon_property/wizard/__init__.py +++ b/yuthon_property/wizard/__init__.py @@ -12,3 +12,4 @@ from . import electric_water_wizard from . import task_push_wizard from . import utility_meter_wizard from . import tenant_import_wizard +from . import batch_invoice_export_wizard diff --git a/yuthon_property/wizard/batch_invoice_export_wizard.py b/yuthon_property/wizard/batch_invoice_export_wizard.py new file mode 100644 index 000000000..91efda067 --- /dev/null +++ b/yuthon_property/wizard/batch_invoice_export_wizard.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +from odoo.exceptions import UserError +import io +from copy import copy +from openpyxl import load_workbook +from openpyxl.styles import Alignment +from odoo.modules.module import get_module_resource +from datetime import datetime +import base64 + + +class BatchInvoiceExportWizard(models.TransientModel): + _name = 'batch.invoice.export.wizard' + _description = '批量开票导出向导' + + include_all = fields.Boolean(string='全部', default=True) + include_deposit = fields.Boolean(string='押金', default=False) + include_penalty = fields.Boolean(string='违约金', default=False) + + active_ids = fields.Char(string='选中记录IDs') + + def action_export(self): + """执行导出""" + active_ids = eval(self.active_ids) if self.active_ids else [] + if not active_ids: + raise UserError('请至少选择一条应收账单记录') + + receivable_records = self.env['property.accounts.receivable'].browse(active_ids) + + # 按应收类型过滤,勾选全部或两个都不勾则不过滤 + if not self.include_all: + allowed = [] + if self.include_deposit: + allowed.append('押金') + if self.include_penalty: + allowed.append('违约金') + if allowed: + receivable_records = receivable_records.filtered( + lambda r: r.management_type_id and r.management_type_id.name in allowed + ) + + if not receivable_records: + raise UserError('过滤后没有符合条件的记录,请检查所选记录的应收类型') + + # 加载模板文件 + template_path = get_module_resource('yuthon_property', 'static', 'tmpl', 'piliangkaipiao (3).xlsx') + workbook = load_workbook(template_path) + sheet = workbook['原始数据'] + + # 统计最常见年月(用于B2) + year_month_list = [] + for record in receivable_records: + if record.due_date: + year_month_list.append((record.due_date.year, record.due_date.month)) + + if year_month_list: + from collections import Counter + most_common_ym = Counter(year_month_list).most_common(1)[0][0] + title_year, title_month = most_common_ym + else: + now = datetime.now() + title_year, title_month = now.year, now.month + + sheet['B2'] = f'{title_year}年{title_month}月' + + # 统计最常见公司(用于C2) + company_list = [] + for record in receivable_records: + if record.contract_id and record.contract_id.company_id: + company_list.append(record.contract_id.company_id.name) + + if company_list: + from collections import Counter + most_common_company = Counter(company_list).most_common(1)[0][0] + else: + most_common_company = '' + + sheet['C2'] = most_common_company + + # 从第4行开始写入数据 + row = 4 + template_row_idx = 4 + if sheet.max_row >= template_row_idx: + template_styles = {} + for col in range(1, 11): + cell = sheet.cell(row=template_row_idx, column=col) + template_styles[col] = { + 'font': copy(cell.font), + 'fill': copy(cell.fill), + 'border': copy(cell.border), + 'alignment': copy(cell.alignment), + } + else: + template_styles = {} + + for index, record in enumerate(receivable_records, start=1): + merged_cells_to_unmerge = [] + for merged_range in sheet.merged_cells.ranges: + if merged_range.min_row <= row <= merged_range.max_row: + merged_cells_to_unmerge.append(str(merged_range)) + for merged_range_str in merged_cells_to_unmerge: + sheet.unmerge_cells(merged_range_str) + + for col in range(1, 11): + cell = sheet.cell(row=row, column=col) + if col in template_styles: + cell.font = copy(template_styles[col]['font']) + cell.fill = copy(template_styles[col]['fill']) + cell.border = copy(template_styles[col]['border']) + cell.alignment = Alignment(horizontal='center', vertical='center') + + # A列:序号 + sheet.cell(row=row, column=1, value=index) + + # B列:应收租期 + date_range = '' + if record.due_date and record.due_date_end: + date_range = f"{record.due_date.strftime('%Y-%m-%d')} ~ {record.due_date_end.strftime('%Y-%m-%d')}" + elif record.due_date: + date_range = record.due_date.strftime('%Y-%m-%d') + sheet.cell(row=row, column=2, value=date_range) + + # C列:金额(未收) + sheet.cell(row=row, column=3, value=record.submitted or 0) + + # D列:物业名称 + sheet.cell(row=row, column=4, value=record.property_name or '') + + # E列:合同编号 + contract_code = record.contract_id.contract_code if record.contract_id else '' + sheet.cell(row=row, column=5, value=contract_code) + + # F列:承租方名称 + sheet.cell(row=row, column=6, value=record.lessee_id.name if record.lessee_id else '') + + # G列:统一社会信用代码/纳税人识别号 + tax_number = '' + if record.lessee_id and record.lessee_id.invoicing_ids: + for inv_line in record.lessee_id.invoicing_ids: + if inv_line.tax_number: + tax_number = inv_line.tax_number + break + sheet.cell(row=row, column=7, value=tax_number) + + # H列:产权证 + title_info = record.contract_id.title_information if record.contract_id else '' + sheet.cell(row=row, column=8, value=title_info) + + # I列:发票类型 + invoicing_type = '' + if record.lessee_id and record.lessee_id.invoicing_ids: + for inv_line in record.lessee_id.invoicing_ids: + if inv_line.invoicing_type_id: + invoicing_type = inv_line.invoicing_type_id.name or '' + break + sheet.cell(row=row, column=9, value=invoicing_type) + + # J列:税率 + tax_rate_value = 0.0 + if record.contract_id and record.contract_id.tax_rate: + tax_rate_name = record.contract_id.tax_rate.name or '0' + try: + if '%' in tax_rate_name: + tax_rate_value = float(tax_rate_name.replace('%', '')) / 100 + else: + tax_rate_value = float(tax_rate_name) + except (ValueError, TypeError): + tax_rate_value = 0.0 + sheet.cell(row=row, column=10, value=tax_rate_value) + + if not sheet.row_dimensions[row].height or sheet.row_dimensions[row].height < 30: + sheet.row_dimensions[row].height = 30 + + row += 1 + + output = io.BytesIO() + workbook.save(output) + output.seek(0) + file_data = base64.b64encode(output.read()).decode('utf-8') + + attachment = self.env['ir.attachment'].create({ + 'name': f'批量开票_{title_year}年{title_month}月.xlsx', + 'type': 'binary', + 'datas': file_data, + 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }) + + return { + 'type': 'ir.actions.act_url', + 'url': f'/web/content/{attachment.id}?download=true', + 'target': 'self', + } diff --git a/yuthon_property/wizard/batch_invoice_export_wizard_views.xml b/yuthon_property/wizard/batch_invoice_export_wizard_views.xml new file mode 100644 index 000000000..4fd8a6280 --- /dev/null +++ b/yuthon_property/wizard/batch_invoice_export_wizard_views.xml @@ -0,0 +1,20 @@ + + + + batch.invoice.export.wizard.form + batch.invoice.export.wizard + +
+ + + + + + +
+
+
+