diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..1d2f67b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(ls:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..73ed9f2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Flask web application that demonstrates OAuth 2.0 authentication with the Xero API. It serves as a companion app showing how to connect to Xero organizations and make real API calls to various Xero endpoints. + +## Development Environment Setup + +### Prerequisites +- Python 3.5+ +- Git +- SSH keys configured for GitHub access (required for Flask-Session dependency) + +### Local Development Commands +```bash +# Create and activate virtual environment +python3 -m venv venv +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt + +# Run the main application +python3 app.py + +# Application will be available at http://localhost:5000/login +``` + +## Configuration + +The application requires a `config.py` file in the root directory with Xero API credentials: +```python +CLIENT_ID = "...client id string..." +CLIENT_SECRET = "...client secret string..." +STATE = "...my super secure state..." +``` + +Use `example_config.py` as a template. The STATE field cannot be empty. + +## Architecture + +### Core Files +- `app.py`: Main Flask application with OAuth2 flow and API demonstrations +- `app2.py`: Additional utility functions for bank transaction matching +- `utils.py`: JSON serialization utilities and custom encoders for Xero Python SDK models +- `default_settings.py`: Flask configuration for development environment +- `logging_settings.py`: Logging configuration for debugging API calls + +### Key Components +- **OAuth2 Flow**: Implemented using flask-oauthlib with token persistence via flask-session +- **Token Management**: Automatic refresh of expired access tokens +- **API Client**: Xero Python SDK v6.1.0 with comprehensive API coverage +- **Session Storage**: File-based session storage in `cache/` directory + +### Xero API Integration +The app demonstrates integration with multiple Xero APIs: +- Accounting API (invoices, contacts, organizations) +- Assets API +- Project API +- Payroll APIs (AU, UK, NZ) +- Files API +- Finance API + +Token exchange between flask-oauthlib and xero-python SDK is handled by decorator functions that persist tokens in Flask sessions. + +## Testing with Demo Company +Always use Xero's Demo Company for testing to avoid affecting real data. The application includes functionality to connect to organizations and perform various API operations safely in the demo environment. \ No newline at end of file diff --git a/app.py b/app.py index a0c9fb7..25d6e7d 100644 --- a/app.py +++ b/app.py @@ -63,6 +63,7 @@ access_token_url="https://identity.xero.com/connect/token", refresh_token_url="https://identity.xero.com/connect/token", scope="files.read profile files accounting.contacts.read payroll.settings accounting.attachments accounting.journals.read accounting.attachments.read projects.read accounting.transactions.read accounting.settings.read payroll.payslip payroll.payruns payroll.employees accounting.transactions assets accounting.contacts accounting.budgets.read offline_access assets.read payroll.timesheets projects openid email accounting.reports.read accounting.settings", + #accounting.reports.bankstatement.read # "paymentservices " # "finance.bankstatementsplus.read finance.cashvalidation.read finance.statements.read finance.accountingactivity.read", ) # type: OAuth2Application @@ -95,9 +96,31 @@ def xero_token_required(function): @wraps(function) def decorator(*args, **kwargs): xero_token = obtain_xero_oauth2_token() + print('xero_token', xero_token) if not xero_token: return redirect(url_for("login", _external=True)) + from app2 import find_or_create_bank_transaction + from datetime import date + target_date = date(2025, 5, 30) + target_amount = '2000.00' + xero_tenant_id = get_xero_tenant_id() + # matches, error = find_or_create_bank_transaction( + # xero_tenant_id=xero_tenant_id, + # api_client=api_client, + # target_date=target_date, + # target_amount=target_amount, + # ) + # print('matches', matches) + # print('error', error) + bank, inv = find_or_create_bank_transaction( + xero_tenant_id=xero_tenant_id, + api_client=api_client, + target_date=date(2025, 6, 3), + target_amount='1000.00', + ) + # if either set, its reconcilated. + import ipdb; ipdb.set_trace() return function(*args, **kwargs) return decorator @@ -170,7 +193,7 @@ def accounting_account_read_all(): code = get_code_snippet("ACCOUNTS","READ_ALL") #[ACCOUNTS:READ_ALL] - xero_tenant_id = get_xero_tenant_id() + xero_tenant_id = get_xero_tenant_id(kwargs=True) accounting_api = AccountingApi(api_client) order = 'Name ASC' @@ -1275,7 +1298,7 @@ def accounting_bank_transaction_attachment_create_by_file_name(): getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") ) json = serialize_model(created_bank_transaction_attachments_by_file_name) - + #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] return render_template( @@ -1970,7 +1993,7 @@ def accounting_bank_transfer_attachment_create_by_file_name(): getvalue(created_bank_transfer_attachments_by_file_name, "attachments.0.url", "") ) json = serialize_model(created_bank_transfer_attachments_by_file_name) - + #[/BANKTRANSFERATTACHMENTS:CREATEBYFILENAME] return render_template( @@ -2513,7 +2536,7 @@ def accounting_branding_theme_payment_service_read_all(): code = get_code_snippet("BRANDINGTHEMEPAYMENTSERVICES","READ_ALL") xero_tenant_id = get_xero_tenant_id() accounting_api = AccountingApi(api_client) - + try: read_branding_themes = accounting_api.get_branding_themes( xero_tenant_id @@ -3123,7 +3146,7 @@ def accounting_contact_read_one_by_contact_number(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -3832,13 +3855,13 @@ def accounting_credit_note_allocation_create(): invoice = Invoice( invoice_id = invoice_id) - + allocation = Allocation( amount = 1.0, date = curr_date, invoice = invoice) - - allocations = Allocations( + + allocations = Allocations( allocations = [allocation]) try: @@ -4079,7 +4102,7 @@ def accounting_credit_note_allocation_create(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -4514,7 +4537,7 @@ def accounting_expense_claim_create(): except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + try: read_receipts = accounting_api.get_receipts( xero_tenant_id @@ -4531,20 +4554,20 @@ def accounting_expense_claim_create(): user = User( user_id = user_id) - + receipt = Receipt( receipt_id = receipt_id, date = curr_date) - + receipts = [] receipts.append(receipt) - + expense_claim = ExpenseClaim( status = "SUBMITTED", user = user, receipts = receipts) - - expense_claims = ExpenseClaims( + + expense_claims = ExpenseClaims( expense_claims = [expense_claim]) try: @@ -4580,7 +4603,7 @@ def accounting_expense_claim_update(): except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + try: read_receipts = accounting_api.get_receipts( xero_tenant_id @@ -4589,7 +4612,7 @@ def accounting_expense_claim_update(): except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + try: read_expense_claims = accounting_api.get_expense_claims( xero_tenant_id @@ -4606,20 +4629,20 @@ def accounting_expense_claim_update(): user = User( user_id = user_id) - + receipt = Receipt( receipt_id = receipt_id, date = curr_date) - + receipts = [] receipts.append(receipt) - + expense_claim = ExpenseClaim( status = "PAID", user = user, receipts = receipts) - - expense_claims = ExpenseClaims( + + expense_claims = ExpenseClaims( expense_claims = [expense_claim]) print(xero_tenant_id, expense_claim_id, expense_claims) @@ -5092,7 +5115,7 @@ def accounting_invoice_attachment_create_by_file_name(): getvalue(created_invoice_attachments_by_file_name, "attachments.0.url", "") ) json = serialize_model(created_invoice_attachments_by_file_name) - + #[/INVOICEATTACHMENTS:CREATEBYFILENAME] return render_template( @@ -5960,7 +5983,7 @@ def accounting_manual_journals_read_one(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -7153,7 +7176,7 @@ def accounting_quotes_create(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -7317,30 +7340,30 @@ def accounting_receipts_create(): xero_tenant_id = get_xero_tenant_id() accounting_api = AccountingApi(api_client) unitdp = 4 - + contact = Contact( contact_id = getvalue(read_contacts, "contacts.0.contact_id", "")) - + user = User( user_id = getvalue(read_users, "users.0.user_id", "")) - + line_item = LineItem( description = "Foobar", quantity = 1.0, unit_amount = 20.0, account_code = "300") - + line_items = [] line_items.append(line_item) - + receipt = Receipt( contact = contact, user = user, line_items = line_items, line_amount_types = LineAmountTypes.INCLUSIVE, status = "DRAFT") - - receipts = Receipts( + + receipts = Receipts( receipts = [receipt]) try: @@ -7579,7 +7602,7 @@ def accounting_receipts_create(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -7938,7 +7961,7 @@ def accounting_repeating_invoices_create_history(): history_record = HistoryRecord( details = "Hello World") - history_records = HistoryRecords( + history_records = HistoryRecords( history_records = [history_record]) try: @@ -8177,7 +8200,7 @@ def accounting_repeating_invoices_create_history(): # getvalue(created_bank_transaction_attachments_by_file_name, "attachments.0.url", "") # ) # json = serialize_model(created_bank_transaction_attachments_by_file_name) - + # #[/BANKTRANSACTIONATTACHMENTS:CREATEBYFILENAME] # return render_template( @@ -8328,7 +8351,7 @@ def accounting_repeating_invoices_create_history(): # getReportBalanceSheet x # getReportBankSummary x # getReportBASorGSTList x -# getReportBASorGST x +# getReportBASorGST x # getReportBudgetSummary x # getReportExecutiveSummary x # getReportProfitAndLoss x @@ -8702,13 +8725,13 @@ def accounting_tax_rate_create(): rate = 20.00) #report_tax_type is invalid for US orgs. - + tax_rate = TaxRate( - name = "Example Tax Rate", - report_tax_type = "INPUT", + name = "Example Tax Rate", + report_tax_type = "INPUT", tax_components = [tax_component]) - - tax_rates = TaxRates( + + tax_rates = TaxRates( tax_rates = [tax_rate]) try: @@ -8748,13 +8771,13 @@ def accounting_tax_rate_update(): rate = 20.00) #report_tax_type is invalid for US orgs. - + tax_rate = TaxRate( - name = getvalue(read_tax_rates, "tax_rates.0.name", ""), + name = getvalue(read_tax_rates, "tax_rates.0.name", ""), tax_components = [updated_tax_component]) - - tax_rates = TaxRates( + + tax_rates = TaxRates( tax_rates = [tax_rate]) try: @@ -9693,7 +9716,7 @@ def projects_task_update(): except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + try: read_tasks = project_api.get_tasks( xero_tenant_id, project_id=project_id @@ -9764,7 +9787,7 @@ def projects_task_delete(): except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + try: read_tasks = project_api.get_tasks( xero_tenant_id, project_id=project_id @@ -11955,7 +11978,7 @@ def files_file_read_all(): #[FILE:READ_ALL] try: read_files = files_api.get_files( - xero_tenant_id, + xero_tenant_id, ) except AccountingBadRequestException as exception: output = "Error: " + exception.reason @@ -11977,16 +12000,16 @@ def files_file_read_one(): xero_tenant_id = get_xero_tenant_id() files_api = FilesApi(api_client) accounting_api = AccountingApi(api_client) - + try: read_files = files_api.get_files( - xero_tenant_id, + xero_tenant_id, ) file_id = getvalue(read_files, "items.0.id", "") except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + #[FILE:READ_ONE] try: read_file = files_api.get_file( @@ -12022,9 +12045,9 @@ def files_file_upload(): try: file_object = files_api.upload_file( - xero_tenant_id, - name = name, - filename= filename, + xero_tenant_id, + name = name, + filename= filename, mime_type = mime_type, body=body ) @@ -12052,7 +12075,7 @@ def files_folder_read_all(): #[FOLDER:READ_ALL] try: folders = files_api.get_folders( - xero_tenant_id, + xero_tenant_id, ) except AccountingBadRequestException as exception: output = "Error: " + exception.reason @@ -12077,13 +12100,13 @@ def files_folder_read_one(): try: read_folders = files_api.get_folders( - xero_tenant_id, + xero_tenant_id, ) folder_id = getvalue(read_folders, "1.id", "") except AccountingBadRequestException as exception: output = "Error: " + exception.reason json = jsonify(exception.error_data) - + #[FOLDER:READ_ONE] try: read_folder = files_api.get_folder( diff --git a/app2.py b/app2.py new file mode 100644 index 0000000..f405b18 --- /dev/null +++ b/app2.py @@ -0,0 +1,240 @@ +from datetime import timedelta +from xero_python.accounting import AccountingApi + + +from xero_python.accounting import AccountingApi, BankTransaction, Contact, LineItem, Account + + +from datetime import date +from xero_python.accounting import BankTransactions +""" + +Sort of auto-reconcile a bank transaction: + +bank_transaction = BankTransaction( + type="SPEND", + contact=Contact( + name="SMART Agency" + ), + line_items=[ + LineItem( + description="Payment to SMART Agency 2", + quantity=1, + unit_amount=4500.00, + account_code="400" + ) + ], + bank_account=Account( + code="090" + ), + date=date(2025, 5, 30), + reference="0195 0210" +) + +response = accounting_api.create_bank_transactions( + xero_tenant_id=xero_tenant_id, + bank_transactions=BankTransactions(bank_transactions=[bank_transaction]) +) + +accounting_api.get_bank_transactions(xero_tenant_id, where='Total=4500 && status!="DELETED"').bank_transactions[1] + +""" + + +def search_for_reconciliation(xero_tenant_id, api_client, target_date, target_amount): + """ + Search for bank transactions matching specific criteria using where filter and pagination. + If no matching transaction is found, create a new one. + Based on Sally's suggestion from Xero support. + Returns a tuple of (result, error_message) + """ + accounting_api = AccountingApi(api_client) + # Search for existing bank transactions using where filter as Sally suggested + where_filter = f'Total=={target_amount} && Status!="DELETED"' + print(f"Searching for bank transactions with filter: {where_filter}") + + # Since it's an Invoice OR Receipt we are searching for, + # and since Invoice documents do not always have paid status. + # ``target_date`` could just be the date from which the + # invoice was created. + where_clause = ( + f'Total=={target_amount} && ' + f'Date>=DateTime({target_date.year},{target_date.month:02d},{target_date.day:02d})' + ) + + transactions = accounting_api.get_bank_transactions( + xero_tenant_id, + where=where_clause, + order="Date DESC", + if_modified_since=target_date + ) + invoices = accounting_api.get_invoices( + xero_tenant_id, + where=where_clause, + order="Date DESC", + ) + not_found = len(transactions.bank_transactions) == 0 and len(invoices.invoices) == 0 + not_reconciled = not_found + if not_reconciled: + print('Not reconciled') + else: + print('Reconciled') + return { + 'bank_transactions': transactions.bank_transactions, + 'invoices': invoices.invoices, + 'is_reconciled': not not_reconciled, + } + + +def find_or_create_bank_transaction(xero_tenant_id, api_client, target_date, target_amount): + """ + Search for bank transactions matching specific criteria using where filter and pagination. + If no matching transaction is found, create a new one. + Based on Sally's suggestion from Xero support. + Returns a tuple of (result, error_message) + """ + accounting_api = AccountingApi(api_client) + # Search for existing bank transactions using where filter as Sally suggested + where_filter = f'Total=={target_amount} && Status!="DELETED"' + print(f"Searching for bank transactions with filter: {where_filter}") + + # Since it's an Invoice OR Receipt we are searching for, + # and since Invoice documents do not always have paid status. + # ``target_date`` could just be the date from which the + # invoice was created. + where_clause = ( + f'Total=={target_amount} && ' + f'Date>=DateTime({target_date.year},{target_date.month:02d},{target_date.day:02d})' + ) + + transactions = accounting_api.get_bank_transactions( + xero_tenant_id, + where=where_clause, + order="Date DESC", + if_modified_since=target_date + ) + invoices = accounting_api.get_invoices( + xero_tenant_id, + where=where_clause, + order="Date DESC", + ) + not_found = len(transactions.bank_transactions) == 0 and len(invoices.invoices) == 0 + not_reconciled = not_found + if not_reconciled: + print('Not reconciled') + else: + print('Reconciled') + return { + 'bank_transactions': transactions.bank_transactions, + 'invoices': invoices.invoices, + 'is_reconciled': not not_reconciled, + } + + try: + # Search for existing bank transactions using where filter as Sally suggested + where_filter = f'Total=={target_amount} && Status!="DELETED"' + print(f"Searching for bank transactions with filter: {where_filter}") + + # Since it's an Invoice OR Receipt we are searching for, + # and since Invoice documents do not always have paid status. + # ``target_date`` could just be the date from which the + # invoice was created. + where_clause = ( + f'Total=={target_amount} && ' + f'Date>=DateTime({target_date.year},{target_date.month:02d},{target_date.day:02d})' + ) + + transactions = accounting_api.get_bank_transactions( + xero_tenant_id, + where=where_clause, + order="Date DESC", + if_modified_since=target_date + ) + invoices = accounting_api.get_invoices( + xero_tenant_id, + where=where_clause, + order="Date DESC", + ) + not_found = len(transactions.bank_transactions) == 0 and len(invoices.invoices) == 0 + not_reconciled = not_found + if not_reconciled: + print('Not reconciled') + else: + print('Reconciled') + return { + 'bank_transactions': transactions.bank_transactions, + 'invoices': invoices.invoices, + } + + print(f"Found {len(transactions.bank_transactions)} transactions matching amount criteria") + + # Look for matches with date (reference is editable so don't require exact match) + date_matches = [] + for tx in transactions.bank_transactions: + # Check if date matches - this is the primary criteria since reference is editable + if tx.date == target_date: + date_matches.append({ + 'bank_transaction_id': tx.bank_transaction_id, + 'total': tx.total, + 'reference': tx.reference, + 'contact_name': tx.contact.name if tx.contact else None, + 'date': tx.date.isoformat() if tx.date else None, + 'status': tx.status, + 'type': tx.type, + 'is_reconciled': tx.is_reconciled, + }) + + if date_matches: + print(f"Found {len(date_matches)} transactions matching date and amount") + return {'action': 'found', 'transactions': date_matches}, None + + # No matching transaction found, create a new one as Sally suggested + print("No matching transaction found. Creating new bank transaction...") + + bank_transaction = BankTransaction( + type="SPEND", + contact=Contact( + name="SMART Agency" + ), + line_items=[ + LineItem( + description="Payment to SMART Agency", + quantity=1, + unit_amount=target_amount, + account_code="400" # Default expense account + ) + ], + bank_account=Account( + code="090" # Default bank account + ), + date=target_date, + reference="SMART Agency" + ) + + response = accounting_api.create_bank_transactions( + xero_tenant_id=xero_tenant_id, + bank_transactions=BankTransactions(bank_transactions=[bank_transaction]) + ) + + if response.bank_transactions: + created_tx = response.bank_transactions[0] + result = { + 'action': 'created', + 'transaction': { + 'bank_transaction_id': created_tx.bank_transaction_id, + 'total': created_tx.total, + 'reference': created_tx.reference, + 'contact_name': created_tx.contact.name if created_tx.contact else None, + 'date': created_tx.date.isoformat() if created_tx.date else None, + 'status': created_tx.status, + 'type': created_tx.type, + } + } + print(f"Successfully created bank transaction: {created_tx.bank_transaction_id}") + return result, None + else: + return None, "Failed to create bank transaction" + + except Exception as e: + print(f"Error details: {str(e)}") + return None, f"Unexpected error: {str(e)}" diff --git a/email-sally.md b/email-sally.md new file mode 100644 index 0000000..b007ecc --- /dev/null +++ b/email-sally.md @@ -0,0 +1,55 @@ + + +What you suggested doesn't seem to work: + +1) +I'm using the Demo Company, it has some unreconciled transactions. + +e.g. +Jakaranda Maple Systems, 2000GBP, 30 May 2025, + + + +2) +I query for this one using the Python library: + +``` +transactions = accounting_api.get_bank_transactions( + xero_tenant_id, + where=f'Total==2000.00 && Status!="DELETED"', + order="Date DESC" +) +invoices = accounting_api.get_invoices( + xero_tenant_id, + where=f'Total==2000.00 && Status!="DELETED"', + order="Date DESC" +) +``` + +3) +The result is + +No transactions: + +``` +{'bank_transactions': [], 'pagination': None, 'warnings': None} +``` + +1 invoice returned but it's NOT the correct unreconciled payment: + +ipdb> invoices.invoices[0].to_dict()['contact']['name'] +'SMART Agency' +ipdb> invoices.invoices[0].to_dict()['total'] +Decimal('2000.00') +``` + +Despite them showing on the UI. + + +---- + +So searching for that unreconciled payment via `get_bank_transactions` or `get_invoices` does not return what we're looking for. + + + + diff --git a/emails.md b/emails.md new file mode 100644 index 0000000..836bbae --- /dev/null +++ b/emails.md @@ -0,0 +1,197 @@ +Thanks for the help Sally. I’ll give it a go. + +-------------------------------- + + +Hi Richard + +In that case you can search invoices and bank transactions using the where filter and pagination and then if there is no matching invoice or transaction, then you can create the transaction. + +Xero Developer Centre: BankTransactions + + +Kind regards + +Sally + + + +----- + +The issue is that the vast majority of invoices/receipts are likely not to be in Xero (I’m referring to expenses/purchases). +So at that moment in time, in Xero, that transaction would be under “Bank Statements” section only, I believe, since it’s not yet reconciled/linked to a Payment record. + +So the latter part of your suggestion makes sense, but not the first part. + +Thanks for the help, +Richard + + +-------------------------------- + + + +Hi Richard + +The best option would be to search for the invoice in Xero from the invoices endpoint and as long as you use pagination and/or include the status filter, you would retrieve details of any payments applied to the invoice. + +Customers can send you a copy of their bank statements, we are contractually not allowed to. + +Please note, for invoice payments you would use the payments endpoint not the bank transactions endpoint. + +Xero Developer Centre: +Invoices +Payments + + +Kind regards + +Sally + + +-------------------------------- + + + +Hi Richard + +Dext were using the bank statements report for many years before it was deprecated and so were allowed to keep their access. + +Due to contractual agreements with the banks who own the data, we can't offer the report to other apps. +Kind regards + +Sally + + + +-------------------------------- + + +Hello, + +I can see dext.com is using the bank statements report for their software (see attached OAuth screenshot). And I can see from their functionality that they are accessing the bank statement reports. + +Given that fact, can you enable it for my application? + +Thankyou + + +-------------------------------- + + + +Hi Richard + +The bank statements report endpoint is deprecated and currently bank statements can only be retrieved through the bank feeds API. The bank feeds API is restricted to banks and similar regulated financial bodies that have signed a partner agreement with us. + +Kind regards + + + +----- + + +Hi Xero Support, + + +I’m using your official Python SDK to access the Bank Statement Report, but I’m running into some issues. + + +I’m currently using the following scopes: + +``` + +[ + +"offline_access", + +"accounting.contacts", + +"accounting.transactions", + +"accounting.attachments", + +"accounting.settings", + +"accounting.reports.read" + +] + +``` + +When I try to call (via your Python lib): + +``` + +accounting_api.get_report_from_id( + +xero_tenant_id=tenant_id, + +report_id='BankStatement', + +) + +``` + +I get the following error: + +``` + +*** xero_python.exceptions.http_status_exceptions.HTTPStatusException: (401) + +Reason: Unauthorized + +HTTP response headers: + +Content-Type: application/json + +Content-Length: 152 + +Server: nginx + +WWW-Authenticate: insufficient_scope + +Xero-Correlation-Id: 6a5d6db8-9495-4e0b-98dd-8b97d2c0b1e0 + +X-AppMinLimit-Remaining: 9997 + +Expires: Sun, 08 Jun 2025 14:37:32 GMT + +Cache-Control: max-age=0, no-cache, no-store + +Pragma: no-cache + +Date: Sun, 08 Jun 2025 14:37:32 GMT + +Connection: keep-alive + +X-Client-TLS-ver: tls1.3 + +HTTP response body: + +{"Type":null,"Title":"Unauthorized","Status":401,"Detail":"AuthorizationUnsuccessful","Instance":"6a5d6db8-9495-4e0b-98dd-8b97d2c0b1e0","Extensions":{} } + +``` + + +To fix this, I tried adding the `accounting.reports.bankstatement.read` scope. However, when I add that scope and re-authorize, the OAuth process fails and redirects me to the following error URL: + + +``` + +https://login.xero.com/identity/error?errorId=CfDJ8FAUJPAPyNBGj391Oz_Gl3abzhs0D5m0BeXmTYpoOAW98wOSkj7LosNbZhb2Wg2bO0bDLK6PzPfZR7IEK2E5FhSKns96Q4tiAgJ6liWIUTaYYxsb35DZ4uN-iU8ZkyEVJ2gQWn43mcm6DxrKKnAxPJa73jnk_fF3PQNQXPsV74y3NbIj2QwX5Qxck5eHQMEFvgDA2Ip7GjImPiU5-4gTreub_wIP70QgIcZw64gTpT1xXq1nWYPKCELaCOxi86OlFiLr8hE2WiXLWhO3iLAjHb5OcNP5G7xoo-4t8tg71d0HG4azMALrVqSPR0JpT-ge7aOOCoQuhqE-OJqJOKeHYfybsuddEhqg6FchNzJabOMX_1K1lR0ztob-G-Bdgp_4TbKkOr2j8YvH11R4mAquxIWal10pNuzM0ovY8zw-jXk8yt9N2mp1umLzsEOkHTquIrkwvNYlnlrA6VsRyBBWWX4LYi5JBw2DE0CaX544qAmB9WeuSwdDlll8K5z7zDOlrDwIE7juv2SDhKc0Z-GDVhTABEuxfKWfkAUWPCa75pyBXh24lFt_HcFCCkZIC0etG4uxa42ROJC5IgItJCz0kBwLnHvHkFGGCWXibYHVSKYzAv9DBAoO70CL0UA8mhtvvw + +``` + + +Could you please advise on how to proceed? Should I be using a different scope, or is there an issue with the setup on my account? + + +Thanks for your help! + + +Best regards, + +Richard O'Dwyer + + diff --git a/requirements.txt b/requirements.txt index 427189d..5c3cad1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -flask -flask-session==0.3.2 +werkzeug==2.0.3 +flask==2.0.3 +flask-session==0.4.0 flask-oauthlib==0.9.6 -xero-python==6.1.0 \ No newline at end of file +xero-python==6.1.0