# Part of Odoo. See LICENSE file for full copyright and licensing details. import contextlib import re from datetime import datetime from odoo import _, fields, models from odoo.tools import plaintext2html from odoo.exceptions import UserError class AccountJournal(models.Model): _inherit = "account.journal" l10n_jp_zengin_merge_transactions = fields.Boolean( string="Merge Transactions", help="Merge collective payments for Zengin files", ) def _default_outbound_payment_methods(self): res = super()._default_outbound_payment_methods() if self._is_payment_method_available("zengin"): res |= self.env.ref('l10n_jp_zengin.account_payment_method_zengin_outbound') return res def _get_bank_statements_available_import_formats(self): rslt = super()._get_bank_statements_available_import_formats() rslt.append('ZENGIN') return rslt def _check_zengin(self, zengin_string): # Match the first 59 characters of the Zengin file, as defined by the zengin specifications return re.match(r'10[13]0\d{6}\d{6}\d{6}\d{4}[ \uFF5F-\uFF9F]{15}\d{3}[ \uFF5F-\uFF9F]{15}', zengin_string) is not None def _parse_bank_statement_file(self, attachment): record_data = False with contextlib.suppress(UnicodeDecodeError): record_data = attachment.raw.decode('SHIFT_JIS') # Zengin files are encoded in SHIFT_JIS if not record_data or not self._check_zengin(record_data): return super()._parse_bank_statement_file(attachment) return self._parse_bank_statement_file_zengin(record_data) def _parse_bank_statement_file_zengin(self, record_data): def rmspaces(s): return s.strip() def parsedate(s): # Zengin has only 10 years of data, so any year before 26 is in the Reiwa era. # The Reiwa era starts from 1st May 2019. # The Heisei era starts from 8th January 1989 and ends on 30th April 2019. # Japanese years are counted from 1. # Examples: # - 260101 -> 2014-01-01 (Heisei 26) # - 010501 -> 2019-05-01 (Reiwa 1) def parse_japanese_year(dy): return dy + 2019 - 1 if dy < 26 else dy + 1989 - 1 dt = str(parse_japanese_year(int(s[0:2].lstrip('0')))) + s[2:6] return datetime.strptime(dt, '%Y%m%d').strftime('%Y-%m-%d') def parse_header(line): result = { 'date': parsedate(line[10:16]), } if line[1:3] == '01': if len(line) != 199: raise UserError(_('Incorrect header length: %(length)s', length=len(line))) statement_type = 'transfer' acc_number = line[60:67] result['name'] = _("Zengin Transfer - %(date)s", date=parsedate(line[10:16])) else: if len(line) != 200: raise UserError(_('Incorrect header length: %(length)s', length=len(line))) statement_type = 'deposit_withdrawal' acc_number = line[66:73] result['name'] = _("Zengin Deposit/Withdrawal - %(date)s", date=parsedate(line[10:16])) if balance_start := rmspaces(line[115:129]): result['balance_start'] = float(balance_start) return statement_type, acc_number, result def get_transfer_note(line, exceed_amount: bool): note = [ _('Bank Name: %(bank)s', bank=rmspaces(line[97:112])), _('Branch Name: %(branch)s', branch=rmspaces(line[112:127])), ] if edi_info := rmspaces(line[128:148]) if exceed_amount else rmspaces(line[152:172]): note.append(_('EDI Information: %(info)s', info=edi_info)) return note def get_deposit_withdrawal_note(line, deposit_type): transaction_category_map = { '10': _('Cash'), '11': _('Transfer'), '12': _('Deposit'), '13': _('Exchange'), '14': _('Transfer'), '18': _('Other'), } bill_type_map = { '1': _('Check'), '2': _('Promissory Note'), '3': _('Bill of Exchange'), } note = [] note.append(_('Transaction Category: %(category)s', category=transaction_category_map.get(line[22:24], _('Unknown')))) if bill_type := rmspaces(line[60:61]): note.extend([ _('Bill Type: %(type)s', type=bill_type_map.get(bill_type, _('Unknown'))), _('Bill Number: %(id)s', id=rmspaces(line[61:68])), ]) if deposit_type in ['regular', 'current', 'savings']: if sending_bank := rmspaces(line[129:144]): note.append(_('Sending Bank: %(bank)s', bank=sending_bank)) if sending_bank_branch := rmspaces(line[144:159]): note.append(_('Sending Bank Branch: %(branch)s', branch=sending_bank_branch)) if summary := rmspaces(line[159:179]): note.append(_('Note: %(note)s', note=summary)) if edi_info := rmspaces(line[179:199]): note.append(_('EDI Information: %(info)s', info=edi_info)) else: # notice, term, fixed_deposit if deposit_date_str := rmspaces(line[71:77]): note.append(_('Initial Deposit Date: %(date)s', date=parsedate(deposit_date_str))) note.append(_('Interest Rate: %(integer)s.%(decimal)s%', line[77:79], line[79:83])) if maturity_date_str := rmspaces(line[83:89]): note.append(_('Maturity Date: %(date)s', date=parsedate(maturity_date_str))) if period_str := rmspaces(line[89:95]): note.append(_('Period: %(date)s', date=parsedate(period_str))) if periodic_interest_str := rmspaces(line[95:102]): note.append(_('Periodic Interest: %(amount)s', amount=periodic_interest_str)) if summary := rmspaces(line[171:191]): note.append(_('Note: %(note)s', note=summary)) return note def parse_transaction_line(statement_type, line, deposit_type=None): transaction = {} if statement_type == 'transfer': if len(line) != 199: raise UserError(_('Incorrect transaction length: %(length)s', length=len(line))) exceed_amount = line[19: 29] == '0000000000' transaction = { 'date': parsedate(line[7:13]), 'amount': float(line[128:140]) if exceed_amount else float(line[19:29]), 'unique_import_id': line[1:7] + '/' + parsedate(line[7:13]), 'partner_name': rmspaces(line[49:97]), 'payment_ref': 'Transfer - ' + line[1:7], 'narration': plaintext2html('\n'.join(get_transfer_note(line, exceed_amount))), } else: if len(line) != 200: raise UserError(_('Incorrect transaction length: %(length)s', length=len(line))) payment_type = 'deposit' if line[21:22] == '1' else 'withdrawal' sign = -1 if payment_type == 'withdrawal' else 1 transaction = { 'date': parsedate(line[9:15]), 'amount': sign * float(line[24: 36]), 'unique_import_id': line[1:9] + '/' + parsedate(line[9:15]), 'payment_ref': payment_type.capitalize() + ' - ' + line[1:9], 'narration': plaintext2html('\n'.join(get_deposit_withdrawal_note(line, deposit_type))), } if sender_name := rmspaces(line[81:129]): transaction['partner_name'] = sender_name return transaction recordlist = record_data.split('\r\n') statement = {"transactions": []} statement_type = False acc_number = False deposit_type = False deposit_type_map = { '1': 'regular', '2': 'current', '4': 'savings', '5': 'notice', '6': 'term', '7': 'fixed_deposit', } for line in recordlist: if not line: pass elif line[0] == '1': # Header statement_type, acc_number, result = parse_header(line) statement.update(result) if statement_type == 'deposit_withdrawal': deposit_type = deposit_type_map.get(line[62:63]) if not deposit_type: raise UserError(_('Unknown deposit type: %(type)s', type=line[62:63])) elif line[0] == '2': # Detail if line[22:24] == '19': continue # Skip correction transactions transaction = parse_transaction_line(statement_type, line, deposit_type) statement['transactions'].append(transaction) elif line[0] == '8': # Footer if statement_type == 'transfer': continue if balance_end := rmspaces(line[115:129]): statement['balance_end_real'] = float(balance_end) return 'JPY', acc_number, [statement]