#!/usr/bin/perl use warnings; use strict; use Config::Simple; use DBI; use DateTime; use DateTime::Format::ISO8601; use GD; use GD::Barcode; use GD::Barcode::QRcode; use Text::Iconv; use Text::QRCode; use utf8; binmode(STDOUT, 'utf8:'); # FIXME die on non-existant config tie my %Config, "Config::Simple", '/etc/mat.conf'; #use Data::Dumper; my $db = DBI->connect($Config{'database'}, "", "", {RaiseError => 1, AutoCommit => 1, sqlite_unicode => 1}); my @recipes; my @ingredients; my @contents; my @comments; sub ceiling { my ( $num ) = @_; my $ret = int($num); $ret++ unless (int($num) eq $num); return $ret; } sub read_ingredients { @ingredients = (); my $all = $db->selectall_hashref("SELECT * FROM ingredients", 'id'); foreach my $id (keys(%{$all})) { $ingredients[$id] = $all->{$id}; } } sub read_recipe_db { # All recipes needs to be read to be able to pick random recipes easily # enough. my $all = $db->selectall_arrayref("SELECT * FROM recipes"); foreach my $row (@$all) { my ($id) = @$row; $recipes[$id]->{'name'} = @$row[1]; $recipes[$id]->{'uri'} = @$row[2]; } $all = $db->selectall_arrayref("SELECT * FROM comments"); foreach my $row (@$all) { my ($id) = @$row; $comments[$id]->{'comment'} = @$row[1]; } read_ingredients; $all = $db->selectall_arrayref("SELECT * FROM contents"); foreach my $row (@$all) { my ($id) = @$row; $contents[$id]->{'recipe_id'} = @$row[1]; $contents[$id]->{'ingredient_id'} = @$row[2]; $contents[$id]->{'quantity'} = @$row[2]; $contents[$id]->{'unit'} = @$row[2]; } } sub get_random_recipe { return int(rand(@recipes)); } sub get_recipe_name ($) { my ( $id ) = @_; if ($recipes[$id]->{'name'}) { return $recipes[$id]->{'name'}; } else { return "NULL"; } } sub get_recipe_uri ($) { my ( $id ) = @_; if ($recipes[$id]->{'uri'}) { return $recipes[$id]->{'uri'}; } else { return "NULL"; } } sub get_comment ($) { my ( $id ) = @_; if ($comments[$id]->{'comment'}) { return $comments[$id]->{'comment'}; } else { return "NULL"; } } my @schedule = ( { day => "Måndag", }, { day => "Tisdag", }, { day => "Onsdag", }, { day => "Torsdag", }, { day => "Fredag", } ); sub convert_to_type { my ( $out_type, $id, $in_amount, $in_type ) = @_; my %out_amount; my $out_factor; my $sql = "SELECT density, piece_weight FROM ingredients WHERE id=".$id.";"; my $ingredient = $db->selectrow_hashref($sql); my $volume; my $weight; if ($in_type eq 'volume') { $volume = 0.001 * $in_amount; # m³ } if ( $in_type eq 'volume' ) { $out_amount{'volume'} = $volume * 1000; # l if ($ingredient->{'density'}) { $out_amount{'weight'} = $ingredient->{'density'} * $volume * 1000; # g } } elsif ( $in_type eq 'weight' ) { $out_amount{'weight'} = $in_amount; if ($ingredient->{'density'}) { $out_amount{'volume'} = $in_amount / $ingredient->{'density'}; # g } } elsif ( $in_type eq 'piece' ) { if ($ingredient->{'piece_weight'}) { my $piece_weight = $ingredient->{'piece_weight'}; # g/piece $out_amount{'weight'} = $in_amount * $piece_weight; # g if ($ingredient->{'density'}) { $out_amount{'volume'} = $out_amount{'weight'} / $ingredient->{'density'}; } } } if ( $out_type eq "piece" ) { if ($ingredient->{'piece_weight'}) { return $out_amount{'weight'} / $ingredient->{'piece_weight'}; # piece } else { return undef; } } return $out_amount{$out_type}; } sub convert_to_unit { my ( $out_unit, $id, $in_amount, $in_unit ) = @_; my $in_type; my $return_unit; my %out_amount; my $out_type; my $out_factor; my %convert; # volume [l], weight [g], piece [st] my $debug_conversion = "no"; # SELECT DISTINCT unit FROM contents; # msk, krm, dl, tsk, cl, l, ml, nypa # st, knippen, burk, paket, påse, blad, ark, skiv # dm, cm, mm # n/a # g, kg if ($in_unit eq "l") { $convert{'volume'} = $in_amount; } elsif ($in_unit eq "dl") { $convert{'volume'} = $in_amount / 10; } elsif ($in_unit eq "cl") { $convert{'volume'} = $in_amount / 100; } elsif ($in_unit eq "ml") { $convert{'volume'} = $in_amount / 1000; } elsif ($in_unit eq "msk") { $convert{'volume'} = $in_amount * 0.015; } elsif ($in_unit eq "tsk") { $convert{'volume'} = $in_amount * 0.005; } elsif ($in_unit eq "krm") { $convert{'volume'} = $in_amount * 0.001; } elsif ($in_unit eq "mg") { $convert{'weight'} = $in_amount / 1000; } elsif ($in_unit eq "g") { $convert{'weight'} = $in_amount; } elsif ($in_unit eq "kg") { $convert{'weight'} = $in_amount * 1000; } elsif ($in_unit eq "st") { $convert{'piece'} = $in_amount; } elsif ($in_unit eq "ark") { $convert{'piece'} = $in_amount; } elsif ( ($in_unit eq "cm") and ($in_unit eq $ingredients[$id]->{'primary_unit'}) ) { # FIXME This really isn't piece_weight, but something else. 1 cm of leek is # not a the same as one leek $convert{'piece'} = $in_amount; } elsif ( ($in_unit eq "knippen") and ($in_unit eq $ingredients[$id]->{'primary_unit'}) ) { # FIXME This really isn't piece_weight, but something else. 1 cm of leek is # not a the same as one leek $convert{'piece'} = $in_amount; } if ( $convert{'volume'} ) { $in_type='volume'; } elsif ( $convert{'weight'} ) { $in_type='weight'; } elsif ( $convert{'piece'} ) { $in_type='piece'; } if ($out_unit eq "weight") { $out_type = 'weight'; $out_factor = 0; } elsif ($out_unit eq "g") { $out_type = 'weight'; $out_factor = 1; } elsif ($out_unit eq "mg") { $out_type = 'weight'; $out_factor = 1000; } elsif ($out_unit eq "kg") { $out_type = 'weight'; $out_factor = 0.001; } elsif ($out_unit eq "st") { $out_type = 'piece'; $out_factor = 1; } elsif ($out_unit eq "l") { $out_type = 'volume'; $out_factor = 1; } elsif ($out_unit eq "dl") { $out_type = 'volume'; $out_factor = 10; } elsif ($out_unit eq "cl") { $out_type = 'volume'; $out_factor = 100; } elsif ($out_unit eq "ml") { $out_type = 'volume'; $out_factor = 1000; } elsif ($out_unit eq "tsk") { $out_type = 'volume'; $out_factor = (1 / 0.005); } elsif ($out_unit eq "msk") { $out_type = 'volume'; $out_factor = (1 / 0.015); } elsif ($out_unit eq "cm") { $out_type = 'piece'; } elsif ($out_unit eq "knippen") { $out_type = 'piece'; } if ( $debug_conversion eq "yes" or $debug_conversion eq $id) { print $ingredients[$id]->{'name'}, " ", $id, " in_unit: ", $in_unit, " out_unit: ", $out_unit, "\n"; print " V:", (defined($convert{'volume'})?$convert{'volume'}:"undef"), " W:", (defined($convert{'weight'})?$convert{'weight'}:"undef"), " P:", (defined($convert{'piece'})?$convert{'piece'}:"undef"), " \n"; } if ( ($out_type eq 'piece') and ($in_unit eq $out_unit) ) { $out_amount{$out_type} = $in_amount; } else { if ( $in_type ) { $out_amount{$out_type} = convert_to_type( $out_type, $id, $convert{$in_type}, $in_type ); } else { print STDERR "Could not convert $id\n"; } } my $sql = "SELECT density, piece_weight FROM ingredients WHERE id=".$id.";"; my $ingredient = $db->selectrow_hashref($sql); if ( $debug_conversion eq "yes" or $debug_conversion eq $id) { print " ", " V:", (defined($out_amount{'volume'})?$out_amount{'volume'}:"undef"), " W:", (defined($out_amount{'weight'})?$out_amount{'weight'}:"undef"), " P:", (defined($out_amount{'piece'})?$out_amount{'piece'}:"undef"), "", " piece_weight: ", (defined($ingredient->{'piece_weight'})?$ingredient->{'piece_weight'}:"undef"), " \n\n"; } if ( defined($out_amount{$out_type}) ) { # unless ( $out_factor ) { # if ( $out_type eq "weight" ) { # if ( $out_amount{$out_type} < 1 ) { # $return_unit = "mg"; # $out_factor = 1000; # } elsif ( $out_amount{$out_type} > 1000 ) { # $return_unit = "kg"; # $out_factor = 0.001; # } else { # $return_unit = "g"; # $out_factor = 1; # } ## printf "> %5.2f, %s", $out_amount{$out_type} * $out_factor, $return_unit; # } # } if ( $out_factor ) { return $out_amount{$out_type} * $out_factor; } } else { return undef; } } sub cmd_setmeal { my ( $date, $mealtype, $recipe_ids ) = @_; if ($date =~ /^Mon|Tue|Wed|Thu|Fri|Sat|Sun/) { my $wday; $wday = 1 if ($date =~ /^Mon/); $wday = 2 if ($date =~ /^Tue/); $wday = 3 if ($date =~ /^Wed/); $wday = 4 if ($date =~ /^Thu/); $wday = 5 if ($date =~ /^Fri/); $wday = 6 if ($date =~ /^Sat/); $wday = 7 if ($date =~ /^Sun/); my $dt = DateTime->now(); while ($dt->wday() != $wday) { $dt->add(days => 1); } $date = $dt->ymd(); } return unless $mealtype =~ "^(frukost)|(elvakaffe)|(lunch)|(fruktstund)|(middag)\$"; return unless $date =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}\$"; return -1 unless $recipe_ids =~ m/^[0-9+]+$|^-$/; my $sql = "DELETE FROM plan WHERE date='$date' and mealtype='$mealtype';"; $db->do($sql); for my $recipe_id (split('\+', $recipe_ids)) { if ($recipe_id ne '-' && get_recipe_name($recipe_id) ne 'NULL') { $sql = "INSERT INTO plan (date, mealtype, recipe_id) VALUES ('$date', '$mealtype', $recipe_id);"; $db->do($sql); } } return 0; } sub cmd_setcomment { my ( $date, $mealtype, $comment ) = @_; if ($date =~ /^Mon|Tue|Wed|Thu|Fri|Sat|Sun/) { my $wday; $wday = 1 if ($date =~ /^Mon/); $wday = 2 if ($date =~ /^Tue/); $wday = 3 if ($date =~ /^Wed/); $wday = 4 if ($date =~ /^Thu/); $wday = 5 if ($date =~ /^Fri/); $wday = 6 if ($date =~ /^Sat/); $wday = 7 if ($date =~ /^Sun/); my $dt = DateTime->now(); while ($dt->wday() != $wday) { $dt->add(days => 1); } $date = $dt->ymd(); } return unless $mealtype =~ "^(frukost)|(elvakaffe)|(lunch)|(fruktstund)|(middag)\$"; return unless $date =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}\$"; my $sql = "INSERT INTO comments VALUES (NULL, '$comment');"; $db->do($sql); my $comment_id = $db->last_insert_id(undef, undef, undef, undef); $sql = "INSERT INTO plan (date, mealtype, comment_id) VALUES ('$date', '$mealtype', $comment_id);"; $db->do($sql); return 0; } sub cmd_movemeal { my ( $source, $destination ) = @_; my $mealtype = "lunch"; return unless $source =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}\$"; return unless $destination =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}\$"; my $sql = "SELECT FROM plan WHERE date='$source';"; # FIXME Run and validate existance of source $db->do($sql); # FIXME Handle if destination already exists $sql = "UPDATE plan SET date='$destination' WHERE date='$source';"; $db->do($sql); return 0; } sub cmd_relocate { my ( $inventory_id, $storage ) = @_; my $sql = "UPDATE inventory SET storage='$storage' WHERE id=$inventory_id;"; $db->do($sql); } sub cmd_inventory { my ( $storage ) = @_; my %total; my $sql = "SELECT recipe_id, count(recipe_id), storage FROM inventory"; $sql .= " WHERE storage='$storage'" if ( $storage ); $sql .= " GROUP BY storage, preparation_date, recipe_id"; $total{'all'} = 0; my $all = $db->selectall_arrayref($sql); foreach my $row (@$all) { $total{'all'} += @$row[1]; $sql = "SELECT m.mealtype FROM typemap AS t JOIN mealtypes AS m ON ". "t.mealtype_id=m.id WHERE t.recipe_id=".@$row[0]." ORDER by m.id;"; my $mealtypes = $db->selectcol_arrayref($sql); printf "%3d x %s (%s)", @$row[1], get_recipe_name(@$row[0]), @$row[0]; foreach my $mealtype ( @$mealtypes ) { if($mealtype eq $$mealtypes[0]) { print " [" if($mealtype eq $$mealtypes[0]); } else { print " "; } print "$mealtype"; print "]" if($mealtype eq $$mealtypes[scalar(@$mealtypes)-1]); $total{$mealtype} += @$row[1] / scalar(@$mealtypes); } if ( $storage ) { print "\n"; } else { print " ", @$row[2], "\n"; } } printf "----\n%3d (", $total{'all'}; delete $total{'all'}; my $s = ""; foreach my $mealtype ( keys(%total) ) { printf "$s%s %3d", $mealtype, $total{$mealtype}; $s = ', '; } printf ")\n"; } sub cmd_randmeal { my ( $date ) = @_; my $recipe_id = get_random_recipe(); return cmd_setmeal($date, $recipe_id); } sub cmd_postpone { my ( $date, $gap ) = @_; my $mealtype = "lunch"; my $startdate; if($date) { return -1 unless $date =~ "^[0-9]{4}-[0-9]{2}-[0-9]{2}\$"; $startdate = DateTime::Format::ISO8601->parse_datetime( $date ); } else { $startdate = DateTime->now(); # FIXME now() is not start of day, set hour, minute and such to 0 } $gap = 1; my $entries = $db->selectall_arrayref("SELECT date FROM plan WHERE date ". "BETWEEN '".$startdate."' and '2999-01-01' ORDER BY date DESC", { Slice => {} }); for my $entry ( @$entries ) { my $nextdate = DateTime::Format::ISO8601->parse_datetime($entry->{date}); $nextdate->add(days => $gap); $db->do("UPDATE plan SET date='".$nextdate->ymd()."' WHERE date='". $entry->{date}."';\n"); } } sub print_label { my ( $id, $dish_name, $amount, $preparation_date, $energy, $proteins, $carbs, $fats ) = @_; my $western_font = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; my $fallback_font = "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf"; my $fontsize = 15; my $x_border = 18; # Obtained with papersize.sh my $y_border = 35; my $x_limit = 324; my $y_limit = 307; my $x_max = $x_limit-$x_border; my $y_max = $y_limit-$y_border; my $qr_border = 4; my $iconv = Text::Iconv->new("UTF-8", "ISO8859-1"); my @texts; $texts[0]->{value} = $Config{'label_name'}." ".$preparation_date; $texts[0]->{encoding} = "latin1"; push @texts, { value => "\n", encoding=>'none' }; #my $row0 = $Config{'label_name'}." ".$preparation_date; #my $row1; #my $row2; for ( split(/ /, $dish_name )) { if ( $iconv->convert($_) ) { push @texts, { value => $_, encoding=>'latin1' }; } else { push @texts, { value => $_, encoding=>'utf8' }; } } if ( $amount ) { push @texts, { value => "(".$amount."g)", encoding => "latin1" }; } if ( $energy ) { push @texts, { value => $energy.' kJ ('.int($energy * 100 / $amount).' kJ/hg)', encoding=>'latin1' }; } else { push @texts, { value => '', encoding=>'latin1' }; } push @texts, { value => "\n", encoding=>'none' }; if ( $proteins ) { push @texts, { value => sprintf('P: %.0f%%', $proteins), encoding=>'latin1' }; } else { push @texts, { value => 'P: ?', encoding=>'latin1' }; } if ( $carbs ) { push @texts, { value => sprintf('C: %.0f%%', $carbs), encoding=>'latin1' }; } else { push @texts, { value => 'C: ?', encoding=>'latin1' }; } if ( $fats ) { push @texts, { value => sprintf('F: %.0f%%', $fats), encoding=>'latin1' }; } else { push @texts, { value => 'F: ?', encoding=>'latin1' }; } my $idbarcode = GD::Barcode::QRcode->new(sprintf("%s%d", $Config{'label_id_prefix'}, $id), { Version=>3 }); my $idbarcode_image=$idbarcode->plot(); my $qr_cropped = new GD::Image($idbarcode_image->width() - 2 * $qr_border, $idbarcode_image->height() - 2 * $qr_border); my $white = $qr_cropped->colorAllocate(255,255,255); my $black = $qr_cropped->colorAllocate(0,0,0); $qr_cropped->fill(0, 0, $white); $qr_cropped->copy($idbarcode_image, 0, 0, $qr_border, $qr_border, $idbarcode_image->width() - $qr_border, $idbarcode_image->height() - $qr_border); my $logo; if ($ENV{'PRINT_WITH_LOGO'}) { $logo = GD::Image->newFromPng("layer1.png"); } my $idtext = new GD::Image(500, $fontsize * 2); $white = $idtext->colorAllocate(255,255,255); $black = $idtext->colorAllocate(0,0,0); $idtext->fill(1, 1, $white); $idtext->string(gdGiantFont, 0, 0, $id, $black); my $label = new GD::Image($x_max, $y_max); $white = $label->colorAllocate(255,255,255); $black = $label->colorAllocate(0,0,0); $label->fill(0, 0, $white); $label->copyResized($qr_cropped, 0, 0, 0, 0, $qr_cropped->width() * 5, $qr_cropped->height() * 5, $qr_cropped->width(), $qr_cropped->height()); $label->copyResized($idtext, 0, 0 + $qr_cropped->height() * 5, 0, 0, $idtext->width() * 2, $idtext->height() * 2, $idtext->width(), $idtext->height()); my %text_pos = ( x => 275, y => 0 ); for ( @texts ) { if ( $_->{value} eq "\n" ) { $text_pos{y} = 0; $text_pos{x} -= $fontsize * 1.6; } else { my $text0 = new GD::Image(500, $fontsize * 3); $white = $text0->colorAllocate(255,255,255); $black = $text0->colorAllocate(0,0,0); $text0->fill(1, 1, $white); my @bounds0; if ( $_->{encoding} eq "latin1" ) { @bounds0 = $text0->stringFT(-1 * $black, $western_font, $fontsize, 0, 0, 2 * $fontsize, $_->{value}); } else { @bounds0 = $text0->stringFT(-1 * $black, $fallback_font, $fontsize, 0, 0, 2 * $fontsize, $_->{value}); } my $text0_image=$text0->copyRotate90(); if ( ($text_pos{y} + $bounds0[2]) > 275 ) { $text_pos{y} = 0; $text_pos{x} -= $fontsize * 1.6; } $label->copyResized($text0_image, $text_pos{x}, $text_pos{y}, 0, 0, $bounds0[1] * 1, $bounds0[2], $bounds0[1], $bounds0[2]); $text_pos{y} += $bounds0[2] + 5; } } if ($ENV{'PRINT_WITH_LOGO'}) { my ($logo_w, $logo_h ) = $logo->getBounds(); my $vl = $logo->copyRotate90(); # $label->copy($vl, 0, $y_max - $logo_w, 0, 0, $logo_h, $logo_w); $label->copy($vl, 0, 267 - $logo_w, 0, 0, $logo_h, $logo_w); } my $offset = new GD::Image($x_max, 267); $white = $offset->colorAllocate(255,255,255); $black = $offset->colorAllocate(0,0,0); $offset->fill(0, 0, $white); $offset->copy($label, 0, 0, 0, 0, $label->width(), $label->height()); open(PNGFILE, ">label.png"); print PNGFILE $offset->png; system($Config{'print_command'}); } sub cmd_storeportion { my ( $recipe_id, $amount, $storage ) = @_; my $energy; my ( $proteins, $carbs, $fats ); my $sql = "SELECT * FROM cookings WHERE recipe_id=$recipe_id AND ". "julianday('now')-julianday(date) < 2;"; my $row = $db->selectrow_hashref($sql); if ($row) { $energy = int($amount * $row->{'specific_energy'} / 100); $proteins = $row->{'proteins'}; $carbs = $row->{'carbs'}; $fats = $row->{'fats'}; } $sql = "INSERT INTO inventory (recipe_id, preparation_date, amount, ". "storage, energy ) VALUES ($recipe_id, ".`date +%Y%m%d|tr -d '\n'`.", $amount, '$storage', ".(defined($energy) ? "$energy" : 'NULL').");"; $db->do($sql); my $inventory_id = $db->last_insert_id(undef, undef, undef, undef); print_label($inventory_id, get_recipe_name($recipe_id), $amount, `date +%Y%m%d|tr -d '\n'`, $energy, $proteins, $carbs, $fats); } sub cmd_reprintlabel { my ( $id ) = @_; my $sql = "SELECT recipe_id, preparation_date, amount, storage, energy FROM ". "inventory WHERE id=$id"; my $row = $db->selectrow_hashref($sql); $sql = "SELECT specific_energy, proteins, carbs, fats FROM cookings WHERE recipe_id=? AND date=?;"; my $sql_date_format = $row->{preparation_date}; $sql_date_format =~ s/(....)(..)(..)/$1-$2-$3/; my $cookings = $db->selectrow_hashref($sql, undef, $row->{recipe_id}, $sql_date_format); if ( ! $row->{'energy'} ) { my $updated_energy = int($cookings->{'specific_energy'}*$row->{'amount'}/100); if ($cookings->{'specific_energy'}) { $sql = "UPDATE inventory SET energy=".$updated_energy." WHERE id=$id;"; my $sth = $db->prepare($sql); my $dummy = $sth->execute(); $row->{'energy'} = $updated_energy; } } print_label($id, get_recipe_name($row->{'recipe_id'}), $row->{'amount'}, $row->{'preparation_date'}, $row->{'energy'}, $cookings->{'proteins'}, $cookings->{'carbs'}, $cookings->{'fats'}); return 0; } sub get_plan_state { my ( $recipe_id, $end_date ) = @_; my $state; my $sql = "SELECT COUNT(id) FROM inventory WHERE recipe_id=$recipe_id AND ". "(storage GLOB '*frys' OR storage GLOB '*kyl');"; my $prepared = $db->selectcol_arrayref($sql); $sql = "SELECT recipe_id FROM plan WHERE recipe_id=$recipe_id AND ". "date >= '".DateTime->now->ymd()."' AND date < '$end_date';"; my $already_planned = $db->selectcol_arrayref($sql); my $portions_left = @{$prepared}[0] - scalar(@{$already_planned}); if ( $portions_left > 0 ) { $state = 'prepared'; } else { $state = 'unavailable'; } return $state; } sub cmd_showplan { my ( $days, $date ) = @_; my $dt; my $weekend_padding = ""; $days = 7 unless defined($days) && $days =~ /^[0-9]{1,2}$/; DateTime->DefaultLocale("sv_SE"); # FIXME Don't hårdkoda svenska, tack! if($date) { return -1 unless $date =~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/; $dt = DateTime::Format::ISO8601->parse_datetime( $date ); } else { $dt = DateTime->now(); } for (my $i = 0; $i < $days; $i++) { my $sql = "SELECT recipe_id, p.mealtype, comment_id FROM plan AS p JOIN ". "mealtypes AS m ON p.mealtype=m.mealtype WHERE date='".$dt->ymd(). "' ORDER by m.id;"; my $plan = $db->selectall_arrayref($sql); print $weekend_padding; my $last_mealtype = ""; for my $meal ( @$plan ) { my $mealtype = $meal->[1]; if ( $mealtype ne $last_mealtype ) { print "\n"; if ($meal->[0]) { printf "%-10s %-10s %3s|%s (%s)", ($last_mealtype eq "" ? $dt->ymd() : ($mealtype eq "elvakaffe" ? $dt->day_name() : "")), # FIXME remove hard coded mealtype $mealtype, ($meal->[0] ? substr(get_plan_state($meal->[0], $dt->ymd()), 0, 1) : " "), get_recipe_name($meal->[0]), $meal->[0], } elsif ($meal->[2]) { printf "%-10s %-13s |%s", ($last_mealtype eq "" ? $dt->ymd() : ($mealtype eq "elvakaffe" ? $dt->day_name() : "")), $mealtype, # FIXME remove hard coded mealtype get_comment($meal->[2]); } else { printf "%-10s %-13s | ", ($last_mealtype eq "" ? $dt->ymd() : ($mealtype eq "elvakaffe" ? $dt->day_name() : "")), $mealtype; # FIXME remove hard coded mealtype } } else { print " + ", get_recipe_name($meal->[0]), " (", $meal->[0], ")"; } $last_mealtype = $mealtype; } print "\n"; $dt->add(days => 1); if($dt->day_of_week == 1) { $weekend_padding = "\n"; } else { $weekend_padding = ""; } } } sub cmd_randweek { # for (my $i = 0; $i < 5; $i++) { # $schedule[$i]->{'recipe'} = get_random_recipe; # } # # for (my $i = 0; $i < 5; $i++) { # print $schedule[$i]->{'day'}.": "; # print get_recipe_name($schedule[$i]->{'recipe'})."\n"; # my $contents = $db->selectall_arrayref("SELECT * FROM contents WHERE ". # "recipe_id=".$schedule[$i]->{'recipe'}); # unless (@$contents) { # print "OBSERVERA: Ingredienslista saknas för denna rätt!\n\n"; # } # } } sub cmd_help() { print "help\n"; print "addrecipe\n"; print "searchrecipes \n"; print "editrecipe \n"; print "showrecipe \n"; print "showingredient \n"; print "movemeal \n"; print "setmeal \n"; print "setcomment \n"; print "randmeal \n"; print "showplan [days] [date]\n"; print "postpone \n"; print "storeportion [amount] [storage]\n"; print "reprintlabel \n"; print "retrieveportion (unimplemented)\n"; print "relocate \n"; print "inventory [storage]\n"; print "shoppinglist \n"; print "queueshow\n"; print "queueadd \n"; print "queuerm \n"; print "queuemv (Yet to be implemented)\n"; } sub interactive_edit_recipe_ingredients { # TODO It would be nice to be able to input ingredients in the format: # my ( $recipe_id ) = @_; my ( $ingredient, $quantity, $unit, $answer, $sql ); while (1) { my @matched_ingredients; print "Ingredient: "; $ingredient = ; chomp $ingredient; last if ($ingredient eq ''); for (@ingredients) { if (grep (/$ingredient/, $_->{'name'})) { print $_->{'name'}, "\n"; push @matched_ingredients, $_->{'id'}; } } if (length(@matched_ingredients == 0)) { $sql = "INSERT INTO ingredients (name) VALUES ('$ingredient');"; print "$sql\n\n"; print "Add new ingredient? (y/n): "; $answer = ; if ($answer eq "y\n") { $db->do($sql); read_ingredients; } } elsif (length(@matched_ingredients == 1)) { my $ingredient_id = $matched_ingredients[0]; print "Amount: "; $quantity = ; chomp $quantity; # my $default_unit = "SELECT unit FROM contents WHERE ingredient_id='" # .$ingredient_id."' LIMIT 1;" print "Unit: "; $unit = ; chomp $unit; if (($quantity ne '') and ($unit ne '')) { $sql = "INSERT INTO contents (recipe_id, ingredient_id, quantity, ". "unit) VALUES ('$recipe_id', '$ingredient_id', '$quantity', ". "'$unit')"; print "$sql\n\n"; print "Add row? (y/n): "; $answer = ; $db->do($sql) if ($answer eq "y\n"); } } } return 0; } sub cmd_editrecipe { my ( $recipe_id ) = @_; my ( $recipe_name, $recipe_uri, $answer, $sql ); return -1 unless($recipe_id and ($recipe_name = get_recipe_name($recipe_id)) ne 'NULL'); $recipe_uri = get_recipe_uri($recipe_id); print "Recipe name: $recipe_name\n"; print "Recipe uri: $recipe_uri\n"; print "Change these? (y/n) "; $answer = ; if ($answer eq "y\n") { print "Recipe name: "; $recipe_name = ; chomp $recipe_name; print "E.g. http://stupid.domain.name/node/755, urn:isbn:9789127118348#102\n"; print "Recipe uri: "; $recipe_uri = ; chomp $recipe_uri; $sql = "UPDATE recipes SET name='$recipe_name', uri='$recipe_uri' WHERE id=$recipe_id;"; print "$sql\n\n"; print "Update database? (y/n): "; $answer = ; if ($answer eq "y\n") { $db->do($sql); } } interactive_edit_recipe_ingredients($recipe_id); } sub cmd_addrecipe { # FIXME Make it possible to provide name and uri as command line arguments # instead of solely interactive mode. my ( $recipe_name, $uri, $servings, $answer, $sql ); print "Recipe name: "; $recipe_name = ; chomp $recipe_name; print "E.g. http://stupid.domain.name/node/755, urn:isbn:9789127118348#102\n"; print "Recipe uri: "; $uri = ; chomp $uri; print "Servings: "; $servings = ; chomp $servings; $sql = "INSERT INTO recipes (name, uri, servings) VALUES (". $db->quote($recipe_name).", ".$db->quote($uri).", ". $db->quote($servings).");"; print "$sql\n\n"; print "Add to database? (y/n): "; $answer = ; if ($answer eq "y\n") { $db->do($sql); } $sql = "SELECT id FROM recipes WHERE name=".$db->quote($recipe_name). " AND uri=".$db->quote($uri).";"; my @recipe_id = $db->selectrow_array($sql); return interactive_edit_recipe_ingredients($recipe_id[0]); } sub ingredient_row { my ( $ingredient, $original_quantity, $original_unit ) = @_; my $ingredient_row = $db->selectrow_hashref("SELECT * FROM ingredients WHERE id=".$ingredient.";"); my $primary_unit; my $primary_quantity; my $weight_unit; my $weight_quantity; my $volume_unit; my $volume_quantity; if ( $ingredient_row->{primary_unit} ) { $primary_unit = $ingredient_row->{primary_unit}; $primary_quantity = convert_to_unit($primary_unit, $ingredient, $original_quantity, $original_unit); } else { $primary_unit = $original_unit; $primary_quantity = $original_quantity; } # if ($original_unit eq "g" ) { # $convert_unit = $ingredient_row->{primary_unit}; # } else { # $convert_unit = "g"; # } # if ($convert_unit) { # $convert_quantity = convert_to_unit($convert_unit, $ingredient, $original_quantity, $original_unit); # } $weight_unit = 'g'; $weight_quantity = convert_to_unit($weight_unit, $ingredient, $original_quantity, $original_unit); $volume_unit = 'ml'; $volume_quantity = convert_to_unit($volume_unit, $ingredient, $original_quantity, $original_unit); # printf "%4s %-8s ", $original_quantity, $original_unit; # if ( $convert_quantity ) { # printf "(%4.4s %-8s)", $convert_quantity, $convert_unit; # } else { # printf "%15s", " "; # } # printf " %s\n", $ingredient_row->{name}; printf "%4.4s %-8s ", $original_quantity, $original_unit; if ( $weight_quantity ) { printf "(%5.4s %-1s,", int($weight_quantity), $weight_unit; } else { printf "(%8s", ""; } if ( $volume_quantity ) { printf "%5.4s %-2s)", int($volume_quantity), $volume_unit; } else { printf "%8s)", ""; } printf " %s\n", $ingredient_row->{name}; } sub cmd_showrecipe { # Argument is recipe_id my ( $recipe_id, $desired_servings ) = @_; my $recipe_row = $db->selectrow_arrayref("SELECT name, uri, servings FROM recipes WHERE id=".$recipe_id.";"); my $servings; my $ingredient_multipler = 1; my $debug_substitution = "no"; if(@$recipe_row[1]) { my $qrcode = Text::QRCode->new(); my $urlqr = $qrcode->plot(@$recipe_row[1]); for ( @$urlqr ) { print "██"; } print "████\n"; for ( @$urlqr ) { print "██"; for ( @$_ ) { if ( $_ eq "*" ) { # print "##"; print " "; } else { print "██"; } } print "██\n"; } for ( @$urlqr ) { print "██"; } print "████\n"; print @$recipe_row[0]; if(defined(@$recipe_row[1])) { printf ", %s\n", @$recipe_row[1]; } else { print "\n"; } } if(defined(@$recipe_row[2])) { $servings = @$recipe_row[2]; if(defined($desired_servings)) { print "Serves: $desired_servings\n\n"; $ingredient_multipler = $desired_servings/$servings; } else { print "Serves: $servings\n\n"; } } else { $servings = 1; print "WARNING servings is not set for recipe $recipe_id!\n\n"; } my $total_weight = 0; my $total_volume = 0; my $parts = $db->selectcol_arrayref("SELECT DISTINCT(recipe_part) FROM contents WHERE recipe_id=".$recipe_id.";"); for my $part ( @$parts ) { my $part_sql = ""; my $ingredient_weight; my $ingredient_volume; if( defined($part) ) { print "part $part\n"; $part_sql = " AND recipe_part='".$part."'"; } my $contents = $db->selectall_hashref("SELECT * FROM contents WHERE recipe_id=".$recipe_id.$part_sql.";", 'ingredient_id'); for my $content ( keys(%$contents)) { $$contents{$content}{quantity} = $$contents{$content}{quantity} * $ingredient_multipler; printf "%8s", ""; ingredient_row($content, $$contents{$content}{quantity}, $$contents{$content}{unit}); $ingredient_weight = convert_to_unit('g', $content, $$contents{$content}{quantity}, $$contents{$content}{unit}); $ingredient_volume = convert_to_unit('dl', $content, $$contents{$content}{quantity}, $$contents{$content}{unit}); $total_weight += ($ingredient_weight?$ingredient_weight:0); $total_volume += ($ingredient_volume?$ingredient_volume:0); my $substitutes = $db->selectall_arrayref("SELECT substitute_ingredient FROM substitutes WHERE original_ingredient=".$content.";"); if ( $substitutes->[0] ) { for ( @$substitutes ) { my $substitute = $db->selectrow_hashref("SELECT * FROM substitutes WHERE original_ingredient=".$content." AND substitute_ingredient=$_->[0];"); my $substituted_quantity; # Attempt at converting original ingredient to unit reqiured for substitution my $convert_me = convert_to_unit($substitute->{original_unit}, $content, $$contents{$content}{quantity}, $$contents{$content}{unit}); if ( defined($convert_me) ) { # Convert according to substitution table $substituted_quantity = $convert_me / $substitute->{original_quantity} * $substitute->{substitute_quantity}; } else { # ( $out_unit, $id, $in_amount, $in_unit ) = @_; $substituted_quantity = 1.0 * $$contents{$content}{quantity} * convert_to_unit($substitute->{original_unit}, $content, 1, $$contents{$content}{unit}) / $substitute->{original_quantity}; } if ( $debug_substitution ne "no" ) { printf "ingredient: %d, recipe_unit: '%s', original_unit: '%s', convert_me: %d, substituted_quantity: %d\n\n", $content, $$contents{$content}{unit}, $substitute->{original_unit}, $convert_me, $substituted_quantity; } printf "%8s", "eller "; ingredient_row($substitute->{substitute_ingredient}, $substituted_quantity, $substitute->{substitute_unit}); } } } print "\n"; } printf "\nTotal ingredients: (about) %5d g, %3.1f dl\n", $total_weight, $total_volume; print "\n"; my $cooking_comments = $db->selectall_hashref("SELECT date, comment FROM cookings WHERE recipe_id=".$recipe_id." AND comment IS NOT NULL;", 'date'); my $eating_comments = $db->selectall_hashref("SELECT date, comment FROM eatings AS e JOIN inventory AS i ON e.inventory_id=i.id WHERE recipe_id=".$recipe_id." AND comment IS NOT NULL;", 'date'); if ( keys(%{$cooking_comments}) or keys(%{$eating_comments}) ) { print "\n"; } for my $date ( keys(%$cooking_comments) ) { printf "%10s %s\n", $date, $cooking_comments->{$date}{'comment'}; } for my $date ( keys(%$eating_comments) ) { printf "%10s %s\n", $date, $eating_comments->{$date}{'comment'}; } return 1; } sub cmd_showingredient { my ( $ingredient_id, $multiplier ) = @_; my $row = $db->selectrow_hashref("SELECT name, primary_unit, density, piece_weight FROM ingredients WHERE id=".$ingredient_id.";"); $multiplier = 1 unless ($multiplier); print "Ingredient: ".$row->{name}."\n"; if ($row->{piece_weight}) { print " Piece weight ($multiplier): ".$multiplier*$row->{piece_weight}." g\n"; } if ($row->{density}) { for my $unit ('ml', 'cl', 'dl', 'l', 'krm', 'tsk', 'msk') { print " $multiplier $unit: ".convert_to_unit('g', $ingredient_id, $multiplier, $unit)." g\n"; } } return 1; } sub cmd_searchrecipes { # Argument is search regexp my ( $regex ) = @_; my $i; for my $recipe (@recipes) { $i++; # while (my ($i, $recipe) = each @recipes) { next if not defined($recipe); if(grep(/$regex/i, $recipe->{'name'})) { printf "%4d| %-50s\n", $i-1, $recipe->{'name'}; } } return 1; } sub cmd_queueadd { my ( $recipe_id, $servings ) = @_; my $sql = "SELECT COUNT(id) FROM queue;"; my $id = $db->selectrow_arrayref($sql); return -1 unless $recipe_id =~ "^[0-9]+\$"; return -1 unless $servings =~ "^[0-9]+\$"; if (get_recipe_name($recipe_id) ne 'NULL') { $sql = "INSERT INTO queue (id, recipe_id, servings) VALUES (@$id[0], ". "$recipe_id, $servings)"; $db->do($sql); } return 0; } sub cmd_queueshow { my $sql = "SELECT id, recipe_id, servings FROM queue"; my $sth = $db->prepare($sql); my $rv = $sth->execute; $db->{RaiseError} = 0; while (my @row_ary = $sth->fetchrow_array) { printf "%3s|%2d x %s (%d)\n", $row_ary[0], $row_ary[2], get_recipe_name($row_ary[1]), $row_ary[1]; } $db->{RaiseError} = 1; } sub cmd_queuerm { my ( $queue_id ) = @_; my $sql = "DELETE FROM queue WHERE id=$queue_id;"; my $sth = $db->prepare($sql); my $rv = $sth->execute; my $id = $queue_id; do { my $next = $id + 1; $sql = "UPDATE queue SET id=$id WHERE id=$next;"; $id++; } while($db->do($sql) == 1); } sub cmd_queuemv { print "Yet to be implemented!\n"; } sub cmd_shoppinglist { # Argument is number of days to shop for my ( $shopdays ) = @_; my $startdate = DateTime->now(); my $enddate = $startdate->clone(); $shopdays=7 unless $shopdays; $enddate->add(days => $shopdays); my $plan_entries = $db->selectall_arrayref("SELECT recipe_id, date FROM plan WHERE recipe_id AND date ". "BETWEEN '".$startdate."' AND '".$enddate."';", { Slice => {} }); my $queue_entries = $db->selectall_arrayref("SELECT recipe_id FROM queue;"); my %recipe_count; for my $entry ( @$plan_entries ) { if (get_plan_state($entry->{'recipe_id'}, $entry->{'date'}) ne "prepared") { $recipe_count{$entry->{recipe_id}}++; } } for my $entry ( @$queue_entries ) { my @s = $db->selectrow_array("SELECT servings FROM queue WHERE ". "recipe_id=@$entry[0]"); $recipe_count{@$entry[0]} += $s[0]; } my @shop_recipes; for my $recipe ( keys(%recipe_count) ) { my $servings_col = $db->selectcol_arrayref("SELECT servings FROM recipes WHERE id=".$recipe.";"); my $servings; if(defined(@$servings_col[0]) && (@$servings_col[0]) gt 0) { $servings = @$servings_col[0]; } else { $servings = 1; print "WARNING servings is not set for recipe $recipe!\n"; } my $cookings = ceiling($recipe_count{$recipe}/$servings); for (my $i=0; $i < $cookings; $i++) { push @shop_recipes, $recipe; } } my @shop_ingredients; for my $recipe (@shop_recipes) { # my $contents = $db->selectall_arrayref("SELECT * FROM contents WHERE recipe_id=".$recipe.";"); # my $contents = $db->selectall_hashref("SELECT * FROM contents WHERE recipe_id=".$recipe.";", 'ingredient_id'); # my $contents = $db->selectall_hashref("SELECT recipe_id, ingredient_id, SUM(quantity), unit FROM contents WHERE recipe_id=".$recipe." GROUP BY ingredient_id;", 'ingredient_id'); my $contents = $db->selectall_hashref("SELECT recipe_id, ingredient_id, SUM(quantity) AS quantity, unit FROM contents WHERE recipe_id=".$recipe." GROUP BY ingredient_id;", 'ingredient_id'); unless (%$contents) { print "WARNING recipe $recipe contains no ingredients!\n"; } for my $content ( keys(%$contents)) { my %shop; my $ingredientrow = $db->selectrow_arrayref("SELECT name, primary_unit FROM ingredients WHERE id=".$content.";"); # print $$contents{$content}{unit}, $content, " ", @$ingredientcol[0], "\n"; # print keys(%$contents), "\n"; $shop{id} = $content; $shop{quantity} = $$contents{$content}{quantity}; # $shop{shop_position} = $shop{unit} = $$contents{$content}{unit}; $shop{ingredient} = @$ingredientrow[0]; $shop{primary_unit} = @$ingredientrow[1]; $shop{recipe}[0] = $recipe; push @shop_ingredients, \%shop; } } my @converted_ingredients; for my $ingredient ( @shop_ingredients ) { my $convert_unit; my $convert_result; $convert_unit = "g"; if ($convert_unit) { $convert_result = convert_to_unit($convert_unit, $ingredient->{'id'}, $ingredient->{'quantity'}, $ingredient->{'unit'}); if ( $convert_result ) { $ingredient->{'unit'} = $convert_unit; $ingredient->{'quantity'} = $convert_result; } } push @converted_ingredients, $ingredient; } my @sorted_ingredients = sort { $a->{ingredient} cmp $b->{ingredient} } @converted_ingredients; my @squeezed_ingredients; for my $squeeze ( @sorted_ingredients ) { if (exists($squeezed_ingredients[-1]) and $squeezed_ingredients[-1]{ingredient} eq $squeeze->{ingredient} and $squeezed_ingredients[-1]{unit} eq $squeeze->{unit}) { $squeezed_ingredients[-1]{quantity} += $squeeze->{quantity}; push @{$squeezed_ingredients[-1]{recipe}}, $squeeze->{recipe}[0] unless grep(/$squeeze->{recipe}[0]/, @{$squeezed_ingredients[-1]{recipe}}); } else { push @squeezed_ingredients, $squeeze; } } for my $shop ( @squeezed_ingredients ) { my $convert_result; if ($shop->{'primary_unit'}) { $convert_result = convert_to_unit($shop->{'primary_unit'}, $shop->{'id'}, $shop->{'quantity'}, $shop->{'unit'}); } printf "%4s %-8s ", $shop->{quantity}, $shop->{unit}; if ($convert_result) { printf "%4.4s %-8s ", $convert_result, $shop->{'primary_unit'}; } else { printf "%4.4s %-8s ", "", ""; } printf "%-40s", $shop->{ingredient}; for ( @{$shop->{recipe}} ) { print get_recipe_name($_), ", "; } print "\n"; } open(my $f, "extrashopping.txt"); while(my $extra = <$f>) { print "$extra"; } return 1; } # MAIN PROGRAM ################################################################ read_recipe_db; my $commands = { "help" => { func => \&cmd_help, min => 0, max => 0 }, "addrecipe" => { func => \&cmd_addrecipe, min => 0, max => 0 }, "searchrecipes" => { func => \&cmd_searchrecipes, min => 1, max => 1 }, "editrecipe" => { func => \&cmd_editrecipe, min => 1, max => 1 }, "showrecipe" => { func => \&cmd_showrecipe, min => 1, max => 2 }, "showingredient" => { func => \&cmd_showingredient, min => 1, max => 2 }, "movemeal" => { func => \&cmd_movemeal, min => 2, max => 2 }, "setmeal" => { func => \&cmd_setmeal, min => 3, max => 3 }, "setcomment" => { func => \&cmd_setcomment, min => 3, max => 3 }, "randmeal" => { func => \&cmd_randmeal, min => 1, max => 1 }, "showplan" => { func => \&cmd_showplan, min => 0, max => 2 }, "postpone" => { func => \&cmd_postpone, min => 2, max => 2 }, "inventory" => { func => \&cmd_inventory, min => 1, max => 1 }, "storeportion" => { func => \&cmd_storeportion, min => 1, max => 3 }, "reprintlabel" => { func => \&cmd_reprintlabel, min => 1, max => 1 }, "relocate" => { func => \&cmd_relocate, min => 2, max => 2 }, "queueadd" => { func => \&cmd_queueadd, min => 2, max => 2 }, "queuerm" => { func => \&cmd_queuerm, min => 1, max => 1 }, "queuemv" => { func => \&cmd_queuemv, min => 2, max => 2 }, "queueshow" => { func => \&cmd_queueshow, min => 0, max => 0 }, "shoppinglist" => { func => \&cmd_shoppinglist, min => 0, max => 1 } }; if ($ARGV[0]) { my $command = $commands->{$ARGV[0]}; my $argcount = $#ARGV; if (defined($command)) { if ($argcount < $command->{'min'}) { print "Too few arguments for command $ARGV[0].\n"; exit 1; } if ($argcount > $command->{'max'}) { print "Too many arguments for command $ARGV[0].\n"; exit 1; } print "Command failed!\n" unless($command->{'func'}(splice @ARGV, 1) >= 0); } else { print "Unknown command $ARGV[0]\n"; exit 1; } } else { cmd_showplan; }