开票导出
This commit is contained in:
@@ -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模板"""
|
||||
# 获取勾选的应收账单记录
|
||||
|
||||
@@ -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
|
||||
|
@@ -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>
|
||||
Reference in New Issue
Block a user