Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ This command will generate a excel file named PRD-000-000-000.xlsx in the curren
To synchronize a product from Excel run:

```
$ ccli product sync --in my_products.xlsx
$ ccli product sync my_products.xlsx
```


Expand Down
2 changes: 1 addition & 1 deletion cnctcli/actions/products/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.

from cnctcli.actions.products.export import dump_product # noqa: F401
from cnctcli.actions.products.sync import sync_product, validate_input_file # noqa: F401
from cnctcli.actions.products.sync import ProductSynchronizer # noqa: F401
16 changes: 10 additions & 6 deletions cnctcli/actions/products/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
# Copyright (c) 2019-2020 Ingram Micro. All Rights Reserved.

ITEMS_COLS_HEADERS = {
'A': 'Name',
'A': 'ID',
'B': 'MPN',
'C': 'Billing Period',
'D': 'Reservation',
'E': 'Description',
'F': 'Yearly Commitment',
'C': 'Name',
'D': 'Description',
'E': 'Type',
'F': 'Precision',
'G': 'Unit',
'H': 'Connect Item ID',
'H': 'Billing Period',
'I': 'Commitment',
'J': 'Status',
'K': 'Created',
'L': 'Modified',
}
54 changes: 42 additions & 12 deletions cnctcli/actions/products/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,57 @@ def _setup_cover_sheet(ws, product):
def _setup_items_header(ws):
color = Color('d3d3d3')
fill = PatternFill('solid', color)
cels = ws['A1': 'H1']
cels = ws['A1': 'L1']
for cel in cels[0]:
ws.column_dimensions[cel.column_letter].width = 25
ws.column_dimensions[cel.column_letter].auto_size = True
cel.fill = fill
cel.value = ITEMS_COLS_HEADERS[cel.column_letter]


def _calculate_commitment(item):
period = item.get('period')
if not period:
return '-'
commitment = item.get('commitment')
if not commitment:
return '-'
count = commitment['count']
if count == 1:
return '-'

multiplier = commitment['multiplier']

if multiplier == 'billing_period':
if period == 'monthly':
years = count // 12
return '{} year{}'.format(
years,
's' if years > 1 else '',
)
else:
return '{} years'.format(count)

# One-time
return '-'


def _fill_item_row(ws, row_idx, item):
ws.cell(row_idx, 1, value=item['display_name'])
ws.cell(row_idx, 1, value=item['id'])
ws.cell(row_idx, 2, value=item['mpn'])
ws.cell(row_idx, 3, value=item['period'])
ws.cell(row_idx, 4, value=item['type'] == 'reservation')
ws.cell(row_idx, 5, value=item['description'])
commitment = item['commitment']['count'] == 12 if item.get('commitment') else False
ws.cell(row_idx, 6, value=commitment)
ws.cell(row_idx, 3, value=item['display_name'])
ws.cell(row_idx, 4, value=item['description'])
ws.cell(row_idx, 5, value=item['type'])
ws.cell(row_idx, 6, value=item['precision'])
ws.cell(row_idx, 7, value=item['unit']['unit'])
ws.cell(row_idx, 8, value=item['id'])
ws.cell(row_idx, 8, value=item.get('period', 'monthly'))
ws.cell(row_idx, 9, value=_calculate_commitment(item))
ws.cell(row_idx, 10, value=item['status'])
ws.cell(row_idx, 11, value=item['events']['created']['at'])
ws.cell(row_idx, 12, value=item['events'].get('updated', {}).get('at'))


def _dump_items(ws, api_url, api_key, product_id):
def _dump_items(ws, api_url, api_key, product_id, silent):
_setup_items_header(ws)

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

items = iter(items)

progress = trange(0, count, position=0)
progress = trange(0, count, position=0, disable=silent)

while True:
try:
Expand All @@ -100,7 +130,7 @@ def _dump_items(ws, api_url, api_key, product_id):
break


def dump_product(api_url, api_key, product_id, output_file):
def dump_product(api_url, api_key, product_id, output_file, silent):
if not output_file:
output_file = os.path.abspath(
os.path.join('.', f'{product_id}.xlsx'),
Expand All @@ -110,7 +140,7 @@ def dump_product(api_url, api_key, product_id, output_file):
wb = Workbook()
_setup_cover_sheet(wb.active, product)

_dump_items(wb.create_sheet('product_items'), api_url, api_key, product_id)
_dump_items(wb.create_sheet('product_items'), api_url, api_key, product_id, silent)
wb.save(output_file)

return output_file
231 changes: 141 additions & 90 deletions cnctcli/actions/products/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,108 +15,159 @@
from cnctcli.actions.products.constants import ITEMS_COLS_HEADERS
from cnctcli.api.products import (
create_item,
create_unit,
get_item,
get_item_by_mpn,
get_product,
get_units,
update_item,
)


def _open_workbook(input_file):
try:
return load_workbook(input_file)
except InvalidFileException as ife:
raise ClickException(str(ife))
except BadZipFile:
raise ClickException(f'{input_file} is not a valid xlsx file.')


def _validate_item_sheet(ws):
cels = ws['A1': 'H1']
for cel in cels[0]:
if cel.value != ITEMS_COLS_HEADERS[cel.column_letter]:
raise ClickException(
f'Invalid input file: column {cel.column_letter} '
f'must be {ITEMS_COLS_HEADERS[cel.column_letter]}'
)

class ProductSynchronizer:
def __init__(self, endpoint, api_key, silent):
self._endpoint = endpoint
self._api_key = api_key
self._silent = silent
self._units = get_units(self._endpoint, self._api_key)
self._product_id = None
self._wb = None

def _open_workbook(self, input_file):
try:
self._wb = load_workbook(input_file)
except InvalidFileException as ife:
raise ClickException(str(ife))
except BadZipFile:
raise ClickException(f'{input_file} is not a valid xlsx file.')

def _validate_item_sheet(self, ws):
cels = ws['A1': 'H1']
for cel in cels[0]:
if cel.value != ITEMS_COLS_HEADERS[cel.column_letter]:
raise ClickException(
f'Invalid input file: column {cel.column_letter} '
f'must be {ITEMS_COLS_HEADERS[cel.column_letter]}'
)

def _get_commitment_count(self, data):
period = data[7]
if period == 'onetime':
return 1
if data[8] == '-':
return 1
try:
years, _ = data[8].split()
years = int(years)
if period == 'monthly':
return years * 12
return years
except: # noqa
return 1

def _get_or_create_unit(self, data):
for unit in self._units:
if unit['id'] == data[6]:
return unit['id']
if unit['type'] == data[4] and unit['description'] == data[6]:
return unit['id']

created = create_unit(
self._endpoint,
self._api_key,
{
'description': data[6],
'type': data[4],
'unit': 'unit' if data[4] == 'reservation' else 'unit-h'
},
)
return created['id']

def _get_item_payload(data):
if data[3] is True:
# reservation
period = 'monthly'
if data[2].lower() == 'yearly':
period = 'yearly'
if data[2].lower() == 'onetime':
period = 'onetime'
return {
'name': data[0],
'mpn': data[1],
'description': data[4],
'ui': {'visibility': True},
def _get_item_payload(self, data):
commitment = {
'commitment': {
'count': 12 if data[5] is True else 1,
'count': self._get_commitment_count(data),
},
'unit': {'id': data[6]},
'type': 'reservation',
'period': period,
}
else:
# PPU
return {
'name': data[0],
payload = {
'mpn': data[1],
'description': data[4],
'name': data[2],
'description': data[3],
'type': data[4],
'precision': data[5],
'unit': {'id': self._get_or_create_unit(data)},
'period': data[7],
'ui': {'visibility': True},
'unit': {'id': data[6]},
'type': 'ppu',
'precision': 'decimal(2)',
}


def validate_input_file(api_url, api_key, input_file):
wb = _open_workbook(input_file)
if len(wb.sheetnames) != 2:
raise ClickException('Invalid input file: not enough sheets.')
product_id = wb.active['B5'].value
get_product(api_url, api_key, product_id)

ws = wb[wb.sheetnames[1]]
_validate_item_sheet(ws)

return product_id, wb


def sync_product(api_url, api_key, product_id, wb):
ws = wb[wb.sheetnames[1]]
row_indexes = trange(2, ws.max_row + 1, position=0)
for row_idx in row_indexes:
data = [ws.cell(row_idx, col_idx).value for col_idx in range(1, 9)]
row_indexes.set_description(f'Processing item {data[7] or data[1]}')
if data[7]:
item = get_item(api_url, api_key, product_id, data[7])
elif data[1]:
item = get_item_by_mpn(api_url, api_key, product_id, data[1])
else:
raise ClickException(
f'Invalid item at row {row_idx}: '
'one between MPN or Connect Item ID must be specified.'
if data[4] == 'reservation':
payload.update(commitment)

return payload

def _update_sheet_row(self, ws, row_idx, item):
ws.cell(row_idx, 1, value=item['id'])
ws.cell(row_idx, 10, value=item['status'])
ws.cell(row_idx, 11, value=item['events']['created']['at'])
ws.cell(row_idx, 12, value=item['events'].get('updated', {}).get('at'))

def validate_input_file(self, input_file):
self._open_workbook(input_file)
if len(self._wb.sheetnames) != 2:
raise ClickException('Invalid input file: not enough sheets.')
ws = self._wb[self._wb.sheetnames[0]]
product_id = ws['B5'].value
get_product(self._endpoint, self._api_key, product_id)
ws = self._wb[self._wb.sheetnames[1]]
self._validate_item_sheet(ws)

self._product_id = product_id
return self._product_id

def sync_product(self):
ws = self._wb[self._wb.sheetnames[1]]
row_indexes = trange(2, ws.max_row + 1, position=0, disable=self._silent)
for row_idx in row_indexes:
data = [ws.cell(row_idx, col_idx).value for col_idx in range(1, 13)]
row_indexes.set_description(f'Processing item {data[0] or data[1]}')
if data[0]:
item = get_item(self._endpoint, self._api_key, self._product_id, data[0])
elif data[1]:
item = get_item_by_mpn(self._endpoint, self._api_key, self._product_id, data[1])
else:
raise ClickException(
f'Invalid item at row {row_idx}: '
'one between MPN or Connect Item ID must be specified.'
)
if item:
row_indexes.set_description(f"Updating item {item['id']}")
if item['status'] == 'published':
payload = {
'name': data[2],
'mpn': data[1],
'description': data[3],
'ui': {'visibility': True},
}
else:
payload = self._get_item_payload(data)
if item['type'] == 'ppu':
del payload['period']
update_item(
self._endpoint,
self._api_key,
self._product_id,
item['id'],
payload,
)
self._update_sheet_row(ws, row_idx, item)
continue
row_indexes.set_description(f"Creating item {data[1]}")
item = create_item(
self._endpoint,
self._api_key,
self._product_id,
self._get_item_payload(data),
)
if item:
row_indexes.set_description(f"Updating item {item['id']}")
update_item(
api_url,
api_key,
product_id,
item['id'],
{
'name': data[0],
'mpn': data[1],
'description': data[4],
'ui': {'visibility': True},
},
)
continue
row_indexes.set_description(f"Creating item {data[1]}")
item = create_item(api_url, api_key, product_id, _get_item_payload(data))
ws.cell(row_idx, 8, value=item['id'])
self._update_sheet_row(ws, row_idx, item)

def save(self, output_file):
self._wb.save(output_file)
Loading