Так случилось, что работал я инженером-проектировщиком в небольшой компании, занимающейся архитектурным освещением. Работа включала в том числе разработку узлов крепления светильников, что требовало, заодно, посчитать все элементы для этого требующиеся. Подсчет в редакторе таблиц без применения макросов меня решительно не устраивал. Найденные альтернативы были слишком громоздкими и/или платными. Так я и пришел к идее взяться за программирование и изобрести свой велосипед.

Задача и имевшиеся знания

Мои знания программирования на тот момент нельзя было назвать нулевыми. Работа с переменными, циклы, условия, функции, всё это - знания входящие в школьную программу. В моем случае они были очень хорошо усвоены (хотя никакого интереса к программированию я на тот момент не проявлял) т.к. в вузе всё это в курсе информатики я проходил повторно. Более высоких материй я, конечно, не знал.

Также я знал, что у популярных языков программирования точно должны быть инструменты работы с таблицами, а также что многие редакторы таблиц имеют инструменты автоматизации.

На первом этапе было решено, что пока просто посчитать количество изделий будет достаточно, а с тем, как перенести эту информацию в спецификацию, я буду какое-то время справляться без автоматизации.

Ни тени сомнения в том, что цель достижима, как вы понимаете, не было. Но уровень приемлемого по трудозатратам функционала был далеко не очевиден.

Выбор инструментария и оформления исходных данных

От применения известных мне Pascal и Matlab было решено отказаться, т.к. я сомневался, что Паскаль имеет необходимый инструментарий и достаточно большое сообщество, а матлаб, вообще, платный. Выбор был между python (деталей выбора языка программирования я уже не помню, но, скорее всего, просто потому что это был популярный на тот момент язык для новичков) и VBA (потому что работал с таблицами я на тот момент в excel). Решено было использовать python как более универсальный инструмент, в силу того что спектр решаемых за приемлемое время задач был неясен. Оглядываясь сейчас назад, можно с уверенностью сказать, что VBA был бы более эффективен для решения задачи т.к. его таки коллеги бы попробовали, а вот возиться с программой на python (пусть даже скомпилированной) никто не захотел. Однако, с не меньшей уверенностью, можно сказать: выбор был сделан верно, т.к. занимался я всем этим в не рабочее время, а python является сильно более интересным объектом изучения.

Состав узлов крепления было решено оформить следующим образом (где key - метка по которой программа должна находить узел):

Название узла

key

материал1

количество материала1

материал2

количество материала2

Количество узлов схожим образом:

KEY

название узла1

количество узлов 1

название узла2

количество узлов 2

Для работы с таблицами была выбрана библиотека pandas (как она была выбрана, я уже, толком, не помню).

Первая версия программы

Синтаксис python не вызвал никаких проблем, работа с библиотеками тоже освоилась нормально и через пару недель (сколько работы было вложено за эти пару недель я, честно говоря, не особо помню, да и в сроках не уверен, т.к. это было давно) была готова первая версия:

Код
#импортируем pandas для работы с таблицами
import pandas as pd
New=pd.read_excel('спецификация.xlsx', sheet_name='Узлы')
#это количество строк
height=len(New)
#Это количество столбцов
breadth=len(New.columns)
#создаём список ключей
key=list()
#Проходимся по всем ячейкам, находим ключи, добавляем их в список ключей
for x in range(height):
    for y in range(breadth):
        if New.iloc[x][y]=='key':
            key.append([x,y])
#удаляем ненужные переменные
del x
del y
#даём столбцам свои имена для удобства обращения к данным
New.columns=range(breadth)
#Создаем таблицу в которой будет удобно считать
count=pd.DataFrame({})
#обращаемся к именам узлов и материалов в соответствии с их расположением
#записываем всё это в новую таблицу. Каждый узел отдельный столбец
for x in key:
    number=New.iloc[x[0]-1,x[1]]
    quantity=New.iloc[x[0]-1,x[1]+1]
    x[0]=x[0]+1
#ставим условие для материалов чтобы не выходить за рамки таблицы
#затем проверяем чтобы тип данных был строкой и записываем данные по материалам
    while x[0]<height and type(New.iloc[x[0],x[1]])==str:
        count.loc[New.loc[x[0],x[1]], number]=New.loc[x[0],x[1]+1]*quantity
        x[0]=x[0]+1
#Записываем отдельную таблицу с суммами материалов по всем узлам
# (axis=1) складывает столбцы. Получаем один столбец с суммой материалов.
# 0 складывает строки аналогичным образом
result=count.sum(axis=1)
#даём единственному столбцу имя
result.name='итог'
#записываем результаты в excel файл
writer = pd.ExcelWriter('подсчет материалов.xlsx')
#записываем не только результаты, но и то как мы к ним пришли
#чисто на всякий случай, мало ли в данных ошибки
result.to_excel(writer, sheet_name='материалы')
count.to_excel(writer, sheet_name='подсчет')
#заканчиваем запись данных
writer.save()

Программа читает лист "Узлы" файла спецификация.xlsx в той же папке. Проходит по всему листу, находит метки "key" и "KEY" для узлов и их количеств, читает все ячейки под меткой до тех пор пока не наткнется на пустую, после чего переходит к следующему узлу. Все это складывается, перемножается и записывается в отдельную таблицу.xlsx в той же папке. Если с данными что-то не так, то ничего не работает, и никакой информации программа не предоставляет.

Пару слов о развитии программы

Первым, что я добавил, были добавление в итоговый файл других колонок для спецификации и сортировка элементов в алфавитном порядке. Программа, при этом, всё еще осталась достаточно простой: 46 строк -> 265 строк. Данные подтягивались из еще одного xlsx файла, местонахождение которого было жестко прописано в коде, а также с отдельного листа в файле исходных данных.

Код
#импортируем pandas для работы с таблицами
import pandas as pd
#нам нужна возможность округлять числа, так что импортируем math
import math as math
import time #может быть стоит убрать если не будет использоваться в выводе ошибок.

#   ---ЗАКАНЧИВАЕМ ИМПОРТ БИБИЛИОТЕК---

#   ---НАЧИНАЕМ ЗАДАВАТЬ ФУНКЦИИ---

#find_key - функция поиска ячеек с текстом 'key' по исходной таблице
def find_key(Data,key_word):
    height=len(Data)                    #это количество строк
    breadth=len(Data.columns)           #Это количество столбцов
    key=list()                          #создаем лист ключей
    for x in range(height):
        for y in range(breadth):        #Проходимся по всем ячейкам
            if Data.iloc[x,y]==key_word:   #Находим ключи
                key.append([x,y])       #Добавляем их в лист ключей
    return(key)

#функция заполняющая таблицу спецификации из выбранной библиотеки
def from_library(Имя_библиотеки, Название_листа, Таблица_для_меток_отсутствия_в_библиотеке):

    Таблица_для_меток_отсутствия_в_библиотеке=pd.DataFrame()
    Таблица_для_меток_отсутствия_в_библиотеке.name='Позиция'

    if Имя_библиотеки==None or Название_листа==None:
        return 2
    
    try:
        library=pd.read_excel(Имя_библиотеки, sheet_name=Название_листа)
        #По умолчанию индексы 0,1.. ставим в качестве индексов 1 столбец
        library.index=library[0] 
    except:
        return 0
    for x in result_sort.index:
        try:
            Specification.loc[x, 'Наименование']=library.loc[x, 'Наименование']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Наименование']=1
        try:
            Specification.loc[x, 'Обозначение']=library.loc[x, 'Обозначение']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Обозначение']=1
        try:
            Specification.loc[x, 'Код']=library.loc[x, 'Код']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Код']=1
        try:
            Specification.loc[x, 'Производитель']=library.loc[x, 'Производитель']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Производитель']=1
        try:
            Specification.loc[x, 'Единица измерения']=library.loc[x, 'Единица измерения']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Единица измерения']=1
        try:
            Specification.loc[x, 'Масса']=library.loc[x, 'Масса']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Масса']=1
        try:
            Specification.loc[x, 'Примечание']=library.loc[x, 'Примечание']
        except:
            Таблица_для_меток_отсутствия_в_библиотеке.loc[x,'Примечание']=1
        Specification.loc[x, 'Количество']=result_sort.loc[x]
    return Таблица_для_меток_отсутствия_в_библиотеке
    
#   ---ЗАКАНЧИВАЕМ ЗАДАВАТЬ ФУНКЦИИ---

#   ---НАЧИНАЕМ ГОТОВИТЬ ИСХОДНЫЕ ДАННЫЕ---

#   ---считываем данные из xlsx---
Data_nodes=pd.read_excel('спецификация.xlsx', sheet_name='Узлы')

#   ---готовим дополнительные объекты которые понадобятся нам при обработке
#это количество строк
height=len(Data_nodes)
#Это количество столбцов
breadth=len(Data_nodes.columns)
#даём столбцам свои имена для удобства обращения к данным
Data_nodes.columns=range(breadth)
#ищем пустоты
#isnull выдаёт DataFrame заполненный True=пусто/Fasle=есть данные
null=pd.isnull(Data_nodes)
#сюда тоже нужны названия столбцов
null.columns=range(breadth)
warnings='Список предупреждений:'

локальная_библиотека='спецификация.xlsx'
лист_локальной_библиотеки='Библиотека'
общая_библиотека='D:\\Python\\2 Библиотека\\Библиотека.xlsx'
лист_общей_библиотеки='Материалы'
дополнительная_библиотека=None
лист_дополнительной_библиотеки=None

#   ---ЗАКАНЧИВАЕМ ПОДГОТОВКУ ИСХОДНЫХ ДАННЫХ---

#   ---НАЧИНАЕМ ОБРАБОТКУ ДАННЫХ---

#   ---проводим проверку исходных данных---

Errors_count=0
#find_key - функция поиска ячеек с определенным текстом по исходной таблице
key=find_key(Data_nodes,'key')      #Используется для поиска узлов
KEY=find_key(Data_nodes,'KEY')[0]   #Используется для поиска списка узлов

#составляем список узлов
Quantity=pd.DataFrame({}) #сюда мы будем писать список
#сдвигаем KEY на 1 ячейку вниз чтобы попасть на список узлов
KEY[0]+=1   #можно заметить что вниз направлена ось х, а ось у соотв. вправо
#ставим условие для узлов чтобы не выходить за рамки таблицы
#затем проверяем чтобы тип данных был строкой и записываем данные по узлам
while KEY[0]<height and type(Data_nodes.iloc[KEY[0],KEY[1]])==str:
    Quantity.loc[Data_nodes.iloc[KEY[0],KEY[1]], 'количество']=Data_nodes.loc[KEY[0],KEY[1]+1]
    if null.loc[KEY[0],KEY[1]+1]==True:     #Если поле количества пустое
        warnings+="\n"+Data_nodes.iloc[KEY[0],KEY[1]]+' - не введено количество узлов данного типа'
    else : None
    KEY[0]=KEY[0]+1

for a in Quantity.index:                    #проверяем по списку узлов
    count=0                                 #устанавливаем счетчик найденных совпадений
    for b in key:                           #проверяем по узлам
        if a==Data_nodes.loc[b[0]-1,b[1]]:  #проверяем на совпадение узла
            count+=1
    if count==1:                            #Если совпадение 1 раз то всё в порядке
        None
    elif count==0:                          #Если совпадений нет, значит нет спецификации по узлу
        warnings+="\n"+a+' - нет спецификации по данному узлу'
    elif count>1:                           #Есди совпадений >1, значит есть >1 спецификации по узлу
        warnings+="\nВАЖНО!!! "+a+' - по данному узлу имеется 2 разных спецификации'
        Errors_count+=1

for b in key:
    count=0
    for a in Quantity.index:
        if a==Data_nodes.loc[b[0]-1,b[1]]:
            count+=1
    if count==1 and null.loc[b[0]-1,b[1]+1]==True:
        None
    elif count==1 and null.loc[b[0]-1,b[1]+1]==False:
        warnings+="\n"+Data_nodes.loc[b[0]-1,b[1]]+' - количество узлов указано дважды:\nВ списке узлов и в спецификации узла.\nРекомендуется убрать одно из чисел. Приоритет отдаётся списку узлов.'
    elif count==0 and null.loc[b[0]-1,b[1]+1]==True:
        warnings+="\n ВАЖНО!!! "+Data_nodes.loc[b[0]-1,b[1]]+' - нет информации по узлу'
        Errors_count+=1

     
      
if Errors_count>0:
    print(warnings)
    time.sleep(5)
    quit()

#   ---заканчиваем проверку исходных данных---

#   ---обрабатываем данные чтобы программа могла с ними работать---

#   Quantity и key были получены во время проверки исходных данных
# Quantity таблица списка узлов с количествами
# key лист с координатами ключей для спецификаций узлов

#Создаем таблицу в которой будет удобно считать
count=pd.DataFrame({})
#обращаемся к именам узлов и материалов в соответствии с их расположением
#записываем всё это в новую таблицу. Каждый узел отдельный столбец
for x in key:
    number=Data_nodes.iloc[x[0]-1,x[1]]
    try:
        quantity=Quantity.loc[number,"количество"]
    except:
        quantity=Data_nodes.iloc[x[0]-1,x[1]+1]
    x[0]=x[0]+1
    #ставим условие для материалов чтобы не выходить за рамки таблицы
    #затем проверяем чтобы тип данных был строкой и записываем данные по материалам
    while x[0]<height and type(Data_nodes.iloc[x[0],x[1]])==str:
        #вновь не забываем следить что не выходим за рамки таблицы
        #если есть данные в столбце множителя используем
        if x[1]<breadth-2 and null.loc[x[0],x[1]+2]!=True:
            #print(1)
            count.loc[Data_nodes.loc[x[0],x[1]], number]=Data_nodes.loc[x[0],x[1]+1]*quantity*Data_nodes.loc[x[0],x[1]+2]
            x[0]=x[0]+1
        else:
            #print(2)
            count.loc[Data_nodes.loc[x[0],x[1]], number]=Data_nodes.loc[x[0],x[1]+1]*quantity
            x[0]=x[0]+1
#   ---закончили первичную обработку---     ---далее программа независима от способа ввода исходных данных---

#   ---подсчитываем количество материалов---

#Записываем отдельную таблицу с суммами материалов по всем узлам
# (axis=1) складывает столбцы. Получаем один столбец с суммой материалов.
# 0 складывает строки аналогичным образом
result=count.sum(axis=1)
#даём единственному столбцу имя
result.name='итог'
#Округляем подсчитанное количество материалов
for x in range(len(result)):
    result.iloc[x]=math.ceil(result.iloc[x])
#Сортируем по названию материалов (заодно и count тоже сортируем)
result_sort=result.sort_index()
count_sort=count.sort_index()

#   ---заканчиваем подсчет количества материалов---

#   ---переходим к созданию таблицы для спецификации---

Specification=pd.DataFrame()
Specification.loc['Позиция', 'Наименование']='Наименование и техническая характеристика'
Specification.loc['Позиция', 'Обозначение']='Тип, марка, обозначение документа, опросного листа'
Specification.loc['Позиция', 'Код']='Код оборудования, изделия, материала'
Specification.loc['Позиция', 'Производитель']='Завод-изготовитель'
Specification.loc['Позиция', 'Единица измерения']='Единица измерения'
Specification.loc['Позиция', 'Количество']='Количество'
Specification.loc['Позиция', 'Масса']='Масса единицы, кг'
Specification.loc['Позиция', 'Примечание']='Примечание'

#   ---заполняем созданную таблицу---
Except0=pd.DataFrame() #можно заменить. Главное чтобы переменная была. Не понятно зачем.
Except1=pd.DataFrame()
Except2=pd.DataFrame()

Except0=from_library(общая_библиотека,лист_общей_библиотеки ,Except0)
if type(Except0)==int and  Except0 == 0:
    warnings+="\nНе найдена библиотека - "+общая_библиотека+" лист:"+лист_общей_библиотеки

Except1=from_library(дополнительная_библиотека,лист_дополнительной_библиотеки ,Except1)
if type(Except1)==int and  Except1 == 0:
    warnings+="\nНе найдена библиотека - "+дополнительная_библиотека+" лист:"+лист_дополнительной_библиотеки

Except2=from_library(локальная_библиотека,лист_локальной_библиотеки ,Except2)
if type(Except2)==int and  Except2 == 0:
    warnings+="\nНе найдена библиотека - "+локальная_библиотека+" лист:"+лист_локальной_библиотеки
    
#   ---заканчиваем заполнение таблицы из библиотек---

#   ---начинаем стилизацию таблицы---


def style_all(v, props=''):
    return props
StyleTimesNewRoman='font-family: "Times New Roman", Times, serif;'
StyleFontSize='font-size:1em;'
StyleHorizontal='text-align:center;'
StyleVertical='vertical-align:middle;'
StyleAll=StyleTimesNewRoman+StyleFontSize+StyleHorizontal+StyleVertical
StyleName=StyleTimesNewRoman+StyleFontSize+StyleVertical+'text-align:left:'
Specification_Styled=Specification.style.applymap(style_all, props=StyleAll)\
                                        .applymap(style_all, props=StyleName, subset=['Наименование','Примечание'])

#   ---заканчиваем стилизацию таблицы---

#   ---записываем данные в файл---

writer = pd.ExcelWriter('подсчет материалов.xlsx')
Specification_Styled.to_excel(writer, sheet_name='материалы')
if общая_библиотека != None and лист_общей_библиотеки != None:
    try: Except0.to_excel(writer, sheet_name='общая библиотека')
    except: None
if дополнительная_библиотека != None and лист_дополнительной_библиотеки != None:
    try:Except1.to_excel(writer, sheet_name='дополнительная библиотека')
    except: None
if локальная_библиотека != None and лист_локальной_библиотеки != None:
    try: Except2.to_excel(writer, sheet_name='локальная библиотека')
    except: None
writer.save()

Требовалось поделить материалы на категории, чтобы не нужно было раскладывать их вручную, а также был необходим метод оповещения пользователя о том, что именно пошло не так, таким образом, теперь нужен был какой-то интерфейс. Как итог программа была переписана с нуля и количество кода в ней сильно выросло.

Затем мне захотелось добавить новых функций и, ввиду моей неопытности, это вылилось еще в 2 полных переписывания программы (хотя некоторые части кода перекочевывали из версии в версию). Писать про эти промежуточные версии я, пожалуй, не буду т.к. не горю желанием копаться в этих спагетти (да и читатель, вероятно, тоже), но приложу небольшой текстовый файлик, который когда-то составлял чисто для себя, на случай если кому-то интересно.

Мои заметки (работа над программой не была непрерывна, так что даты с трудоемкостью, по большей части, не связаны)

1.0 программа доведена до состояния минимального функционала.
Отличие от более ранней версии в том что материалы узла расположены в один столбец что удобнее чем в строку.
1.01 задействована пустота в 1 столбец между узлами. Добавлена возможность добавления множителя к элементам узла.
Множитель нужно дописывать во все узлы. Требуется доработка?
(Доработка не требуется т.к. множители чаще разные чем одинаковые.)
1.02 добавлено округление итогового количества материалов в большую сторону
1.03 результаты теперь сортируются в алфавитном порядке
1.04 добавлено построение таблицы с материалами для спецификации с вытягиванием информации из двух библиотек: общей и локальной.
1.05 исправлена ошибка из-за которой отсекались строки "масса" и "примечание". Не была убрана попытка забрать количество из библиотеки
1.06 шрифты итоговой таблицы изменены на TimesNewRoman 12.
Добавлено выравнивание текста в ячейках по центру (для Наименований и Примечаний также по левому краю).
1.07 добавлена таблица со списком узлов как альтернативный способ ввода количества узлов.
!!!Не работоспособна. Не считает материалы, хотя достаёт данные из библиотек.
Вероятно версия 1.07 не была протестирована...
1.08 Исправлена ошибка версии 1.07 "Quantity[number,"количество"]->Quantity.loc[number,"количество"]" Восстановлена работа программы.
1.09 Изменено расположение библиотеки

25.09.2022
2.0 Программа полностью переписана. Попытался следовать принципам ООП, не уверен что хорошо получилось.
Улучшения:
1)Добавлена возможность расчета подузлов.
2)Добавлен графический интерфейс
3)Добавлена возможность выбирать файл с исходными данными
(Ранее программа могла работать лишь с файлами имеющими строго определенное имя)
4)Добавлена возможность раздельного расчета материалов по группам узлов
5)Исключена необходимость копировать программу в рабочую папку
6)Исключена необходимость создавать в файле с исходными данными локальную библиотеку
(возможность создания таковой всё еще на месте, но программа работает если листа Библиотека нет)
Возможности для расширения:
1)Добавлен подсчет материалов которых нет в библиотеки
(однако, на данный момент эта информация никак не выводится)
2)Добавлено чтение узлов "как есть". Будет полезно для дальнейшей реализации проверки исходных данных.
3)Метки узлов и списка узлов вынесены в отдельные переменные передающиеся в экземпляры.
(таким образом, можно без особых проблем добавить возможность смены меток через графический интерфейс)
4)Предусмотрена возможность распечатать только материалы по всем узлам сразу или только по категориям.
(пока не уверен насколько это будет полезно, может галочки в интерфейс добавлю)
5)Сортировка таблиц выделена в отдельный метод в классе Библиотека.
(Таким образом, можно удобно доработать метод сортировки таблиц при желании.)
Нейтральные изменения:
1)Ключи теперь ставятся на 1 ячейку правее (над количеством) т.к. ячейка использовавшаяся ранее
удобна для ввода групп узла.
Ухудшения:
1)Ввиду наличия интерфейса программа требует более двух кликов для расчета.
2)Программа запускается с ярлыка на рабочем столе что не всегда удобно т.к. оба монитора при работе могут быть заняты.
(Придется что-то сворачивать, впрочем, это не выглядит как серьезное неудобство)
Очевидно плохое решение:
1)Рабочая папка и файл выбираются отдельно, что не имеет никакого смысла. Рабочая папка должна меняться при выборе файла.
(1.Едва-ли возможность вывода результата расчета в папку не совпадающую с папкой местонахождения файла исходных данных
принесет какую-либо пользу.)
Примечание:
Не написал комментарии к коду. Большая ошибка... (а может и нет, зависит от того насколько много будет изменений в будущем)

26.09.2022
2.01 Добавлен вывод материалов отсутствующих в библиотеках справа от кнопок.

01.10.2022
2.02
1)Немного улучшил структуру кода: Выделил интерфейс и расчет в отдельные классы.
2)При выборе файла автоматически выбирается папка в которой находится этот файл. (кнопку выбора папки пока не стал убирать)
3)Убрал контроль размера окна чтобы его размеры вычислялись ткинтером автоматически.

14.11.2022
2.03
1)Исправлено округление суммы полученных материалов с обычного округления на округление вверх

09.03.2023
2.04
1)Переместил библиотеку в папку программы.

08.04.2023
3.0 Программа еще раз полностью переписана. Отсутствие комментариев таки не особо помешало т.к. старый код я не особо использовал =)
Улучшения:
1) В случае возникновения проблем программа сообщает где в исходных данных могут быть проблемы.
2) Реализована сортировка таблиц.
3) Улучшен интерфейс и настройки программы.
4) Добавлена возможность разнесения количества узлов и их спецификаций по разным листам.
5) Добавлена возможность оставлять в большее количество спецификаций узлов чем типов узлов есть в проекте.
Ухудшения:
1) Больше нет возможности добавлять подузлы (не стал писать этот функционал т.к. не пользовался им).

В итоге, всё это вылилось в программу способную сформировать таблицу спецификации целиком (конечная версия программы отмечена 4.0. В заметках про неё ничего нет, т.к. эту версию я уже забросил на github. Ну и 4.0 только потому что я бросил следить за версиями, по идее, должно было быть что-то вроде 4.хх). При этом, можно разделить материалы по отдельным группам (по принципу: материалы для крепления светильника такого-то типа - нумерованный/не нумерованный список материалов), а узлы можно вложить в другие узлы.

Итоговый результат

Программа обладает простым, но вполне приятным, на мой вкус, интерфейсом. Можно выбрать файл исходных данных и отдельно путь к папке, в которую будет выведен результат работы (при выборе исходных данных, автоматически выбирается та же папка), можно немного настроить что-то под себя, но, по итогу, настройки в интерфейс я так и не добавил (есть только кнопка "вернуть на по-умолчанию"), так что все настройки только через текстовые файлы в папке программы.

Интерфейс

Для создания интерфейса использовалась стандартная библиотека tkinter.

Привожу изображение с частью обоев рабочего стола, т.к. 100% пользователей используют именно эти обои
Привожу изображение с частью обоев рабочего стола, т.к. 100% пользователей используют именно эти обои

В общем-то, gui имеет минимальный функционал. 2 наиболее используемые кнопки выделены цветом, чтобы они быстрее считывались взглядом. Галочка "доп.узлы" отключает поднятие ошибки при наличии узлов, для которых не найдено количество, такие узлы просто игнорируются при расчете.

Показывать окно выбора файла не буду, т.к. это стандартное диалоговое окно tkinter.

Так выглядит окно после выполненного расчета
Так выглядит окно после выполненного расчета

Отображается путь до файла исходных данных, папка в которой будет создан итоговый файл, результат расчета, время и возможные предупреждения.

А вот что отобразится если исходные данные некорректны
А вот что отобразится если исходные данные некорректны

Как видно, программа может выдать простенькое описание того что не так с исходными данными.

А вот окошко настроек
А вот окошко настроек

Материалы сортируются по категориям (которые явно показываются в спецификации) и подкатегориям (которые лишь меняют порядок следования материалов внутри категории). Данные по категориям и подкатегориям хранятся в базе данных вместе с остальной информацией, но на отдельном листе. Базой данных выступает обычная таблица xlsx, после изучения SQL, я пришел к выводу, что нет смысла применять настолько мощные инструменты для столь небольшой задачи.

Описание Library.xlsx

Описание всех имеющихся категорий находится на отдельном листе "Категории".

Пример оформления категорий
Пример оформления категорий
Таблица с данными для категории
Таблица с данными для категории

Материалы расставляются в порядке категорий (Столбец Category в таблице категорий. Столбец Category name содержит информацию о том, как эта категория должна быть обозначена в итоговом оформлении. Столбец Category priority - покадок категорий), а внутри категорий в порядке подкатегорий (Столбец subcategory), в подкатегориях по приоритету (Столбец "Приоритет", чем меньше приоритет тем выше в таблице материал).

Помимо приоритета и данных о материале, есть еще столбец "Есть множитель". Этот столбец нужен, чтобы выводить предупреждение в случае отсутствия/наличия дополнительного множителя, если он предполагается/не предполагается, это бывает удобно, если итоговое количество измеряем в метрах/литрах/килограммах, а не штуках.

Если для подкатегории не заполнены столбцы данных категории, то они дублируются от подкатегории строкой выше (если она пуста то строкой еще выше, пока не будет найдена не пустая строка).

Данные о листах с исходными данными хранятся на листе "Метаданные". Узлы при этом могут быть 4 типов:
1) Нумерованный список (ol) - все что в него входит выводится с нумерацией после названия узла, сам узел номера не имеет
2) Не нумерованный список (ul) - сам узел имеет номер, но все что в него входит выводится без нумерации
3) Список с вложенной нумерацией (num) - сам узел имеет номер, все элементы получают следующую ступень нумерации (узел X.Y, материалы X.Y.1 - X.Y.n)
4) Обычный узел (node) - материалы из обычных узлов раскладываются по категориям спецификации
5) Подузел (subnode) - подузлы могут вкладываться в прочие узлы или в другие подузлы (но только если уровень подузла выше чем уровень того, в который мы его вкладываем. По умолчанию уровень подузла нулевой, уровень указывается слева от метки подузла)

Описание файла исходных данных

Под описание того, где хранятся данные, выделен отдельный лист. Скорее всего, далеко не оптимальная реализация, но это давало мне достаточную гибкость и не требовало особо много времени, поэтому было решено что "и так пойдет".

Оформление "метаданных"
Оформление "метаданных"

Таким образом, можно было иметь сколько угодно листов под оформление, причем лист "спецификация" можно было совместить с листом "узлы".

Количества узлов
Количества узлов

Принцип оформления количеств узлов не изменился, но появилась возможность разделить список на несколько. Иногда бывало удобно, но использовалось редко.

Способы оформления узлов
Способы оформления узлов
Подузлы вложенные друг в друга матрешкой
Подузлы вложенные друг в друга матрешкой

Все типы узлов оформляются более-менее одинаково. Различия в метках и в данных указываемых в ячейке левее метки. Для node - ячейка не несет информации. Для num, ul, ol - в ней должно быть указано название материала, данные которого будут указаны для описания группы материалов (в моем случае это, обычно, было что-то вроде "материалы для крепления светильников типа тип_светильника"), количества в итоговом файле в этой строке не будет. Ну и у подузлов (sub) в этой ячейке указывается уровень подузла. Подузлы меньшего уровня не могут быть вложены в подузлы большего.

Локальная библиотека оформляется также как глобальная, но должна иметь столбец подкатегории материала.

В итоге получается неплохая табличка. Однако при изменении узлов новые материалы всегда добавляются согласно их категории, что не удобно в случае, если спецификация уже была выдана, т.к. позиции старых элементов не должны меняться, решается это костыльно: путем копирования спецификации в локальную библиотеку. Решать эту проблему я так и не стал.

Примеры итоговых таблиц
Это результат расчета файла примера из папки проекта
Это результат расчета файла примера из папки проекта
А вот уже что-то из того что осталось у меня на компе с реального проекта
А вот уже что-то из того что осталось у меня на компе с реального проекта

Разбирать сам код проекта я, пожалуй, в этой статье не буду по 3 причинам:
1) Мне лень копаться в собственном легаси коде
2) Пользователям не знакомым с python это будет не интересно
3) Пользователям знакомым с python едва-ли будет приятно копаться в чужом легаси коде

Итоги и моё мнение по автоматизации

По итогу, я написал еще несколько программ для работы, а программирование закрепилось в качестве хобби, именно поэтому я и продолжал работать над проектом, несмотря на то что основную задачу код уже выполнял. Описанная в статье программа служила мне верой и правдой, и значительно скрасила одну из наиболее скучных частей работы. Выводы я могу сделать следующие:

1) Если вы не собираетесь автоматизировать сразу кучу всего, да так чтобы оно между собой было связано, маленькие и невзрачные программы окупаются по времени гораздо лучше чем большие и красивые. Небольшая программка не требует много времени на создание и уже обеспечивает основную экономию рабочего времени. Может вы и не сможете похвалиться друзьям какую модную программу написали (хотя чего это я, большинство людей вообще не разбирается в программировании, так что похвалиться вы всё равно сможете), зато у вас будет отличная рабочая лошадка.

2) Если вам интересно поизучать программирование, и время на обучение вы вычеркиваете из "себестоимости", то на автоматизацию уходит не так много времени. Можно написать что-то вполне удобное и прилично выглядящее, не потратив на это слишком много времени.

3) Если вы пишете код для себя, то красота этого кода не слишком важна, но важно сохранять код достаточно читаемым чтобы вы не путались в нем до тех пор, пока не вычистите все критичные баги. С высокой долей вероятности, вы ни разу не прикоснетесь к коду после того как он начнет нормально работать, а другие люди не будут его читать вообще никогда. Совершенный код можете оставить программистам, у них проекты сильно больше в объеме, там читаемость критична.

4) Если вы хотите чтобы другие люди использовали ваши программы, постарайтесь обеспечить чтобы они применялись по принципу: что-то уже знакомое + 2-3 дополнительных кнопки. Опыт с python программами был неудачным, даже в уже скомпилированном виде (да, питон можно компилировать, костыли никто не отменял). И это при том что в виде пользователей были инженеры - люди умственного труда. Как показала практика, даже если разобраться для них не особо проблема (они к такому привычные), делать они этого не особо хотят, т.к. умственного труда им работа и так достаточно подкидывает, а тут еще вы со своим кодом. А вот макросы в excel и nanocad, вполне себе, коллегами применяются.

Ссылка на репозиторий. Код распространяется по свободной лицензии (MIT).

Комментарии (5)


  1. mnhunter81
    30.07.2024 05:58
    +4

    Малая автоматизация как она есть. Автору - респект.


  1. economist75
    30.07.2024 05:58
    +3

    Верно, VBA был бы проще. Если в качестве БД используется Excel - чаще всего VBA оптимален и достаточен.
    Но Python+Pandas добавляют ту самую кондовость при работе с данными, которой не хватает в случае если эти данные снаружи и постоянно растут вширь и в длину. Несколько замечаний все же выдам.

    1. В Pandas нет псевдонимов полей, но прекрасно работают русские имена колонок, к которым легко обращаться вот так:
      df.цена = (df.сумма / df.колво).fillna('') Срезы в Python прекрасны, но обилие [ ] [ ] скобок ухудшает понимание кода. При работе в JupyterLab (это самая удобная IDE/среда для подобных проектов и вообще для "анализа) - работает авто-дополнение по нажатию Tab, не надо имена полей набирать руками. Конечно, имена полей д.б. без пробелов, из спецсимволов только _, регистр - нижний. Это простое правило сделает таблицы ближе к людям.

    2. Стайлер в Pandas слабоват (а openpyxl сложен), поэтому форматирование Excel/Calc (XLSX/ODS-файлов) до идеала лучше делать на VBA, код для которого проще Записать (макро-рекордером в Excel), чем Написать. Вы будете приятно удивлены тем фактом что 90% записанного в Excel кода на VBA сработает автоматически и в свободном, бесплатном LibreOffice Calc (при открытии в нем XLS/XLSM/XLSB-файла). Заметьте, записывать надо в Excel, т.к. встроенный макрорекордер в Calc слаб и пишет в "свой" диалект Basic, способен перехватить лишь ~50% команд пользователя через т.н. Dispatcher. В Calc теперь есть все функции листа Excel на том же русском языке, включая новые-модные (заимствованные, к слову, из Pandas). VBA-код для beauty-форматирования при открытии файла можно запускать автоматически. Т.к. он специфичен для каждого типа документа - в первых строках макроса надо определить что именно открыто. Делайте это просто, по содержимому специфических ячеек: If [C3].Value="position" Then Сall Спека Так вы вызовете нужный "форматирующий" макрос.

    3. Использование эл. таблиц позволяет эффективно нагрузить работой коллег. Будучи причастными к результату - они гораздо охотнее будут им пользоваться (коллаборация вообще страшная штука). Пока вы в IDE пишете код - люди с опытом в Excel/Calc нарисуют за вас в ячейках листа полноценный интерфейс, пошаговые "визарды", сделают валидацию на Данные-Проверка и Усл. форматировании, раскрасят как им нра и вообще будут счастливы.


    1. Glasssparrow Автор
      30.07.2024 05:58

      1. Про df.цена = (df.сумма / df.колво).fillna('') не знал, перебирал все столбцы через цикл, может в будущем воспользуюсь. В таком случае, действительно стоило бы имена строк и столбцов оформить доступным для python образом.

      2. Стайлер pandas действительно слабоват, по итогу перешел на openpyxl (последняя версия описанной программы, как раз, его использует). Он, действительно, достаточно сложен, но простейшее форматирование на нем делается вполне приемлемыми усилиями. Мне требовалось лишь выравнивание, шрифт и границы, думаю в большинстве случаев этого должно быть достаточно. Не уверен что форматирование будет удобно делать макросами, т.к. они достаточно ограничены. Но если допилить их в VBA, то вполне. Однако довольно широкий ряд задач можно решить одними макросами, не написав ни строчки кода.

      3. Коллаборация действительно страшная штука. Замечено, что даже если просто обсудить функционал макросов и оформление таблиц с коллегами, их интерес к проекту возрастает многократно =)

      Благодарю за интересный комментарий!


  1. Andrey_Solomatin
    30.07.2024 05:58

    Хороший опыт. Надоест быть инженером, сможете пойти программистом.


  1. Paveldpv
    30.07.2024 05:58

    Круто)! очень приятно читать про подобный опыт на реально выполненных задачах ))