bash + MySQL: скрипт домашней бухгалтерии

Автор: | 02/22/2014
 

terminalХардкорный bash-скрипт для ведения домашней бухгалтерии прямо из консоли 🙂

Пока в нём только «наличные» и, соответственно, только один тип операций — «ввод-вывод» этих самых наличных.

Есть желание в будущем добавить несколько типов платежей (наличные, несколько платёжных карт, webmoney) и переписать на Python или Java с нормальным GUI.
Основная идея написания скрипта была «не дать себе засохнуть» (с) как в плане общения с bash, так и SQL.
Дополнительные сведения в статьях BASH: описание циклов for, while, until и примеры использования и BASH: использование функций, примеры.
Сначала — нам потребуется отдельная база данных:

# mysql -u root -p
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 48050
Server version: 5.5.35 Source distribution
mysql> create database setevoy_money_db1;
Query OK, 1 row affected (0.02 sec)
mysql> grant all on setevoy_money_db1.* to ‘setevoy’@'localhost’;
Query OK, 0 rows affected (0.50 sec)

Далее — создадим две таблицы — в одной будем записывать остаток средств (cash_avail), в другой — куда мы их тратим или откуда мы их получаем (transactions):

mysql> create table transactions (transaction_id int unsigned primary key auto_increment not null, transaction_date date not null, operation_type enum(‘IN’,'OUT’), amount int unsigned not null, description longtext not null);
Query OK, 0 rows affected (0.02 sec)
mysql> create table cash_avail (id int unsigned primary key auto_increment not null, date date not null, amount int unsigned not null);
Query OK, 0 rows affected (0.20 sec)

Вот как они выглядят:

mysql> show tables;
+-----------------------------+
| Tables_in_setevoy_money_db1 |
+-----------------------------+
| cash_avail                  |
| transactions                |
+-----------------------------+
4 rows in set (0.00 sec)
mysql> desc cash_avail;
+--------+------------------+------+-----+---------+----------------+
| Field  | Type             | Null | Key | Default | Extra          |
+--------+------------------+------+-----+---------+----------------+
| id     | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| date   | date             | NO   |     | NULL    |                |
| amount | int(10) unsigned | NO   |     | NULL    |                |
+--------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> desc transactions;
+------------------+------------------+------+-----+---------+----------------+
| Field            | Type             | Null | Key | Default | Extra          |
+------------------+------------------+------+-----+---------+----------------+
| transaction_id   | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| transaction_date | date             | NO   |     | NULL    |                |
| operation_type   | enum('IN','OUT') | YES  |     | NULL    |                |
| sum              | int(10) unsigned | NO   |     | NULL    |                |
| description      | longtext         | NO   |     | NULL    |                |
+------------------+------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Ещё хорошо было бы добавить в cash_avail для каждой записи колонку transactions_id из таблицы transactions.

Пара примеров по работе с таблицами/колонками.

Изменить колонку sum на sum_changed в таблице transactions:

mysql> alter table transactions change sum sum_changed int unsigned not null;
Query OK, 27 rows affected (0.07 sec)
Records: 27  Duplicates: 0  Warnings: 0

mysql> desc transactions;
+------------------+------------------+------+-----+---------+----------------+
| Field            | Type             | Null | Key | Default | Extra          |
+------------------+------------------+------+-----+---------+----------------+
| transaction_id   | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| transaction_date | date             | NO   |     | NULL    |                |
| operation_type   | enum('IN','OUT') | YES  |     | NULL    |                |
| sum_changed      | int(10) unsigned | NO   |     | NULL    |                |
| description      | longtext         | NO   |     | NULL    |                |
+------------------+------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Обновить запись с id=71 в таблице cash_avail и установить новое значение поля amount:

mysql> update cash_avail set amount='1000' where id=71;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from cash_avail where id=71;
+----+------------+--------+
| id | date       | amount |
+----+------------+--------+
| 71 | 2014-02-22 |   1000 |
+----+------------+--------+
1 row in set (0.00 sec)

Или так:

mysql> update cash_avail set amount = replace(amount, '100', '1000') where instr(amount, '100') > 0;

Удалять «лишние» записи — пока только вручную. Для этого — выполняем:

mysql> delete from transactions where transaction_date='2014-02-22';

Больше примеров работы с MySQLНаиболее используемые команды MySQL, Простая работа с MySQL в простых примерах #2 — наполнение таблиц, Простая работа с MySQL в простых примерах #1 — работа с базами, создание таблиц.

Теперь — перейдём к самим скриптам.

Основной скрипт, управляющий — money.sh. Дополнительно к нему — файл functions, в котором просто описываются используемые функции, и который подгружается при запуске скрипта money.sh.

Т.к. основные действия выполняются из функций — рассмотрим сначала functions:

# описываем соединение с сервером MySQL, и передаём первым аргументом нужные команды
connect () {
mysql -D setevoy_money_db1 -u setevoy -pMySeCrEtPaSs -Bse "$1"
}

# иногда дата не задаётся вручную и/или не передаётся переменная, будем использовать функцию для определения переменной, если она пустая
setdate () {
if [ -z $opdate ]
then
opdate=`date +%Y-%m-%d`
fi
}

# фунция collect используется для сбора данных, которые будут использованы функцией insert при добавлении записи о транзакции
collect () {
read -p "Please - enter date (format YYYY-MM-DD, left blank for current): " opdate
# вызываем функцию setdate для проверки значений переменной opdate, если пустая - установим текущую дату
setdate
# тип операции - вход/расход средств, с помощью case in проверяем что бы тип был только один их двух предложенных
while read -p "Please select type - IN or OUT: " optype
do
case $optype in
in|out)
break
;;
*)
echo -e "nPlease - enter IN of OUT!"
esac
done
# указание полученной/затраченной суммы
read -p "Please enter sum: " opsum
# описание тразакции - откуда получили деньги или на что потратили
read -p "Please enter desription: " opdesc
}

# операция вставки в таблицу transactions
insert () {
setdate
connect "insert into transactions values(null, '$opdate', '$optype', '$opsum', '$opdesc');"
# получаем текущий остаток из балицы cash_avail с помощью функции avail
local now=`avail`
# проверяем тип операции - ввод или вывод, и устанавливаем соответствующее значение переменной
# в зависимости от переменной мы или добавляем или отнимаем из переменной now и добавляем новую запись в таблицу cash_avail
if [ $optype = "out" ]
then
local last=`expr  $now - $opsum`
else
local last=`expr  $now + $opsum`
fi
connect "insert into cash_avail values(null, '$opdate', '$last');"
}

# проверка последней записи amount по последнему id в таблице cash_avail
avail () {
connect "select amount from cash_avail where id = (select max(id) from cash_avail);"
}

# проверка последних операций
lastaction () {
# если fromdate будет пустая - выборка с самого начала таблицы
read -p "Enter FROM date: " fromdate
# дата, до которой надо получить данные
read -p "Enter TO date (left blank for current): " todate
# проверяем - если переменная todate пустая - то устанавливаем текущую дату
if [ -z $todate ]
then
todate=`date +%Y-%m-%d`
fi
# можно сделать выборку только по операциям ввода, вывода - или по всем
read -p "Select operation type - IN or OUT (left blank for both): " loptype
if [ -z $loptype ]
then
loptype="%"
fi
echo
connect "select  transaction_date, operation_type, sum, description from transactions where transaction_date between '$fromdate' and '$todate' and operation_type like '$loptype';"
echo
}

# операция вывода списка операций ввода/вывода, выводящая результат в сумме
spending () {
read -p "Enter FROM date: " fromdate
read -p "Enter TO date (left blank for current): " todate
if [ -z $todate ]
then
todate=`date +%Y-%m-%d`
fi
while read -p "Select operation type - IN or OUT: " loptype
do
# смысла смотреть общую сумму и по затратам и по расходам смысла нет - поэтому проверяем, что бы был точно определён тип
if [ -z $loptype ]
then
echo -e "Please - make your choice!"
else
break
fi
done
# немного украшательства текста
if [ $loptype == "in" ]
then
say="earn"
else
say="spend"
fi
# делаем выборку в таблице transactions, и суммируем полученные данные из колонки sum
echo -e "nFrom $fromdate to $todate you $say: "
connect "select sum(sum) from transactions where operation_type='$loptype' and transaction_date between '$fromdate' and '$todate';"
echo
}
# просто функция Да/Нет
answer () {
while read -n1 response; do
echo
case $response in
[yY])
printf "$1n"
return 0
break
;;
[nN])
printf "$3n"
return 1
break
;;
*)
printf "Please, enter y (yes) or n (no)! "
esac
done
}

Теперь перейдём к «управляющему» скрипту money.sh:

#!/usr/local/bin/bash
# подгружаем список функций
source "/home/setevoy/scripts/money/functions"
# рассказываем как использовать скрипт и его опции
usage="nPlease - select action:nn1) Show available cashn2) Insert new transactionn3) Show last transactionsn4) Show spending for periodn"

echo -e "$usage"
# проверяем ответ, если пустой - то снова печатаем USAGE, хотя лучше бы тут сделать с использованием case или regex для точности
while read -n1 -p "Enter 1, 2, 3 or 4: " select
do
if [ -z $select ]
then
echo -e "$usage"
else
break
fi
done

# пошли перебирать ответы
# смотрим доступные средства - вызываем функцию avail
if [ $select == 1 ]
then
echo -en "nnAvailable cash: `avail`nn"
fi

# тут добавляем новую операцию ввода/вывода наличиных
if [ $select == 2 ]
then
echo -e "nnYou selected "Insert new operation".n"
# собираем данные для ввода с помощью функции collect
while collect
do
# перед вводом в базу - даём возможность перепроверить их
echo -en "nYou entered:nndate: $opdatenoperation type: $optypensum: $opsumndescription: $opdescnnIs it correct? Y/n: "
# вызываем функцию answer, если ответ Y (return 0) - то вызываем функцию вставки данных в базу и выводим текущий остаток
answer && insert && echo -e "Now available: `avail`"
# Можно добавить ещё операцию
echo -en "nAdd next? Y/n: "
answer || break
done
fi

# список операций ввода/вывода
if [ $select == 3 ]
then
echo -e "nnYou select "Show last operations".n"
lastaction
fi

# и функция вывода суммы затраченных/полученных средств за период
if [ $select == 4 ]
then
echo -e "nnYour choice #4 - "Show spending for period".n"
spending
fi

Последний штрих — в файл .bashrc добавим alias, что бы не «тянуться» к скрипту напрямую:

alias money="/home/setevoy/scripts/money/money.sh"

Примеры работы:

$ money

Please - select action:

1) Show available cash
2) Insert new transaction
3) Show last transactions
4) Show spending for period

Enter 1, 2, 3 or 4: 1

Available cash: 100
$ money

Please - select action:

1) Show available cash
2) Insert new transaction
3) Show last transactions
4) Show spending for period

Enter 1, 2, 3 or 4: 2

You selected "Insert new operation".

Please - enter date (format YYYY-MM-DD, left blank for current):
Please select type - IN or OUT: in
Please enter sum: 10
Please enter desription: test transaction; income

You entered:

date: 2014-02-22
operation type: in
sum: 10
description: test transaction; income

Is it correct? Y/n: y

Now available: 110

Add next? Y/n: n
$ money

Please - select action:

1) Show available cash
2) Insert new transaction
3) Show last transactions
4) Show spending for period

Enter 1, 2, 3 or 4: 4

Your choice #4 - "Show spending for period".

Enter FROM date: 2014-02-01
Enter TO date (left blank for current):
Select operation type - IN or OUT: out

From 2014-02-01 to 2014-02-22 you spend:
60