Skip to content

Commit 69ba21b

Browse files
Merge pull request #2203 from softlayer/issues2201
Sums up child items in invoices-detail
2 parents deaa4eb + 413fa88 commit 69ba21b

File tree

5 files changed

+123
-26
lines changed

5 files changed

+123
-26
lines changed

SoftLayer/CLI/account/invoice_detail.py

+37-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
help="Shows a very detailed list of charges")
1717
@environment.pass_env
1818
def cli(env, identifier, details):
19-
"""Invoice details"""
19+
"""Invoice details
20+
21+
Will display the top level invoice items for a given invoice. The cost displayed is the sum of the item's
22+
cost along with all its child items.
23+
The --details option will display any child items a top level item may have. Parent items will appear
24+
in this list as well to display their specific cost.
25+
"""
2026

2127
manager = AccountManager(env.client)
2228
top_items = manager.get_billing_items(identifier)
@@ -49,16 +55,31 @@ def get_invoice_table(identifier, top_items, details):
4955
description = nice_string(item.get('description'))
5056
if fqdn != '.':
5157
description = "%s (%s)" % (item.get('description'), fqdn)
58+
total_recur, total_single = sum_item_charges(item)
5259
table.add_row([
5360
item.get('id'),
5461
category,
5562
nice_string(description),
56-
"$%.2f" % float(item.get('oneTimeAfterTaxAmount')),
57-
"$%.2f" % float(item.get('recurringAfterTaxAmount')),
63+
f"${total_single:,.2f}",
64+
f"${total_recur:,.2f}",
5865
utils.clean_time(item.get('createDate'), out_format="%Y-%m-%d"),
5966
utils.lookup(item, 'location', 'name')
6067
])
6168
if details:
69+
# This item has children, so we want to print out the parent item too. This will match the
70+
# invoice from the portal. https://github.com/softlayer/softlayer-python/issues/2201
71+
if len(item.get('children')) > 0:
72+
single = float(item.get('oneTimeAfterTaxAmount', 0.0))
73+
recurring = float(item.get('recurringAfterTaxAmount', 0.0))
74+
table.add_row([
75+
'>>>',
76+
category,
77+
nice_string(description),
78+
f"${single:,.2f}",
79+
f"${recurring:,.2f}",
80+
'---',
81+
'---'
82+
])
6283
for child in item.get('children', []):
6384
table.add_row([
6485
'>>>',
@@ -70,3 +91,16 @@ def get_invoice_table(identifier, top_items, details):
7091
'---'
7192
])
7293
return table
94+
95+
96+
def sum_item_charges(item: dict) -> (float, float):
97+
"""Takes a billing Item, sums up its child items and returns recurring, one_time prices"""
98+
99+
# API returns floats as strings in this case
100+
single = float(item.get('oneTimeAfterTaxAmount', 0.0))
101+
recurring = float(item.get('recurringAfterTaxAmount', 0.0))
102+
for child in item.get('children', []):
103+
single = single + float(child.get('oneTimeAfterTaxAmount', 0.0))
104+
recurring = recurring + float(child.get('recurringAfterTaxAmount', 0.0))
105+
106+
return (recurring, single)
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,49 @@
11
getInvoiceTopLevelItems = [
22
{
3-
'categoryCode': 'sov_sec_ip_addresses_priv',
4-
'createDate': '2018-04-04T23:15:20-06:00',
5-
'description': '64 Portable Private IP Addresses',
6-
'id': 724951323,
7-
'oneTimeAfterTaxAmount': '0',
8-
'recurringAfterTaxAmount': '0',
9-
'hostName': 'bleg',
10-
'domainName': 'beh.com',
11-
'category': {'name': 'Private (only) Secondary VLAN IP Addresses'},
12-
'children': [
3+
"categoryCode": "sov_sec_ip_addresses_priv",
4+
"createDate": "2018-04-04T23:15:20-06:00",
5+
"description": "64 Portable Private IP Addresses",
6+
"id": 724951323,
7+
"oneTimeAfterTaxAmount": "0",
8+
"recurringAfterTaxAmount": "0",
9+
"hostName": "bleg",
10+
"domainName": "beh.com",
11+
"category": {"name": "Private (only) Secondary VLAN IP Addresses"},
12+
"children": [
1313
{
14-
'id': 12345,
15-
'category': {'name': 'Fake Child Category'},
16-
'description': 'Blah',
17-
'oneTimeAfterTaxAmount': 55.50,
18-
'recurringAfterTaxAmount': 0.10
14+
"id": 12345,
15+
"category": {"name": "Fake Child Category"},
16+
"description": "Blah",
17+
"oneTimeAfterTaxAmount": 55.50,
18+
"recurringAfterTaxAmount": 0.10
1919
}
2020
],
21-
'location': {'name': 'fra02'}
21+
"location": {"name": "fra02"}
22+
},
23+
{
24+
"categoryCode": "reserved_capacity",
25+
"createDate": "2024-07-03T22:08:36-07:00",
26+
"description": "B1.1x2 (1 Year Term) (721hrs * .025)",
27+
"id": 1111222,
28+
"oneTimeAfterTaxAmount": "0",
29+
"recurringAfterTaxAmount": "18.03",
30+
"category": {"name": "Reserved Capacity"},
31+
"children": [
32+
{
33+
"description": "1 x 2.0 GHz or higher Core",
34+
"id": 29819,
35+
"oneTimeAfterTaxAmount": "0",
36+
"recurringAfterTaxAmount": "10.00",
37+
"category": {"name": "Computing Instance"}
38+
},
39+
{
40+
"description": "2 GB",
41+
"id": 123456,
42+
"oneTimeAfterTaxAmount": "0",
43+
"recurringAfterTaxAmount": "2.33",
44+
"category": {"name": "RAM"}
45+
}
46+
],
47+
"location": {"name": "dal10"}
2248
}
2349
]

SoftLayer/testing/xmlrpc.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
~~~~~~~~~~~~~~~~~~~~~~~~
44
XMP-RPC server which can use a transport to proxy requests for testing.
55
6+
If you want to spin up a test XML server to make fake API calls with, try this:
7+
8+
quick-server.py
9+
---
10+
import SoftLayer
11+
from SoftLayer.testing import xmlrpc
12+
13+
my_xport = SoftLayer.FixtureTransport()
14+
my_server = xmlrpc.create_test_server(my_xport, "localhost", port=4321)
15+
print(f"Server running on http://{my_server.server_name}:{my_server.server_port}")
16+
---
17+
$> python quick-server.py
18+
$> curl -X POST -d "<?xml version='1.0' encoding='iso-8859-1'?><methodCall><methodName> \
19+
getInvoiceTopLevelItems</methodName><params><param><value><struct><member><name>headers</name> \
20+
<value><struct><member><name>SoftLayer_Billing_InvoiceInitParameters</name><value><struct> \
21+
<member><name>id</name><value><string>1234</string></value></member></struct></value></member> \
22+
</struct></value></member></struct></value></param></params></methodCall>" \
23+
http://127.0.0.1:4321/SoftLayer_Billing_Invoice
24+
625
:license: MIT, see LICENSE for more details.
726
"""
827
import http.server
@@ -60,6 +79,7 @@ def do_POST(self):
6079
self.send_response(200)
6180
self.send_header("Content-type", "application/xml; charset=UTF-8")
6281
self.end_headers()
82+
6383
try:
6484
self.wfile.write(response_body.encode('utf-8'))
6585
except UnicodeDecodeError:
@@ -78,9 +98,16 @@ def do_POST(self):
7898
response = xmlrpc.client.Fault(ex.faultCode, str(ex.reason))
7999
response_body = xmlrpc.client.dumps(response, allow_none=True, methodresponse=True)
80100
self.wfile.write(response_body.encode('utf-8'))
81-
except Exception:
101+
except OverflowError as ex:
102+
self.send_response(555)
103+
self.send_header("Content-type", "application/xml; charset=UTF-8")
104+
self.end_headers()
105+
response_body = '''<error>OverflowError in XML response.</error>'''
106+
self.wfile.write(response_body.encode('utf-8'))
107+
logging.exception("Error while handling request: %s", ex)
108+
except Exception as ex:
82109
self.send_response(500)
83-
logging.exception("Error while handling request")
110+
logging.exception("Error while handling request: %s", ex)
84111

85112
def log_message(self, fmt, *args):
86113
"""Override log_message."""

SoftLayer/transports/xmlrpc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def __call__(self, request):
121121
_ex = error_mapping.get(ex.faultCode, exceptions.SoftLayerAPIError)
122122
raise _ex(ex.faultCode, ex.faultString) from ex
123123
except requests.HTTPError as ex:
124-
raise exceptions.TransportError(ex.response.status_code, str(ex))
124+
err_message = f"{str(ex)} :: {ex.response.content}"
125+
raise exceptions.TransportError(ex.response.status_code, err_message)
125126
except requests.RequestException as ex:
126127
raise exceptions.TransportError(0, str(ex))
127128

tests/CLI/modules/account_tests.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,11 @@ def test_event_jsonraw_output(self):
4444
command = '--format jsonraw account events'
4545
command_params = command.split()
4646
result = self.run_command(command_params)
47-
4847
json_text_tables = result.stdout.split('\n')
49-
print(f"RESULT: {result.output}")
5048
# removing an extra item due to an additional Newline at the end of the output
5149
json_text_tables.pop()
5250
# each item in the json_text_tables should be a list
5351
for json_text_table in json_text_tables:
54-
print(f"TESTING THIS: \n{json_text_table}\n")
5552
json_table = json.loads(json_text_table)
5653
self.assertIsInstance(json_table, list)
5754

@@ -66,6 +63,18 @@ def test_invoice_detail_details(self):
6663
self.assert_no_fail(result)
6764
self.assert_called_with('SoftLayer_Billing_Invoice', 'getInvoiceTopLevelItems', identifier='1234')
6865

66+
def test_invoice_detail_sum_children(self):
67+
result = self.run_command(['--format=json', 'account', 'invoice-detail', '1234', '--details'])
68+
self.assert_no_fail(result)
69+
json_out = json.loads(result.output)
70+
self.assertEqual(len(json_out), 7)
71+
self.assertEqual(json_out[0]['Item Id'], 724951323)
72+
self.assertEqual(json_out[0]['Single'], '$55.50')
73+
self.assertEqual(json_out[0]['Monthly'], '$0.10')
74+
self.assertEqual(json_out[3]['Item Id'], 1111222)
75+
self.assertEqual(json_out[3]['Single'], '$0.00')
76+
self.assertEqual(json_out[3]['Monthly'], '$30.36')
77+
6978
def test_invoice_detail_csv_output_format(self):
7079
result = self.run_command(["--format", "csv", 'account', 'invoice-detail', '1234'])
7180
result_output = result.output.replace('\r', '').split('\n')
@@ -74,7 +83,7 @@ def test_invoice_detail_csv_output_format(self):
7483
'"Create Date","Location"')
7584
self.assertEqual(result_output[1], '724951323,"Private (only) Secondary VLAN IP Addresses",'
7685
'"64 Portable Private IP Addresses (bleg.beh.com)",'
77-
'"$0.00","$0.00","2018-04-04","fra02"')
86+
'"$55.50","$0.10","2018-04-04","fra02"')
7887

7988
# slcli account invoices
8089
def test_invoices(self):

0 commit comments

Comments
 (0)