开票导出

This commit is contained in:
李鹏宇
2026-04-21 15:41:09 +08:00
parent bdd6ebbb93
commit 2729c83a6d
6 changed files with 229 additions and 175 deletions
+1
View File
@@ -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': {
@@ -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模板"""
# 获取勾选的应收账单记录
+2 -1
View File
@@ -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
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
103 access_invoicing_information_line invoicing.information.line model_invoicing_information_line base.group_user 1 1 1 1
104 access_tenant_import_wizard tenant.import.wizard model_tenant_import_wizard base.group_user 1 1 1 1
105 access_batch_invoice_export_wizard batch.invoice.export.wizard model_batch_invoice_export_wizard base.group_user 1 1 1 1
106
107
+1
View File
@@ -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
@@ -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',
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="batch_invoice_export_wizard_form" model="ir.ui.view">
<field name="name">batch.invoice.export.wizard.form</field>
<field name="model">batch.invoice.export.wizard</field>
<field name="arch" type="xml">
<form string="批量开票导出">
<group string="应收类型">
<field name="include_all"/>
<field name="include_deposit"/>
<field name="include_penalty"/>
</group>
<footer>
<button name="action_export" string="导出" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>