[IMP] account_bank_statement_import_online_paypal: black, isort, prettier

This commit is contained in:
Alexey Pelykh
2021-09-23 09:10:02 +02:00
committed by Omar (Comunitea)
parent 1078564f35
commit 15c36bd914
4 changed files with 646 additions and 639 deletions

View File

@@ -3,21 +3,15 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{ {
'name': 'Online Bank Statements: PayPal.com', "name": "Online Bank Statements: PayPal.com",
'version': '12.0.1.1.0', "version": "12.0.1.1.0",
'author': "author": "CorporateHub, " "Odoo Community Association (OCA)",
'CorporateHub, ' "maintainers": ["alexey-pelykh"],
'Odoo Community Association (OCA)', "website": "https://github.com/OCA/bank-statement-import/",
'maintainers': ['alexey-pelykh'], "license": "AGPL-3",
'website': 'https://github.com/OCA/bank-statement-import/', "category": "Accounting",
'license': 'AGPL-3', "summary": "Online bank statements for PayPal.com",
'category': 'Accounting', "depends": ["account_bank_statement_import_online",],
'summary': 'Online bank statements for PayPal.com', "data": ["views/online_bank_statement_provider.xml",],
'depends': [ "installable": True,
'account_bank_statement_import_online',
],
'data': [
'views/online_bank_statement_provider.xml',
],
'installable': True,
} }

View File

@@ -1,194 +1,189 @@
# Copyright 2019-2020 Brainbean Apps (https://brainbeanapps.com) # Copyright 2019-2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from base64 import b64encode
from datetime import datetime
from dateutil.relativedelta import relativedelta
import dateutil.parser
from decimal import Decimal
import itertools import itertools
import json import json
import pytz import urllib.request
from base64 import b64encode
from datetime import datetime
from decimal import Decimal
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlencode from urllib.parse import urlencode
import urllib.request
from odoo import models, api, _ import dateutil.parser
import pytz
from dateutil.relativedelta import relativedelta
from odoo import _, api, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
PAYPAL_API_BASE = "https://api.paypal.com"
PAYPAL_API_BASE = 'https://api.paypal.com' TRANSACTIONS_SCOPE = "https://uri.paypal.com/services/reporting/search/read"
TRANSACTIONS_SCOPE = 'https://uri.paypal.com/services/reporting/search/read'
EVENT_DESCRIPTIONS = { EVENT_DESCRIPTIONS = {
'T0000': _('General PayPal-to-PayPal payment'), "T0000": _("General PayPal-to-PayPal payment"),
'T0001': _('MassPay payment'), "T0001": _("MassPay payment"),
'T0002': _('Subscription payment'), "T0002": _("Subscription payment"),
'T0003': _('Pre-approved payment (BillUser API)'), "T0003": _("Pre-approved payment (BillUser API)"),
'T0004': _('eBay auction payment'), "T0004": _("eBay auction payment"),
'T0005': _('Direct payment API'), "T0005": _("Direct payment API"),
'T0006': _('PayPal Checkout APIs'), "T0006": _("PayPal Checkout APIs"),
'T0007': _('Website payments standard payment'), "T0007": _("Website payments standard payment"),
'T0008': _('Postage payment to carrier'), "T0008": _("Postage payment to carrier"),
'T0009': _('Gift certificate payment, purchase of gift certificate'), "T0009": _("Gift certificate payment, purchase of gift certificate"),
'T0010': _('Third-party auction payment'), "T0010": _("Third-party auction payment"),
'T0011': _('Mobile payment, made through a mobile phone'), "T0011": _("Mobile payment, made through a mobile phone"),
'T0012': _('Virtual terminal payment'), "T0012": _("Virtual terminal payment"),
'T0013': _('Donation payment'), "T0013": _("Donation payment"),
'T0014': _('Rebate payments'), "T0014": _("Rebate payments"),
'T0015': _('Third-party payout'), "T0015": _("Third-party payout"),
'T0016': _('Third-party recoupment'), "T0016": _("Third-party recoupment"),
'T0017': _('Store-to-store transfers'), "T0017": _("Store-to-store transfers"),
'T0018': _('PayPal Here payment'), "T0018": _("PayPal Here payment"),
'T0019': _('Generic instrument-funded payment'), "T0019": _("Generic instrument-funded payment"),
'T0100': _('General non-payment fee'), "T0100": _("General non-payment fee"),
'T0101': _('Website payments. Pro account monthly fee'), "T0101": _("Website payments. Pro account monthly fee"),
'T0102': _('Foreign bank withdrawal fee'), "T0102": _("Foreign bank withdrawal fee"),
'T0103': _('WorldLink check withdrawal fee'), "T0103": _("WorldLink check withdrawal fee"),
'T0104': _('Mass payment batch fee'), "T0104": _("Mass payment batch fee"),
'T0105': _('Check withdrawal'), "T0105": _("Check withdrawal"),
'T0106': _('Chargeback processing fee'), "T0106": _("Chargeback processing fee"),
'T0107': _('Payment fee'), "T0107": _("Payment fee"),
'T0108': _('ATM withdrawal'), "T0108": _("ATM withdrawal"),
'T0109': _('Auto-sweep from account'), "T0109": _("Auto-sweep from account"),
'T0110': _('International credit card withdrawal'), "T0110": _("International credit card withdrawal"),
'T0111': _('Warranty fee for warranty purchase'), "T0111": _("Warranty fee for warranty purchase"),
'T0112': _('Gift certificate expiration fee'), "T0112": _("Gift certificate expiration fee"),
'T0113': _('Partner fee'), "T0113": _("Partner fee"),
'T0200': _('General currency conversion'), "T0200": _("General currency conversion"),
'T0201': _('User-initiated currency conversion'), "T0201": _("User-initiated currency conversion"),
'T0202': _('Currency conversion required to cover negative balance'), "T0202": _("Currency conversion required to cover negative balance"),
'T0300': _('General funding of PayPal account'), "T0300": _("General funding of PayPal account"),
'T0301': _('PayPal balance manager funding of PayPal account'), "T0301": _("PayPal balance manager funding of PayPal account"),
'T0302': _('ACH funding for funds recovery from account balance'), "T0302": _("ACH funding for funds recovery from account balance"),
'T0303': _('Electronic funds transfer (EFT)'), "T0303": _("Electronic funds transfer (EFT)"),
'T0400': _('General withdrawal from PayPal account'), "T0400": _("General withdrawal from PayPal account"),
'T0401': _('AutoSweep'), "T0401": _("AutoSweep"),
'T0500': _('General PayPal debit card transaction'), "T0500": _("General PayPal debit card transaction"),
'T0501': _('Virtual PayPal debit card transaction'), "T0501": _("Virtual PayPal debit card transaction"),
'T0502': _('PayPal debit card withdrawal to ATM'), "T0502": _("PayPal debit card withdrawal to ATM"),
'T0503': _('Hidden virtual PayPal debit card transaction'), "T0503": _("Hidden virtual PayPal debit card transaction"),
'T0504': _('PayPal debit card cash advance'), "T0504": _("PayPal debit card cash advance"),
'T0505': _('PayPal debit authorization'), "T0505": _("PayPal debit authorization"),
'T0600': _('General credit card withdrawal'), "T0600": _("General credit card withdrawal"),
'T0700': _('General credit card deposit'), "T0700": _("General credit card deposit"),
'T0701': _('Credit card deposit for negative PayPal account balance'), "T0701": _("Credit card deposit for negative PayPal account balance"),
'T0800': _('General bonus'), "T0800": _("General bonus"),
'T0801': _('Debit card cash back bonus'), "T0801": _("Debit card cash back bonus"),
'T0802': _('Merchant referral account bonus'), "T0802": _("Merchant referral account bonus"),
'T0803': _('Balance manager account bonus'), "T0803": _("Balance manager account bonus"),
'T0804': _('PayPal buyer warranty bonus'), "T0804": _("PayPal buyer warranty bonus"),
'T0805': _( "T0805": _(
'PayPal protection bonus, payout for PayPal buyer protection, payout ' "PayPal protection bonus, payout for PayPal buyer protection, payout "
'for full protection with PayPal buyer credit.' "for full protection with PayPal buyer credit."
), ),
'T0806': _('Bonus for first ACH use'), "T0806": _("Bonus for first ACH use"),
'T0807': _('Credit card security charge refund'), "T0807": _("Credit card security charge refund"),
'T0808': _('Credit card cash back bonus'), "T0808": _("Credit card cash back bonus"),
'T0900': _('General incentive or certificate redemption'), "T0900": _("General incentive or certificate redemption"),
'T0901': _('Gift certificate redemption'), "T0901": _("Gift certificate redemption"),
'T0902': _('Points incentive redemption'), "T0902": _("Points incentive redemption"),
'T0903': _('Coupon redemption'), "T0903": _("Coupon redemption"),
'T0904': _('eBay loyalty incentive'), "T0904": _("eBay loyalty incentive"),
'T0905': _('Offers used as funding source'), "T0905": _("Offers used as funding source"),
'T1000': _('Bill pay transaction'), "T1000": _("Bill pay transaction"),
'T1100': _('General reversal'), "T1100": _("General reversal"),
'T1101': _('Reversal of ACH withdrawal transaction'), "T1101": _("Reversal of ACH withdrawal transaction"),
'T1102': _('Reversal of debit card transaction'), "T1102": _("Reversal of debit card transaction"),
'T1103': _('Reversal of points usage'), "T1103": _("Reversal of points usage"),
'T1104': _('Reversal of ACH deposit'), "T1104": _("Reversal of ACH deposit"),
'T1105': _('Reversal of general account hold'), "T1105": _("Reversal of general account hold"),
'T1106': _('Payment reversal, initiated by PayPal'), "T1106": _("Payment reversal, initiated by PayPal"),
'T1107': _('Payment refund, initiated by merchant'), "T1107": _("Payment refund, initiated by merchant"),
'T1108': _('Fee reversal'), "T1108": _("Fee reversal"),
'T1109': _('Fee refund'), "T1109": _("Fee refund"),
'T1110': _('Hold for dispute investigation'), "T1110": _("Hold for dispute investigation"),
'T1111': _('Cancellation of hold for dispute resolution'), "T1111": _("Cancellation of hold for dispute resolution"),
'T1112': _('MAM reversal'), "T1112": _("MAM reversal"),
'T1113': _('Non-reference credit payment'), "T1113": _("Non-reference credit payment"),
'T1114': _('MassPay reversal transaction'), "T1114": _("MassPay reversal transaction"),
'T1115': _('MassPay refund transaction'), "T1115": _("MassPay refund transaction"),
'T1116': _('Instant payment review (IPR) reversal'), "T1116": _("Instant payment review (IPR) reversal"),
'T1117': _('Rebate or cash back reversal'), "T1117": _("Rebate or cash back reversal"),
'T1118': _('Generic instrument/Open Wallet reversals (seller side)'), "T1118": _("Generic instrument/Open Wallet reversals (seller side)"),
'T1119': _('Generic instrument/Open Wallet reversals (buyer side)'), "T1119": _("Generic instrument/Open Wallet reversals (buyer side)"),
'T1200': _('General account adjustment'), "T1200": _("General account adjustment"),
'T1201': _('Chargeback'), "T1201": _("Chargeback"),
'T1202': _('Chargeback reversal'), "T1202": _("Chargeback reversal"),
'T1203': _('Charge-off adjustment'), "T1203": _("Charge-off adjustment"),
'T1204': _('Incentive adjustment'), "T1204": _("Incentive adjustment"),
'T1205': _('Reimbursement of chargeback'), "T1205": _("Reimbursement of chargeback"),
'T1207': _('Chargeback re-presentment rejection'), "T1207": _("Chargeback re-presentment rejection"),
'T1208': _('Chargeback cancellation'), "T1208": _("Chargeback cancellation"),
'T1300': _('General authorization'), "T1300": _("General authorization"),
'T1301': _('Reauthorization'), "T1301": _("Reauthorization"),
'T1302': _('Void of authorization'), "T1302": _("Void of authorization"),
'T1400': _('General dividend'), "T1400": _("General dividend"),
'T1500': _('General temporary hold'), "T1500": _("General temporary hold"),
'T1501': _('Account hold for open authorization'), "T1501": _("Account hold for open authorization"),
'T1502': _('Account hold for ACH deposit'), "T1502": _("Account hold for ACH deposit"),
'T1503': _('Temporary hold on available balance'), "T1503": _("Temporary hold on available balance"),
'T1600': _('PayPal buyer credit payment funding'), "T1600": _("PayPal buyer credit payment funding"),
'T1601': _('BML credit, transfer from BML'), "T1601": _("BML credit, transfer from BML"),
'T1602': _('Buyer credit payment'), "T1602": _("Buyer credit payment"),
'T1603': _('Buyer credit payment withdrawal, transfer to BML'), "T1603": _("Buyer credit payment withdrawal, transfer to BML"),
'T1700': _('General withdrawal to non-bank institution'), "T1700": _("General withdrawal to non-bank institution"),
'T1701': _('WorldLink withdrawal'), "T1701": _("WorldLink withdrawal"),
'T1800': _('General buyer credit payment'), "T1800": _("General buyer credit payment"),
'T1801': _('BML withdrawal, transfer to BML'), "T1801": _("BML withdrawal, transfer to BML"),
'T1900': _('General adjustment without business-related event'), "T1900": _("General adjustment without business-related event"),
'T2000': _('General intra-account transfer'), "T2000": _("General intra-account transfer"),
'T2001': _('Settlement consolidation'), "T2001": _("Settlement consolidation"),
'T2002': _('Transfer of funds from payable'), "T2002": _("Transfer of funds from payable"),
'T2003': _('Transfer to external GL entity'), "T2003": _("Transfer to external GL entity"),
'T2101': _('General hold'), "T2101": _("General hold"),
'T2102': _('General hold release'), "T2102": _("General hold release"),
'T2103': _('Reserve hold'), "T2103": _("Reserve hold"),
'T2104': _('Reserve release'), "T2104": _("Reserve release"),
'T2105': _('Payment review hold'), "T2105": _("Payment review hold"),
'T2106': _('Payment review release'), "T2106": _("Payment review release"),
'T2107': _('Payment hold'), "T2107": _("Payment hold"),
'T2108': _('Payment hold release'), "T2108": _("Payment hold release"),
'T2109': _('Gift certificate purchase'), "T2109": _("Gift certificate purchase"),
'T2110': _('Gift certificate redemption'), "T2110": _("Gift certificate redemption"),
'T2111': _('Funds not yet available'), "T2111": _("Funds not yet available"),
'T2112': _('Funds available'), "T2112": _("Funds available"),
'T2113': _('Blocked payments'), "T2113": _("Blocked payments"),
'T2201': _('Transfer to and from a credit-card-funded restricted balance'), "T2201": _("Transfer to and from a credit-card-funded restricted balance"),
'T3000': _('Generic instrument/Open Wallet transaction'), "T3000": _("Generic instrument/Open Wallet transaction"),
'T5000': _('Deferred disbursement, funds collected for disbursement'), "T5000": _("Deferred disbursement, funds collected for disbursement"),
'T5001': _('Delayed disbursement, funds disbursed'), "T5001": _("Delayed disbursement, funds disbursed"),
'T9700': _('Account receivable for shipping'), "T9700": _("Account receivable for shipping"),
'T9701': _('Funds payable: PayPal-provided funds that must be paid back'), "T9701": _("Funds payable: PayPal-provided funds that must be paid back"),
'T9702': _( "T9702": _("Funds receivable: PayPal-provided funds that are being paid back"),
'Funds receivable: PayPal-provided funds that are being paid back' "T9800": _("Display only transaction"),
), "T9900": _("Other"),
'T9800': _('Display only transaction'),
'T9900': _('Other'),
} }
NO_DATA_FOR_DATE_AVAIL_MSG = 'Data for the given start date is not available.' NO_DATA_FOR_DATE_AVAIL_MSG = "Data for the given start date is not available."
class OnlineBankStatementProviderPayPal(models.Model): class OnlineBankStatementProviderPayPal(models.Model):
_inherit = 'online.bank.statement.provider' _inherit = "online.bank.statement.provider"
@api.model @api.model
def _get_available_services(self): def _get_available_services(self):
return super()._get_available_services() + [ return super()._get_available_services() + [
('paypal', 'PayPal.com'), ("paypal", "PayPal.com"),
] ]
@api.multi @api.multi
def _obtain_statement_data(self, date_since, date_until): def _obtain_statement_data(self, date_since, date_until):
self.ensure_one() self.ensure_one()
if self.service != 'paypal': if self.service != "paypal":
return super()._obtain_statement_data( return super()._obtain_statement_data(
date_since, date_since, date_until,
date_until,
) # pragma: no cover ) # pragma: no cover
currency = ( currency = (self.currency_id or self.company_id.currency_id).name
self.currency_id or self.company_id.currency_id
).name
if date_since.tzinfo: if date_since.tzinfo:
date_since = date_since.astimezone(pytz.utc).replace(tzinfo=None) date_since = date_since.astimezone(pytz.utc).replace(tzinfo=None)
@@ -196,215 +191,178 @@ class OnlineBankStatementProviderPayPal(models.Model):
date_until = date_until.astimezone(pytz.utc).replace(tzinfo=None) date_until = date_until.astimezone(pytz.utc).replace(tzinfo=None)
if date_since < datetime.utcnow() - relativedelta(years=3): if date_since < datetime.utcnow() - relativedelta(years=3):
raise UserError(_( raise UserError(
'PayPal allows retrieving transactions only up to 3 years in ' _(
'the past. Please import older transactions manually. See ' "PayPal allows retrieving transactions only up to 3 years in "
'https://www.paypal.com/us/smarthelp/article/why-can\'t-i' "the past. Please import older transactions manually. See "
'-access-transaction-history-greater-than-3-years-ts2241' "https://www.paypal.com/us/smarthelp/article/why-can't-i"
)) "-access-transaction-history-greater-than-3-years-ts2241"
)
)
token = self._paypal_get_token() token = self._paypal_get_token()
transactions = self._paypal_get_transactions( transactions = self._paypal_get_transactions(
token, token, currency, date_since, date_until
currency,
date_since,
date_until
) )
if not transactions: if not transactions:
balance = self._paypal_get_balance( balance = self._paypal_get_balance(token, currency, date_since)
token, return [], {"balance_start": balance, "balance_end_real": balance,}
currency,
date_since
)
return [], {
'balance_start': balance,
'balance_end_real': balance,
}
# Normalize transactions, sort by date, and get lines # Normalize transactions, sort by date, and get lines
transactions = list(sorted( transactions = list(
sorted(
transactions, transactions,
key=lambda transaction: self._paypal_get_transaction_date( key=lambda transaction: self._paypal_get_transaction_date(transaction),
transaction )
)
lines = list(
itertools.chain.from_iterable(
map(lambda x: self._paypal_transaction_to_lines(x), transactions)
)
) )
))
lines = list(itertools.chain.from_iterable(map(
lambda x: self._paypal_transaction_to_lines(x),
transactions
)))
first_transaction = transactions[0] first_transaction = transactions[0]
first_transaction_id = \ first_transaction_id = first_transaction["transaction_info"]["transaction_id"]
first_transaction['transaction_info']['transaction_id'] first_transaction_date = self._paypal_get_transaction_date(first_transaction)
first_transaction_date = self._paypal_get_transaction_date(
first_transaction
)
first_transaction = self._paypal_get_transaction( first_transaction = self._paypal_get_transaction(
token, token, first_transaction_id, first_transaction_date
first_transaction_id,
first_transaction_date
) )
if not first_transaction: if not first_transaction:
raise UserError(_('Failed to resolve transaction %s (%s)') % ( raise UserError(
first_transaction_id, _("Failed to resolve transaction %s (%s)")
first_transaction_date % (first_transaction_id, first_transaction_date)
))
balance_start = self._paypal_get_transaction_ending_balance(
first_transaction
)
balance_start -= self._paypal_get_transaction_total_amount(
first_transaction
)
balance_start -= self._paypal_get_transaction_fee_amount(
first_transaction
) )
balance_start = self._paypal_get_transaction_ending_balance(first_transaction)
balance_start -= self._paypal_get_transaction_total_amount(first_transaction)
balance_start -= self._paypal_get_transaction_fee_amount(first_transaction)
last_transaction = transactions[-1] last_transaction = transactions[-1]
last_transaction_id = \ last_transaction_id = last_transaction["transaction_info"]["transaction_id"]
last_transaction['transaction_info']['transaction_id'] last_transaction_date = self._paypal_get_transaction_date(last_transaction)
last_transaction_date = self._paypal_get_transaction_date(
last_transaction
)
last_transaction = self._paypal_get_transaction( last_transaction = self._paypal_get_transaction(
token, token, last_transaction_id, last_transaction_date
last_transaction_id,
last_transaction_date
) )
if not last_transaction: if not last_transaction:
raise UserError(_('Failed to resolve transaction %s (%s)') % ( raise UserError(
last_transaction_id, _("Failed to resolve transaction %s (%s)")
last_transaction_date % (last_transaction_id, last_transaction_date)
))
balance_end = self._paypal_get_transaction_ending_balance(
last_transaction
) )
balance_end = self._paypal_get_transaction_ending_balance(last_transaction)
return lines, { return lines, {"balance_start": balance_start, "balance_end_real": balance_end,}
'balance_start': balance_start,
'balance_end_real': balance_end,
}
@api.model @api.model
def _paypal_preparse_transaction(self, transaction): def _paypal_preparse_transaction(self, transaction):
date = dateutil.parser.parse( date = (
self._paypal_get_transaction_date(transaction) dateutil.parser.parse(self._paypal_get_transaction_date(transaction))
).astimezone(pytz.utc).replace(tzinfo=None) .astimezone(pytz.utc)
transaction['transaction_info']['transaction_updated_date'] = date .replace(tzinfo=None)
)
transaction["transaction_info"]["transaction_updated_date"] = date
return transaction return transaction
@api.model @api.model
def _paypal_transaction_to_lines(self, data): def _paypal_transaction_to_lines(self, data):
transaction = data['transaction_info'] transaction = data["transaction_info"]
payer = data['payer_info'] payer = data["payer_info"]
transaction_id = transaction['transaction_id'] transaction_id = transaction["transaction_id"]
event_code = transaction['transaction_event_code'] event_code = transaction["transaction_event_code"]
date = self._paypal_get_transaction_date(data) date = self._paypal_get_transaction_date(data)
total_amount = self._paypal_get_transaction_total_amount(data) total_amount = self._paypal_get_transaction_total_amount(data)
fee_amount = self._paypal_get_transaction_fee_amount(data) fee_amount = self._paypal_get_transaction_fee_amount(data)
transaction_subject = transaction.get('transaction_subject') transaction_subject = transaction.get("transaction_subject")
transaction_note = transaction.get('transaction_note') transaction_note = transaction.get("transaction_note")
invoice = transaction.get('invoice_id') invoice = transaction.get("invoice_id")
payer_name = payer.get('payer_name', {}) payer_name = payer.get("payer_name", {})
payer_email = payer_name.get('email_address') payer_email = payer_name.get("email_address")
if invoice: if invoice:
invoice = _('Invoice %s') % invoice invoice = _("Invoice %s") % invoice
note = transaction_id note = transaction_id
if transaction_subject or transaction_note: if transaction_subject or transaction_note:
note = '%s: %s' % ( note = "{}: {}".format(note, transaction_subject or transaction_note)
note,
transaction_subject or transaction_note
)
if payer_email: if payer_email:
note += ' (%s)' % payer_email note += " (%s)" % payer_email
unique_import_id = '%s-%s' % ( unique_import_id = "{}-{}".format(transaction_id, int(date.timestamp()))
transaction_id, name = (
int(date.timestamp()), invoice
or transaction_subject
or transaction_note
or EVENT_DESCRIPTIONS.get(event_code)
or ""
) )
name = invoice \
or transaction_subject \
or transaction_note \
or EVENT_DESCRIPTIONS.get(event_code) \
or ''
line = { line = {
'name': name, "name": name,
'amount': str(total_amount), "amount": str(total_amount),
'date': date, "date": date,
'note': note, "note": note,
'unique_import_id': unique_import_id, "unique_import_id": unique_import_id,
} }
payer_full_name = payer_name.get('full_name') or \ payer_full_name = payer_name.get("full_name") or payer_name.get(
payer_name.get('alternate_full_name') "alternate_full_name"
)
if payer_full_name: if payer_full_name:
line.update({ line.update(
'partner_name': payer_full_name, {"partner_name": payer_full_name,}
}) )
lines = [line] lines = [line]
if fee_amount: if fee_amount:
lines += [{ lines += [
'name': _('Fee for %s') % (name or transaction_id), {
'amount': str(fee_amount), "name": _("Fee for %s") % (name or transaction_id),
'date': date, "amount": str(fee_amount),
'partner_name': 'PayPal', "date": date,
'unique_import_id': '%s-FEE' % unique_import_id, "partner_name": "PayPal",
'note': _('Transaction fee for %s') % note, "unique_import_id": "%s-FEE" % unique_import_id,
}] "note": _("Transaction fee for %s") % note,
}
]
return lines return lines
@api.multi @api.multi
def _paypal_get_token(self): def _paypal_get_token(self):
self.ensure_one() self.ensure_one()
data = self._paypal_retrieve( data = self._paypal_retrieve(
(self.api_base or PAYPAL_API_BASE) + '/v1/oauth2/token', (self.api_base or PAYPAL_API_BASE) + "/v1/oauth2/token",
(self.username, self.password), (self.username, self.password),
data=urlencode({ data=urlencode({"grant_type": "client_credentials",}).encode("utf-8"),
'grant_type': 'client_credentials',
}).encode('utf-8')
) )
if 'scope' not in data or TRANSACTIONS_SCOPE not in data['scope']: if "scope" not in data or TRANSACTIONS_SCOPE not in data["scope"]:
raise UserError(_( raise UserError(_("PayPal App features are configured incorrectly!"))
'PayPal App features are configured incorrectly!' if "token_type" not in data or data["token_type"] != "Bearer":
)) raise UserError(_("Invalid token type!"))
if 'token_type' not in data or data['token_type'] != 'Bearer': if "access_token" not in data:
raise UserError(_('Invalid token type!')) raise UserError(_("Failed to acquire token using Client ID and Secret!"))
if 'access_token' not in data: return data["access_token"]
raise UserError(_(
'Failed to acquire token using Client ID and Secret!'
))
return data['access_token']
@api.multi @api.multi
def _paypal_get_balance(self, token, currency, as_of_timestamp): def _paypal_get_balance(self, token, currency, as_of_timestamp):
self.ensure_one() self.ensure_one()
url = (self.api_base or PAYPAL_API_BASE) \ url = (
+ '/v1/reporting/balances?currency_code=%s&as_of_time=%s' % ( self.api_base or PAYPAL_API_BASE
) + "/v1/reporting/balances?currency_code={}&as_of_time={}".format(
currency, currency,
as_of_timestamp.isoformat() + 'Z', as_of_timestamp.isoformat() + "Z",
) )
data = self._paypal_retrieve(url, token) data = self._paypal_retrieve(url, token)
available_balance = data['balances'][0].get('available_balance') available_balance = data["balances"][0].get("available_balance")
if not available_balance: if not available_balance:
return Decimal() return Decimal()
return Decimal(available_balance['value']) return Decimal(available_balance["value"])
@api.multi @api.multi
def _paypal_get_transaction(self, token, transaction_id, timestamp): def _paypal_get_transaction(self, token, transaction_id, timestamp):
self.ensure_one() self.ensure_one()
transaction_date = timestamp.isoformat() + 'Z' transaction_date = timestamp.isoformat() + "Z"
url = (self.api_base or PAYPAL_API_BASE) \ url = (
+ '/v1/reporting/transactions' \ (self.api_base or PAYPAL_API_BASE)
+ ( + "/v1/reporting/transactions"
'?start_date=%s' + ("?start_date=%s" "&end_date=%s" "&fields=all")
'&end_date=%s' % (transaction_date, transaction_date,)
'&fields=all'
) % (
transaction_date,
transaction_date,
) )
data = self._paypal_retrieve(url, token) data = self._paypal_retrieve(url, token)
transactions = data['transaction_details'] transactions = data["transaction_details"]
for transaction in transactions: for transaction in transactions:
if transaction['transaction_info']['transaction_id'] != \ if transaction["transaction_info"]["transaction_id"] != transaction_id:
transaction_id:
continue continue
return transaction return transaction
return None return None
@@ -422,48 +380,49 @@ class OnlineBankStatementProviderPayPal(models.Model):
page = 1 page = 1
total_pages = None total_pages = None
while total_pages is None or page <= total_pages: while total_pages is None or page <= total_pages:
url = (self.api_base or PAYPAL_API_BASE) \ url = (
+ '/v1/reporting/transactions' \ (self.api_base or PAYPAL_API_BASE)
+ "/v1/reporting/transactions"
+ ( + (
'?transaction_currency=%s' "?transaction_currency=%s"
'&start_date=%s' "&start_date=%s"
'&end_date=%s' "&end_date=%s"
'&fields=all' "&fields=all"
'&balance_affecting_records_only=Y' "&balance_affecting_records_only=Y"
'&page_size=500' "&page_size=500"
'&page=%d' "&page=%d"
% ( % (
currency, currency,
interval_start.isoformat() + 'Z', interval_start.isoformat() + "Z",
interval_end.isoformat() + 'Z', interval_end.isoformat() + "Z",
page, page,
)) )
)
)
# NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst) # NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst)
invalid_data_workaround = self.env.context.get( invalid_data_workaround = self.env.context.get(
'test_account_bank_statement_import_online_paypal_monday', "test_account_bank_statement_import_online_paypal_monday",
interval_start.weekday() == 0 and ( interval_start.weekday() == 0
datetime.utcnow() - interval_start and (datetime.utcnow() - interval_start).total_seconds() < 28800,
).total_seconds() < 28800
) )
data = self.with_context( data = self.with_context(
invalid_data_workaround=invalid_data_workaround, invalid_data_workaround=invalid_data_workaround,
)._paypal_retrieve(url, token) )._paypal_retrieve(url, token)
interval_transactions = map( interval_transactions = map(
lambda transaction: self._paypal_preparse_transaction( lambda transaction: self._paypal_preparse_transaction(transaction),
transaction data["transaction_details"],
),
data['transaction_details']
) )
transactions += list(filter( transactions += list(
lambda transaction: filter(
interval_start <= self._paypal_get_transaction_date( lambda transaction: interval_start
transaction <= self._paypal_get_transaction_date(transaction)
) < interval_end, < interval_end,
interval_transactions interval_transactions,
)) )
total_pages = data['total_pages'] )
total_pages = data["total_pages"]
page += 1 page += 1
interval_start += interval_step interval_start += interval_step
return transactions return transactions
@@ -471,45 +430,46 @@ class OnlineBankStatementProviderPayPal(models.Model):
@api.model @api.model
def _paypal_get_transaction_date(self, transaction): def _paypal_get_transaction_date(self, transaction):
# NOTE: CSV reports from PayPal use this date, search as well # NOTE: CSV reports from PayPal use this date, search as well
return transaction['transaction_info']['transaction_updated_date'] return transaction["transaction_info"]["transaction_updated_date"]
@api.model @api.model
def _paypal_get_transaction_total_amount(self, transaction): def _paypal_get_transaction_total_amount(self, transaction):
transaction_amount = \ transaction_amount = transaction["transaction_info"].get("transaction_amount")
transaction['transaction_info'].get('transaction_amount')
if not transaction_amount: if not transaction_amount:
return Decimal() return Decimal()
return Decimal(transaction_amount['value']) return Decimal(transaction_amount["value"])
@api.model @api.model
def _paypal_get_transaction_fee_amount(self, transaction): def _paypal_get_transaction_fee_amount(self, transaction):
fee_amount = transaction['transaction_info'].get('fee_amount') fee_amount = transaction["transaction_info"].get("fee_amount")
if not fee_amount: if not fee_amount:
return Decimal() return Decimal()
return Decimal(fee_amount['value']) return Decimal(fee_amount["value"])
@api.model @api.model
def _paypal_get_transaction_ending_balance(self, transaction): def _paypal_get_transaction_ending_balance(self, transaction):
# NOTE: 'available_balance' instead of 'ending_balance' as per CSV file # NOTE: 'available_balance' instead of 'ending_balance' as per CSV file
transaction_amount = \ transaction_amount = transaction["transaction_info"].get("available_balance")
transaction['transaction_info'].get('available_balance')
if not transaction_amount: if not transaction_amount:
return Decimal() return Decimal()
return Decimal(transaction_amount['value']) return Decimal(transaction_amount["value"])
@api.model @api.model
def _paypal_decode_error(self, content): def _paypal_decode_error(self, content):
if 'name' in content: if "name" in content:
return UserError('%s: %s' % ( return UserError(
content['name'], "%s: %s"
content.get('message', _('Unknown error')), % (content["name"], content.get("message", _("Unknown error")),)
)) )
if 'error' in content: if "error" in content:
return UserError('%s: %s' % ( return UserError(
content['error'], "%s: %s"
content.get('error_description', _('Unknown error')), % (
)) content["error"],
content.get("error_description", _("Unknown error")),
)
)
return None return None
@@ -517,19 +477,21 @@ class OnlineBankStatementProviderPayPal(models.Model):
def _paypal_retrieve(self, url, auth, data=None): def _paypal_retrieve(self, url, auth, data=None):
try: try:
with self._paypal_urlopen(url, auth, data) as response: with self._paypal_urlopen(url, auth, data) as response:
content = response.read().decode('utf-8') content = response.read().decode("utf-8")
except HTTPError as e: except HTTPError as e:
content = json.loads(e.read().decode('utf-8')) content = json.loads(e.read().decode("utf-8"))
# NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst) # NOTE: Workaround for INVALID_REQUEST (see ROADMAP.rst)
if self.env.context.get('invalid_data_workaround') \ if (
and content.get('name') == 'INVALID_REQUEST' \ self.env.context.get("invalid_data_workaround")
and content.get('message') == NO_DATA_FOR_DATE_AVAIL_MSG: and content.get("name") == "INVALID_REQUEST"
and content.get("message") == NO_DATA_FOR_DATE_AVAIL_MSG
):
return { return {
'transaction_details': [], "transaction_details": [],
'page': 1, "page": 1,
'total_items': 0, "total_items": 0,
'total_pages': 0, "total_pages": 0,
} }
raise self._paypal_decode_error(content) or e raise self._paypal_decode_error(content) or e
@@ -538,21 +500,18 @@ class OnlineBankStatementProviderPayPal(models.Model):
@api.model @api.model
def _paypal_urlopen(self, url, auth, data=None): def _paypal_urlopen(self, url, auth, data=None):
if not auth: if not auth:
raise UserError(_('No authentication specified!')) raise UserError(_("No authentication specified!"))
request = urllib.request.Request(url, data=data) request = urllib.request.Request(url, data=data)
if isinstance(auth, tuple): if isinstance(auth, tuple):
request.add_header( request.add_header(
'Authorization', "Authorization",
'Basic %s' % str( "Basic %s"
b64encode(('%s:%s' % (auth[0], auth[1])).encode('utf-8')), % str(
'utf-8' b64encode(("{}:{}".format(auth[0], auth[1])).encode("utf-8")), "utf-8"
) ),
) )
elif isinstance(auth, str): elif isinstance(auth, str):
request.add_header( request.add_header("Authorization", "Bearer %s" % auth)
'Authorization',
'Bearer %s' % auth
)
else: else:
raise UserError(_('Unknown authentication specified!')) raise UserError(_("Unknown authentication specified!"))
return urllib.request.urlopen(request) return urllib.request.urlopen(request)

View File

@@ -1,22 +1,23 @@
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import datetime
from dateutil.relativedelta import relativedelta
from decimal import Decimal
import json import json
from datetime import datetime
from decimal import Decimal
from unittest import mock from unittest import mock
from urllib.error import HTTPError from urllib.error import HTTPError
from dateutil.relativedelta import relativedelta
from odoo import fields from odoo import fields
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tests import common from odoo.tests import common
_module_ns = 'odoo.addons.account_bank_statement_import_online_paypal' _module_ns = "odoo.addons.account_bank_statement_import_online_paypal"
_provider_class = ( _provider_class = (
_module_ns _module_ns
+ '.models.online_bank_statement_provider_paypal' + ".models.online_bank_statement_provider_paypal"
+ '.OnlineBankStatementProviderPayPal' + ".OnlineBankStatementProviderPayPal"
) )
@@ -25,7 +26,7 @@ class FakeHTTPError(HTTPError):
self.content = content self.content = content
def read(self): def read(self):
return self.content.encode('utf-8') return self.content.encode("utf-8")
class UrlopenRetValMock: class UrlopenRetValMock:
@@ -42,165 +43,178 @@ class UrlopenRetValMock:
def read(self): def read(self):
if self.throw: if self.throw:
raise FakeHTTPError(self.content) raise FakeHTTPError(self.content)
return self.content.encode('utf-8') return self.content.encode("utf-8")
class TestAccountBankAccountStatementImportOnlinePayPal( class TestAccountBankAccountStatementImportOnlinePayPal(common.TransactionCase):
common.TransactionCase
):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.now = fields.Datetime.now() self.now = fields.Datetime.now()
self.currency_eur = self.env.ref('base.EUR') self.currency_eur = self.env.ref("base.EUR")
self.currency_usd = self.env.ref('base.USD') self.currency_usd = self.env.ref("base.USD")
self.AccountJournal = self.env['account.journal'] self.AccountJournal = self.env["account.journal"]
self.OnlineBankStatementProvider = self.env[ self.OnlineBankStatementProvider = self.env["online.bank.statement.provider"]
'online.bank.statement.provider' self.AccountBankStatement = self.env["account.bank.statement"]
] self.AccountBankStatementLine = self.env["account.bank.statement.line"]
self.AccountBankStatement = self.env['account.bank.statement']
self.AccountBankStatementLine = self.env['account.bank.statement.line']
Provider = self.OnlineBankStatementProvider Provider = self.OnlineBankStatementProvider
self.paypal_parse_transaction = lambda payload: ( self.paypal_parse_transaction = lambda payload: (
Provider._paypal_transaction_to_lines( Provider._paypal_transaction_to_lines(
Provider._paypal_preparse_transaction( Provider._paypal_preparse_transaction(
json.loads( json.loads(payload, parse_float=Decimal,)
payload,
parse_float=Decimal,
)
) )
) )
) )
self.mock_token = lambda: mock.patch( self.mock_token = lambda: mock.patch(
_provider_class + '._paypal_get_token', _provider_class + "._paypal_get_token", return_value="--TOKEN--",
return_value='--TOKEN--',
) )
def test_good_token(self): def test_good_token(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"scope": "https://uri.paypal.com/services/reporting/search/read", "scope": "https://uri.paypal.com/services/reporting/search/read",
"access_token": "---TOKEN---", "access_token": "---TOKEN---",
"token_type": "Bearer", "token_type": "Bearer",
"app_id": "APP-1234567890", "app_id": "APP-1234567890",
"expires_in": 32400, "expires_in": 32400,
"nonce": "---NONCE---" "nonce": "---NONCE---"
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
token = None token = None
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
): ):
token = provider._paypal_get_token() token = provider._paypal_get_token()
self.assertEqual(token, '---TOKEN---') self.assertEqual(token, "---TOKEN---")
def test_bad_token_scope(self): def test_bad_token_scope(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"scope": "openid https://uri.paypal.com/services/applications/webhooks", "scope": "openid https://uri.paypal.com/services/applications/webhooks",
"access_token": "---TOKEN---", "access_token": "---TOKEN---",
"token_type": "Bearer", "token_type": "Bearer",
"app_id": "APP-1234567890", "app_id": "APP-1234567890",
"expires_in": 32400, "expires_in": 32400,
"nonce": "---NONCE---" "nonce": "---NONCE---"
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
): ):
with self.assertRaises(Exception): with self.assertRaises(Exception):
provider._paypal_get_token() provider._paypal_get_token()
def test_bad_token_type(self): def test_bad_token_type(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"scope": "https://uri.paypal.com/services/reporting/search/read", "scope": "https://uri.paypal.com/services/reporting/search/read",
"access_token": "---TOKEN---", "access_token": "---TOKEN---",
"token_type": "NotBearer", "token_type": "NotBearer",
"app_id": "APP-1234567890", "app_id": "APP-1234567890",
"expires_in": 32400, "expires_in": 32400,
"nonce": "---NONCE---" "nonce": "---NONCE---"
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
): ):
with self.assertRaises(Exception): with self.assertRaises(Exception):
provider._paypal_get_token() provider._paypal_get_token()
def test_no_token(self): def test_no_token(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"scope": "https://uri.paypal.com/services/reporting/search/read", "scope": "https://uri.paypal.com/services/reporting/search/read",
"token_type": "Bearer", "token_type": "Bearer",
"app_id": "APP-1234567890", "app_id": "APP-1234567890",
"expires_in": 32400, "expires_in": 32400,
"nonce": "---NONCE---" "nonce": "---NONCE---"
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
): ):
with self.assertRaises(Exception): with self.assertRaises(Exception):
provider._paypal_get_token() provider._paypal_get_token()
def test_no_data_on_monday(self): def test_no_data_on_monday(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response_1 = UrlopenRetValMock("""{ mocked_response_1 = UrlopenRetValMock(
"""{
"debug_id": "eec890ebd5798", "debug_id": "eec890ebd5798",
"details": "xxxxxx", "details": "xxxxxx",
"links": "xxxxxx", "links": "xxxxxx",
"message": "Data for the given start date is not available.", "message": "Data for the given start date is not available.",
"name": "INVALID_REQUEST" "name": "INVALID_REQUEST"
}""", throw=True) }""",
mocked_response_2 = UrlopenRetValMock("""{ throw=True,
)
mocked_response_2 = UrlopenRetValMock(
"""{
"balances": [ "balances": [
{ {
"currency": "EUR", "currency": "EUR",
@@ -222,79 +236,85 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"account_id": "1234567890", "account_id": "1234567890",
"as_of_time": "2019-08-01T00:00:00+0000", "as_of_time": "2019-08-01T00:00:00+0000",
"last_refresh_time": "2019-08-01T00:00:00+0000" "last_refresh_time": "2019-08-01T00:00:00+0000"
}""") }"""
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_urlopen', _provider_class + "._paypal_urlopen",
side_effect=[mocked_response_1, mocked_response_2], side_effect=[mocked_response_1, mocked_response_2],
), self.mock_token(): ), self.mock_token():
data = provider.with_context( data = provider.with_context(
test_account_bank_statement_import_online_paypal_monday=True, test_account_bank_statement_import_online_paypal_monday=True,
)._obtain_statement_data( )._obtain_statement_data(self.now - relativedelta(hours=1), self.now,)
self.now - relativedelta(hours=1),
self.now,
)
self.assertEqual(data, ([], { self.assertEqual(data, ([], {"balance_start": 0.75, "balance_end_real": 0.75,}))
'balance_start': 0.75,
'balance_end_real': 0.75,
}))
def test_error_handling_1(self): def test_error_handling_1(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = UrlopenRetValMock("""{ mocked_response = UrlopenRetValMock(
"""{
"message": "MESSAGE", "message": "MESSAGE",
"name": "ERROR" "name": "ERROR"
}""", throw=True) }""",
throw=True,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_urlopen', _provider_class + "._paypal_urlopen", return_value=mocked_response,
return_value=mocked_response,
): ):
with self.assertRaises(UserError): with self.assertRaises(UserError):
provider._paypal_retrieve('https://url', '') provider._paypal_retrieve("https://url", "")
def test_error_handling_2(self): def test_error_handling_2(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = UrlopenRetValMock("""{ mocked_response = UrlopenRetValMock(
"""{
"error_description": "ERROR DESCRIPTION", "error_description": "ERROR DESCRIPTION",
"error": "ERROR" "error": "ERROR"
}""", throw=True) }""",
throw=True,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_urlopen', _provider_class + "._paypal_urlopen", return_value=mocked_response,
return_value=mocked_response,
): ):
with self.assertRaises(UserError): with self.assertRaises(UserError):
provider._paypal_retrieve('https://url', '') provider._paypal_retrieve("https://url", "")
def test_empty_pull(self): def test_empty_pull(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response_1 = json.loads("""{ mocked_response_1 = json.loads(
"""{
"transaction_details": [], "transaction_details": [],
"account_number": "1234567890", "account_number": "1234567890",
"start_date": "2019-08-01T00:00:00+0000", "start_date": "2019-08-01T00:00:00+0000",
@@ -303,8 +323,11 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"page": 1, "page": 1,
"total_items": 0, "total_items": 0,
"total_pages": 0 "total_pages": 0
}""", parse_float=Decimal) }""",
mocked_response_2 = json.loads("""{ parse_float=Decimal,
)
mocked_response_2 = json.loads(
"""{
"balances": [ "balances": [
{ {
"currency": "EUR", "currency": "EUR",
@@ -326,33 +349,34 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"account_id": "1234567890", "account_id": "1234567890",
"as_of_time": "2019-08-01T00:00:00+0000", "as_of_time": "2019-08-01T00:00:00+0000",
"last_refresh_time": "2019-08-01T00:00:00+0000" "last_refresh_time": "2019-08-01T00:00:00+0000"
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve",
side_effect=[mocked_response_1, mocked_response_2], side_effect=[mocked_response_1, mocked_response_2],
), self.mock_token(): ), self.mock_token():
data = provider._obtain_statement_data( data = provider._obtain_statement_data(
self.now - relativedelta(hours=1), self.now - relativedelta(hours=1), self.now,
self.now,
) )
self.assertEqual(data, ([], { self.assertEqual(data, ([], {"balance_start": 0.75, "balance_end_real": 0.75,}))
'balance_start': 0.75,
'balance_end_real': 0.75,
}))
def test_ancient_pull(self): def test_ancient_pull(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"transaction_details": [], "transaction_details": [],
"account_number": "1234567890", "account_number": "1234567890",
"start_date": "2019-08-01T00:00:00+0000", "start_date": "2019-08-01T00:00:00+0000",
@@ -361,29 +385,32 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"page": 1, "page": 1,
"total_items": 0, "total_items": 0,
"total_pages": 0 "total_pages": 0
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
), self.mock_token(): ), self.mock_token():
with self.assertRaises(Exception): with self.assertRaises(Exception):
provider._obtain_statement_data( provider._obtain_statement_data(
self.now - relativedelta(years=5), self.now - relativedelta(years=5), self.now,
self.now,
) )
def test_pull(self): def test_pull(self):
journal = self.AccountJournal.create({ journal = self.AccountJournal.create(
'name': 'Bank', {
'type': 'bank', "name": "Bank",
'code': 'BANK', "type": "bank",
'currency_id': self.currency_eur.id, "code": "BANK",
'bank_statements_source': 'online', "currency_id": self.currency_eur.id,
'online_bank_statement_provider': 'paypal', "bank_statements_source": "online",
}) "online_bank_statement_provider": "paypal",
}
)
provider = journal.online_bank_statement_provider_id provider = journal.online_bank_statement_provider_id
mocked_response = json.loads("""{ mocked_response = json.loads(
"""{
"transaction_details": [{ "transaction_details": [{
"transaction_info": { "transaction_info": {
"paypal_account_id": "1234567890", "paypal_account_id": "1234567890",
@@ -476,40 +503,44 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"page": 1, "page": 1,
"total_items": 1, "total_items": 1,
"total_pages": 1 "total_pages": 1
}""", parse_float=Decimal) }""",
parse_float=Decimal,
)
with mock.patch( with mock.patch(
_provider_class + '._paypal_retrieve', _provider_class + "._paypal_retrieve", return_value=mocked_response,
return_value=mocked_response,
), self.mock_token(): ), self.mock_token():
data = provider._obtain_statement_data( data = provider._obtain_statement_data(
datetime(2019, 8, 1), datetime(2019, 8, 1), datetime(2019, 8, 2),
datetime(2019, 8, 2),
) )
self.assertEqual(len(data[0]), 2) self.assertEqual(len(data[0]), 2)
self.assertEqual(data[0][0], { self.assertEqual(
'date': datetime(2019, 8, 1), data[0][0],
'amount': '1000.00', {
'name': 'Invoice 1', "date": datetime(2019, 8, 1),
'note': '1234567890: Payment for Invoice(s) 1', "amount": "1000.00",
'partner_name': 'Acme, Inc.', "name": "Invoice 1",
'unique_import_id': '1234567890-1564617600', "note": "1234567890: Payment for Invoice(s) 1",
}) "partner_name": "Acme, Inc.",
self.assertEqual(data[0][1], { "unique_import_id": "1234567890-1564617600",
'date': datetime(2019, 8, 1), },
'amount': '-100.00', )
'name': 'Fee for Invoice 1', self.assertEqual(
'note': 'Transaction fee for 1234567890: Payment for Invoice(s) 1', data[0][1],
'partner_name': 'PayPal', {
'unique_import_id': '1234567890-1564617600-FEE', "date": datetime(2019, 8, 1),
}) "amount": "-100.00",
self.assertEqual(data[1], { "name": "Fee for Invoice 1",
'balance_start': 0.0, "note": "Transaction fee for 1234567890: Payment for Invoice(s) 1",
'balance_end_real': 900.0, "partner_name": "PayPal",
}) "unique_import_id": "1234567890-1564617600-FEE",
},
)
self.assertEqual(data[1], {"balance_start": 0.0, "balance_end_real": 900.0,})
def test_transaction_parse_1(self): def test_transaction_parse_1(self):
lines = self.paypal_parse_transaction("""{ lines = self.paypal_parse_transaction(
"""{
"transaction_info": { "transaction_info": {
"paypal_account_id": "1234567890", "paypal_account_id": "1234567890",
"transaction_id": "1234567890", "transaction_id": "1234567890",
@@ -551,19 +582,24 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"store_info": {}, "store_info": {},
"auction_info": {}, "auction_info": {},
"incentive_info": {} "incentive_info": {}
}""") }"""
)
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], { self.assertEqual(
'date': datetime(2019, 8, 1), lines[0],
'amount': '1000.00', {
'name': 'Invoice 1', "date": datetime(2019, 8, 1),
'note': '1234567890: Payment for Invoice(s) 1', "amount": "1000.00",
'partner_name': 'Acme, Inc.', "name": "Invoice 1",
'unique_import_id': '1234567890-1564617600', "note": "1234567890: Payment for Invoice(s) 1",
}) "partner_name": "Acme, Inc.",
"unique_import_id": "1234567890-1564617600",
},
)
def test_transaction_parse_2(self): def test_transaction_parse_2(self):
lines = self.paypal_parse_transaction("""{ lines = self.paypal_parse_transaction(
"""{
"transaction_info": { "transaction_info": {
"paypal_account_id": "1234567890", "paypal_account_id": "1234567890",
"transaction_id": "1234567890", "transaction_id": "1234567890",
@@ -605,19 +641,24 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"store_info": {}, "store_info": {},
"auction_info": {}, "auction_info": {},
"incentive_info": {} "incentive_info": {}
}""") }"""
)
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], { self.assertEqual(
'date': datetime(2019, 8, 1), lines[0],
'amount': '1000.00', {
'name': 'Invoice 1', "date": datetime(2019, 8, 1),
'note': '1234567890: Payment for Invoice(s) 1', "amount": "1000.00",
'partner_name': 'Acme, Inc.', "name": "Invoice 1",
'unique_import_id': '1234567890-1564617600', "note": "1234567890: Payment for Invoice(s) 1",
}) "partner_name": "Acme, Inc.",
"unique_import_id": "1234567890-1564617600",
},
)
def test_transaction_parse_3(self): def test_transaction_parse_3(self):
lines = self.paypal_parse_transaction("""{ lines = self.paypal_parse_transaction(
"""{
"transaction_info": { "transaction_info": {
"paypal_account_id": "1234567890", "paypal_account_id": "1234567890",
"transaction_id": "1234567890", "transaction_id": "1234567890",
@@ -659,27 +700,35 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"store_info": {}, "store_info": {},
"auction_info": {}, "auction_info": {},
"incentive_info": {} "incentive_info": {}
}""") }"""
)
self.assertEqual(len(lines), 2) self.assertEqual(len(lines), 2)
self.assertEqual(lines[0], { self.assertEqual(
'date': datetime(2019, 8, 1), lines[0],
'amount': '1000.00', {
'name': 'Invoice 1', "date": datetime(2019, 8, 1),
'note': '1234567890: Payment for Invoice(s) 1', "amount": "1000.00",
'partner_name': 'Acme, Inc.', "name": "Invoice 1",
'unique_import_id': '1234567890-1564617600', "note": "1234567890: Payment for Invoice(s) 1",
}) "partner_name": "Acme, Inc.",
self.assertEqual(lines[1], { "unique_import_id": "1234567890-1564617600",
'date': datetime(2019, 8, 1), },
'amount': '-100.00', )
'name': 'Fee for Invoice 1', self.assertEqual(
'note': 'Transaction fee for 1234567890: Payment for Invoice(s) 1', lines[1],
'partner_name': 'PayPal', {
'unique_import_id': '1234567890-1564617600-FEE', "date": datetime(2019, 8, 1),
}) "amount": "-100.00",
"name": "Fee for Invoice 1",
"note": "Transaction fee for 1234567890: Payment for Invoice(s) 1",
"partner_name": "PayPal",
"unique_import_id": "1234567890-1564617600-FEE",
},
)
def test_transaction_parse_4(self): def test_transaction_parse_4(self):
lines = self.paypal_parse_transaction("""{ lines = self.paypal_parse_transaction(
"""{
"transaction_info": { "transaction_info": {
"paypal_account_id": "1234567890", "paypal_account_id": "1234567890",
"transaction_id": "1234567890", "transaction_id": "1234567890",
@@ -717,13 +766,17 @@ class TestAccountBankAccountStatementImportOnlinePayPal(
"store_info": {}, "store_info": {},
"auction_info": {}, "auction_info": {},
"incentive_info": {} "incentive_info": {}
}""") }"""
)
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
self.assertEqual(lines[0], { self.assertEqual(
'date': datetime(2019, 8, 1), lines[0],
'amount': '1000.00', {
'name': 'Invoice 1', "date": datetime(2019, 8, 1),
'note': '1234567890: Payment for Invoice(s) 1', "amount": "1000.00",
'partner_name': 'Acme, Inc.', "name": "Invoice 1",
'unique_import_id': '1234567890-1564617600', "note": "1234567890: Payment for Invoice(s) 1",
}) "partner_name": "Acme, Inc.",
"unique_import_id": "1234567890-1564617600",
},
)

View File

@@ -4,11 +4,13 @@
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
--> -->
<odoo> <odoo>
<record model="ir.ui.view" id="online_bank_statement_provider_form"> <record model="ir.ui.view" id="online_bank_statement_provider_form">
<field name="name">online.bank.statement.provider.form</field> <field name="name">online.bank.statement.provider.form</field>
<field name="model">online.bank.statement.provider</field> <field name="model">online.bank.statement.provider</field>
<field name="inherit_id" ref="account_bank_statement_import_online.online_bank_statement_provider_form"/> <field
name="inherit_id"
ref="account_bank_statement_import_online.online_bank_statement_provider_form"
/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//page[@name='configuration']" position="inside"> <xpath expr="//page[@name='configuration']" position="inside">
<group attrs="{'invisible': [('service', '!=', 'paypal')]}"> <group attrs="{'invisible': [('service', '!=', 'paypal')]}">
@@ -35,5 +37,4 @@
</xpath> </xpath>
</field> </field>
</record> </record>
</odoo> </odoo>