Skip to content

Commit 92fed7a

Browse files
author
ffaraoneim
authored
Merge pull request #3 from cloudblue/feedback_20200929
Feedback 20200929
2 parents 536ec90 + 81528ea commit 92fed7a

File tree

10 files changed

+309
-161
lines changed

10 files changed

+309
-161
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ This command will generate a excel file named PRD-000-000-000.xlsx in the curren
137137
To synchronize a product from Excel run:
138138

139139
```
140-
$ ccli product sync --in my_products.xlsx
140+
$ ccli product sync my_products.xlsx
141141
```
142142

143143

cnctcli/actions/products/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.
55

66
from cnctcli.actions.products.export import dump_product # noqa: F401
7-
from cnctcli.actions.products.sync import sync_product, validate_input_file # noqa: F401
7+
from cnctcli.actions.products.sync import ProductSynchronizer # noqa: F401

cnctcli/actions/products/constants.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.
55

66
ITEMS_COLS_HEADERS = {
7-
'A': 'Name',
7+
'A': 'ID',
88
'B': 'MPN',
9-
'C': 'Billing Period',
10-
'D': 'Reservation',
11-
'E': 'Description',
12-
'F': 'Yearly Commitment',
9+
'C': 'Name',
10+
'D': 'Description',
11+
'E': 'Type',
12+
'F': 'Precision',
1313
'G': 'Unit',
14-
'H': 'Connect Item ID',
14+
'H': 'Billing Period',
15+
'I': 'Commitment',
16+
'J': 'Status',
17+
'K': 'Created',
18+
'L': 'Modified',
1519
}

cnctcli/actions/products/export.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,57 @@ def _setup_cover_sheet(ws, product):
4646
def _setup_items_header(ws):
4747
color = Color('d3d3d3')
4848
fill = PatternFill('solid', color)
49-
cels = ws['A1': 'H1']
49+
cels = ws['A1': 'L1']
5050
for cel in cels[0]:
5151
ws.column_dimensions[cel.column_letter].width = 25
5252
ws.column_dimensions[cel.column_letter].auto_size = True
5353
cel.fill = fill
5454
cel.value = ITEMS_COLS_HEADERS[cel.column_letter]
5555

5656

57+
def _calculate_commitment(item):
58+
period = item.get('period')
59+
if not period:
60+
return '-'
61+
commitment = item.get('commitment')
62+
if not commitment:
63+
return '-'
64+
count = commitment['count']
65+
if count == 1:
66+
return '-'
67+
68+
multiplier = commitment['multiplier']
69+
70+
if multiplier == 'billing_period':
71+
if period == 'monthly':
72+
years = count // 12
73+
return '{} year{}'.format(
74+
years,
75+
's' if years > 1 else '',
76+
)
77+
else:
78+
return '{} years'.format(count)
79+
80+
# One-time
81+
return '-'
82+
83+
5784
def _fill_item_row(ws, row_idx, item):
58-
ws.cell(row_idx, 1, value=item['display_name'])
85+
ws.cell(row_idx, 1, value=item['id'])
5986
ws.cell(row_idx, 2, value=item['mpn'])
60-
ws.cell(row_idx, 3, value=item['period'])
61-
ws.cell(row_idx, 4, value=item['type'] == 'reservation')
62-
ws.cell(row_idx, 5, value=item['description'])
63-
commitment = item['commitment']['count'] == 12 if item.get('commitment') else False
64-
ws.cell(row_idx, 6, value=commitment)
87+
ws.cell(row_idx, 3, value=item['display_name'])
88+
ws.cell(row_idx, 4, value=item['description'])
89+
ws.cell(row_idx, 5, value=item['type'])
90+
ws.cell(row_idx, 6, value=item['precision'])
6591
ws.cell(row_idx, 7, value=item['unit']['unit'])
66-
ws.cell(row_idx, 8, value=item['id'])
92+
ws.cell(row_idx, 8, value=item.get('period', 'monthly'))
93+
ws.cell(row_idx, 9, value=_calculate_commitment(item))
94+
ws.cell(row_idx, 10, value=item['status'])
95+
ws.cell(row_idx, 11, value=item['events']['created']['at'])
96+
ws.cell(row_idx, 12, value=item['events'].get('updated', {}).get('at'))
6797

6898

69-
def _dump_items(ws, api_url, api_key, product_id):
99+
def _dump_items(ws, api_url, api_key, product_id, silent):
70100
_setup_items_header(ws)
71101

72102
processed_items = 0
@@ -81,7 +111,7 @@ def _dump_items(ws, api_url, api_key, product_id):
81111

82112
items = iter(items)
83113

84-
progress = trange(0, count, position=0)
114+
progress = trange(0, count, position=0, disable=silent)
85115

86116
while True:
87117
try:
@@ -100,7 +130,7 @@ def _dump_items(ws, api_url, api_key, product_id):
100130
break
101131

102132

103-
def dump_product(api_url, api_key, product_id, output_file):
133+
def dump_product(api_url, api_key, product_id, output_file, silent):
104134
if not output_file:
105135
output_file = os.path.abspath(
106136
os.path.join('.', f'{product_id}.xlsx'),
@@ -110,7 +140,7 @@ def dump_product(api_url, api_key, product_id, output_file):
110140
wb = Workbook()
111141
_setup_cover_sheet(wb.active, product)
112142

113-
_dump_items(wb.create_sheet('product_items'), api_url, api_key, product_id)
143+
_dump_items(wb.create_sheet('product_items'), api_url, api_key, product_id, silent)
114144
wb.save(output_file)
115145

116146
return output_file

cnctcli/actions/products/sync.py

Lines changed: 141 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,108 +15,159 @@
1515
from cnctcli.actions.products.constants import ITEMS_COLS_HEADERS
1616
from cnctcli.api.products import (
1717
create_item,
18+
create_unit,
1819
get_item,
1920
get_item_by_mpn,
2021
get_product,
22+
get_units,
2123
update_item,
2224
)
2325

2426

25-
def _open_workbook(input_file):
26-
try:
27-
return load_workbook(input_file)
28-
except InvalidFileException as ife:
29-
raise ClickException(str(ife))
30-
except BadZipFile:
31-
raise ClickException(f'{input_file} is not a valid xlsx file.')
32-
33-
34-
def _validate_item_sheet(ws):
35-
cels = ws['A1': 'H1']
36-
for cel in cels[0]:
37-
if cel.value != ITEMS_COLS_HEADERS[cel.column_letter]:
38-
raise ClickException(
39-
f'Invalid input file: column {cel.column_letter} '
40-
f'must be {ITEMS_COLS_HEADERS[cel.column_letter]}'
41-
)
42-
27+
class ProductSynchronizer:
28+
def __init__(self, endpoint, api_key, silent):
29+
self._endpoint = endpoint
30+
self._api_key = api_key
31+
self._silent = silent
32+
self._units = get_units(self._endpoint, self._api_key)
33+
self._product_id = None
34+
self._wb = None
35+
36+
def _open_workbook(self, input_file):
37+
try:
38+
self._wb = load_workbook(input_file)
39+
except InvalidFileException as ife:
40+
raise ClickException(str(ife))
41+
except BadZipFile:
42+
raise ClickException(f'{input_file} is not a valid xlsx file.')
43+
44+
def _validate_item_sheet(self, ws):
45+
cels = ws['A1': 'H1']
46+
for cel in cels[0]:
47+
if cel.value != ITEMS_COLS_HEADERS[cel.column_letter]:
48+
raise ClickException(
49+
f'Invalid input file: column {cel.column_letter} '
50+
f'must be {ITEMS_COLS_HEADERS[cel.column_letter]}'
51+
)
52+
53+
def _get_commitment_count(self, data):
54+
period = data[7]
55+
if period == 'onetime':
56+
return 1
57+
if data[8] == '-':
58+
return 1
59+
try:
60+
years, _ = data[8].split()
61+
years = int(years)
62+
if period == 'monthly':
63+
return years * 12
64+
return years
65+
except: # noqa
66+
return 1
67+
68+
def _get_or_create_unit(self, data):
69+
for unit in self._units:
70+
if unit['id'] == data[6]:
71+
return unit['id']
72+
if unit['type'] == data[4] and unit['description'] == data[6]:
73+
return unit['id']
74+
75+
created = create_unit(
76+
self._endpoint,
77+
self._api_key,
78+
{
79+
'description': data[6],
80+
'type': data[4],
81+
'unit': 'unit' if data[4] == 'reservation' else 'unit-h'
82+
},
83+
)
84+
return created['id']
4385

44-
def _get_item_payload(data):
45-
if data[3] is True:
46-
# reservation
47-
period = 'monthly'
48-
if data[2].lower() == 'yearly':
49-
period = 'yearly'
50-
if data[2].lower() == 'onetime':
51-
period = 'onetime'
52-
return {
53-
'name': data[0],
54-
'mpn': data[1],
55-
'description': data[4],
56-
'ui': {'visibility': True},
86+
def _get_item_payload(self, data):
87+
commitment = {
5788
'commitment': {
58-
'count': 12 if data[5] is True else 1,
89+
'count': self._get_commitment_count(data),
5990
},
60-
'unit': {'id': data[6]},
61-
'type': 'reservation',
62-
'period': period,
6391
}
64-
else:
65-
# PPU
66-
return {
67-
'name': data[0],
92+
payload = {
6893
'mpn': data[1],
69-
'description': data[4],
94+
'name': data[2],
95+
'description': data[3],
96+
'type': data[4],
97+
'precision': data[5],
98+
'unit': {'id': self._get_or_create_unit(data)},
99+
'period': data[7],
70100
'ui': {'visibility': True},
71-
'unit': {'id': data[6]},
72-
'type': 'ppu',
73-
'precision': 'decimal(2)',
74101
}
75-
76-
77-
def validate_input_file(api_url, api_key, input_file):
78-
wb = _open_workbook(input_file)
79-
if len(wb.sheetnames) != 2:
80-
raise ClickException('Invalid input file: not enough sheets.')
81-
product_id = wb.active['B5'].value
82-
get_product(api_url, api_key, product_id)
83-
84-
ws = wb[wb.sheetnames[1]]
85-
_validate_item_sheet(ws)
86-
87-
return product_id, wb
88-
89-
90-
def sync_product(api_url, api_key, product_id, wb):
91-
ws = wb[wb.sheetnames[1]]
92-
row_indexes = trange(2, ws.max_row + 1, position=0)
93-
for row_idx in row_indexes:
94-
data = [ws.cell(row_idx, col_idx).value for col_idx in range(1, 9)]
95-
row_indexes.set_description(f'Processing item {data[7] or data[1]}')
96-
if data[7]:
97-
item = get_item(api_url, api_key, product_id, data[7])
98-
elif data[1]:
99-
item = get_item_by_mpn(api_url, api_key, product_id, data[1])
100-
else:
101-
raise ClickException(
102-
f'Invalid item at row {row_idx}: '
103-
'one between MPN or Connect Item ID must be specified.'
102+
if data[4] == 'reservation':
103+
payload.update(commitment)
104+
105+
return payload
106+
107+
def _update_sheet_row(self, ws, row_idx, item):
108+
ws.cell(row_idx, 1, value=item['id'])
109+
ws.cell(row_idx, 10, value=item['status'])
110+
ws.cell(row_idx, 11, value=item['events']['created']['at'])
111+
ws.cell(row_idx, 12, value=item['events'].get('updated', {}).get('at'))
112+
113+
def validate_input_file(self, input_file):
114+
self._open_workbook(input_file)
115+
if len(self._wb.sheetnames) != 2:
116+
raise ClickException('Invalid input file: not enough sheets.')
117+
ws = self._wb[self._wb.sheetnames[0]]
118+
product_id = ws['B5'].value
119+
get_product(self._endpoint, self._api_key, product_id)
120+
ws = self._wb[self._wb.sheetnames[1]]
121+
self._validate_item_sheet(ws)
122+
123+
self._product_id = product_id
124+
return self._product_id
125+
126+
def sync_product(self):
127+
ws = self._wb[self._wb.sheetnames[1]]
128+
row_indexes = trange(2, ws.max_row + 1, position=0, disable=self._silent)
129+
for row_idx in row_indexes:
130+
data = [ws.cell(row_idx, col_idx).value for col_idx in range(1, 13)]
131+
row_indexes.set_description(f'Processing item {data[0] or data[1]}')
132+
if data[0]:
133+
item = get_item(self._endpoint, self._api_key, self._product_id, data[0])
134+
elif data[1]:
135+
item = get_item_by_mpn(self._endpoint, self._api_key, self._product_id, data[1])
136+
else:
137+
raise ClickException(
138+
f'Invalid item at row {row_idx}: '
139+
'one between MPN or Connect Item ID must be specified.'
140+
)
141+
if item:
142+
row_indexes.set_description(f"Updating item {item['id']}")
143+
if item['status'] == 'published':
144+
payload = {
145+
'name': data[2],
146+
'mpn': data[1],
147+
'description': data[3],
148+
'ui': {'visibility': True},
149+
}
150+
else:
151+
payload = self._get_item_payload(data)
152+
if item['type'] == 'ppu':
153+
del payload['period']
154+
update_item(
155+
self._endpoint,
156+
self._api_key,
157+
self._product_id,
158+
item['id'],
159+
payload,
160+
)
161+
self._update_sheet_row(ws, row_idx, item)
162+
continue
163+
row_indexes.set_description(f"Creating item {data[1]}")
164+
item = create_item(
165+
self._endpoint,
166+
self._api_key,
167+
self._product_id,
168+
self._get_item_payload(data),
104169
)
105-
if item:
106-
row_indexes.set_description(f"Updating item {item['id']}")
107-
update_item(
108-
api_url,
109-
api_key,
110-
product_id,
111-
item['id'],
112-
{
113-
'name': data[0],
114-
'mpn': data[1],
115-
'description': data[4],
116-
'ui': {'visibility': True},
117-
},
118-
)
119-
continue
120-
row_indexes.set_description(f"Creating item {data[1]}")
121-
item = create_item(api_url, api_key, product_id, _get_item_payload(data))
122-
ws.cell(row_idx, 8, value=item['id'])
170+
self._update_sheet_row(ws, row_idx, item)
171+
172+
def save(self, output_file):
173+
self._wb.save(output_file)

0 commit comments

Comments
 (0)