Для дома, для семьи

Материал из Eludia
Перейти к: навигация, поиск
Она схватила ему за руку и неоднократно спросила: где ты девал деньги?

А. Аверченко, «Почтовый ящик «Сатирикона» (1909, № 48, стр. 8)».

Содержание

Этот очерк является продолжением или, в некотором смысле, новой версией Краткого курса. Его основное назначение — показать процесс разработки реального приложения на базе Eludia.pm специалисту, который начинает знакомиться с нашей технологией.

Потребность в новой статье[1] обусловлена тем, что:

  • краткий курс несколько устарел, со времени его написания API обогатился новыми функциями, которые стоит использовать с самого начала;
  • краткий курс был одноразовым экспериментом по измереню скорости разработки в чистом вакууме. Повторить его вполне реально, но неинтерсно.

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

Работа велась в течение 3 астрономических суток 5-6 сессиями по 0.5-2 часа в (довольно напряжённых) домашних условиях. Помимо собственно приложения за это время серьёзному усовершенствованию подверглись backend для СУБД SQLite и модуль Eludia::Server. Соответствующие подробности в описании опущены, так как к логике системы они прямого отношения не имеют.

Постановка задачи

Рассмотрим молодую семью с 2 компьютерами и 1 маршрутизатором, перед которой стоит необходимость наладить мониторинг бюджетных расходов. Требуется разработать информационную систему, позволяющую:

  • вводить и редактировать первичные данные о расходах (в основном чеки);
  • разбивать расходы по статьям и степени необходимости;
  • вести поиск с различными фильтрами в реестре трат;
  • просматривать статистику в необходимых разрезах;
  • сопоставлять цены разных поставщиков и в разные периоды на те товары, для которых это имеет смысл.

Начало работы

Этот раздел мы позволим себе не приводить, так как, с одной стороны, в плане разработки приложения здесь нет абсолютно ничего нового по сравнению с уже упоминавшимся Кратким курсом, с другой — в реальности имела место доработка ядра под SQLite и автономную работу, о чём, возможно, будет отдельный рассказ.

В общем, за точку отсчёта примем шаблонное приложение с готовым редактором пользователей (users). Разумеется, StEludio считается запущенным.

Справочники

Примерно половина любой информационной системы — простые справочники, без дочерних объектов, иерархии, истории и прочих фокусов. Это — фундамент. С него и начнём.

Степени необходимости

Любой подсчёт денег ведётся для того, чтобы их экономить. А для этого весьма не лишне распределять траты по мере их осмысленности. Для начала зададимся 5 градациями.

Переходим к модели (F6), нажимаем Ctrl-N, вводим имя таблицы: voc_needs. Зачищаем шаблонный код:

columns => {
	label   => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
},

и приписываем в конец наши предопределённые строки:

data => [
	{id => 1, fake => 0, label => 'Вопрос жизни и смерти'},
	{id => 2, fake => 0, label => 'По делу, причём дешёво'},
	{id => 3, fake => 0, label => 'Нужная вещь по разумной цене'},
	{id => 4, fake => 0, label => 'Дорогое удовольствие'},
	{id => 5, fake => 0, label => 'Бездарная трата денег'},
],

Сохраняемся. Так, табличка готова.

Статьи расходов

Разумеется, расходы должны делиться и по физической природе. Этот справочник хочется сделать не статическим, а, наоборот, произвольно редактируемым. Будем считать, что статья расходов — это просто название. Поехали.

В том же редакторе модели нажимаем Ctrl-N, вводим имя таблицы: voc_articles. Оставляем в описании лишь

columns => {
	label   => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
},

Теперь F6 в редактор экранов. Заходим в тип menu (щёлкаем по строке 'menu' в левом верхнем списке либо фокусируемся на нём F8, дальше ходим стрелками и выбираем Enter'ом) и вписываем в select_menu фрагмент:

{
 name  => '_vocs',
 label => 'Справочники',
 no_page => 1,
 items => [
  {
   name  => 'voc_articles',
   label => 'Статьи расходов',
  },
 ],
},

Можно обновить страницу: пункт меню показался на своём месте.

Polouchka 01.gif

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

Жмём Ctrl-N и снова вводим voc_articles (теперь это тип экрана). По шаблону генерируется select_voc_articles. Убираем оттуда всё, что связано со справочником пользователей users, остаётся:

sub select_voc_articles {

 sql (

  {},

  voc_articles => [

   'id_user',

   ['label LIKE %?%' => $_REQUEST {q}],

   [ LIMIT => [0 + $_REQUEST {start}, $conf -> {portion}]],

  ],

 ); 

}

Теперь отобразим полученную выборку: разработаем презентационную процедурку. Нажимаем Alt-W, получаем draw_voc_articles. После удаления лишних кнопок (которые вполне могли бы пригодиться в других обстоятельствах) получаем:

sub draw_voc_articles {

 my ($data) = @_;

 return

  draw_table (

   sub {

    draw_cells ({
     href => "/?type=voc_articles&id=$$i{id}",
    },[
     $i -> {label},
    ])

   },

   $data -> {voc_articles},

   {

    name => 't1',

    title => {label => 'Статьи расходов'},

    top_toolbar => [{
      keep_params => ['type', 'select'],
     },

     {
      icon  => 'create',
      label => '&Добавить',
      href  => '?type=voc_articles&action=create',
     },

     {
      type  => 'input_text',
      label => 'Искать',
      name  => 'q',
      keep_params => [],
     },

     {
      type    => 'pager',
     },

     fake_select (),

    ],

   }

  );

}

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

Polouchka 02.gif

Аккуратно, но пусто. Самое время добавить запись. По поводу действия добавления беспокоиться не стоит: процедура do_create_DEFAULT всё сделает сама. Но нарисовать форму редактирования всё-таки надо.

Снова подготовим данные для экрана. Нажимаем Alt-G и получаем get_item_of_voc_articles. Урезаем его до:

sub get_item_of_voc_articles {

 my $data = sql ('voc_articles');

 $_REQUEST {__read_only} ||= !($_REQUEST {__edit} || $data -> {fake} > 0);

 return $data;

}

А теперь — внешний вид. Клавиши Alt-M переключают нас на draw_item_of_voc_articles. Следуя всё тому же принципу минимализма, упрощаем предложенный код до

sub draw_item_of_voc_articles {

 my ($data) = @_;

 $_REQUEST {__focused_input} = '_label';

 draw_form ({

   right_buttons => [ del ($data)],

   no_edit => $data -> {no_del},

   path => [
    {type => 'voc_articles', name => 'Статьи расходов'},
    {type => 'voc_articles', name => $data -> {label}, id => $data -> {id}},
   ],

  },

  $data,

  [
   {
    name    => 'label',
    label   => 'Наименование',
    size    => 80,
    max_len => 255,
   },

  ],

 )

}

Посмотрим:

Polouchka 03.gif

И, как только видим кнопку "Применить", сразу задумаываемся о проверке данных. Практически на автомате нажимаем Ctrl-Alt-U и переходим к validate_update_voc_articles:

sub validate_update_voc_articles {

 $_REQUEST {_label} or return "#_label#:Вы забыли ввести наименование";

 undef;
	
}

На этом всё. Скалярные данные укладываются в БД самостоятельно (поскольку поля формы названы так же, как столбцы таблицы), удаление и восстановление работают автоматически.

Polouchka 04.gif

Поставщики

Поставщиками (товаров и услуг) в нашей системе будут называться все те, кому мы платим за что-либо. Есть искушение запрограммировать здесь строгий справочник контрагентов с ИНН/КПП, ОГРН и прочими правильными реквизитами, но такое решение было бы совершенно неадекватно поставленной задаче: пришлось бы вести и вычитывать огрмный реестр, без малейшей пользы для себя.

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

Итак, рисуем справочник поставщиков. Переходим к модели (F6), нажимаем Ctrl-N, вводим имя таблицы: voc_suppliers. Пишем следующее:

columns => {
	label        => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
	prc_discount => {TYPE_NAME => 'int'},
},

Далее прописываем тип voc_suppliers в меню и создаём select_voc_suppliers точно так же, как это описано выше для voc_articles.

Переходим к отрисовке таблицы (draw_voc_suppliers). Тут тоже всё аналогично, надо только позаботиться об отображении дополнительного поля. Приведём только 2 первых аргумента вызова draw_table:

[
	'Наименование',
	'Скидка',
],

sub {

	draw_cells ({
		href => "/?type=voc_suppliers&id=$$i{id}",
	},[

		$i -> {label},

		{
			label   => $i -> {prc_discount},
			picture => '## %',
		},

	])

},

Следующая иллюстрация выполнена при уже заполненной таблице. Но не фотографировать же отдельно шапку и отдельно строки... В общем, ниже по тексту большинство скриншотов таблиц будет приведено с небольшим забеганием вперёд (всего на 1 шаг).

Polouchka 05.gif

Теперь — форма просмотра/редактрирования. В процедуре извлечения данных get_item_of_voc_suppliers нет ровно ничего нового, а в отрисовку (draw_item_of_voc_suppliers) добавляем описание поля (после label):

{
	name    => 'prc_discount',
	label   => 'Скидка, %',
	size    => 2,
},
Polouchka 06.gif

Осталось только проверка данных:

sub validate_update_voc_suppliers {

	$_REQUEST {_label} or return "#_label#:Вы забыли ввести наименование";
	
	$_REQUEST {_prc_discount} eq  or $_REQUEST {_prc_discount} =~ /^\d\d?$/
	 or return "#_prc_discount#:Некорректная величина скидки";

	undef;

}

и справочник готов.

Polouchka 07.gif

Товары (и услуги)

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

Да, насчёт упрощения ввода — это не оговорка. Конечно, в любой учётной системе гораздо проще вводить строки в символьное поле, чем поддерживать классификатор, но здесь мы применим ввод с подсказкой, так что комфорт у оператора будет почти такой же, как в случае "мусорного" текста, только чуть выше.

А пока всё-таки нарисуем справочник как таковой. Как всегда, в начале переходим к модели (F6), нажимаем Ctrl-N, вводим имя таблицы: voc_goods. Пишем:

columns => {
	id_voc_article => {TYPE_NAME => 'int'},
	label          => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
},

Возвращаемся к процедурам (F6) и после прописи в меню создаём select_voc_goods (Alt-S). Поскольку новая таблица ссылается на справочник, в выборке стоит предусмотреть JOIN с ним, а заодно неплохо бы реализовать соответствующий фильтр и достать строки для его drop-down списка. В шаблонах всё это заложено, правда для справочника users. Поэтому, в отличие от прошлых разов, здесь мы не будем выкидывать из сгенерированного кода того, что касается ссылки на users, а поменяем (Ctrl-R) все вхождения строки 'user' на 'voc_article'. Вот что получится:

sub select_voc_goods {

	my $data = sql (
	
		add_vocabularies ({},
			voc_articles => {},
		),
		
		voc_goods => [
	
			'id_voc_article',
			
			['label LIKE %?%' => $_REQUEST {q}],
			
			[ LIMIT => [0 + $_REQUEST {start}, $conf -> {portion}]],
		
		],
			
		'voc_articles'
		
	);
	
	return $data;
	
}

Жмём Alt-W и занимаемся визуализацией. Тут тоже всё аналогично вышерассмотренным справочникам, отличие лишь в доплнительном поле, которое берётся из присоединённой таблицы да в фильтре по статьям расходов, который точно так же переделывается из шаблонного кода:

sub draw_voc_goods {

	my ($data) = @_;

	return

		draw_table (

			[
				'Наименование',
				'К-во',
				'Статья',
			],

			sub {

				draw_cells ({
					href => "/?type=voc_goods&id=$$i{id}",
				},[
	
					$i -> {label},
					$i -> {quantity},
					$i -> {voc_article} -> {label},

				])

			},

			$data -> {voc_goods},

			{
				
				name => 't1',
				
				title => {label => 'Товары и услуги'},

				top_toolbar => [{
						keep_params => ['type', 'select'],
					},

					{
						icon  => 'create',
						label => '&Добавить',
						href  => '?type=voc_goods&action=create',
					},

					{
						type  => 'input_text',
						label => 'Искать',
						name  => 'q',
						keep_params => [],
					},

					{
						type   => 'input_select',
						name   => 'id_voc_article',
						values => $data -> {voc_articles},
						empty  => '[Все статьи]',
					},

					{
						type    => 'pager',
					},

					fake_select (),

				],

			}

		);

}
Polouchka 08.gif

Теперь достаём данные для формы ввода (Alt-G, get_item_of_voc_goods). Снова всё знакомо, снова единственная новая деталь — справочник статей расходов:

sub get_item_of_voc_goods {

	my $data = sql ('voc_goods');

	$_REQUEST {__read_only} ||= !($_REQUEST {__edit} || $data -> {fake} > 0);

	add_vocabularies ($data,
		voc_articles => {},
	);

	return $data;

}

Не забываем отобразить его на экране (Alt-W, draw_item_of_voc_goods):

sub draw_item_of_voc_goods {

	my ($data) = @_;

	$_REQUEST {__focused_input} = '_label';

	draw_form ({
	
			right_buttons => [del ($data)],
			
			no_edit => $data -> {no_del},
			
			path => [
				{type => 'voc_goods', name => 'Товары и услуги'},
				{type => 'voc_goods', name => $data -> {label}, id => $data -> {id}},
			],
			
		},
		
		$data,
		
		[

			{
				name    => 'label',
				label   => 'Наименование',
				size    => 80,
				max_len => 255,
			},
			{
				name   => 'id_voc_article',
				label  => 'Статья',
				type   => 'select',
				values => $data -> {voc_articles},
				empty  => '[Выберите статью]',
				other  => '/?type=voc_articles',
			},

		],

	);

}
Polouchka 09.gif

... и гарантируем, что пустое значение не может быть введено (Ctrl-Alt-U, validate_update_voc_goods):

sub validate_update_voc_goods {

	$_REQUEST {_label}          or return "#_label#:Вы забыли ввести наименование";
	$_REQUEST {_id_voc_article} or return "#_id_voc_article#:Вы забыли выбрать статью расходов";

	undef;	

}

Polouchka 10.gif

Справочник товаров готов. А больше справочников в этой системе и не будет.

Ввод данных

Выше отмечалось, что основным источником перичных данных для нас являются чеки. Взглянув на чек, нетрудно сообразить, что он состоит из нескольких скалярных реквизитов (как дата) и набора строк — с иным набором реквизитов. Таким образом, сама собой возникает нехитрая модель из двух сущностей: чек и строка.

Реестр чеков

Займёмся для начала чеком как таковым. Очевидно, для него необходимо запоминать дату и поставщика. Неплохо также сохранять ссылку на покупателя (будем считать, что все покупатели являются потенциальными пользователями системы, так что справочником будет таблица users). Кроме того, внесём в список реквизитов итоговую сумму. Конечно, её всегда можно будет рассчитать по строкам, но мы готовы пожертвовать неколькими байтами в день ради того, чтобы не выполнять запрос с группировкой при каждом показе таблицы.

Итак, переходим к модели (F6), нажимаем Ctrl-N, вводим имя таблицы: tickets. Пишем:

columns => {
	id_voc_supplier => {TYPE_NAME => 'int'},
	id_user         => {TYPE_NAME => 'int'},
	dt              => {TYPE_NAME => 'date'},
	total           => {TYPE_NAME => 'decimal', COLUMN_SIZE => 15, DECIMAL_DIGITS => 2},
},

Пожалуй, сразу прикинем расклад по индексам. Какие наиболее вероятные фильтры на выборку чеков? Наверное, по дате и по поставщику. При этом естественный порядок сортировки — последний чек наверху. А последним он считается, во-первых, по формальной дате (ввод задним числом в отечественной информационной системе — святое дело) и, во-вторых, по порядку ввода, то есть id. Сопоставив всё это, приписываем после полей описания индексов:

keys => {
	dt => 'dt,id',
	id_voc_supplier => 'id_voc_supplier,dt,id',
},

Далее, точно так же, как для справочников, переключаемся на процедуры (F6), прописываемся в меню (тип menu, Alt-S), после чего снова нажимаем Ctrl-N снова вводим tickets, и генерируем select_tickets. Здесь многое нам уже знакомо, но есть несколько новых моментов:

  • проставим вышеоговоренный порядок сортировки (по умолчанию использовалось бы поле label, которого в нашей модели у чека нет);
  • добавим 2 фильтра по дате: от и до;
  • предусмотрим фильтр по поставщику, причём таким образом, чтобы он выглядел как текстовый: в поле q будет вводиться часть наименования (поставщиков может набраться несколько десятков, а длинный select неудобен).
sub select_tickets {

	sql (
	
		add_vocabularies ({},
			users => {},
		),
		
		tickets => [
	
			'id_user',
			
			['id_voc_supplier IN' => 
				sql ('voc_suppliers(id)' => [
					['label LIKE ?%' => $_REQUEST {q}]
				])
			],
			['dt >=' => dt_iso ($_REQUEST {dt_from})],
			['dt <+' => dt_iso ($_REQUEST {dt_to})  ],
			[ ORDER => 'dt DESC, id DESC'],			
			[ LIMIT => [0 + $_REQUEST {start}, $conf -> {portion}]],
		
		],
			
		'users', 'voc_suppliers'
		
	);

}

А теперь — визуализация таблицы (Alt-W, draw_tickets):

sub draw_tickets {

	my ($data) = @_;

	return

		draw_table (

			[
				'Когда',
				'Где',
				'Сколько',
				'Кто',
			],

			sub {
			
				__d ($i, 'dt');

				draw_cells ({
					href => "/?type=tickets&id=$$i{id}",
				},[
	
					{
						label => $i -> {dt},
						attributes => {width => 1},
					},
					{
						label => $i -> {voc_supplier} -> {label},
						attributes => {width => 1},
					},
					{
						label   => $i -> {total},
						picture => $money,
						attributes => {width => 1},
					},
					$i -> {user} -> {label},

				])

			},

			$data -> {tickets},

			{
				
				name => 't1',
				
				title => {label => 'Чеки'},

				top_toolbar => [{
						keep_params => ['type', 'select'],
					}, 
					{
						icon  => 'create',
						label => '&Добавить',
						href  => '?type=tickets&action=create',
					}, 
					{
						type  => 'input_date',
						label => 'C',
						name  => 'dt_from',
					},
					{
						type  => 'input_date',
						label => 'по',
						name  => 'dt_to',
					},
					{
						type  => 'input_text',
						label => '&Поставщик',
						name  => 'q',
						keep_params => [],
					},
 					{
						type   => 'input_select',
						name   => 'id_user',
						values => $data -> {users},
						empty  => '[Все покупатели]',
					},
 					{
						type    => 'pager',
					}, 
					fake_select (), 
				],
				
			}
 		);
 }
Polouchka 11.gif

Форма ввода чека

В случае справочника сейчас было бы самое время нажать Alt-G, но в данном случае есть маленькая тонкость: в момент создания новой записи необходимо позаботиться о значениях полей dt и id_user. В принципе можно было бы положиться на неявную do_create_DEFAULT и тут, достаточно было бы приписать нужные параметры к ссылке с кнопки "Добавить". Но это некрасиво. Мы поступим корректно: нажмём Alt-C и впишем в скобки вновь созданной процедуры do_create_tickets следующее:

sub do_create_tickets {

	$_REQUEST {id} = sql_do_insert (tickets => {
		id_user => $_USER -> {id},
		dt      => dt_iso (Today),
	});

}

Вот теперь Alt-G (get_item_of_tickets): извлекаем данные. Пока всё почки как у справочников.

sub get_item_of_tickets {

	my $data = sql (tickets => ['id'], 'voc_suppliers');
		
	$_REQUEST {__read_only} ||= !($_REQUEST {__edit} || $data -> {fake} > 0);

	add_vocabularies ($data,
		users => {},
		voc_suppliers => {},
	),

	return $data;

}

В визуализации (Alt-M, draw_item_of_tickets) — тоже:

sub draw_item_of_tickets {

	my ($data) = @_;

	__d ($data, 'dt');

	$_REQUEST {__focused_input} = '_dt';

	draw_form ({
	
			right_buttons => [del ($data)],
			
			no_edit => $data -> {no_del},
			
			path => [
				{type => 'tickets', name => 'Чеки'},
				{type => 'tickets', name => "$data->{voc_supplier}->{label} $data->{dt}", id => $data -> {id}},
			],
			
		},
		
		$data,
		
		[

			{
				name    => 'dt',
				label   => 'Дата',
				type    => 'date',
			},

			{
				name   => 'id_user',
				label  => 'Кто',
				type   => 'select',
				values => $data -> {users},
				empty  => '[Выберите покупателя]',
			},

			{
				name   => 'id_voc_supplier',
				label  => 'Где',
				type   => 'select',
				values => $data -> {voc_suppliers},
				empty  => '[Выберите поставщика]',
				other  => {
					href   => '/?type=voc_suppliers',
					width  => 600,
					height => 400,
					label  => '[справочник...]',
				},
			},

			{
				name    => 'total',
				label   => 'Итого',
				size    => 10,
				picture => $money,
				off     =>
					$data -> {total} == 0
					|| !$_REQUEST {__read_only}
				,
			},
		],

	)

}
Polouchka 12.gif

Отметим только, что при выборе поставщика можно открыть соответствующий справочник в модальном диалоге (опция other) и по необходимости дополнить его. Нередактируемое поле total пока не будет отображаться (опция off), оно зарезервировано на весьма близкое будущее.

Polouchka 13.gif

Как всегда, не забываем о валидации (Ctrl-Alt-U, validate_update_tickets). В данном случае, помимо проверки параметров на непустоту, тут ещё преобразуется дата к формату ISO:

sub validate_update_tickets {

	vld_date ('dt');
	
	$_REQUEST {_id_user}         or return "#_id_user#:Вы забыли выбрать покупателя";
	$_REQUEST {_id_voc_supplier} or return "#_id_userid_voc_supplier#:Вы забыли выбрать поставщика";

	undef;	

}

Итак, карточка чека готова.

Polouchka 14.gif

Строки чека

Мы подошли к проектированию последней, самой детальной сущности в нашй схеме: строки чека. Она должна представлять данные об атомарных покупках. Какие у неё должны быть реквизиты? Переходим к модели (F6), нажимаем Ctrl-N, вводим имя таблицы: ticket_lines и начинаем прикидывать содержимое скобки columns.

Первым делом, разумеется, ссылка на чек. И порядковый номер врамках чека:

	id_ticket       => {TYPE_NAME => 'int'},
	ord             => {TYPE_NAME => 'int'},

Далее, скопируем все реквизиты чека в строку. Пусть их дублируются. Потом проще искать будет:

	id_voc_supplier => {TYPE_NAME => 'int'},
	id_user         => {TYPE_NAME => 'int'},
	dt              => {TYPE_NAME => 'date'},

Само собой, стоимость товара:

	price           => {TYPE_NAME => 'decimal', COLUMN_SIZE => 15, DECIMAL_DIGITS => 2},

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

	prc_discount    => {TYPE_NAME => 'int'},

и предусмотрим отдельное поле для реально уплаченной суммы, после вычета скидок:

	total           => {TYPE_NAME => 'decimal', COLUMN_SIZE => 15, DECIMAL_DIGITS => 2},

Приправим это всё ссылками на справочники для аналитики:

	id_voc_article  => {TYPE_NAME => 'int'},
	id_voc_goods    => {TYPE_NAME => 'int'},
	id_voc_need     => {TYPE_NAME => 'int'},

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

	cnt             => {TYPE_NAME => 'decimal', COLUMN_SIZE => 15, DECIMAL_DIGITS => 3, NULLABLE => 0, COLUMN_DEF => 1},

И, наконец, любимое поле всех заказчиков: примечание.

	note            => {TYPE_NAME => 'text'},

Не поленимся сразу задать индексы. Хотя ничто не мешало бы сделать это и позже (при обнаружении заметных задержек на простых запросах):

keys => {
	id_ticket => 'id_ticket,ord',
	id_voc_goods => 'id_voc_goods,dt',
	dt        => 'dt',
},

Теперь возвращаемся к процедурам (F6) и... нет, на этот раз в меню не ходим и тип не порождаем. Роль реестра строк будет играть уже имеющаяся форма чека. Обратно Alt-G, и приписываем в get_item_of_tickets такое:

sql ($data, ticket_lines => [
	[ id_ticket   => $data -> {id} ],
	[ ORDER       => ['ord'] ],
], 'voc_suppliers', 'voc_goods', 'voc_needs', 'users');

Теперь, по мере создания дочерних строк (совсем скоро) они попадут в список $data -> {ticket_lines}. Соответственно, нарисуем таблицу, которая их отобразит (а заодно, на панели чуть выше — кнопку создания):

sub draw_item_of_tickets {

# то же, что раньше

	.

	draw_table (
	
		[
			'№ п/п',
			'Что',
			'Номинал',
			'Уплачено',
			'Значимость',
			'Примечание',
		],

		sub {
		
			draw_cells ({
				href     => "/?type=ticket_lines&id=$i->{id}",
				is_total => !$i -> {ord},
			},[
				
				$i -> {ord},
				$i -> {voc_goods} -> {label},
				{
					label   => $i -> {price},
					picture => $money,
				},
				{
					label   => $i -> {total},
					picture => $money,
				},
				$i -> {voc_need} -> {label},
				$i -> {note},
				
			]),
		
		},
		
		$data -> {ticket_lines},
		
		{
			
			title => {label => 'Позиции'},
			
			off   => !$_REQUEST {__read_only},
			
			name  => 't1',
						
			top_toolbar => [{
				keep_params => ['type', 'id'],
			},
				{
					icon  => 'create',
					label => '&Добавить',
					href  => "/?type=ticket_lines&action=create&id_ticket=$data->{id}",
				},
				
				fake_select (),
				
			],
			
		}

	);

}
Polouchka 15.gif

Кнопка есть — хочется нажать. ОК, но сначала всё же позаботимся об автогенерации очередного номера и наследовании реквизитов чека и поставщика (раз уж взялись их дублировать). Ctrl-N, имя типа ticket_lines, Alt-C и пишем:

sub do_create_ticket_lines {

	my $ticket = sql (tickets => $_REQUEST {id_ticket}, 'voc_suppliers');

	$_REQUEST {id} = sql_do_insert (ticket_lines => {
	
		id_ticket       => $ticket -> {id},
		ord             => 1 + sql_select_scalar ('SELECT MAX(ord) FROM ticket_lines WHERE fake = 0 AND id_ticket = ?', $ticket -> {id}),
		
		id_voc_supplier => $ticket -> {id_voc_supplier},
		id_user         => $ticket -> {id_user},
		dt              => $ticket -> {dt},
	
		prc_discount    => $ticket -> {voc_supplier} -> {prc_discount},
		
		id_voc_need     => 3,
	
	});

}

Вычисление ord может показаться наивным для многопользовательской информационной системы, но если никто не будет параллельно вводить один и тот же чек более чем в одиночку (представить себе только), то всё будет хорошо.

Теперь нажмём Alt-G и набросаем процедуру извлечения данных get_item_of_ticket_lines. Всё как всегда, разве только упоминается немного больше родительских таблиц. Но ведь это всего лишь строки.

sub get_item_of_ticket_lines {

	my $data = sql (ticket_lines => ['id'], 'tickets', 'voc_suppliers', 'users');

	$_REQUEST {__read_only} ||= !($_REQUEST {__edit} || $data -> {fake} > 0);

	add_vocabularies ($data,
		voc_needs    => {order => 'id'},
		voc_articles => {},
	);
	
	return $data;

}

А вот отрисовка этой записи (Alt-M, draw_item_of_ticket_lines) выглядит чуть побогаче. Впрочем, в полном соответствии с набором реквизитов.

sub draw_item_of_ticket_lines {

	my ($data) = @_;
	
	__d ($data, 'dt');

	$_REQUEST {__focused_input} = $data -> {id_voc_goods} ? '_price' : '_id_voc_goods';

	draw_form ({
	
			right_buttons => [del ($data)],
			
			no_edit => $data -> {no_del},
			
			path => [
				{type => 'tickets', name => "$data->{voc_supplier}->{label} $data->{dt}", id => $data -> {ticket} -> {id}},
				{type => 'ticket_lines', name => $data -> {ord}, id => $data -> {id}},
			],
			
			additional_buttons => [
				{
					icon   => 'create',
					label  => 'Добавить ещё (F2)',
					href   => "/?type=ticket_lines&action=create&id_ticket=$data->{id_ticket}",
					target => 'invisible',
					off    => !$_REQUEST {__read_only},
					hotkey => {code => F2},
				},
				{
					icon    => 'create',
					label   => 'Клонировать (F11)',
					href    => {action => 'clone'},
					target  => 'invisible',
					confirm => 'Что, правда, одно и то же?',
					off     => !$_REQUEST {__read_only},
					hotkey  => {code => F11},
				},
			],
			
		},
		
		$data,
		
		[
		
			{
				label   => 'Кто',
				type    => 'static',
				value   => $data -> {user} -> {label},
			},
			
			{
				label   => 'Где',
				type    => 'static',
				value   => $data -> {voc_supplier} -> {label},
			},
			
			{
				label   => 'Когда',
				type    => 'static',
				value   => $data -> {dt},
			},

			{
				name    => 'ord',
				label   => '№ п/п',
				size    => 5,
			},
			
			[

				{
					name   => 'id_voc_goods',
					label  => 'Что',
					type   => 'suggest',
					size   => 60,
					values => sub {sql (voc_goods => ['id',
						['UPPER(label) LIKE UPPER(?)' => $_REQUEST {_id_voc_goods} . '%'],
					])},
					href   => "/?type=voc_goods&id=$data->{id_voc_goods}",
				},
			
			],
			[

			{
				name    => 'price',
				label   => 'Почём',
				size    => 10,
				picture => $money,
			},
			{
				name    => 'cnt',
				label   => 'Сколько',
				size    => 5,
				picture => $weight,
			},
			],
			
			{
				label   => 'Скидка',
				type    => 'static',
				value   => $data -> {voc_supplier} -> {prc_discount},
				off     => $data -> {voc_supplier} -> {prc_discount} == 0,
			},
			{
				name    => 'total',
				label   => 'Итого',
				size    => 10,
				picture => $money,
				off     =>
					$data -> {voc_supplier} -> {prc_discount} == 0
					|| !$_REQUEST {__read_only}
				,
			},
			{
				name   => 'id_voc_need',
				label  => 'Что это было',
				type   => 'radio',
				values => $data -> {voc_needs},
			},

			{
				name    => 'note',
				label   => 'Примечание',
				type    => 'text',
				rows    => 5,
				cols    => 60,
				off     =>
					!$data -> {note} && $_REQUEST {__read_only}
				,
			},
			
			{
				name   => 'id_voc_article',
				label  => 'Статья',
				type   => 'select',
				values => $data -> {voc_articles},
				empty  => ' ',
				other  => '/?type=voc_articles',
			},

		],

	)

}
Polouchka 16.gif

Процедура валидации в данном случае тоже несколько усложняется. Основной смысл здесь в том, чтобы корректно обработать 2 случая: когда пользователь выбирает уже известный товар (определён $_REQUEST {_id_voc_goods__id}) или наоборот: когда он вводит новую строку, которую нужно тут же занести в справочник. Делается это так:

sub validate_update_ticket_lines {

	$_REQUEST {_ord} =~ /^\d+$/ or return "#_ord#:Некорректный номер строки";
	
	$_REQUEST {_price} > 0 or return "#_price#:Некорректная стоимость";
	
	if ($_REQUEST {_id_voc_goods__id}) {
	
		$_REQUEST {_id_voc_article} = sql_select_scalar ('SELECT id_voc_article FROM voc_goods WHERE id = ?', $_REQUEST {_id_voc_goods__id});
		
		$_REQUEST {_id_voc_goods} = $_REQUEST {_id_voc_goods__id};
	
	}
	else {
	
		$_REQUEST {_id_voc_goods__label} or return "#_id_voc_goods#:Так за что уплачено-то?";
		$_REQUEST {_id_voc_article}      or return "#_id_voc_article#:А что это за статья?";
		
		$_REQUEST {_id_voc_goods} = sql_do_insert (voc_goods => {
		
			fake           => 0,
			id_voc_article => $_REQUEST {_id_voc_article},
			label          => $_REQUEST {_id_voc_goods__label},
		
		});
	
	}

	undef;	

}

Подготовленные таким образом данные можно было бы записать в БД, как всегда, процедурой по умолчанию. Кабы не глобальная сумма по чеку. И не скидки (которые правильно вычисляются именно через сумму чека). Таким образом, мы (впервые за всё приложение) подошли к необходимости явно описать процедуру записи данных. Ну и опишем. Alt-U.

sub do_update_ticket_lines {

	do_update_DEFAULT ();

	my $data = sql (ticket_lines => ['id'], 'tickets', 'voc_suppliers', 'users');
	
	$data -> {voc_supplier} -> {prc_discount} ||= 0;

	my ($sum_price, $total) = sql_select_array ('SELECT SUM(price), SUM(price) * (1 - ? / 100) FROM ticket_lines WHERE fake = 0 AND id_ticket = ?', $data -> {voc_supplier} -> {prc_discount}, $data -> {id_ticket});
	
	sql_do ('UPDATE tickets SET total = ? WHERE id = ?', $total, $data -> {id_ticket});	
	
	if ($data -> {voc_supplier} -> {prc_discount}) {
	
		sql_do ('UPDATE ticket_lines SET total = price * ? / ? WHERE fake = 0 AND id_ticket = ?', $total, $sum_price, $data -> {id_ticket});	
	
	}
	else {

		sql_do ('UPDATE ticket_lines SET total = price WHERE fake = 0 AND id_ticket = ?', $data -> {id_ticket});	

	}

}

На форме просмотра строки мы предусмотрели кнопку клонирования записи. Если вы когда-нибудь занимались вводом реальных чеков, то наверняка поймёте, насколько облегчает работу эта функция. Давайте же запрограммируем соотвестсвующее действие. Нажмём Ctrl-B, введём имя clone и (опираясь на опыт разработки do_update_ticket_lines) напишем:

sub do_clone_ticket_lines {

	my $data = sql (ticket_lines);
	
	delete $data -> {id};
	
	$data -> {ord} = 1 + sql_select_scalar ('SELECT MAX(ord) FROM ticket_lines WHERE fake = 0 AND id_ticket = ?', $data -> {id_ticket});
	
	$_REQUEST {id} = sql_do_insert (ticket_lines => $data);
	
	my $data = sql (ticket_lines => ['id'], 'tickets', 'voc_suppliers', 'users');
	
	$data -> {voc_supplier} -> {prc_discount} ||= 0;

	my ($sum_price, $total) = sql_select_array ('SELECT SUM(price), SUM(price) * (1 - ? / 100) FROM ticket_lines WHERE fake = 0 AND id_ticket = ?', $data -> {voc_supplier} -> {prc_discount}, $data -> {id_ticket});
	
	sql_do ('UPDATE tickets SET total = ? WHERE id = ?', $total, $data -> {id_ticket});	
	
	if ($data -> {voc_supplier} -> {prc_discount}) {
	
		sql_do ('UPDATE ticket_lines SET total = price * ? / ? WHERE fake = 0 AND id_ticket = ?', $total, $sum_price, $data -> {id_ticket});	
	
	}
	else {

		sql_do ('UPDATE ticket_lines SET total = price WHERE fake = 0 AND id_ticket = ?', $data -> {id_ticket});	

	}	

}
Polouchka 17.gif

Теперь вернёмся на минутку в get_item_of_tickets и добавим туда, в конец, следующий фрагмент:

if (@{$data -> {ticket_lines}} > 0) {
	$data -> {no_del} ||= 1;
	add_totals ($data -> {ticket_lines});
	delete $data -> {ticket_lines} -> [-1] -> {ord};
}

Это, во-первых, блокирует редактирование и удаление непустого чека, а во-вторых, добавит итоговую строку-автосумму.

Сравнение цен

Выше, на карточке строки чека, мы предусмотрели ссылку (href) с поля id_voc_goods. Эта ссылка активна, когда форма находится в режиме просмотра. Перейдя по ссылке, мы видим карточку товара и... Естественно было бы онаружить тут же и историю соответствующих покупок, не правда ли? Это легко устроить. Достаточно перейти к редактированию get_item_of_voc_goods, добавить туда

sql ($data, ticket_lines => [
	[ id_voc_goods => $data -> {id} ],
	[ ORDER        => ['dt DESC'] ],
], 'voc_suppliers', 'voc_needs', 'users');

а потом в draw_item_of_voc_goods:

.

draw_table (

	[
		'Когда',
		'Почём',
		'Сколько',
		'Цена',
		'Где',
		'Зачем',
		'Кто',
		'Примечание',
	],

	sub {
	
		__d ($i, 'dt');
	
		draw_cells ({
			href     => "/?type=ticket_lines&id=$i->{id}",
			is_total => !$i -> {ord},
		},[
			
			$i -> {dt},
			{
				label   => $i -> {total},
				picture => $money,
			},
			{
				label   => $i -> {cnt},
				picture => $weight,
			},
			{
				label   => $i -> {total} / $i -> {cnt},
				picture => $money,
			},
			$i -> {voc_supplier} -> {label},
			$i -> {voc_need} -> {label},
			$i -> {user} -> {label},
			$i -> {note},
			
		]),
	
	},
	
	$data -> {ticket_lines},
	
	{
		
		title => {label => 'Позиции'},
		
		off   => !$_REQUEST {__read_only},
		
		name  => 't1',
					
		top_toolbar => [{
			keep_params => ['type', 'id'],
		},
			
			fake_select (),
			
		],
		
	}

);

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

Polouchka 18.gif

Реестр покупок

История покупок интересна не только в контексте отдельных чеков и товаров, но и с различными другими фильтрами, в том числе комбинированными. Это наводит на мысль о создании нового экрана-реестра: примерно как уже есть для чеков, только на этот раз для их строк. Таблица БД уже спроекирована и даже частично заполнена, тип экрана ticket_lines зарегистрирован. Можно просто прописать его в меню и идти создавать

sub select_ticket_lines {

	sql (
	
		add_vocabularies ({},
			users => {},
			voc_needs    => {order => 'id'},
			voc_articles => {},
			voc_suppliers => {},
		),
				
		ticket_lines => [
	
			'id_user',			
			'id_voc_supplier',
			'id_voc_article',
			'id_voc_need',
			
			['id_voc_goods IN' => sql ('voc_goods(id)' => [['label LIKE %?%' => $_REQUEST {q}]])],
			
			['dt >=' => dt_iso ($_REQUEST {dt_from})],
			['dt <+' => dt_iso ($_REQUEST {dt_to})  ],
			
			[ ORDER => ['dt DESC',
				total => 'total DESC',
			]],
			
			[ LIMIT => [0 + $_REQUEST {start}, 50]],
		
		],
			
		'users', 'voc_needs', 'voc_articles', 'voc_suppliers', 'voc_goods'
		
	);	

}

и (Alt-W)

sub draw_ticket_lines {

	my ($data) = @_;

	return

		draw_table (

			[
				'Когда',
				'Чего',
				'Сколько',
				{
					label => 'Почём',
					order => 'total',
				},
				'Где',
				'На что',
				'И как',
				'Кто',
			],

			sub {
			
				__d ($i, 'dt');

				draw_cells ({
					href => "/?type=ticket_lines&id=$$i{id}",
				},[
	
					$i -> {dt},
					$i -> {voc_goods} -> {label},
					{
						label   => $i -> {cnt},
						picture => $weight,
					},
					{
						label   => $i -> {total},
						picture => $money,
					},
					$i -> {voc_supplier} -> {label},
					$i -> {voc_article} -> {label},
					$i -> {voc_need} -> {label},
					$i -> {user} -> {label},

				])

			},

			$data -> {ticket_lines},

			{
				
				name => 't1',
				
				title => {label => 'Траты'},

				top_toolbar => [{
						keep_params => ['type', 'select'],
					},

					{
						icon => 'cancel',
						href => esc_href (),
						hotkey => {code => Esc},
						off  => $_REQUEST {__last_query_string} == 1,
					},

					{
						type  => 'input_text',
						label => '&Искать',
						name  => 'q',
						keep_params => [],
					},

					{
						type  => 'input_date',
						label => 'C',
						name  => 'dt_from',
					},
					{
						type  => 'input_date',
						label => 'по',
						name  => 'dt_to',
					},

					{
						type   => 'input_select',
						name   => 'id_user',
						values => $data -> {users},
						empty  => '[Все покупатели]',
					},

					{
						type   => 'input_select',
						name   => 'id_voc_supplier',
						values => $data -> {voc_suppliers},
						empty  => '[Все поставщики]',
					},

					{
						type   => 'input_select',
						name   => 'id_voc_article',
						values => $data -> {voc_articles},
						empty  => '[Все статьи]',
					},

					{
						type   => 'input_select',
						name   => 'id_voc_need',
						values => $data -> {voc_needs},
						empty  => '[Все уровни]',
					},

					{
						type   => 'pager',
					},

				],
				
			}

		);

}
Polouchka 19.gif

Сводный отчёт

Сквозной реестр с фильтрами — это хорошо, но нашей изначальной целью была удобная для просмотра и оценки суммарная статистика затрат, в том числе в разрезе по статьям и степени необходимости. И цель эта совсем близка. Всё, что нам нужно — ещё один экран с таблицей, причём никакого ввода не будет, все данные уже готовы. Так что добавляем в меню тип экрана ticket_lines_stats и пишем

sub select_ticket_lines_stats {

	my $data = {};
	
	my ($y, $m, $d) = Today;
	
	$_REQUEST {dt_to}   ||= dt_dmy ($y, $m, $d);
	$_REQUEST {dt_from} ||= dt_dmy ($y, $m, 1);
	
	my $collect = sub {
		$data -> {an} -> {$i -> {id_voc_article}, $i -> {id_voc_need}} = $i -> {total};
		$data -> {an} -> {-1, $i -> {id_voc_need}} += $i -> {total};
		$data -> {an} -> {-1, -1} += $i -> {total};
		$data -> {a}  -> {$i -> {id_voc_article}}  += $i -> {total};
		$data -> {a}  -> {-1}  += $i -> {total};
	};

	sql_select_loop (<<EOS, $collect, dt_iso ($_REQUEST {dt_from}), dt_iso ($_REQUEST {dt_to}) . ' 23:59:59');
		SELECT
			ticket_lines.id_voc_article
			, ticket_lines.id_voc_need
			, SUM(ticket_lines.total) AS total
		FROM
			ticket_lines
		WHERE
			ticket_lines.dt BETWEEN ? AND ?
		GROUP BY
			1, 2
EOS

	add_vocabularies ($data,
		voc_needs    => {order => 'id'},
		voc_articles => {in => [-1, keys %{$data -> {a}}]},
	);
	
	$data -> {voc_articles} = [
		(sort {$data -> {a} -> {$b -> {id}} <=> $data -> {a} -> {$a -> {id}}} @{$data -> {voc_articles}}),
		{id => -1, label => 'Итого'},
	];
	
	return $data;

}

Этот программный код следует признать значительно менее прозрачным, чем всё, что было приведено выше. Здесь мы вышли за рамки применимости функции sql и написали SQL-запрос вручную. Мы размещаем его результат в одно- (a) и дву- (an) мерный массивы. Эти структуры данных практически напрямую соответствуют тому, что мы хотим увидеть в таблице: суммы по статьям (a: articles) и статьям-необходимости (an: articles-needs). Отображается же это всё следующей процедурой:

sub draw_ticket_lines_stats {

	my ($data) = @_;
 
	draw_table (

		[
			'Статья',
			'Всего',
			'%',
			(map {$_ -> {label}} @{$data -> {voc_needs}}),
		],

		sub {

			draw_cells ({
				href => {type => 'ticket_lines', id_voc_article => $i -> {id}, order => 'total'},
				is_total => $i -> {id} == -1,
			},[
	
				$i -> {label},
				
				{
					label   => $data -> {a} -> {$i -> {id}},
					picture => $money,
				},
				{
					label   => 100 * $data -> {a} -> {$i -> {id}} / $data -> {a} -> {-1},
					picture => '##,# %',
					off     => $i -> {id} == -1,
				},
				
				(map {{
					label   => $data -> {an} -> {$i -> {id}, $_ -> {id}},
					href => {type => 'ticket_lines', id_voc_article => $i -> {id}, id_voc_need => $_ -> {id}, order => 'total'},
					picture => $money,
				}} @{$data -> {voc_needs}})

			])

		},

		$data -> {voc_articles},

		{
			
			name => 't1',
			
			title => {label => 'Статистика'},
 
			top_toolbar => [{
					keep_params => ['type', 'select'],
				},


				{
					type  => 'input_date',
					label => 'C',
					name  => 'dt_from',
				},
				{
					type  => 'input_date',
					label => 'по',
					name  => 'dt_to',
				},
 
			],

		}

	);

}
Polouchka 20.gif

Ссылки с суммарных цифр ведут на страницу реестра покупок с соответствующими фильтрами, то есть попросту на списки их слагаемых.

Упаковка и установка

Допустим, все вышеописанные шаги вы производили на своей рабочей станции, где во множестве экземпляров установлены различные WEB-серверы, СУБД и, разумеется, Perl. Допустим, у вас всё работает. Теперь задача в том, чтобы побыстрее и попроще перенести приложение на домашний компьютер, где кроме основной Висты нет практически ничего.

Нет проблем, сделаем. Прежде всего, определимся с WEB- и SQL-серверами. Поскольку основная цель оптимизации — простота установки (в идеале это просто копирование файлов, после которого всё сразу работает), желательно, чтобы отдельных процессов не было вообще. И это вполне реально. У нас есть Eludia::Server: тонкая надстройка над модулем HTTP::Daemon, при помощи которой WEB-сервер в виде Perl-скрипта пишется в несколько строк. А модуль DBD::SQLite поможет нам встроить в тот же скрипт добротный транзакционный SQL-движок. Разумеется, такая комбинация способна функционировать только в весьма немногопользовательском режиме, но ведь это как раз наш случай.

Допустим для простоты, что целевой директорией установки нашего комплекта будет C:/Program Files/Polouchka. Заведём соответствующий каталог и скопируем в C:/Program Files/Polouchka/Perl живую рабочую инсталляцию Perl5 (в нашем случае это был C:/Perl от ActiveState), в C:/Program Files/Polouchka/Eludia/core — ядро Eludia.pm, а в C:/Program Files/Polouchka/Applications/Polouchka — директорию приложения. Откроем файл C:/Program Files/Polouchka/Applications/Polouchka/conf/httpd.conf и настроим нам необходимые пути:

DocumentRoot "C:/Program Files/Polouchka/Applications/polouchka/docroot"

ErrorLog  "C:/Program Files/Polouchka/Applications/polouchka/logs/error.log"
CustomLog "C:/Program Files/Polouchka/Applications/polouchka/logs/access.log" combined

<perl >

	use lib 'C:/Program Files/Polouchka/Eludia/core';
	use lib 'C:/Program Files/Polouchka/Eludia/core/lib';

	use Eludia::Loader

	'C:/Program Files/Polouchka/Applications/polouchka/lib' => 'POLOUCHKA' 

	, {

		db_dsn => "dbi:SQLite:dbname=C:/Program Files/Polouchka/Applications/polouchka/sql/dat.dat",
		
		...

	};
      
</perl >

Теперь присупаем к разработке встроенного WEB-сервера. Дело это простое и недолгое, как вообще любое дело, где используется Perl. Вот исходный текст C:/Program Files/Polouchka/Applications/Polouchka/run.pl:

#!/usr/bin/perl -w

use lib 'C:/Program Files/Polouchka/Eludia/core';
use lib 'C:/Program Files/Polouchka/Eludia/core/lib';

use Eludia::Server;

1;

Для пущей красоты и автоматизма сделаем нашему приложению HTA-оболочку (C:/Program Files/Polouchka/Applications/Polouchka/polouchka.hta), причём устоим там автоматический заход под нужным пользователем:

< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
< HTML>
< HEAD>
< TITLE>Polouchka< /TITLE>
< hta:application 
	id="oZP" 
	applicationname="Polouchka" 
	border="thick"
	caption="yes"
	navigable="no"
	scroll="no"
	selection="yes"
	showintaskbar="yes"
	singleinstance="yes"
>
< /HEAD>

< BODY leftMargin=0 topMargin=0 rightMargin=0 bottomMargin=0 scroll="no">

  < iframe 
  	src="http://127.0.0.1/?type=logon&action=execute&login=...&password=..."
  	application="yes" 
  	height=100% 
  	width=100% 
  	name="application_frame" 
  	frameborder=0
  >

< /BODY>
< /HTML>

Осталось последнее: глобальный пускач C:/Program Files/Polouchka/start.cmd, который открывает всё, что нужно, в правильной последовательности:

@echo off
cd Applications\Polouchka
start ..\..\Perl\bin\wperl run.pl
..\..\Perl\bin\wperl -e 'sleep 3'
start polouchka.hta

Ну и, как говорил медвежатник Квинто в 1-м "Ва-банке": "Пакуйте!"

После распаковки останется только сделать ярлычок для start.cmd на рабочем столе. Двойной клик... Работает. Можно вводить чеки и анализировать статистику.

Если домашний маршрутизатор работает, то можно одновременно подключиться со 2-го компьютера (указав браузеру в строке адреса IP машины, на которой запущен run.pl) и заняться обсуждением бюджетной политики по-взрослому, то есть когда каждый смотрит в свой монитор.

Заключение

Всё описанное в этом очерке имело место в реальности. Некоторые детали опущены, кое-что переставлено местами, но адекватности изложения это не нарушает. Текст написан спустя 2 недели после внедрения системы; в настоящее время она используется и приносит ощутимую пользу.

По поводу логики приложения можно высказать множество упрёков. В частности:

  • "степень обоснованности" в действительности представляет собой 2 независимых измерения: необходимость покупки и справедливость цены;
  • в поле "количество" смешано 2 разных понятия: количество товара в упаковке и количество упаковок в покупке;
  • смена категории товара в справочнике не отражается на истории покупок;
  • нет механизма слияния дубликатов в справочнике товаров.

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

Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Разработчику
Администратору
Инструменты