from odoo import models, fields, api, _, SUPERUSER_ID from odoo.exceptions import UserError, ValidationError from dateutil.relativedelta import relativedelta from datetime import datetime, time, timedelta import xlrd import base64 import pandas as pd import re import logging _logger = logging.getLogger(__name__) class PropertyStatementsWizard(models.Model): _name = 'property.statements.wizard' _description = '银行流水向导弹窗' selection_bank = fields.Selection([('js', '建设银行'), ('gs', '工商银行'), ('ms', '民生'), ('zc', '广州银行(中城)'), ('dj', '农商行(东进)')], string="选择银行", default='js') excel_file = fields.Binary(string="上传Excel表") excel_name = fields.Char(string="文件名") def confirm(self): """ 导入excel文件,按行读取数据并处理 """ if not self.excel_file: raise ValidationError('请上传文件。') file_content = base64.decodebytes(self.excel_file) # zc/dj 使用 openpyxl 或 xlrd 按银行类型区分 # 统计导入前总数 count_before = self.env['property.bank.statements'].search_count([]) if self.selection_bank in ('zc', 'dj'): book = xlrd.open_workbook(file_contents=file_content) sh = book.sheet_by_index(0) if self.selection_bank == 'zc': # 广州银行:行0-4为表头信息,第3行(索引3)读取本方账号和开户行,数据从第6行(索引6)开始 your_own_account = str(sh.row(3)[1].value).strip() bank_branch_own = str(sh.row(3)[4].value).strip() for rx in range(6, sh.nrows): row = sh.row(rx) self.property_bank_statements_zc_data(row, your_own_account, bank_branch_own) elif self.selection_bank == 'dj': # 农商行(东进):行0-2为表头信息,第1行(索引1)读取本方账号,数据从第4行(索引4)开始 your_own_account = str(sh.row(1)[1].value).strip() for rx in range(4, sh.nrows): row = sh.row(rx) self.property_bank_statements_dj_data(row, your_own_account) elif self.selection_bank in ('gs', 'xy'): # 工商银行(兴源格式):xlsx文件,数据从第2行(索引1)开始 import openpyxl, io wb = openpyxl.load_workbook(io.BytesIO(file_content)) ws = wb.active for i, row_cells in enumerate(ws.iter_rows(values_only=True)): if i < 1: # 跳过表头行 continue self.property_bank_statements_xy_data_openpyxl(row_cells) else: book = xlrd.open_workbook(file_contents=file_content) sh = book.sheet_by_index(0) for rx in range(1, sh.nrows): row = sh.row(rx) if self.selection_bank == 'js': self.property_bank_statements_jh_data(row) elif self.selection_bank == 'sh': self.property_bank_statements_sh_data(row) elif self.selection_bank == 'ms': self.property_bank_statements_ms_data(row) count_after = self.env['property.bank.statements'].search_count([]) _logger.info('[银行流水导入] 银行=%s 导入前总条数=%d 导入后总条数=%d 新增=%d', self.selection_bank, count_before, count_after, count_after - count_before) # 查询导入数据中 lending_direction 分布 loan_count = self.env['property.bank.statements'].search_count([('lending_direction', '=', 'loan')]) borrow_count = self.env['property.bank.statements'].search_count([('lending_direction', '=', 'borrow')]) none_count = self.env['property.bank.statements'].search_count([('lending_direction', '=', False)]) _logger.info('[银行流水导入] lending_direction分布: 贷(loan)=%d 借(borrow)=%d 无=%d', loan_count, borrow_count, none_count) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': '导入成功', 'message': '银行流水数据已成功导入!', 'type': 'success', 'sticky': False, } } def property_bank_statements_jh_data(self, row): your_own_account = row[11].value start_time = None if row[0].value: time_str = str(row[0].value).strip() dt = datetime.strptime(time_str, '%Y%m%d %H:%M:%S') dt = dt - timedelta(hours=8) start_time = fields.Datetime.to_string(dt) if row[2].value: original_amount = float(row[2].value) lending_direction = 'borrow' elif row[3].value: original_amount = float(row[3].value) lending_direction = 'loan' name_account = row[1].value name = row[6].value bank_branch = row[7].value remake = row[5].value extract = row[4].value your_own_unit = self.classify_by_last_four(your_own_account).referred company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1) if your_own_unit: company_id = self.classify_by_last_four(your_own_account).company_id bank_data = { 'name': name, 'your_own_unit': your_own_unit, 'no_amount': original_amount, 'company_id': company_id.id, 'bank_branch': bank_branch, 'original_amount': original_amount, 'start_time': start_time, 'your_own_account': your_own_account, 'extract': extract, 'name_account': name_account, 'remake': remake, 'lending_direction': lending_direction } existing_record = self.env['property.bank.statements'].search( [('start_time', '=', start_time), ('original_amount', '=', original_amount), ('remake', '=', remake) ], limit=1 ) if not existing_record: self.env['property.bank.statements'].create(bank_data) def property_bank_statements_sh_data(self, row): your_own_account = row[12].value if row[2].value: clean_str = row[2].value.replace('\n', ' ') # 处理后:'2025-01-17 10:10:31' start_time = datetime.strptime(clean_str, '%Y-%m-%d %H:%M:%S') else: start_time = None if row[3].value != '-': lending_direction = 'borrow' original_amount = float(row[3].value) if row[4].value != '-': lending_direction = 'loan' original_amount = float(row[4].value) name_account = row[5].value name = row[6].value remake = row[11].value bank_branch = row[7].value extract = row[10].value serial_number = row[0].value lending_direction = lending_direction your_own_unit = self.classify_by_last_four(your_own_account).referred company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1) if your_own_unit: company_id = your_own_unit.company_id bank_data = { 'name': name, 'serial_number': serial_number, 'your_own_unit': your_own_unit, 'company_id': company_id.id, 'bank_branch': bank_branch, 'no_amount': original_amount, 'original_amount': original_amount, 'start_time': start_time - timedelta(hours=8) , 'your_own_account': your_own_account, 'extract': extract, 'name_account': name_account, 'remake': remake, 'lending_direction': lending_direction } if original_amount and serial_number: existing_record = self.env['property.bank.statements'].search( [('serial_number', '=', serial_number)], limit=1 ) if not existing_record: self.env['property.bank.statements'].create(bank_data) else: existing_record.write(bank_data) elif original_amount: self.env['property.bank.statements'].create(bank_data) def property_bank_statements_ms_data(self, row): if row[2].value: start_time = datetime.strptime(row[2].value, '%Y-%m-%d %H:%M:%S') else: start_time = None name = row[1].value number = row[0].value if row[3].value == "贷": lending_direction = "loan" elif row[3].value == "借": lending_direction = "borrow" name_account = row[4].value bank_number = row[5].value purpose = row[6].value extract = row[7].value remake = row[8].value company_id = self.env['res.company'].browse(3) pass def property_bank_statements_zc_data(self, row, your_own_account, bank_branch_own): """广州银行(中城)解析:数据从第6行开始,列结构: 0:交易日期, 1:交易时间, 2:凭证种类, 3:凭证号, 4:对方账号, 5:对方户名, 6:借方金额, 7:贷方金额, 8:摘要, 9:余额, 10:备注(用途) """ # 交易日期+时间 拼成 datetime date_val = str(row[0].value).strip() time_val = str(row[1].value).strip() if date_val and time_val and date_val != 'None': try: start_time = datetime.strptime(date_val + ' ' + time_val, '%Y%m%d %H:%M:%S') except Exception: start_time = None else: start_time = None # 借贷方向与金额 borrow_str = str(row[6].value).replace(',', '').strip() loan_str = str(row[7].value).replace(',', '').strip() original_amount = 0.0 lending_direction = False if borrow_str and borrow_str not in ('', '0', 'None', '-'): try: original_amount = float(borrow_str) lending_direction = 'borrow' except Exception: pass if loan_str and loan_str not in ('', '0', 'None', '-'): try: original_amount = float(loan_str) lending_direction = 'loan' except Exception: pass if not original_amount: return name = str(row[4].value).strip() # 对方账号 name_account = str(row[5].value).strip() # 对方户名 remake = str(row[8].value).strip() # 摘要 extract = str(row[10].value).strip() # 备注(用途) your_own_unit = self.classify_by_last_four(your_own_account) company_id = self._get_company_by_unit(your_own_unit) if not company_id: return # 广州银行凭证号全为空,用 交易日期+时间+对方账号 组合去重 dedup_key = date_val + time_val + name existing_record = self.env['property.bank.statements'].search( [('serial_number', '=', dedup_key)], limit=1 ) bank_data = { 'name': name, 'name_account': name_account, 'serial_number': dedup_key, 'your_own_account': your_own_account, 'your_own_unit': your_own_unit, 'bank_branch': bank_branch_own, 'original_amount': original_amount, 'lending_direction': lending_direction, 'start_time': start_time - timedelta(hours=8) if start_time else False, 'extract': extract, 'remake': remake, 'company_id': company_id.id, } if not existing_record: bank_data['bank_state'] = 'not_reconciled' self.env['property.bank.statements'].create(bank_data) else: # 已存在记录,不修改 bank_state existing_record.write(bank_data) def property_bank_statements_dj_data(self, row, your_own_account): """农商行(东进)解析:数据从第4行开始,列结构: 0:序号, 1:电子回单号, 2:交易日期, 3:支出, 4:存入, 5:账户余额, 6:对方账号, 7:对方户名, 8:对方行名称, 9:摘要, 10:用途/其他摘要, 11:自然日期, 12:自然时间 """ serial_number = str(row[1].value).strip() if not serial_number or serial_number == 'None': return # 自然日期+自然时间 拼成 datetime date_val = str(row[11].value).strip() time_val = str(row[12].value).strip() if date_val and time_val and date_val != 'None': try: start_time = datetime.strptime(date_val + ' ' + time_val, '%Y%m%d %H:%M:%S') except Exception: start_time = None else: start_time = None # 支出/存入 取非0那个 borrow_str = str(row[3].value).replace(',', '').strip() loan_str = str(row[4].value).replace(',', '').strip() original_amount = 0.0 lending_direction = False if borrow_str and borrow_str not in ('', '0', '0.0', 'None', '-'): try: val = float(borrow_str) if val > 0: original_amount = val lending_direction = 'borrow' except Exception: pass if loan_str and loan_str not in ('', '0', '0.0', 'None', '-'): try: val = float(loan_str) if val > 0: original_amount = val lending_direction = 'loan' except Exception: pass if not original_amount: return name = str(row[6].value).strip() # 对方账号 name_account = str(row[7].value).strip() # 对方户名 bank_branch = str(row[8].value).strip() # 对方行名称 remake = str(row[9].value).strip() # 摘要 extract = str(row[10].value).strip() # 用途 your_own_unit = self.classify_by_last_four(your_own_account) company_id = self._get_company_by_unit(your_own_unit) if not company_id: return existing_record = self.env['property.bank.statements'].search( [('serial_number', '=', serial_number)], limit=1 ) bank_data = { 'name': name, 'name_account': name_account, 'serial_number': serial_number, 'your_own_account': your_own_account, 'your_own_unit': your_own_unit, 'bank_branch': bank_branch, 'original_amount': original_amount, 'lending_direction': lending_direction, 'start_time': start_time - timedelta(hours=8) if start_time else False, 'extract': extract, 'remake': remake, 'company_id': company_id.id, } if not existing_record: bank_data['bank_state'] = 'not_reconciled' self.env['property.bank.statements'].create(bank_data) else: # 已存在记录,不修改 bank_state existing_record.write(bank_data) def property_bank_statements_gc_data(self, row): """工商银行(兴源)解析:数据从第2行开始,列结构(共34列): 0:交易时间, 1:对方单位, 2:摘要, 4:转入金额(贷时取), 7:借贷标志, 8:发生额(借时取), 9:对方账号, 16:本方账号, 22:对方行名, 31:顺序号 金额取数规则:H列=贷 取E列(索引4转入金额),H列=借 取I列(索引8发生额) """ serial_number = str(row[31].value).strip() if row[31].value else '' # 交易时间 time_val = str(row[0].value).strip() if row[0].value else '' if time_val and time_val != 'None': try: start_time = datetime.strptime(time_val, '%Y-%m-%d %H:%M:%S') except Exception: start_time = None else: start_time = None # 借贷方向,并按规则取对应金额列 direction_val = str(row[7].value).strip() if row[7].value else '' if direction_val == '贷': lending_direction = 'loan' amount_str = str(row[4].value).replace(',', '').strip() if row[4].value else '' # E列:转入金额 elif direction_val == '借': lending_direction = 'borrow' amount_str = str(row[8].value).replace(',', '').strip() if row[8].value else '' # I列:发生额 else: return try: original_amount = float(amount_str) except Exception: return if not original_amount: return name = str(row[9].value).strip() if row[9].value else '' # 对方账号 name_account = str(row[1].value).strip() if row[1].value else '' # 对方单位 remake = str(row[2].value).strip() if row[2].value else '' # 摘要 bank_branch = str(row[22].value).strip() if row[22].value else '' # 对方行名 your_own_account = str(row[16].value).strip() if row[16].value else '' your_own_unit = self.classify_by_last_four(your_own_account) company_id = self._get_company_by_unit(your_own_unit) if not company_id: return # 用顺序号去重,没有顺序号则用时间+对方账号 dedup_key = serial_number if serial_number and serial_number != '0' else (time_val + name) existing_record = self.env['property.bank.statements'].search( [('serial_number', '=', dedup_key)], limit=1 ) bank_data = { 'name': name, 'name_account': name_account, 'serial_number': dedup_key, 'your_own_account': your_own_account, 'your_own_unit': your_own_unit, 'bank_branch': bank_branch, 'original_amount': original_amount, 'lending_direction': lending_direction, 'start_time': start_time - timedelta(hours=8) if start_time else False, 'remake': remake, 'company_id': company_id.id, 'bank_state': 'not_reconciled', } if not existing_record: self.env['property.bank.statements'].create(bank_data) else: existing_record.write(bank_data) def property_bank_statements_xy_data_openpyxl(self, row): """工商银行(兴源)openpyxl版本解析,row为元组,列结构: 0:交易时间, 1:对方单位, 2:摘要, 4:转入金额(贷时取), 7:借贷标志, 8:发生额(借时取), 9:对方账号, 16:本方账号, 22:对方行名, 31:顺序号 """ def get_val(idx): v = row[idx] if len(row) > idx and row[idx] is not None else '' return str(v).strip() time_val = get_val(0) if time_val and time_val != 'None': try: start_time = datetime.strptime(time_val, '%Y-%m-%d %H:%M:%S') except Exception: start_time = None else: start_time = None direction_val = get_val(7) if direction_val == '贷': lending_direction = 'loan' amount_str = get_val(4).replace(',', '') # E列:转入金额 elif direction_val == '借': lending_direction = 'borrow' amount_str = get_val(8).replace(',', '') # I列:发生额 else: return try: original_amount = float(amount_str) except Exception: return if not original_amount: return name = get_val(9) # 对方账号 name_account = get_val(1) # 对方单位 remake = get_val(2) # 摘要 bank_branch = get_val(22) # 对方行名 your_own_account = get_val(16) serial_number = get_val(31) # 顺序号 your_own_unit = self.classify_by_last_four(your_own_account) company_id = self._get_company_by_unit(your_own_unit) if not company_id: return dedup_key = serial_number if serial_number and serial_number != '0' else (time_val + name) existing_record = self.env['property.bank.statements'].search( [('serial_number', '=', dedup_key)], limit=1 ) bank_data = { 'name': name, 'name_account': name_account, 'serial_number': dedup_key, 'your_own_account': your_own_account, 'your_own_unit': your_own_unit, 'bank_branch': bank_branch, 'original_amount': original_amount, 'lending_direction': lending_direction, 'start_time': start_time - timedelta(hours=8) if start_time else False, 'remake': remake, 'company_id': company_id.id, } if not existing_record: bank_data['bank_state'] = 'not_reconciled' self.env['property.bank.statements'].create(bank_data) else: # 已存在记录,不修改 bank_state existing_record.write(bank_data) def _get_company_by_unit(self, unit_name): if unit_name == '天河物管': return self.env['res.company'].sudo().browse(3) elif unit_name == '天河粮储': return self.env['res.company'].sudo().search([('name', '=', '天河粮储')], limit=1) else: return self.env['res.company'].sudo().search([('name', '=', '集团本部')], limit=1) def classify_by_last_four(self, input_str): if not isinstance(input_str, str) or not input_str.strip(): return '' account = input_str.strip() # 从物业公司配置表中查找匹配的收租公司账号 company_config = self.env['yuthon.property.company'].sudo().search( [('rent_account', '=', account)], limit=1 ) if company_config: return company_config # 兼容旧逻辑:账号后四位模糊匹配 last_four = account[-4:] if len(account) >= 4 else '' company_config = self.env['yuthon.property.company'].sudo().search( [('rent_account', 'like', last_four)], limit=1 ) if company_config: return company_config return ''