# -*- 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_rent = 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) # 按应收类型过滤 allowed = [] if self.include_rent: allowed.append('租金') 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 ) # 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.lessor_id: referred = record.contract_id.lessor_id.referred if referred: company_list.append(referred) 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 # 统计最常见权属类型(用于文件名) tenure_type_map = {'self': '自有', 'agency': '代管'} tenure_list = [] for record in receivable_records: if record.contract_id and record.contract_id.tenure_type: tenure_list.append(record.contract_id.tenure_type) if tenure_list: from collections import Counter most_common_tenure = Counter(tenure_list).most_common(1)[0][0] tenure_type_label = tenure_type_map.get(most_common_tenure, most_common_tenure) else: tenure_type_label = '' # 从第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.original_amount or 0) - (record.discount_amount 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'{most_common_company}_{tenure_type_label}_{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', }