From 05d5d2eb16d31ac71498d5775c8b55363d421556 Mon Sep 17 00:00:00 2001 From: rs Date: Sun, 20 Mar 2022 09:30:26 +0300 Subject: [PATCH 01/10] Added: - keyboards for inline buttons --- src/keyboards/keyboards.py | 96 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/keyboards/keyboards.py diff --git a/src/keyboards/keyboards.py b/src/keyboards/keyboards.py new file mode 100644 index 0000000..0f3ceb3 --- /dev/null +++ b/src/keyboards/keyboards.py @@ -0,0 +1,96 @@ +from telegram import InlineKeyboardButton +from telegram import InlineKeyboardMarkup + + +def navigation_buttons(previous_step: str): + return [ + [ + InlineKeyboardButton(text='В меню', + callback_data='navi_main_menu'), + InlineKeyboardButton(text='Назад', + callback_data='navi_' + previous_step), + ], + ] + + +def approve_buttons(previous_step: str): + return [ + [ + InlineKeyboardButton(text='Все верно', + callback_data='item_ok'), + ], + ] + + +def navigation_only_keyboard(previous_step: str): + return InlineKeyboardMarkup(navigation_buttons(previous_step)) + + +def main_menu_keyboard() -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton(text='Сформируем счет', + callback_data='invoice'), + ], + [ + InlineKeyboardButton(text='Выставим акт (WIP)', + callback_data='act'), + ], + ] + return InlineKeyboardMarkup(buttons) + + +def confirm_legal_entity_keyboard(previous_step: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(approve_buttons(previous_step)) + + +def final_confirm_keyboard(previous_step: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(approve_buttons(previous_step)) + + +def select_by_kpp_keyboard(entities) -> InlineKeyboardMarkup: + buttons = [] + + for entity in entities: + buttons.append( + [ + InlineKeyboardButton(text=entity.name, + callback_data=entity.kpp), + ], + ) + + # buttons.append(navigation_buttons(previous_step='inn')[0]) + + return InlineKeyboardMarkup(buttons) + + +def select_item_keyboard(items) -> InlineKeyboardMarkup: + buttons = [] + + for item in items: + buttons.append( + [ + InlineKeyboardButton(text=f'{item.user_name}, цена: {item.price}р.', + callback_data=str(item.user_name)), + ], + ) + buttons.append( + [ + InlineKeyboardButton(text='-', + callback_data='item_minus_' + item.user_name), + InlineKeyboardButton(text=f'{item.amount} шт.', + callback_data='ss'), + InlineKeyboardButton(text='+', + callback_data='item_plus_' + item.user_name), + ], + ) + + buttons.append( + [ + InlineKeyboardButton(text='Готов', + callback_data='item_finish'), + ], + ) + # buttons.append(navigation_buttons(previous_step='inn')[0]) + + return InlineKeyboardMarkup(buttons) From 6f78b7f8f617688f42aff6846e22dae307db3d7b Mon Sep 17 00:00:00 2001 From: rs Date: Sun, 20 Mar 2022 09:36:06 +0300 Subject: [PATCH 02/10] Added new steps in conversation: - main_menu, start_invoice, confirm_legal_entity, select_item, send invoice --- src/bot.py | 144 +++++++++++++++++++++++++++++++++++---------- src/models/item.py | 2 + src/t.py | 3 +- 3 files changed, 116 insertions(+), 33 deletions(-) diff --git a/src/bot.py b/src/bot.py index be2ec9c..fe75ee3 100644 --- a/src/bot.py +++ b/src/bot.py @@ -2,8 +2,8 @@ import os from typing import TYPE_CHECKING -from telegram import ReplyKeyboardMarkup from telegram import Update +from telegram.ext import CallbackQueryHandler from telegram.ext import CommandHandler from telegram.ext import ConversationHandler from telegram.ext import Dispatcher @@ -12,10 +12,11 @@ from telegram.ext import Updater from .clients.dadata import get_by_inn +from .keyboards import keyboards as kb from .models.item import DATABASE from .models.item import parse_input -INN, CONFIRM_LEGAL_ENTITY, ADD_ITEM, CHANGE_PRICE_OR_AMOUNT_OF_LAST_ITEM = range(4) +MAIN_MENU, INN, CONFIRM_LEGAL_ENTITY, SELECT_COURSE, SELECT_ITEMS, SEND_INVOICE = range(6) if TYPE_CHECKING: from .t import CallbackContext @@ -28,64 +29,142 @@ def enable_logging() -> None: ) -def start(update: Update, context: 'CallbackContext') -> int: - update.message.reply_text('Введите ИНН') +def main_menu(update: Update, context: 'CallbackContext') -> int: + update.message.reply_text( + text='Что будем делать?', + reply_markup=kb.main_menu_keyboard(), + ) + + return MAIN_MENU + + +def start_invoice(update: Update, context: 'CallbackContext') -> int: + query = update.callback_query + + query.message.reply_text( + text='Для начала найдем вашу организацию по ИНН.\nВведите ИНН', + # reply_markup=kb.navigation_only_keyboard(previous_step='main_menu'), + ) context.user_data['invoice'] = { 'legal_entity': None, # type: ignore 'items': [], } + query.answer() + + update.callback_query.message.edit_reply_markup() + return INN def inn(update: Update, context: 'CallbackContext') -> int | None: inn: str = update.message.text - entities = get_by_inn(inn) + context.user_data['entities'] = get_by_inn(inn) - if len(entities) == 1: - context.user_data['invoice']['legal_entity'] = entities[0] + if len(context.user_data['entities']) > 0: update.message.reply_text( - text=f'{entities[0]}. Если ошиблись — наберите /start', - reply_markup=ReplyKeyboardMarkup([[i['user_name']] for i in DATABASE.values()]), # type: ignore + text='Мы нашли вот эти организации, выберите вашу', + reply_markup=kb.select_by_kpp_keyboard(entities=context.user_data['entities']), ) - return ADD_ITEM - if len(entities) == 0: + return CONFIRM_LEGAL_ENTITY + + if len(context.user_data['entities']) == 0: update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') - elif len(entities) > 2: - update.message.reply_text('WIP, пока не умеем работать с несколькими юрлицами') + # if len(entities) == 1: + # context.user_data['invoice']['legal_entity'] = entities[0] + # update.message.reply_text( + # text=f'{entities[0]}. Если ошиблись — наберите /start', + # reply_markup=ReplyKeyboardMarkup([[i['user_name']] for i in DATABASE.values()]), # type: ignore + # ) + # return ADD_ITEM + # + # if len(entities) == 0: + # update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') + # elif len(entities) > 2: + # update.message.reply_text('WIP, пока не умеем работать с несколькими юрлицами') -def add_item(update: Update, context: 'CallbackContext') -> int: - item = parse_input(update.message.text) - context.user_data['invoice']['items'] = [item] - legal_entity = context.user_data['invoice']['legal_entity'] +def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: + entities = context.user_data['entities'] - update.message.reply_text( - text=f'Ок, {item}. Выставляем на {legal_entity.name}? Если хотите поменять цену или количество — напишите мне.', - reply_markup=ReplyKeyboardMarkup([['Выставляем!']]), - ) + legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] + + context.user_data['invoice']['legal_entity'] = legal_entity + + update.callback_query.message.reply_text(text=f'Вы выбрали: {legal_entity}\nВсе верно?', + reply_markup=kb.confirm_legal_entity_keyboard(previous_step='inn')) + + update.callback_query.message.edit_reply_markup() - return CHANGE_PRICE_OR_AMOUNT_OF_LAST_ITEM + return SELECT_ITEMS -def change_price_or_amount_of_last_item(update: Update, context: 'CallbackContext') -> None: - number = int(update.message.text) +def select_item(update: Update, context: 'CallbackContext') -> int: + if update.callback_query.data.split('_')[1] == 'ok': + items_initial_load = [parse_input(i['user_name']) for i in DATABASE.values()] - if number < 100: - context.user_data['invoice']['items'][0].amount = number + context.user_data['invoice']['items'] = items_initial_load + + update.callback_query.message.reply_text( + text='Выберите тариф и кол-во участников', + reply_markup=kb.select_item_keyboard(items_initial_load), + ) + + items = context.user_data['invoice']['items'] + + if update.callback_query.data.split('_')[1] == 'minus': + for item in items: + if item.user_name == update.callback_query.data.split('_')[2] and item.amount > 0: + item.amount -= 1 + + update.callback_query.message.edit_text( + text='Выберите тариф и кол-во участников', + reply_markup=kb.select_item_keyboard(items), + ) + + if update.callback_query.data.split('_')[1] == 'plus': + for item in items: + if item.user_name == update.callback_query.data.split('_')[2]: + item.amount += 1 + + update.callback_query.message.edit_text( + text=f'Выберите курс и количество билетов', + reply_markup=kb.select_item_keyboard(items), + ) + + update.callback_query.answer() + + if update.callback_query.data.split('_')[1] == 'finish': + selected_items = '' + + legal_entity = context.user_data['invoice']['legal_entity'] + + for item in items: + if item.amount > 0: + selected_items += str(item) + '\n' + + update.callback_query.message.reply_text( + text=f'Отлично, давайте все проверим.\nВы выбрали:\n{selected_items}\nЮр лицо:\n{legal_entity}\nЕсли все верно выставляем счет', + reply_markup=kb.final_confirm_keyboard(previous_step='select_item')) + + update.callback_query.message.edit_reply_markup() + + return SEND_INVOICE else: - context.user_data['invoice']['items'][0].price = number + return SELECT_ITEMS + +def send_invoice(update: Update, context: 'CallbackContext') -> None: item = context.user_data['invoice']['items'][0] legal_entity = context.user_data['invoice']['legal_entity'] update.message.reply_text( text=f'Ок, {item}. Выставляем на {legal_entity.name}? Если хотите поменять цену или количество — напишите мне.', - reply_markup=ReplyKeyboardMarkup([['Выставляем!']]), + # reply_markup=ReplyKeyboardMarkup([['Выставляем!']]), ) @@ -100,11 +179,14 @@ def main() -> None: dispatcher.add_handler( ConversationHandler( - entry_points=[CommandHandler('start', callback=start)], + entry_points=[CommandHandler('start', callback=main_menu)], states={ + MAIN_MENU: [CallbackQueryHandler(callback=start_invoice, pattern='invoice')], INN: [MessageHandler(callback=inn, filters=Filters.text)], - ADD_ITEM: [MessageHandler(callback=add_item, filters=Filters.text)], - CHANGE_PRICE_OR_AMOUNT_OF_LAST_ITEM: [MessageHandler(callback=change_price_or_amount_of_last_item, filters=Filters.text)], + CONFIRM_LEGAL_ENTITY: [CallbackQueryHandler(callback=confirm_legal_entity)], + SELECT_ITEMS: [CallbackQueryHandler(callback=select_item, pattern='^item_.')], + SEND_INVOICE: [CallbackQueryHandler(callback=send_invoice, ), + ], }, fallbacks=[], allow_reentry=True, diff --git a/src/models/item.py b/src/models/item.py index b77f324..7bc6bc2 100644 --- a/src/models/item.py +++ b/src/models/item.py @@ -3,6 +3,7 @@ @dataclass class Item: + user_name: str name: str price: int | None = 0 amount: int | None = 1 @@ -10,6 +11,7 @@ class Item: @classmethod def from_database(cls, database_record: dict) -> 'Item': return cls( + user_name=database_record['user_name'], name=database_record['name'], price=database_record['price'], ) diff --git a/src/t.py b/src/t.py index a5ad335..f25689b 100644 --- a/src/t.py +++ b/src/t.py @@ -15,12 +15,11 @@ class InvoiceData(TypedDict): class UserData(TypedDict): invoice: InvoiceData - + entities: list[LegalEntity] class CallbackContext(_CallbackContext[UserData, dict, dict]): user_data: UserData - class MessageUpdate(Update): message: Message From f881a87158732ec69beabce9996bcd088f319cd9 Mon Sep 17 00:00:00 2001 From: rs Date: Sun, 20 Mar 2022 09:46:59 +0300 Subject: [PATCH 03/10] =?UTF-8?q?fix=20for=20=D0=98=D0=9F=20legal=20entity?= =?UTF-8?q?=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bot.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/bot.py b/src/bot.py index fe75ee3..06e976a 100644 --- a/src/bot.py +++ b/src/bot.py @@ -74,24 +74,14 @@ def inn(update: Update, context: 'CallbackContext') -> int | None: if len(context.user_data['entities']) == 0: update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') - # if len(entities) == 1: - # context.user_data['invoice']['legal_entity'] = entities[0] - # update.message.reply_text( - # text=f'{entities[0]}. Если ошиблись — наберите /start', - # reply_markup=ReplyKeyboardMarkup([[i['user_name']] for i in DATABASE.values()]), # type: ignore - # ) - # return ADD_ITEM - # - # if len(entities) == 0: - # update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') - # elif len(entities) > 2: - # update.message.reply_text('WIP, пока не умеем работать с несколькими юрлицами') - def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: entities = context.user_data['entities'] - legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] + if len(entities) == 1: + legal_entity = entities[0] + else: + legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] context.user_data['invoice']['legal_entity'] = legal_entity From df3b9de3a72f42c954d6100d826ba6a46cf44ebb Mon Sep 17 00:00:00 2001 From: rs Date: Sat, 26 Mar 2022 17:33:12 +0300 Subject: [PATCH 04/10] refactor code --- src/bot.py | 104 ++++++++++++++++++------------------- src/keyboards/keyboards.py | 44 ++++++---------- src/models/item.py | 4 +- src/t.py | 2 + 4 files changed, 71 insertions(+), 83 deletions(-) diff --git a/src/bot.py b/src/bot.py index 06e976a..ceaeb10 100644 --- a/src/bot.py +++ b/src/bot.py @@ -12,8 +12,10 @@ from telegram.ext import Updater from .clients.dadata import get_by_inn +from .clients.tinkoff import get_invoice from .keyboards import keyboards as kb from .models.item import DATABASE +from .models.item import Item from .models.item import parse_input MAIN_MENU, INN, CONFIRM_LEGAL_ENTITY, SELECT_COURSE, SELECT_ITEMS, SEND_INVOICE = range(6) @@ -39,11 +41,8 @@ def main_menu(update: Update, context: 'CallbackContext') -> int: def start_invoice(update: Update, context: 'CallbackContext') -> int: - query = update.callback_query - - query.message.reply_text( + update.callback_query.message.reply_text( text='Для начала найдем вашу организацию по ИНН.\nВведите ИНН', - # reply_markup=kb.navigation_only_keyboard(previous_step='main_menu'), ) context.user_data['invoice'] = { @@ -51,9 +50,7 @@ def start_invoice(update: Update, context: 'CallbackContext') -> int: 'items': [], } - query.answer() - - update.callback_query.message.edit_reply_markup() + update.callback_query.answer() return INN @@ -78,7 +75,7 @@ def inn(update: Update, context: 'CallbackContext') -> int | None: def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: entities = context.user_data['entities'] - if len(entities) == 1: + if len(entities) == 1: # when legal entity doesn't have kpp (ИП) legal_entity = entities[0] else: legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] @@ -86,49 +83,51 @@ def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: context.user_data['invoice']['legal_entity'] = legal_entity update.callback_query.message.reply_text(text=f'Вы выбрали: {legal_entity}\nВсе верно?', - reply_markup=kb.confirm_legal_entity_keyboard(previous_step='inn')) - - update.callback_query.message.edit_reply_markup() + reply_markup=kb.confirm_legal_entity_keyboard()) return SELECT_ITEMS -def select_item(update: Update, context: 'CallbackContext') -> int: - if update.callback_query.data.split('_')[1] == 'ok': - items_initial_load = [parse_input(i['user_name']) for i in DATABASE.values()] +def update_item_amount(callback: str, items: list[Item], update: Update, context: 'CallbackContext') -> None: + sign = callback.split('_')[1] + item_to_update = callback.split('_')[2] - context.user_data['invoice']['items'] = items_initial_load + for item in items: + if item.user_name != item_to_update: + continue - update.callback_query.message.reply_text( - text='Выберите тариф и кол-во участников', - reply_markup=kb.select_item_keyboard(items_initial_load), - ) + if sign == 'minus' and item.amount > 0: + item.amount -= 1 + else: + item.amount += 1 - items = context.user_data['invoice']['items'] + update.callback_query.message.edit_text( + text='Выберите тариф и кол-во участников', + reply_markup=kb.select_item_keyboard(items), + ) - if update.callback_query.data.split('_')[1] == 'minus': - for item in items: - if item.user_name == update.callback_query.data.split('_')[2] and item.amount > 0: - item.amount -= 1 - update.callback_query.message.edit_text( - text='Выберите тариф и кол-во участников', - reply_markup=kb.select_item_keyboard(items), - ) +def item_initial_load(update: Update, context: 'CallbackContext') -> None: + items_initial_load = [parse_input(str(item['user_name'])) for item in DATABASE.values()] - if update.callback_query.data.split('_')[1] == 'plus': - for item in items: - if item.user_name == update.callback_query.data.split('_')[2]: - item.amount += 1 + context.user_data['invoice']['items'] = items_initial_load - update.callback_query.message.edit_text( - text=f'Выберите курс и количество билетов', - reply_markup=kb.select_item_keyboard(items), - ) + update.callback_query.message.reply_text( + text='Выберите тариф и кол-во участников', + reply_markup=kb.select_item_keyboard(items_initial_load), + ) - update.callback_query.answer() - if update.callback_query.data.split('_')[1] == 'finish': +def select_item(update: Update, context: 'CallbackContext') -> int: + callback = update.callback_query.data + items = context.user_data['invoice']['items'] + + if callback == 'approved': + item_initial_load(update=update, context=context) + update.callback_query.answer() + return SELECT_ITEMS + + if callback == 'item_finish': selected_items = '' legal_entity = context.user_data['invoice']['legal_entity'] @@ -139,23 +138,25 @@ def select_item(update: Update, context: 'CallbackContext') -> int: update.callback_query.message.reply_text( text=f'Отлично, давайте все проверим.\nВы выбрали:\n{selected_items}\nЮр лицо:\n{legal_entity}\nЕсли все верно выставляем счет', - reply_markup=kb.final_confirm_keyboard(previous_step='select_item')) - - update.callback_query.message.edit_reply_markup() + reply_markup=kb.final_confirm_keyboard()) + update.callback_query.answer() return SEND_INVOICE - else: - return SELECT_ITEMS + # update amount of items + update_item_amount(callback=callback, items=items, update=update, context=context) + update.callback_query.answer() + return SELECT_ITEMS + + +def send_invoice(update: Update, context: 'CallbackContext') -> int: + items = [item for item in context.user_data['invoice']['items'] if item.amount > 0] -def send_invoice(update: Update, context: 'CallbackContext') -> None: - item = context.user_data['invoice']['items'][0] legal_entity = context.user_data['invoice']['legal_entity'] - update.message.reply_text( - text=f'Ок, {item}. Выставляем на {legal_entity.name}? Если хотите поменять цену или количество — напишите мне.', - # reply_markup=ReplyKeyboardMarkup([['Выставляем!']]), - ) + update.callback_query.message.reply_text(text=get_invoice(legal_entity=legal_entity, items=items)) + + return SEND_INVOICE def main() -> None: @@ -174,9 +175,8 @@ def main() -> None: MAIN_MENU: [CallbackQueryHandler(callback=start_invoice, pattern='invoice')], INN: [MessageHandler(callback=inn, filters=Filters.text)], CONFIRM_LEGAL_ENTITY: [CallbackQueryHandler(callback=confirm_legal_entity)], - SELECT_ITEMS: [CallbackQueryHandler(callback=select_item, pattern='^item_.')], - SEND_INVOICE: [CallbackQueryHandler(callback=send_invoice, ), - ], + SELECT_ITEMS: [CallbackQueryHandler(callback=select_item, pattern='^item_.|approved')], + SEND_INVOICE: [CallbackQueryHandler(callback=send_invoice)], }, fallbacks=[], allow_reentry=True, diff --git a/src/keyboards/keyboards.py b/src/keyboards/keyboards.py index 0f3ceb3..c4bb6b5 100644 --- a/src/keyboards/keyboards.py +++ b/src/keyboards/keyboards.py @@ -1,31 +1,21 @@ +from typing import List + from telegram import InlineKeyboardButton from telegram import InlineKeyboardMarkup - -def navigation_buttons(previous_step: str): - return [ - [ - InlineKeyboardButton(text='В меню', - callback_data='navi_main_menu'), - InlineKeyboardButton(text='Назад', - callback_data='navi_' + previous_step), - ], - ] +from ..models.item import Item +from ..models.legal_entity import LegalEntity -def approve_buttons(previous_step: str): +def approve_buttons(text: str) -> List[List[InlineKeyboardButton]]: return [ [ - InlineKeyboardButton(text='Все верно', - callback_data='item_ok'), + InlineKeyboardButton(text=text, + callback_data='approved'), ], ] -def navigation_only_keyboard(previous_step: str): - return InlineKeyboardMarkup(navigation_buttons(previous_step)) - - def main_menu_keyboard() -> InlineKeyboardMarkup: buttons = [ [ @@ -40,15 +30,15 @@ def main_menu_keyboard() -> InlineKeyboardMarkup: return InlineKeyboardMarkup(buttons) -def confirm_legal_entity_keyboard(previous_step: str) -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(approve_buttons(previous_step)) +def confirm_legal_entity_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(approve_buttons(text='Все верно')) -def final_confirm_keyboard(previous_step: str) -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(approve_buttons(previous_step)) +def final_confirm_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(approve_buttons(text='Все верно, выставляем счет')) -def select_by_kpp_keyboard(entities) -> InlineKeyboardMarkup: +def select_by_kpp_keyboard(entities: list[LegalEntity]) -> InlineKeyboardMarkup: buttons = [] for entity in entities: @@ -59,12 +49,10 @@ def select_by_kpp_keyboard(entities) -> InlineKeyboardMarkup: ], ) - # buttons.append(navigation_buttons(previous_step='inn')[0]) - return InlineKeyboardMarkup(buttons) -def select_item_keyboard(items) -> InlineKeyboardMarkup: +def select_item_keyboard(items: list[Item]) -> InlineKeyboardMarkup: buttons = [] for item in items: @@ -79,7 +67,7 @@ def select_item_keyboard(items) -> InlineKeyboardMarkup: InlineKeyboardButton(text='-', callback_data='item_minus_' + item.user_name), InlineKeyboardButton(text=f'{item.amount} шт.', - callback_data='ss'), + callback_data='...'), InlineKeyboardButton(text='+', callback_data='item_plus_' + item.user_name), ], @@ -87,10 +75,8 @@ def select_item_keyboard(items) -> InlineKeyboardMarkup: buttons.append( [ - InlineKeyboardButton(text='Готов', + InlineKeyboardButton(text='Готово', callback_data='item_finish'), ], ) - # buttons.append(navigation_buttons(previous_step='inn')[0]) - return InlineKeyboardMarkup(buttons) diff --git a/src/models/item.py b/src/models/item.py index 7bc6bc2..f158f84 100644 --- a/src/models/item.py +++ b/src/models/item.py @@ -3,10 +3,10 @@ @dataclass class Item: - user_name: str name: str price: int | None = 0 - amount: int | None = 1 + amount: int = 0 + user_name: str = '' @classmethod def from_database(cls, database_record: dict) -> 'Item': diff --git a/src/t.py b/src/t.py index f25689b..abf74d5 100644 --- a/src/t.py +++ b/src/t.py @@ -17,9 +17,11 @@ class UserData(TypedDict): invoice: InvoiceData entities: list[LegalEntity] + class CallbackContext(_CallbackContext[UserData, dict, dict]): user_data: UserData + class MessageUpdate(Update): message: Message From 60f77fc24655dbd2e72259c76fa485e7d3504f1d Mon Sep 17 00:00:00 2001 From: rs Date: Sat, 26 Mar 2022 17:52:23 +0300 Subject: [PATCH 05/10] added invoice number generation --- src/clients/tinkoff.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/clients/tinkoff.py b/src/clients/tinkoff.py index 81e1d2b..7bc31d3 100644 --- a/src/clients/tinkoff.py +++ b/src/clients/tinkoff.py @@ -1,4 +1,6 @@ +from datetime import date import os +from random import randint from urllib.parse import urljoin from dotenv import load_dotenv @@ -35,7 +37,7 @@ def get_invoice(legal_entity: LegalEntity, items: list[Item]) -> str: result = post( url='v1/invoice/send', payload={ - 'invoiceNumber': '100501', + 'invoiceNumber': generate_invoice_number(), 'payer': legal_entity.to_tinkoff(), 'items': [item.to_tinkoff() for item in items], }, @@ -47,6 +49,12 @@ def get_invoice(legal_entity: LegalEntity, items: list[Item]) -> str: return result['pdfUrl'] +def generate_invoice_number() -> str: + today = date.today().strftime('%d%m%Y') + digits = str(randint(1000, 9999)) + return digits + today + + if __name__ == '__main__': invoice = get_invoice( legal_entity=LegalEntity(name='ООО Федя и Самат', inn='7722496158', kpp='772201001'), From a36532004b3d65d172c2ca01566db77435ca839b Mon Sep 17 00:00:00 2001 From: rs Date: Sat, 26 Mar 2022 23:37:03 +0300 Subject: [PATCH 06/10] new keyboards and buttons --- src/keyboards/keyboards.py | 71 ++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/src/keyboards/keyboards.py b/src/keyboards/keyboards.py index c4bb6b5..773b44b 100644 --- a/src/keyboards/keyboards.py +++ b/src/keyboards/keyboards.py @@ -7,12 +7,24 @@ from ..models.legal_entity import LegalEntity -def approve_buttons(text: str) -> List[List[InlineKeyboardButton]]: +def return_to_main_menu_button(text: str = 'В меню') -> List[InlineKeyboardButton]: return [ - [ - InlineKeyboardButton(text=text, - callback_data='approved'), - ], + InlineKeyboardButton(text=text, + callback_data='main_menu'), + ] + + +def approve_button(text: str) -> List[InlineKeyboardButton]: + return [ + InlineKeyboardButton(text=text, + callback_data='approved'), + ] + + +def edit_button(text: str, previous_step: str) -> List[InlineKeyboardButton]: + return [ + InlineKeyboardButton(text=text, + callback_data='return_to_' + previous_step), ] @@ -27,15 +39,28 @@ def main_menu_keyboard() -> InlineKeyboardMarkup: callback_data='act'), ], ] + return InlineKeyboardMarkup(buttons) -def confirm_legal_entity_keyboard() -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(approve_buttons(text='Все верно')) +def confirm_legal_entity_and_get_invoice_keyboard() -> InlineKeyboardMarkup: + buttons = [ + approve_button(text='Все верно, выставляем счет!'), + edit_button(text='Изменить юр.лицо', + previous_step='legal_entity'), + ] + return InlineKeyboardMarkup(buttons) -def final_confirm_keyboard() -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(approve_buttons(text='Все верно, выставляем счет')) +def confirm_items_keyboard(show_approve: bool) -> InlineKeyboardMarkup: + buttons = [] + if show_approve: + buttons.append(approve_button(text='Все верно')) + + buttons.append(edit_button(text='Поменять кол-во', + previous_step='select_item')) + + return InlineKeyboardMarkup(buttons) def select_by_kpp_keyboard(entities: list[LegalEntity]) -> InlineKeyboardMarkup: @@ -52,6 +77,17 @@ def select_by_kpp_keyboard(entities: list[LegalEntity]) -> InlineKeyboardMarkup: return InlineKeyboardMarkup(buttons) +def select_course_keyboard() -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton(text='Асинхронная архитектура', # replace hardcode + callback_data='course_aa'), + ], + ] + + return InlineKeyboardMarkup(buttons) + + def select_item_keyboard(items: list[Item]) -> InlineKeyboardMarkup: buttons = [] @@ -59,7 +95,7 @@ def select_item_keyboard(items: list[Item]) -> InlineKeyboardMarkup: buttons.append( [ InlineKeyboardButton(text=f'{item.user_name}, цена: {item.price}р.', - callback_data=str(item.user_name)), + callback_data='item_noaction'), ], ) buttons.append( @@ -67,7 +103,7 @@ def select_item_keyboard(items: list[Item]) -> InlineKeyboardMarkup: InlineKeyboardButton(text='-', callback_data='item_minus_' + item.user_name), InlineKeyboardButton(text=f'{item.amount} шт.', - callback_data='...'), + callback_data='item_noaction'), InlineKeyboardButton(text='+', callback_data='item_plus_' + item.user_name), ], @@ -79,4 +115,17 @@ def select_item_keyboard(items: list[Item]) -> InlineKeyboardMarkup: callback_data='item_finish'), ], ) + + return InlineKeyboardMarkup(buttons) + + +def link_to_invoice_keyboard(url: str) -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton(text='Скачать счет', + url=url), + ], + return_to_main_menu_button(), + ] + return InlineKeyboardMarkup(buttons) From 307dc5278fc264ebe2348d80c8fad75665ba597e Mon Sep 17 00:00:00 2001 From: rs Date: Sat, 26 Mar 2022 23:38:01 +0300 Subject: [PATCH 07/10] changed step order added navigation buttons --- src/bot.py | 200 +++++++++++++++++++++++++++++---------------- src/models/item.py | 2 +- 2 files changed, 131 insertions(+), 71 deletions(-) diff --git a/src/bot.py b/src/bot.py index ceaeb10..2fbb249 100644 --- a/src/bot.py +++ b/src/bot.py @@ -18,7 +18,8 @@ from .models.item import Item from .models.item import parse_input -MAIN_MENU, INN, CONFIRM_LEGAL_ENTITY, SELECT_COURSE, SELECT_ITEMS, SEND_INVOICE = range(6) +MAIN_MENU, SELECT_COURSE, SELECT_ITEMS, FIND_LEGAL_ENTITY, SELECT_LEGAL_ENTITY, CONFIRM_LEGAL_ENTITY, SEND_INVOICE = range( + 7) if TYPE_CHECKING: from .t import CallbackContext @@ -31,61 +32,51 @@ def enable_logging() -> None: ) -def main_menu(update: Update, context: 'CallbackContext') -> int: - update.message.reply_text( - text='Что будем делать?', +def button_main_menu(update: Update, context: 'CallbackContext') -> int: + update.callback_query.message.reply_text( + text='Привет! Я бот-помощник Школы сильных программистов. ' + 'С моей помощью вы можете получить счет для оплаты ' + 'обучения сотрудников от юрлица', reply_markup=kb.main_menu_keyboard(), ) + return SELECT_COURSE - return MAIN_MENU + +def command_main_menu(update: Update, context: 'CallbackContext') -> int: + update.message.reply_text( + text='Привет! Я бот-помощник Школы сильных программистов. ' + 'С моей помощью вы можете получить счет для оплаты ' + 'обучения сотрудников от юрлица', + reply_markup=kb.main_menu_keyboard(), + ) + return SELECT_COURSE -def start_invoice(update: Update, context: 'CallbackContext') -> int: +def select_course(update: Update, context: 'CallbackContext') -> int: update.callback_query.message.reply_text( - text='Для начала найдем вашу организацию по ИНН.\nВведите ИНН', + text='Доступ к какому курсу вы хотите приобрести?', + reply_markup=kb.select_course_keyboard(), ) - context.user_data['invoice'] = { - 'legal_entity': None, # type: ignore - 'items': [], - } - update.callback_query.answer() - return INN - - -def inn(update: Update, context: 'CallbackContext') -> int | None: - inn: str = update.message.text - - context.user_data['entities'] = get_by_inn(inn) - - if len(context.user_data['entities']) > 0: - update.message.reply_text( - text='Мы нашли вот эти организации, выберите вашу', - reply_markup=kb.select_by_kpp_keyboard(entities=context.user_data['entities']), - ) - - return CONFIRM_LEGAL_ENTITY - - if len(context.user_data['entities']) == 0: - update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') - + return SELECT_ITEMS -def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: - entities = context.user_data['entities'] - if len(entities) == 1: # when legal entity doesn't have kpp (ИП) - legal_entity = entities[0] - else: - legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] +def item_initial_load(update: Update, context: 'CallbackContext') -> None: + context.user_data['invoice'] = { + 'legal_entity': None, # type: ignore + 'items': [], + } - context.user_data['invoice']['legal_entity'] = legal_entity + items_initial_load = [parse_input(str(item['user_name'])) for item in DATABASE.values()] - update.callback_query.message.reply_text(text=f'Вы выбрали: {legal_entity}\nВсе верно?', - reply_markup=kb.confirm_legal_entity_keyboard()) + context.user_data['invoice']['items'] = items_initial_load - return SELECT_ITEMS + update.callback_query.message.reply_text( + text='Выберите тариф и кол-во участников', + reply_markup=kb.select_item_keyboard(items_initial_load), + ) def update_item_amount(callback: str, items: list[Item], update: Update, context: 'CallbackContext') -> None: @@ -98,50 +89,63 @@ def update_item_amount(callback: str, items: list[Item], update: Update, context if sign == 'minus' and item.amount > 0: item.amount -= 1 - else: + elif sign == 'plus': item.amount += 1 + update.callback_query.answer() + update.callback_query.message.edit_text( text='Выберите тариф и кол-во участников', reply_markup=kb.select_item_keyboard(items), ) -def item_initial_load(update: Update, context: 'CallbackContext') -> None: - items_initial_load = [parse_input(str(item['user_name'])) for item in DATABASE.values()] +def item_confirm_message(update: Update, context: 'CallbackContext', items: list[Item]) -> None: + selected_items = '' + total_amount = 0 - context.user_data['invoice']['items'] = items_initial_load + for item in items: + if item.amount > 0: + selected_items += str(item) + '\n' + + total_amount += item.amount * item.price + + if total_amount == 0: + text = 'Похоже вы ничего не выбрали' + show_approve = False + else: + text = f'Отлично, давайте проверим.\n\nВы выбрали:\n\n{selected_items}\n\n' \ + f'Итоговая сумма: {total_amount}\n\nВсе верно?' + show_approve = True update.callback_query.message.reply_text( - text='Выберите тариф и кол-во участников', - reply_markup=kb.select_item_keyboard(items_initial_load), - ) + text=text, + reply_markup=kb.confirm_items_keyboard(show_approve=show_approve)) def select_item(update: Update, context: 'CallbackContext') -> int: callback = update.callback_query.data - items = context.user_data['invoice']['items'] - if callback == 'approved': + # initial load of items + if callback == 'course_aa': item_initial_load(update=update, context=context) update.callback_query.answer() return SELECT_ITEMS - if callback == 'item_finish': - selected_items = '' - - legal_entity = context.user_data['invoice']['legal_entity'] - - for item in items: - if item.amount > 0: - selected_items += str(item) + '\n' + # buttons with no action + if callback == 'item_noaction': + update.callback_query.answer() + return SELECT_ITEMS - update.callback_query.message.reply_text( - text=f'Отлично, давайте все проверим.\nВы выбрали:\n{selected_items}\nЮр лицо:\n{legal_entity}\nЕсли все верно выставляем счет', - reply_markup=kb.final_confirm_keyboard()) + items = context.user_data['invoice']['items'] + # "Готово" button + if callback == 'item_finish': + item_confirm_message(update=update, + context=context, + items=items) update.callback_query.answer() - return SEND_INVOICE + return FIND_LEGAL_ENTITY # update amount of items update_item_amount(callback=callback, items=items, update=update, context=context) @@ -149,14 +153,65 @@ def select_item(update: Update, context: 'CallbackContext') -> int: return SELECT_ITEMS +def find_legal_entity_by_inn(update: Update, context: 'CallbackContext') -> int: + update.callback_query.message.reply_text( + text='Введите ИНН компании, чтобы мы могли выставить вам счет', + ) + + update.callback_query.answer() + + return SELECT_LEGAL_ENTITY + + +def select_legal_entity_by_kpp(update: Update, context: 'CallbackContext') -> int | None: + inn: str = update.message.text + + context.user_data['entities'] = get_by_inn(inn) + + if len(context.user_data['entities']) > 0: + update.message.reply_text( + text='Мы нашли вот эти организации, выберите вашу', + reply_markup=kb.select_by_kpp_keyboard(entities=context.user_data['entities']), + ) + + return CONFIRM_LEGAL_ENTITY + + if len(context.user_data['entities']) == 0: + update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') + + return SELECT_LEGAL_ENTITY + + +def confirm_legal_entity(update: Update, context: 'CallbackContext') -> int: + entities = context.user_data['entities'] + + if len(entities) == 1: # when legal entity doesn't have kpp (ИП) + legal_entity = entities[0] + else: + legal_entity = [entity for entity in entities if entity.kpp == update.callback_query.data][0] + + context.user_data['invoice']['legal_entity'] = legal_entity + + update.callback_query.message.reply_text(text=f'Вы выбрали: {legal_entity}\nВсе верно?', + reply_markup=kb.confirm_legal_entity_and_get_invoice_keyboard()) + + return SEND_INVOICE + + def send_invoice(update: Update, context: 'CallbackContext') -> int: items = [item for item in context.user_data['invoice']['items'] if item.amount > 0] legal_entity = context.user_data['invoice']['legal_entity'] - update.callback_query.message.reply_text(text=get_invoice(legal_entity=legal_entity, items=items)) + text = 'Ура! Вас счет готов.' \ + 'Если возникли ошибки — напишите пожалуйста на support@education.borshev.com.' - return SEND_INVOICE + update.callback_query.message.reply_text(text=text, + reply_markup=kb.link_to_invoice_keyboard( + url=get_invoice(legal_entity=legal_entity, + items=items))) + + return MAIN_MENU def main() -> None: @@ -170,15 +225,20 @@ def main() -> None: dispatcher.add_handler( ConversationHandler( - entry_points=[CommandHandler('start', callback=main_menu)], + entry_points=[CommandHandler('start', callback=command_main_menu)], states={ - MAIN_MENU: [CallbackQueryHandler(callback=start_invoice, pattern='invoice')], - INN: [MessageHandler(callback=inn, filters=Filters.text)], + SELECT_COURSE: [CallbackQueryHandler(callback=select_course, pattern='invoice')], + SELECT_ITEMS: [CallbackQueryHandler(callback=select_item, pattern='^item_.|course_aa')], + FIND_LEGAL_ENTITY: [CallbackQueryHandler(callback=find_legal_entity_by_inn, pattern='approved')], + SELECT_LEGAL_ENTITY: [MessageHandler(callback=select_legal_entity_by_kpp, filters=Filters.text)], CONFIRM_LEGAL_ENTITY: [CallbackQueryHandler(callback=confirm_legal_entity)], - SELECT_ITEMS: [CallbackQueryHandler(callback=select_item, pattern='^item_.|approved')], - SEND_INVOICE: [CallbackQueryHandler(callback=send_invoice)], + SEND_INVOICE: [CallbackQueryHandler(callback=send_invoice, pattern='approved')], }, - fallbacks=[], + fallbacks=[ + CallbackQueryHandler(callback=button_main_menu, pattern='main_menu'), + CallbackQueryHandler(callback=find_legal_entity_by_inn, pattern='return_to_legal_entity'), + CallbackQueryHandler(callback=select_item, pattern='return_to_select_item'), + ], allow_reentry=True, ), ) diff --git a/src/models/item.py b/src/models/item.py index f158f84..9bd9bcf 100644 --- a/src/models/item.py +++ b/src/models/item.py @@ -4,7 +4,7 @@ @dataclass class Item: name: str - price: int | None = 0 + price: int = 0 amount: int = 0 user_name: str = '' From 226b7291c3ae5863b54e646927796e45727eb709 Mon Sep 17 00:00:00 2001 From: rs Date: Sun, 27 Mar 2022 12:29:26 +0300 Subject: [PATCH 08/10] =?UTF-8?q?bug=20fix:=20select=20legal=20entity=20fo?= =?UTF-8?q?r=20=D0=98=D0=9F=20(no=20kpp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/keyboards/keyboards.py | 7 ++++++- src/models/legal_entity.py | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/keyboards/keyboards.py b/src/keyboards/keyboards.py index 773b44b..5872439 100644 --- a/src/keyboards/keyboards.py +++ b/src/keyboards/keyboards.py @@ -67,10 +67,15 @@ def select_by_kpp_keyboard(entities: list[LegalEntity]) -> InlineKeyboardMarkup: buttons = [] for entity in entities: + if entity.kpp is None: + kpp_for_callback = 0 + else: + kpp_for_callback = entity.kpp + buttons.append( [ InlineKeyboardButton(text=entity.name, - callback_data=entity.kpp), + callback_data=kpp_for_callback), ], ) diff --git a/src/models/legal_entity.py b/src/models/legal_entity.py index 54d1168..b4e581e 100644 --- a/src/models/legal_entity.py +++ b/src/models/legal_entity.py @@ -9,14 +9,17 @@ class LegalEntity: kpp: str def __str__(self) -> str: - return f'{self.name}, ИНН {self.inn}, КПП {self.kpp}' + if self.kpp is None: + return f'{self.name}, ИНН {self.inn}' + else: + return f'{self.name}, ИНН {self.inn}, КПП {self.kpp}' @classmethod def from_dadata_response(cls, response: dict) -> 'LegalEntity': return cls( name=response['value'], inn=response['data']['inn'], - kpp=response['data'].get('kpp', 0), + kpp=response['data'].get('kpp', None), ) def to_tinkoff(self) -> dict: From e6ddbff34af0798029a839833c2551ac155372a9 Mon Sep 17 00:00:00 2001 From: rs Date: Sun, 27 Mar 2022 19:16:02 +0300 Subject: [PATCH 09/10] new keyboard for wrong inn --- src/bot.py | 3 ++- src/keyboards/keyboards.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bot.py b/src/bot.py index 2fbb249..6a5f071 100644 --- a/src/bot.py +++ b/src/bot.py @@ -177,7 +177,8 @@ def select_legal_entity_by_kpp(update: Update, context: 'CallbackContext') -> in return CONFIRM_LEGAL_ENTITY if len(context.user_data['entities']) == 0: - update.message.reply_text('Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде') + update.message.reply_text(text='Чё-т ничего не нашлось :( Попробуйте ещё раз или напишите Феде', + reply_markup=kb.wrong_inn_keyboard()) return SELECT_LEGAL_ENTITY diff --git a/src/keyboards/keyboards.py b/src/keyboards/keyboards.py index 5872439..1736a37 100644 --- a/src/keyboards/keyboards.py +++ b/src/keyboards/keyboards.py @@ -28,6 +28,13 @@ def edit_button(text: str, previous_step: str) -> List[InlineKeyboardButton]: ] +def wrong_inn_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup([ + edit_button(text='Ввести инн заново', + previous_step='legal_entity'), + ]) + + def main_menu_keyboard() -> InlineKeyboardMarkup: buttons = [ [ From 9f356d79a8afb5d8b892fd0310177ba41de6ac9c Mon Sep 17 00:00:00 2001 From: rs Date: Wed, 20 Apr 2022 17:39:15 +0300 Subject: [PATCH 10/10] correct prices --- src/models/item.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/item.py b/src/models/item.py index 9bd9bcf..3595e1b 100644 --- a/src/models/item.py +++ b/src/models/item.py @@ -33,17 +33,17 @@ def __str__(self) -> str: 'aa-3-self': { 'user_name': 'АА-3 самостоятельный', 'name': 'Участие в курсе «Асинхронная Архитектура на плошадке education.borshev.com (тариф «самостоятельный»)', - 'price': 10_000, + 'price': 15_500, }, 'aa-3-full': { 'user_name': 'АА-3 тусовка', 'name': 'Участие в курсе «Асинхронная Архитектура на плошадке education.borshev.com (тариф «в тусовке»)', - 'price': 20_000, + 'price': 26_000, }, 'aa-3-vip': { 'user_name': 'АА-3 VIP', 'name': 'Участие в курсе «Асинхронная Архитектура на плошадке education.borshev.com (тариф «VIP»)', - 'price': 32_000, + 'price': 40_000, }, }