1 package BSE::UI::AdminShop;
3 use base 'BSE::UI::AdminDispatch';
7 use BSE::TB::OrderItems;
9 #use Squirrel::ImageEditor;
10 use Constants qw(:shop $SHOPID $PRODUCTPARENT
11 $SHOP_URI $CGI_URI $IMAGES_URI $AUTO_GENERATE);
15 use BSE::Util::Tags qw(tag_hash);
16 use BSE::Util::Iterate;
17 use BSE::WebUtil 'refresh_to_admin';
18 use BSE::Util::HTML qw(:default popup_menu);
20 use BSE::Shop::Util qw(:payment order_item_opts nice_options);
22 our $VERSION = "1.001";
26 order_list => 'shop_order_list',
27 order_list_filled => 'shop_order_list',
28 order_list_unfilled => 'shop_order_list',
29 order_list_unpaid => 'shop_order_list',
30 order_list_incomplete => 'shop_order_list',
31 order_detail => 'shop_order_detail',
32 order_filled => 'shop_order_filled',
33 order_paid => 'shop_order_paid',
34 order_unpaid => 'shop_order_unpaid',
37 paypal_refund => 'bse_shop_order_refund_paypal',
59 sub embedded_catalog {
60 my ($req, $catalog, $template) = @_;
62 my $session = $req->session;
64 my $products = Products->new;
66 if ($session->{showstepkids}) {
67 my @allkids = $catalog->allkids;
68 my %allgen = map { $_->{generator} => 1 } @allkids;
69 for my $gen (keys %allgen) {
70 (my $file = $gen . ".pm") =~ s!::!/!g;
73 @list = grep UNIVERSAL::isa($_->{generator}, 'Generate::Product'), $catalog->allkids;
74 @list = map { $products->getByPkey($_->{id}) } @list;
77 @list = sort { $b->{displayOrder} <=> $a->{displayOrder} }
78 $products->getBy(parentid=>$catalog->{id});
81 my $subcat_index = -1;
82 my @subcats = sort { $b->{displayOrder} <=> $a->{displayOrder} }
83 grep $_->{generator} eq 'Generate::Catalog',
84 Articles->children($catalog->{id});
86 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
92 catalog => [ \&tag_hash, $catalog ],
93 date => sub { display_date($list[$list_index]{$_[0]}) },
94 money => sub { sprintf("%.2f", $list[$list_index]{$_[0]}/100.0) },
95 iterate_products_reset => sub { $list_index = -1; },
98 return ++$list_index < @list;
102 $list_index >= 0 && $list_index < @list
103 or return '** outside products iterator **';
104 my $value = $list[$list_index]{$_[0]};
105 defined $value or return '';
106 return escape_html($value)
108 ifProducts => sub { @list },
109 iterate_subcats_reset =>
113 iterate_subcats => sub { ++$subcat_index < @subcats },
114 subcat => sub { escape_html($subcats[$subcat_index]{$_[0]}) },
115 ifSubcats => sub { @subcats },
117 sub { $list[$list_index]{listed} == 0 ? "Hidden" : " " },
120 my ($arg, $acts, $funcname, $templater) = @_;
122 $req->user_can(edit_reorder_children => $catalog)
124 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
125 defined $img_prefix or $img_prefix = '';
126 defined $urladd or $urladd = '';
127 @list > 1 or return '';
128 # links to move products up/down
129 my $refreshto = $ENV{SCRIPT_NAME}."$urladd#cat".$catalog->{id};
131 if ($list_index < $#list) {
132 if ($session->{showstepkids}) {
133 $down_url = "$CGI_URI/admin/move.pl?stepparent=$catalog->{id}&d=swap&id=$list[$list_index]{id}&other=$list[$list_index+1]{id}";
136 $down_url = "$CGI_URI/admin/move.pl?id=$list[$list_index]{id}&d=swap&other=$list[$list_index+1]{id}";
140 if ($list_index > 0) {
141 if ($session->{showstepkids}) {
142 $up_url = "$CGI_URI/admin/move.pl?stepparent=$catalog->{id}&d=swap&id=$list[$list_index]{id}&other=$list[$list_index-1]{id}";
145 $up_url = "$CGI_URI/admin/move.pl?id=$list[$list_index]{id}&d=swap&other=$list[$list_index-1]{id}";
148 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
150 script=>sub { $ENV{SCRIPT_NAME} },
153 my ($which, $template) = split ' ', $_[0];
154 $which eq 'subcat' or return "Unknown object $which embedded";
155 return embedded_catalog($req, $subcats[$subcat_index], $template);
159 my ($arg, $acts, $funcname, $templater) = @_;
161 $req->user_can(edit_reorder_children => $catalog)
163 @subcats > 1 or return '';
164 # links to move catalogs up/down
165 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
166 defined $img_prefix or $img_prefix = '';
167 defined $urladd or $urladd = '';
168 my $refreshto = $ENV{SCRIPT_NAME}.$urladd;
170 if ($subcat_index < $#subcats) {
171 $down_url = "$CGI_URI/admin/move.pl?id=$subcats[$subcat_index]{id}&d=swap&other=$subcats[$subcat_index+1]{id}&all=1";
174 if ($subcat_index > 0) {
175 $up_url = "$CGI_URI/admin/move.pl?id=$subcats[$subcat_index]{id}&d=swap&other=$subcats[$subcat_index-1]{id}&all=1";
177 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
181 return BSE::Template->get_page('admin/'.$template, $req->cfg, \%acts);
184 sub req_product_list {
185 my ($class, $req, $message) = @_;
188 my $session = $req->session;
189 my $shopid = $req->cfg->entryErr('articles', 'shop');
190 my $shop = Articles->getByPkey($shopid);
191 my @catalogs = sort { $b->{displayOrder} <=> $a->{displayOrder} }
192 grep $_->{generator} eq 'Generate::Catalog', Articles->children($shopid);
193 my $catalog_index = -1;
194 $message ||= $cgi->param('m') || $cgi->param('message') || '';
195 if (defined $cgi->param('showstepkids')) {
196 $session->{showstepkids} = $cgi->param('showstepkids');
198 exists $session->{showstepkids} or $session->{showstepkids} = 1;
199 my $products = Products->new;
200 my @products = sort { $b->{displayOrder} <=> $a->{displayOrder} }
201 $products->getBy(parentid => $shopid);
204 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
206 my $it = BSE::Util::Iterate->new;
212 catalog=> sub { escape_html($catalogs[$catalog_index]{$_[0]}) },
213 iterate_catalogs => sub { ++$catalog_index < @catalogs },
214 shopid=>sub { $shopid },
215 shop => [ \&tag_hash, $shop ],
216 script=>sub { $ENV{SCRIPT_NAME} },
217 message => sub { $message },
220 my ($which, $template) = split ' ', $_[0];
221 $which eq 'catalog' or return "Unknown object $which embedded";
222 return embedded_catalog($req, $catalogs[$catalog_index], $template);
226 my ($arg, $acts, $funcname, $templater) = @_;
228 $req->user_can(edit_reorder_children => $shopid)
230 @catalogs > 1 or return '';
231 # links to move catalogs up/down
232 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
233 defined $img_prefix or $img_prefix = '';
234 defined $urladd or $urladd = '';
235 my $refreshto = $ENV{SCRIPT_NAME} . $urladd;
237 if ($catalog_index < $#catalogs) {
238 $down_url = "$CGI_URI/admin/move.pl?id=$catalogs[$catalog_index]{id}&d=swap&other=$catalogs[$catalog_index+1]{id}";
241 if ($catalog_index > 0) {
242 $up_url = "$CGI_URI/admin/move.pl?id=$catalogs[$catalog_index]{id}&d=swap&other=$catalogs[$catalog_index-1]{id}";
244 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
246 ifShowStepKids => sub { $session->{showstepkids} },
247 $it->make_iterator(undef, 'product', 'products', \@products, \$product_index),
250 my ($arg, $acts, $funcname, $templater) = @_;
252 $req->user_can(edit_reorder_children => $shop)
254 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
255 defined $img_prefix or $img_prefix = '';
256 defined $urladd or $urladd = '';
257 @products > 1 or return '';
258 # links to move products up/down
259 my $refreshto = $ENV{SCRIPT_NAME}."$urladd#cat".$shop->{id};
261 if ($product_index < $#products) {
262 if ($session->{showstepkids}) {
263 $down_url = "$CGI_URI/admin/move.pl?stepparent=$shop->{id}&d=swap&id=$products[$product_index]{id}&other=$products[$product_index+1]{id}";
266 $down_url = "$CGI_URI/admin/move.pl?id=$products[$product_index]{id}&d=swap&other=$products[$product_index+1]{id}";
270 if ($product_index > 0) {
271 if ($session->{showstepkids}) {
272 $up_url = "$CGI_URI/admin/move.pl?stepparent=$shop->{id}&d=swap&id=$products[$product_index]{id}&other=$products[$product_index-1]{id}";
275 $up_url = "$CGI_URI/admin/move.pl?id=$products[$product_index]{id}&d=swap&other=$products[$product_index-1]{id}";
278 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
282 return $req->dyn_response('admin/product_list', \%acts);
285 sub req_product_detail {
286 my ($class, $req) = @_;
289 my $id = $cgi->param('id');
291 my $product = Products->getByPkey($id)) {
292 return product_form($req, $product, '', '', 'admin/product_detail');
295 return $class->req_product_list($req);
300 my ($req, $product, $action, $message, $template) = @_;
303 $message ||= $cgi->param('m') || $cgi->param('message') || '';
304 $template ||= 'admin/product_detail';
306 my $shopid = $req->cfg->entryErr('articles', 'shop');
307 my @work = [ $shopid, '' ];
309 my ($parent, $title) = @{shift @work};
311 push(@catalogs, { id=>$parent, display=>$title }) if $title;
312 my @kids = sort { $b->{displayOrder} <=> $a->{displayOrder} }
313 grep $_->{generator} eq 'Generate::Catalog',
314 Articles->children($parent);
315 $title .= ' / ' if $title;
316 unshift(@work, map [ $_->{id}, $title.$_->{title} ], @kids);
319 if ($product->{id}) {
320 require BSE::TB::ArticleFiles;
321 @files = BSE::TB::ArticleFiles->getBy(articleId=>$product->{id});
326 push(@templates, "shopitem.tmpl")
327 if grep -e "$_/shopitem.tmpl", BSE::Template->template_dirs($req->cfg);
328 for my $dir (BSE::Template->template_dirs($req->cfg)) {
329 if (opendir PROD_TEMPL, "$dir/products") {
330 push @templates, map "products/$_",
331 grep -f "$dir/products/$_" && /\.tmpl$/i, readdir PROD_TEMPL;
336 @templates = sort { lc($a) cmp lc($b) }
337 grep !$seen_templates{$_}++, @templates;
343 $realproduct = UNIVERSAL::isa($product, 'Product') ? $product : Products->getByPkey($product->{id});
345 @stepcats = OtherParents->getBy(childId=>$product->{id})
347 my @stepcat_targets = $realproduct->step_parents if $realproduct;
348 my %stepcat_targets = map { $_->{id}, $_ } @stepcat_targets;
349 my @stepcat_possibles = grep !$stepcat_targets{$_->{id}}, @catalogs;
351 @images = $product->images
353 # @images = $imageEditor->images()
357 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
362 BSE::Util::Tags->basic(\%acts, $cgi, $req->cfg),
363 BSE::Util::Tags->admin(\%acts, $req->cfg),
364 BSE::Util::Tags->secure($req),
367 return popup_menu(-name=>'parentid',
368 -values=>[ map $_->{id}, @catalogs ],
369 -labels=>{ map { @$_{qw/id display/} } @catalogs },
370 -default=>($product->{parentid} || $PRODUCTPARENT));
372 product => [ \&tag_hash, $product ],
373 action => sub { $action },
374 message => sub { $message },
375 script=>sub { $ENV{SCRIPT_NAME} },
376 ifImage => sub { $product->{imageName} },
377 hiddenNote => sub { $product->{listed} ? " " : "Hidden" },
380 return popup_menu(-name=>'template', -values=>\@templates,
381 -default=>$product->{id} ? $product->{template} :
384 ifStepcats => sub { @stepcats },
385 iterate_stepcats_reset => sub { $stepcat_index = -1; },
386 iterate_stepcats => sub { ++$stepcat_index < @stepcats },
387 stepcat => sub { escape_html($stepcats[$stepcat_index]{$_[0]}) },
390 escape_html($stepcat_targets[$stepcat_index]{$_[0]});
394 my ($arg, $acts, $funcname, $templater) = @_;
396 unless $req->user_can(edit_reorder_stepparents => $product),
397 @stepcats > 1 or return '';
398 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
399 $img_prefix = '' unless defined $img_prefix;
400 $urladd = '' unless defined $urladd;
401 my $refreshto = escape_uri($ENV{SCRIPT_NAME}
402 ."?id=$product->{id}&$template=1$urladd#step");
404 if ($stepcat_index < $#stepcats) {
405 $down_url = "$CGI_URI/admin/move.pl?stepchild=$product->{id}&id=$stepcats[$stepcat_index]{parentId}&d=swap&other=$stepcats[$stepcat_index+1]{parentId}&all=1";
408 if ($stepcat_index > 0) {
409 $up_url = "$CGI_URI/admin/move.pl?stepchild=$product->{id}&id=$stepcats[$stepcat_index]{parentId}&d=swap&other=$stepcats[$stepcat_index-1]{parentId}&all=1";
411 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
413 ifStepcatPossibles => sub { @stepcat_possibles },
414 stepcat_possibles => sub {
415 popup_menu(-name=>'stepcat',
416 -values=>[ map $_->{id}, @stepcat_possibles ],
417 -labels=>{ map { $_->{id}, $_->{display}} @catalogs });
420 make_iterator(\@files, 'file', 'files', \$file_index),
422 make_iterator(\@images, 'image', 'images', \$image_index),
425 return $req->dyn_response($template, \%acts);
428 #####################
432 my ($req, $template, $title, @orders) = @_;
436 my $from = $cgi->param('from');
437 my $to = $cgi->param('to');
438 use BSE::Util::SQL qw/now_sqldate sql_to_date date_to_sql/;
439 use BSE::Util::Valid qw/valid_date/;
440 my $today = now_sqldate();
441 for my $what ($from, $to) {
443 if ($what eq 'today') {
446 elsif (valid_date($what)) {
447 $what = date_to_sql($what);
454 my $message = $cgi->param('m');
455 defined $message or $message = '';
456 $message = escape_html($message);
457 if (defined $from || defined $to) {
458 $from ||= '1900-01-01';
459 $to ||= '2999-12-31';
460 $cgi->param('from', sql_to_date($from));
461 $cgi->param('to', sql_to_date($to));
463 @orders = grep $from le $_->{orderDate} && $_->{orderDate} le $to,
467 my $order_index = -1;
471 BSE::Util::Tags->basic(\%acts, $cgi, $req->cfg),
472 BSE::Util::Tags->admin(\%acts, $req->cfg),
473 BSE::Util::Tags->secure($req),
474 #order=> sub { escape_html($orders_work[$order_index]{$_[0]}) },
475 DevHelp::Tags->make_iterator2
476 ( [ \&iter_orders, \@orders ],
477 'order', 'orders', \@orders_work, \$order_index, 'NoCache'),
478 script => sub { $ENV{SCRIPT_NAME} },
479 title => sub { $title },
480 ifHaveParam => sub { defined $cgi->param($_[0]) },
481 ifParam => sub { $cgi->param($_[0]) },
484 my $value = $cgi->param($_[0]);
485 defined $value or $value = '';
490 $req->dyn_response("admin/$template", \%acts);
494 my ($orders, $args) = @_;
496 return bse_sort({ id => 'n', total => 'n', filled=>'n' }, $args, @$orders);
500 my ($class, $req) = @_;
502 my $orders = BSE::TB::Orders->new;
503 my @orders = sort { $b->{orderDate} cmp $a->{orderDate} }
504 grep $_->{complete}, $orders->all;
505 my $template = $req->cgi->param('template');
506 unless (defined $template && $template =~ /^\w+$/) {
507 $template = 'order_list';
510 return order_list_low($req, $template, 'Order list', @orders);
513 sub req_order_list_filled {
514 my ($class, $req) = @_;
516 my $orders = BSE::TB::Orders->new;
517 my @orders = sort { $b->{orderDate} cmp $a->{orderDate} }
518 grep $_->{complete} && $_->{filled} && $_->{paidFor}, $orders->all;
520 return order_list_low($req, 'order_list_filled', 'Order list - Filled orders', @orders);
523 sub req_order_list_unfilled {
524 my ($class, $req) = @_;
526 my $orders = BSE::TB::Orders->new;
527 my @orders = sort { $b->{orderDate} cmp $a->{orderDate} }
528 grep $_->{complete} && !$_->{filled} && $_->{paidFor}, $orders->all;
530 return order_list_low($req, 'order_list_unfilled',
531 'Order list - Unfilled orders', @orders);
535 sub req_order_list_unpaid {
536 my ($class, $req) = @_;
538 my $orders = BSE::TB::Orders->new;
539 my @orders = sort { $b->{orderDate} cmp $a->{orderDate} }
540 grep $_->{complete} && !$_->{paidFor}, $orders->all;
542 return order_list_low($req, 'order_list_unpaid',
543 'Order list - Incomplete orders', @orders);
546 sub req_order_list_incomplete {
547 my ($class, $req) = @_;
549 my $orders = BSE::TB::Orders->new;
550 my @orders = sort { $b->{orderDate} cmp $a->{orderDate} }
551 grep !$_->{complete}, $orders->all;
553 return order_list_low($req, 'order_list_incomplete',
554 'Order list - Incomplete orders', @orders);
558 my ($order, $rsiteuser, $arg) = @_;
560 unless ($$rsiteuser) {
561 $$rsiteuser = $order->siteuser || {};
564 my $siteuser = $$rsiteuser;
565 return '' unless $siteuser->{id};
567 my $value = $siteuser->{$arg};
568 defined $value or $value = '';
570 return escape_html($value);
573 sub req_order_detail {
574 my ($class, $req, $message) = @_;
577 my $id = $cgi->param('id');
579 my $order = BSE::TB::Orders->getByPkey($id)) {
580 $message ||= $cgi->param('m') || '';
581 my @lines = $order->items;
582 my @products = map { Products->getByPkey($_->{productId}) } @lines;
586 my $option_index = -1;
591 BSE::Util::Tags->basic(\%acts, $cgi, $req->cfg),
592 BSE::Util::Tags->admin(\%acts, $req->cfg),
593 BSE::Util::Tags->secure($req),
594 item => sub { escape_html($lines[$line_index]{$_[0]}) },
595 iterate_items_reset => sub { $line_index = -1 },
598 if (++$line_index < @lines ) {
600 @options = order_item_opts($req,
602 $products[$line_index]);
607 order => [ \&tag_hash, $order ],
610 sprintf("%.2f", $lines[$line_index]{units} * $lines[$line_index]{$_[0]}/100.0)
612 product => sub { escape_html($products[$line_index]{$_[0]}) },
613 script => sub { $ENV{SCRIPT_NAME} },
614 iterate_options_reset => sub { $option_index = -1 },
615 iterate_options => sub { ++$option_index < @options },
616 option => sub { escape_html($options[$option_index]{$_[0]}) },
617 ifOptions => sub { @options },
618 options => sub { nice_options(@options) },
619 message => sub { $message },
620 siteuser => [ \&tag_siteuser, $order, \$siteuser, ],
623 return $req->dyn_response('admin/order_detail', \%acts);
626 return $class->req_order_list($req);
630 sub req_order_filled {
631 my ($class, $req) = @_;
633 my $id = $req->cgi->param('id');
635 my $order = BSE::TB::Orders->getByPkey($id)) {
636 my $filled = $req->cgi->param('filled');
637 $order->{filled} = $filled;
638 if ($order->{filled}) {
639 $order->{whenFilled} = epoch_to_sql_datetime(time);
640 my $user = $req->user;
642 $order->{whoFilled} = $user->{logon};
645 $order->{whoFilled} = defined($ENV{REMOTE_USER})
646 ? $ENV{REMOTE_USER} : "-unknown-";
650 if ($req->cgi->param('detail')) {
651 return $class->req_order_detail($req);
654 return $class->req_order_list($req);
658 return $class->req_order_list($req);
663 my ($class, $req) = @_;
665 return $class->_set_order_paid($req, 1);
668 sub req_order_unpaid {
669 my ($class, $req) = @_;
671 return $class->_set_order_paid($req, 0);
674 sub _set_order_paid {
675 my ($class, $req, $value) = @_;
677 my $id = $req->cgi->param('id');
679 my $order = BSE::TB::Orders->getByPkey($id)) {
680 if ($order->paidFor != $value) {
682 $order->set_paymentType(PAYMENT_MANUAL);
685 $order->paymentType == PAYMENT_MANUAL
686 or return $class->req_order_detail($req, "You can only unpay manually paid orders");
689 $order->set_paidFor($value);
690 my $user = $req->user;
691 my $name = $user ? $user->logon : "--unknown--";
693 $order->{instructions} .= "\nMarked " . ($value ? "paid" : "unpaid" ) . " by $name " . POSIX::strftime("%H:%M %d/%m/%Y", localtime);
697 return BSE::Template->get_refresh("$ENV{SCRIPT_NAME}?a_order_detail=1&id=$id", $req->cfg);
700 return $class->req_order_list($req);
704 sub req_paypal_refund {
705 my ($self, $req) = @_;
707 my $id = $req->cgi->param('id');
709 my $order = BSE::TB::Orders->getByPkey($id)) {
712 unless (BSE::PayPal->refund_order(order => $order,
715 return $self->req_order_detail($req, $msg);
718 return $req->get_refresh($req->url(shopadmin => { "a_order_detail" => 1, id => $id }));
721 $req->flash_error("Missing or invalid order id");
722 return $self->req_order_list($req);
726 #####################
728 # perhaps some of these belong in a class...
730 # format an ANSI SQL date for display
734 if ( my ($year, $month, $day) =
735 ($date =~ /^(\d+)-(\d+)-(\d+)/)) {
736 return sprintf("%02d/%02d/%04d", $day, $month, $year);
741 # convert a user entered date from dd/mm/yyyy to ANSI sql format
742 # we try to parse flexibly here
745 my ($year, $month, $day);
748 if (($day, $month, $year) = ($$str =~ m!(\d+)/(\d+)/(\d+)!)) {
749 $year += 2000 if $year < 100;
751 return $$str = sprintf("%04d-%02d-%02d", $year, $month, $day);
759 $$money =~ /^\s*(\d+(\.\d*)|\.\d+)/
761 return $$money = sprintf("%.0f ", $$money * 100);
764 # convert an epoch time to sql format
766 use POSIX 'strftime';
769 return strftime('%Y-%m-%d', localtime $time);
772 # convert an epoch time to sql format
773 sub epoch_to_sql_datetime {
774 use POSIX 'strftime';
777 return strftime('%Y-%m-%d %H:%M', localtime $time);
786 shopadmin.pl - administration for the online-store tables
790 (This is a CGI script.)
794 shopadmin.pl gives a UI to edit the product table, and view the orders and
799 shopadmin.pl uses a few templates from the templates/admin directory.
801 =head2 product_list.tmpl
805 =item product I<name>
807 Access to product fields.
811 Formats the I<name> field of the product as a date.
815 Formats the I<name> integer field as a 2 decimal place money value.
817 =item iterator ... products
819 Iterates over the products database in reverse expire order.
823 The name of the current script for use in URLs.
827 An error message that may have been passed in the 'message' parameter.
831 'Deleted' if the expire date of the current product has passed.
835 =head2 add_product.tmpl
836 =head2 edit_product.tmpl
837 =head2 product_detail.tmpl
839 These use the same tags.
843 =item product I<name>
845 The specified field of the product.
849 Formats the given field of the product as a date.
853 Formats the given integer field of the product as money.
857 Either 'Add New' or 'Edit'.
861 The message parameter passed into the script.
865 The name of the script, for use in urls.
869 Conditional, true if the product has an image.
873 "Hidden" if the product is hidden.
877 =head2 order_list.tmpl
879 Used to display the list of orders. You can also specify a template
880 parameter to the order_list target, and perform filtering and sorting
887 The given field of the order.
889 =item iterator ... orders [filter-sort-spec]
891 Iterates over the orders in reverse orderDate order.
893 The [filter-sort-spec] can contain none, either or both of the following:
897 =item filter= field op value, ...
899 filter the data by checking the given expression.
901 eg. filter= filled == 0
903 =item sort= [+|-] keyword, ...
905 Sorts the result by the specified fields, in reverse if preceded by '-'.
911 The given field of the current order formatted as money.
915 The given field of the current order formatted as a date.
919 The name of the script, for use in urls.
923 =head2 order_detail.tmpl
925 Used to display the details for an order.
931 Displays the given field of a line item
933 =item iterator ... items
935 Iterates over the line items in the order.
939 The given field of the order.
941 =item money I<func> I<args>
943 Formats the given functions return value as money.
945 =item date I<func> I<args>
947 Formats the given function return value as a date.
949 =item extension I<name>
951 Takes the given field for the current item multiplied by the units column.
953 =item product I<name>
955 The given product field of the product for the current item.
959 The name of the current script (for use in urls).
961 =item iterator ... options
963 Iterates over the options set for the current order item.
965 =item option I<field>
967 Access to a field of the option, any of id, value, desc or label.
971 Conditional tag, true if the current product has any options.
975 A laid-out list of the options set for the current order item.