#!/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 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_arrayref("SELECT * FROM ingredients"); foreach my $row (@$all) { my ($id) = @$row; $ingredients[$id]->{'id'} = @$row[0]; $ingredients[$id]->{'name'} = @$row[1]; } } 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_unit { my ( $out_unit, $id, $in_amount, $in_unit ) = @_; my %out_amount; my %convert; # volume [l], weight [g], piece [st] # 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 "g") { $convert{'weight'} = $in_amount; } elsif ($in_unit eq "kg") { $convert{'weight'} = $in_amount * 1000; } elsif ($in_unit eq "st") { $convert{'piece'} = $in_amount; } my $sql = "SELECT density, piece_weight FROM ingredients WHERE id=".$id.";"; my $ingredient = $db->selectrow_hashref($sql); my $volume; my $density; my $weight; $density = $ingredient->{density}; # kg/m³ my $piece_weight; if ( $convert{'volume'} ) { $volume = 0.001 * $convert{'volume'}; # m³ if ($ingredient->{density}) { $out_amount{'weight'} = $density * $volume * 1000; # g } } elsif ( $convert{'weight'} ) { $weight = $convert{'weight'}; # g $out_amount{'weight'} = $weight; if ($ingredient->{'piece_weight'}) { $piece_weight = $ingredient->{'piece_weight'}; # g/piece $out_amount{'piece'} = $convert{'weight'} / $ingredient->{'piece_weight'}; # piece } if ($ingredient->{density}) { $out_amount{'volume'} = $weight / $density; # g } } elsif ( $convert{'piece'} ) { if ($ingredient->{'piece_weight'}) { $piece_weight = $ingredient->{'piece_weight'}; # g/piece $out_amount{'weight'} = $convert{'piece'} * $piece_weight; # g } } if ($out_unit eq "g") { if($out_amount{'weight'}) { return $out_amount{'weight'}; } else { return } } elsif ($out_unit eq "kg") { return $out_amount{'weight'}; } elsif ($out_unit eq "st") { return $out_amount{'piece'}; } elsif ($out_unit eq "l") { return $out_amount{'volume'}; } 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 ) = @_; my $fontname = "/usr/share/fonts/truetype/freefont/FreeSans.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 $latin1_dish = $iconv->convert($dish_name); my $row0 = $Config{'label_name'}." ".$preparation_date; my $row1; my $row2; if( length($latin1_dish) < 15) { $row1 = $latin1_dish; $row2 = "(".$amount."g)"; } else { $row1 = $latin1_dish; $row1 =~ s/(.{15,23}) .*/$1/; $row2 = substr($latin1_dish, (length $row1) + 1).' ('.$amount.'g)'; } my $row3; if ($energy) { $row3 = $energy.' kJ ('.int($energy * 100 / $amount).' kJ/hg)'; } else { $row3 = ''; } 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 $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 = $text0->stringFT(-1 * $black, $fontname, $fontsize, 0, 0, 2 * $fontsize, $row0); my $text0_image=$text0->copyRotate90(); my $text1 = new GD::Image(500, $fontsize * 3); $white = $text1->colorAllocate(255,255,255); $black = $text1->colorAllocate(0,0,0); $text1->fill(1, 1, $white); $text1->filledRectangle(0, 0, $text1->width(), $text1->height(), $white); my @bounds1 = $text1->stringFT(-1 * $black, $fontname, $fontsize, 0, 0, 2 * $fontsize, $row1); my $text1_image=$text1->copyRotate90(); my $text2 = new GD::Image(500, $fontsize * 3); $white = $text2->colorAllocate(255,255,255); $black = $text2->colorAllocate(0,0,0); $text2->fill(1, 1, $white); $text2->filledRectangle(0, 0, $text2->width(), $text2->height(), $white); my @bounds2 = $text2->stringFT(-1 * $black, $fontname, $fontsize, 0, 0, 2 * $fontsize, $row2); my $text2_image=$text2->copyRotate90(); my $text3 = new GD::Image(500, $fontsize * 3); $white = $text3->colorAllocate(255,255,255); $black = $text3->colorAllocate(0,0,0); $text3->fill(1, 1, $white); $text3->filledRectangle(0, 0, $text3->width(), $text3->height(), $white); my @bounds3 = $text3->stringFT(-1 * $black, $fontname, $fontsize, 0, 0, 2 * $fontsize, $row3); my $text3_image=$text3->copyRotate90(); 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()); $label->copyResized($text0_image, $x_max - 2 * $fontsize, 0, 0, 0, $bounds0[1] * 1, $bounds0[2], $bounds0[1], $bounds0[2]); $label->copyResized($text1_image, $x_max - 4 * $fontsize, 0, 0, 0, $bounds1[1] * 1, $bounds1[2], $bounds1[1], $bounds1[2]); $label->copyResized($text2_image, $x_max - 6 * $fontsize, 0, 0, 0, $bounds2[1] * 1, $bounds2[2], $bounds2[1], $bounds2[2]); $label->copyResized($text3_image, $x_max - 8 * $fontsize, 0, 0, 0, $bounds3[1] * 1, $bounds3[2], $bounds3[1], $bounds3[2]); 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 $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); } $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); } 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); print_label($id, get_recipe_name($row->{'recipe_id'}), $row->{'amount'}, $row->{'preparation_date'}, $row->{'energy'}); 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 "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 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; 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 $contents = $db->selectall_hashref("SELECT * FROM contents WHERE recipe_id=".$recipe_id.";", 'ingredient_id'); for my $content ( keys(%$contents)) { $$contents{$content}{quantity} = $$contents{$content}{quantity} * $ingredient_multipler; my $ingredientcol = $db->selectrow_arrayref("SELECT name, primary_unit FROM ingredients WHERE id=".$content.";"); my $convert_unit; my $convert_result; if ($$contents{$content}{unit} eq "g" ) { $convert_unit = @$ingredientcol[1]; } else { $convert_unit = "g"; } if ($convert_unit) { $convert_result = convert_to_unit($convert_unit, $content, $$contents{$content}{quantity}, $$contents{$content}{unit}); } printf "%4s %-8s ", $$contents{$content}{quantity}, $$contents{$content}{unit}; if ( $convert_result ) { printf "(%4.4s %-8s)", $convert_result, $convert_unit; } else { printf "%15s", " "; } printf " %s\n", @$ingredientcol[0]; } 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'); unless (%$contents) { print "WARNING recipe $recipe contains no ingredients!\n"; } for my $content ( keys(%$contents)) { my %shop; # print %$contents, "\n"; my $ingredientrow = $db->selectrow_arrayref("SELECT name, primary_unit FROM ingredients WHERE id=".$content.";"); # print $content, " ", @$ingredientcol[0], "\n"; # 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 }, "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; }