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 tag_error_img tag_object_plain tag_object);
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.008";
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',
35 order_save => 'shop_order_save',
38 paypal_refund => 'bse_shop_order_refund_paypal',
60 sub embedded_catalog {
61 my ($req, $catalog, $template) = @_;
63 my $session = $req->session;
65 my $products = Products->new;
67 if ($session->{showstepkids}) {
68 my @allkids = $catalog->allkids;
69 my %allgen = map { $_->{generator} => 1 } @allkids;
70 for my $gen (keys %allgen) {
71 (my $file = $gen . ".pm") =~ s!::!/!g;
74 @list = grep UNIVERSAL::isa($_->{generator}, 'Generate::Product'), $catalog->allkids;
75 @list = map { $products->getByPkey($_->{id}) } @list;
78 @list = sort { $b->{displayOrder} <=> $a->{displayOrder} }
79 $products->getBy(parentid=>$catalog->{id});
82 my $subcat_index = -1;
83 my @subcats = sort { $b->{displayOrder} <=> $a->{displayOrder} }
84 grep $_->{generator} eq 'Generate::Catalog',
85 Articles->children($catalog->{id});
87 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
93 catalog => [ \&tag_hash, $catalog ],
94 date => sub { display_date($list[$list_index]{$_[0]}) },
95 money => sub { sprintf("%.2f", $list[$list_index]{$_[0]}/100.0) },
96 iterate_products_reset => sub { $list_index = -1; },
99 return ++$list_index < @list;
103 $list_index >= 0 && $list_index < @list
104 or return '** outside products iterator **';
105 my $value = $list[$list_index]{$_[0]};
106 defined $value or return '';
107 return escape_html($value)
109 ifProducts => sub { @list },
110 iterate_subcats_reset =>
114 iterate_subcats => sub { ++$subcat_index < @subcats },
115 subcat => sub { escape_html($subcats[$subcat_index]{$_[0]}) },
116 ifSubcats => sub { @subcats },
118 sub { $list[$list_index]{listed} == 0 ? "Hidden" : " " },
121 my ($arg, $acts, $funcname, $templater) = @_;
123 $req->user_can(edit_reorder_children => $catalog)
125 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
126 defined $img_prefix or $img_prefix = '';
127 defined $urladd or $urladd = '';
128 @list > 1 or return '';
129 # links to move products up/down
130 my $refreshto = $ENV{SCRIPT_NAME}."$urladd#cat".$catalog->{id};
132 if ($list_index < $#list) {
133 if ($session->{showstepkids}) {
134 $down_url = "$CGI_URI/admin/move.pl?stepparent=$catalog->{id}&d=swap&id=$list[$list_index]{id}&other=$list[$list_index+1]{id}";
137 $down_url = "$CGI_URI/admin/move.pl?id=$list[$list_index]{id}&d=swap&other=$list[$list_index+1]{id}";
141 if ($list_index > 0) {
142 if ($session->{showstepkids}) {
143 $up_url = "$CGI_URI/admin/move.pl?stepparent=$catalog->{id}&d=swap&id=$list[$list_index]{id}&other=$list[$list_index-1]{id}";
146 $up_url = "$CGI_URI/admin/move.pl?id=$list[$list_index]{id}&d=swap&other=$list[$list_index-1]{id}";
149 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
151 script=>sub { $ENV{SCRIPT_NAME} },
154 my ($which, $template) = split ' ', $_[0];
155 $which eq 'subcat' or return "Unknown object $which embedded";
156 return embedded_catalog($req, $subcats[$subcat_index], $template);
160 my ($arg, $acts, $funcname, $templater) = @_;
162 $req->user_can(edit_reorder_children => $catalog)
164 @subcats > 1 or return '';
165 # links to move catalogs up/down
166 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
167 defined $img_prefix or $img_prefix = '';
168 defined $urladd or $urladd = '';
169 my $refreshto = $ENV{SCRIPT_NAME}.$urladd;
171 if ($subcat_index < $#subcats) {
172 $down_url = "$CGI_URI/admin/move.pl?id=$subcats[$subcat_index]{id}&d=swap&other=$subcats[$subcat_index+1]{id}&all=1";
175 if ($subcat_index > 0) {
176 $up_url = "$CGI_URI/admin/move.pl?id=$subcats[$subcat_index]{id}&d=swap&other=$subcats[$subcat_index-1]{id}&all=1";
178 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
182 return BSE::Template->get_page('admin/'.$template, $req->cfg, \%acts);
185 sub req_product_list {
186 my ($class, $req, $message) = @_;
189 my $session = $req->session;
190 my $shopid = $req->cfg->entryErr('articles', 'shop');
191 my $shop = Articles->getByPkey($shopid);
192 my @catalogs = sort { $b->{displayOrder} <=> $a->{displayOrder} }
193 grep $_->{generator} eq 'Generate::Catalog', Articles->children($shopid);
194 my $catalog_index = -1;
195 $message ||= $cgi->param('m') || $cgi->param('message') || '';
196 if (defined $cgi->param('showstepkids')) {
197 $session->{showstepkids} = $cgi->param('showstepkids');
199 exists $session->{showstepkids} or $session->{showstepkids} = 1;
200 my $products = Products->new;
201 my @products = sort { $b->{displayOrder} <=> $a->{displayOrder} }
202 $products->getBy(parentid => $shopid);
205 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
207 my $it = BSE::Util::Iterate->new;
213 catalog=> sub { escape_html($catalogs[$catalog_index]{$_[0]}) },
214 iterate_catalogs => sub { ++$catalog_index < @catalogs },
215 shopid=>sub { $shopid },
216 shop => [ \&tag_hash, $shop ],
217 script=>sub { $ENV{SCRIPT_NAME} },
218 message => sub { $message },
221 my ($which, $template) = split ' ', $_[0];
222 $which eq 'catalog' or return "Unknown object $which embedded";
223 return embedded_catalog($req, $catalogs[$catalog_index], $template);
227 my ($arg, $acts, $funcname, $templater) = @_;
229 $req->user_can(edit_reorder_children => $shopid)
231 @catalogs > 1 or return '';
232 # links to move catalogs up/down
233 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
234 defined $img_prefix or $img_prefix = '';
235 defined $urladd or $urladd = '';
236 my $refreshto = $ENV{SCRIPT_NAME} . $urladd;
238 if ($catalog_index < $#catalogs) {
239 $down_url = "$CGI_URI/admin/move.pl?id=$catalogs[$catalog_index]{id}&d=swap&other=$catalogs[$catalog_index+1]{id}";
242 if ($catalog_index > 0) {
243 $up_url = "$CGI_URI/admin/move.pl?id=$catalogs[$catalog_index]{id}&d=swap&other=$catalogs[$catalog_index-1]{id}";
245 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
247 ifShowStepKids => sub { $session->{showstepkids} },
248 $it->make_iterator(undef, 'product', 'products', \@products, \$product_index),
251 my ($arg, $acts, $funcname, $templater) = @_;
253 $req->user_can(edit_reorder_children => $shop)
255 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
256 defined $img_prefix or $img_prefix = '';
257 defined $urladd or $urladd = '';
258 @products > 1 or return '';
259 # links to move products up/down
260 my $refreshto = $ENV{SCRIPT_NAME}."$urladd#cat".$shop->{id};
262 if ($product_index < $#products) {
263 if ($session->{showstepkids}) {
264 $down_url = "$CGI_URI/admin/move.pl?stepparent=$shop->{id}&d=swap&id=$products[$product_index]{id}&other=$products[$product_index+1]{id}";
267 $down_url = "$CGI_URI/admin/move.pl?id=$products[$product_index]{id}&d=swap&other=$products[$product_index+1]{id}";
271 if ($product_index > 0) {
272 if ($session->{showstepkids}) {
273 $up_url = "$CGI_URI/admin/move.pl?stepparent=$shop->{id}&d=swap&id=$products[$product_index]{id}&other=$products[$product_index-1]{id}";
276 $up_url = "$CGI_URI/admin/move.pl?id=$products[$product_index]{id}&d=swap&other=$products[$product_index-1]{id}";
279 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
283 return $req->dyn_response('admin/product_list', \%acts);
286 sub req_product_detail {
287 my ($class, $req) = @_;
290 my $id = $cgi->param('id');
292 my $product = Products->getByPkey($id)) {
293 return product_form($req, $product, '', '', 'admin/product_detail');
296 return $class->req_product_list($req);
301 my ($req, $product, $action, $message, $template) = @_;
304 $message ||= $cgi->param('m') || $cgi->param('message') || '';
305 $template ||= 'admin/product_detail';
307 my $shopid = $req->cfg->entryErr('articles', 'shop');
308 my @work = [ $shopid, '' ];
310 my ($parent, $title) = @{shift @work};
312 push(@catalogs, { id=>$parent, display=>$title }) if $title;
313 my @kids = sort { $b->{displayOrder} <=> $a->{displayOrder} }
314 grep $_->{generator} eq 'Generate::Catalog',
315 Articles->children($parent);
316 $title .= ' / ' if $title;
317 unshift(@work, map [ $_->{id}, $title.$_->{title} ], @kids);
320 if ($product->{id}) {
321 require BSE::TB::ArticleFiles;
322 @files = BSE::TB::ArticleFiles->getBy(articleId=>$product->{id});
327 push(@templates, "shopitem.tmpl")
328 if grep -e "$_/shopitem.tmpl", BSE::Template->template_dirs($req->cfg);
329 for my $dir (BSE::Template->template_dirs($req->cfg)) {
330 if (opendir PROD_TEMPL, "$dir/products") {
331 push @templates, map "products/$_",
332 grep -f "$dir/products/$_" && /\.tmpl$/i, readdir PROD_TEMPL;
337 @templates = sort { lc($a) cmp lc($b) }
338 grep !$seen_templates{$_}++, @templates;
344 $realproduct = UNIVERSAL::isa($product, 'Product') ? $product : Products->getByPkey($product->{id});
346 @stepcats = OtherParents->getBy(childId=>$product->{id})
348 my @stepcat_targets = $realproduct->step_parents if $realproduct;
349 my %stepcat_targets = map { $_->{id}, $_ } @stepcat_targets;
350 my @stepcat_possibles = grep !$stepcat_targets{$_->{id}}, @catalogs;
352 @images = $product->images
354 # @images = $imageEditor->images()
358 my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" />!;
363 BSE::Util::Tags->basic(\%acts, $cgi, $req->cfg),
364 BSE::Util::Tags->admin(\%acts, $req->cfg),
365 BSE::Util::Tags->secure($req),
368 return popup_menu(-name=>'parentid',
369 -values=>[ map $_->{id}, @catalogs ],
370 -labels=>{ map { @$_{qw/id display/} } @catalogs },
371 -default=>($product->{parentid} || $PRODUCTPARENT));
373 product => [ \&tag_hash, $product ],
374 action => sub { $action },
375 message => sub { $message },
376 script=>sub { $ENV{SCRIPT_NAME} },
377 ifImage => sub { $product->{imageName} },
378 hiddenNote => sub { $product->{listed} ? " " : "Hidden" },
381 return popup_menu(-name=>'template', -values=>\@templates,
382 -default=>$product->{id} ? $product->{template} :
385 ifStepcats => sub { @stepcats },
386 iterate_stepcats_reset => sub { $stepcat_index = -1; },
387 iterate_stepcats => sub { ++$stepcat_index < @stepcats },
388 stepcat => sub { escape_html($stepcats[$stepcat_index]{$_[0]}) },
391 escape_html($stepcat_targets[$stepcat_index]{$_[0]});
395 my ($arg, $acts, $funcname, $templater) = @_;
397 unless $req->user_can(edit_reorder_stepparents => $product),
398 @stepcats > 1 or return '';
399 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
400 $img_prefix = '' unless defined $img_prefix;
401 $urladd = '' unless defined $urladd;
402 my $refreshto = escape_uri($ENV{SCRIPT_NAME}
403 ."?id=$product->{id}&$template=1$urladd#step");
405 if ($stepcat_index < $#stepcats) {
406 $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";
409 if ($stepcat_index > 0) {
410 $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";
412 return make_arrows($req->cfg, $down_url, $up_url, $refreshto, $img_prefix);
414 ifStepcatPossibles => sub { @stepcat_possibles },
415 stepcat_possibles => sub {
416 popup_menu(-name=>'stepcat',
417 -values=>[ map $_->{id}, @stepcat_possibles ],
418 -labels=>{ map { $_->{id}, $_->{display}} @catalogs });
421 make_iterator(\@files, 'file', 'files', \$file_index),
423 make_iterator(\@images, 'image', 'images', \$image_index),
426 return $req->dyn_response($template, \%acts);
429 =item tag all_order_count
430 X<tags, shop admin, all_order_count>C<all_order_count>
432 Returns a count of orders matching a set of conditions.
436 sub tag_all_order_count {
437 my ($args, $acts, $funcname, $templater) = @_;
441 if (eval "\$query = [ $args ]; 1 ") {
442 return BSE::TB::Orders->getCount($query);
445 return "<!-- error handling args: $@ -->";
449 return BSE::TB::Orders->getCount();
453 #####################
457 my ($req, $template, $title, $conds, $options) = @_;
462 my $order = delete $options->{order};
463 defined $order or $order = 'id desc';
464 my $datelimit = delete $options->{datelimit};
465 defined $datelimit or $datelimit = 1;
467 my $from = $cgi->param('from');
468 my $to = $cgi->param('to');
469 use BSE::Util::SQL qw/now_sqldate sql_to_date date_to_sql sql_date/;
470 use BSE::Util::Valid qw/valid_date/;
471 my $today = now_sqldate();
472 for my $what ($from, $to) {
474 if ($what eq 'today') {
477 elsif (valid_date($what)) {
478 $what = date_to_sql($what);
486 $from ||= sql_date(time() - 30 * 86_400);
488 if (defined $from || defined $to) {
489 $from ||= '1900-01-01';
490 $to ||= '2999-12-31';
491 $cgi->param('from', sql_to_date($from));
492 $cgi->param('to', sql_to_date($to));
494 [ between => 'orderDate', $from, $to." 23:59:59" ];
496 my @ids = BSE::TB::Orders->getColumnBy
506 for my $key (qw(from to)) {
507 my $value = $cgi->param($key);
508 if (defined $value) {
509 push @param, "$key=" . escape_uri($value);
512 $search_param = join('&', map escape_html($_), @param);
515 my $message = $cgi->param('m');
516 defined $message or $message = '';
517 $message = escape_html($message);
519 my $it = BSE::Util::Iterate::Objects->new;
527 fetch => [ getByPkey => 'BSE::TB::Orders' ],
531 session => $req->session,
533 perpage_parm => "pp=50",
535 title => sub { $title },
536 ifHaveParam => sub { defined $cgi->param($_[0]) },
537 ifParam => sub { $cgi->param($_[0]) },
540 all_order_count => \&tag_all_order_count,
541 search_param => $search_param,
543 $req->dyn_response("admin/$template", \%acts);
547 my ($orders, $args) = @_;
549 return bse_sort({ id => 'n', total => 'n', filled=>'n' }, $args, @$orders);
552 =item target order_list
553 X<shopadmin targets, order_list>X<order_list target>
555 List all completed orders.
557 By default limits to the last 30 days.
562 my ($class, $req) = @_;
564 my $template = $req->cgi->param('template');
565 unless (defined $template && $template =~ /^\w+$/) {
566 $template = 'order_list';
571 [ '<>', complete => 0 ],
574 return order_list_low($req, $template, 'Order list', \@conds);
577 =item target order_list_filled
578 X<shopadmin targets, order_list_filled>X<order_list_filled target>
580 List all filled orders.
582 By default limits to the last 30 days.
586 sub req_order_list_filled {
587 my ($class, $req) = @_;
591 [ '<>', complete => 0 ],
592 [ '<>', filled => 0 ],
593 #[ '<>', paidFor => 0 ],
596 return order_list_low($req, 'order_list_filled', 'Order list - Filled orders',
600 =item target order_list_unfilled
601 X<shopadmin targets, order_list_unfilled>X<order_list_unfilled target>
603 List completed but unfilled orders.
605 Unlike the other order lists, this lists oldest order first, and does
606 not limit to the last 30 days.
610 sub req_order_list_unfilled {
611 my ($class, $req) = @_;
615 [ '<>', complete => 0 ],
619 return order_list_low($req, 'order_list_unfilled',
620 'Order list - Unfilled orders',
621 \@conds, { order => 'id asc', datelimit => 0 });
624 sub req_order_list_unpaid {
625 my ($class, $req) = @_;
629 [ '<>', complete => 0 ],
633 return order_list_low($req, 'order_list_unpaid',
634 'Order list - Unpaid orders', \@conds);
637 =item target order_list_incomplete
638 X<shopadmin targets, order_list_incomplete>X<order_list_incomplete>
640 List incomplete orders, ie. orders that the user abandoned before the
641 payment step was complete.
643 By default limits to the last 30 days.
647 sub req_order_list_incomplete {
648 my ($class, $req) = @_;
655 return order_list_low($req, 'order_list_incomplete',
656 'Order list - Incomplete orders', \@conds);
660 my ($order, $rsiteuser, $arg) = @_;
662 unless ($$rsiteuser) {
663 $$rsiteuser = $order->siteuser || {};
666 my $siteuser = $$rsiteuser;
667 return '' unless $siteuser->{id};
669 my $value = $siteuser->{$arg};
670 defined $value or $value = '';
672 return escape_html($value);
675 sub tag_shipping_method_select {
676 my ($self, $order) = @_;
678 my @methods = BSE::TB::Orders->dummy_shipping_methods;
682 -name => "shipping_name",
683 -values => [ map $_->{id}, @methods ],
684 -labels => { map { $_->{id} => $_->{name} } @methods },
685 -id => "shipping_name",
686 -default => $order->shipping_name,
690 sub tag_stage_select {
691 my ($self, $req, $order) = @_;
693 my @stages = BSE::TB::Orders->settable_stages;
695 my %stage_labels = BSE::TB::Orders->stage_labels;
700 -default => $order->stage,
701 -labels => \%stage_labels,
705 sub req_order_detail {
706 my ($class, $req, $errors) = @_;
709 my $id = $cgi->param('id');
711 my $order = BSE::TB::Orders->getByPkey($id)) {
712 my $message = $req->message($errors);
713 my @lines = $order->items;
714 my @products = map { Products->getByPkey($_->{productId}) } @lines;
718 my $option_index = -1;
720 my $it = BSE::Util::Iterate->new;
726 item => sub { escape_html($lines[$line_index]{$_[0]}) },
727 iterate_items_reset => sub { $line_index = -1 },
730 if (++$line_index < @lines ) {
732 @options = order_item_opts($req,
734 $products[$line_index]);
739 order => [ \&tag_object, $order ],
742 sprintf("%.2f", $lines[$line_index]{units} * $lines[$line_index]{$_[0]}/100.0)
744 product => sub { escape_html($products[$line_index]{$_[0]}) },
745 script => sub { $ENV{SCRIPT_NAME} },
746 iterate_options_reset => sub { $option_index = -1 },
747 iterate_options => sub { ++$option_index < @options },
748 option => sub { escape_html($options[$option_index]{$_[0]}) },
749 ifOptions => sub { @options },
750 options => sub { nice_options(@options) },
752 error_img => [ \&tag_error_img, $errors ],
753 siteuser => [ \&tag_siteuser, $order, \$siteuser, ],
756 single => "shipping_method",
757 plural => "shipping_methods",
758 code => [ dummy_shipping_methods => "BSE::TB::Orders" ],
760 shipping_method_select =>
761 [ tag_shipping_method_select => $class, $order ],
763 [ tag_stage_select => $class, $req, $order ],
764 stage_description => escape_html($order->stage_description($req->language)),
767 return $req->dyn_response('admin/order_detail', \%acts);
770 return $class->req_order_list($req);
774 sub req_order_filled {
775 my ($class, $req) = @_;
777 my $id = $req->cgi->param('id');
779 my $order = BSE::TB::Orders->getByPkey($id)) {
780 my $filled = $req->cgi->param('filled');
781 $order->{filled} = $filled;
782 if ($order->{filled}) {
783 $order->{whenFilled} = epoch_to_sql_datetime(time);
784 my $user = $req->user;
786 $order->{whoFilled} = $user->{logon};
789 $order->{whoFilled} = defined($ENV{REMOTE_USER})
790 ? $ENV{REMOTE_USER} : "-unknown-";
794 if ($req->cgi->param('detail')) {
795 return $class->req_order_detail($req);
798 return $class->req_order_list($req);
802 return $class->req_order_list($req);
807 my ($class, $req) = @_;
809 return $class->_set_order_paid($req, 1);
812 sub req_order_unpaid {
813 my ($class, $req) = @_;
815 return $class->_set_order_paid($req, 0);
818 sub _set_order_paid {
819 my ($class, $req, $value) = @_;
821 my $id = $req->cgi->param('id');
823 my $order = BSE::TB::Orders->getByPkey($id)) {
824 if ($order->paidFor != $value) {
826 $order->set_paymentType(PAYMENT_MANUAL);
829 $order->paymentType == PAYMENT_MANUAL
830 or return $class->req_order_detail($req, "You can only unpay manually paid orders");
833 $order->set_paidFor($value);
834 my $user = $req->user;
835 my $name = $user ? $user->logon : "--unknown--";
837 $order->{instructions} .= "\nMarked " . ($value ? "paid" : "unpaid" ) . " by $name " . POSIX::strftime("%H:%M %d/%m/%Y", localtime);
841 return $req->get_refresh
842 ($req->url("shopadmin", { a_order_detail => 1, id => $id }));
845 return $class->req_order_list($req);
849 sub req_paypal_refund {
850 my ($self, $req) = @_;
852 my $id = $req->cgi->param('id');
854 my $order = BSE::TB::Orders->getByPkey($id)) {
857 unless (BSE::PayPal->refund_order(order => $order,
860 return $self->req_order_detail($req, $msg);
863 return $req->get_refresh($req->url(shopadmin => { "a_order_detail" => 1, id => $id }));
866 $req->flash_error("Missing or invalid order id");
867 return $self->req_order_list($req);
873 Make changes to an order, only a limited set of fields can be changed.
875 Parameters, all optional:
881 id - id of the order. Required.
885 shipping_method - if automated shipping calculations are disabled, the
886 id of the dummy shipping method to set for the order.
890 freight_tracking - the freight tracking code for the shipment.
894 stage - order stage, one of unprocessed, backorder, picked, shipped,
899 Requires csrfp token C<shop_order_save>.
904 my ($self, $req) = @_;
906 $req->check_csrf("shop_order_save")
907 or return $self->req_product_list($req, "Bad or missing csrf token: " . $req->csrf_error);
910 my $id = $cgi->param("id");
911 $id && $id =~ /^[0-9]+$/
912 or return $self->req_product_list($req, "No order id supplied");
914 my $order = BSE::TB::Orders->getByPkey($id)
915 or return $self->req_product_list($req, "No such order id");
920 my $new_freight_tracking = 0;
921 my $code = $cgi->param("freight_tracking");
922 if (defined $code && $code ne $order->freight_tracking) {
923 $order->set_freight_tracking($code);
924 ++$new_freight_tracking;
928 my $new_shipping_name = 0;
929 unless ($req->cfg->entry("shop", "shipping", 0)) {
930 my $shipping_name = $cgi->param("shipping_name");
931 if (defined $shipping_name
932 && $shipping_name ne $order->shipping_name) {
933 my @ship = BSE::TB::Orders->dummy_shipping_methods();
934 my ($entry) = grep $_->{id} eq $shipping_name, @ship;
936 $order->set_shipping_name($entry->{id});
937 $order->set_shipping_method($entry->{name});
938 ++$new_shipping_name;
942 $errors{shipping_method} = "msg:bse/admin/shop/saveorder/badmethod:$shipping_name";
948 my ($stage) = $cgi->param("stage");
949 my ($stage_note) = $cgi->param("stage_note");
950 if (defined $stage && $stage ne $order->stage
951 || defined $stage_note && $stage_note =~ /\S/) {
952 my @stages = BSE::TB::Orders->settable_stages;
953 if (grep $_ eq $stage, @stages) {
958 $errors{stage} = "msg:bse/admin/shop/saveorder/badstage:$stage";
963 and return $self->req_order_detail($req, \%errors);
966 if ($new_freight_tracking) {
969 component => "shopadmin:orders:saveorder",
971 msg => "New freight tracking code set: '" . $order->freight_tracking . "'",
975 if ($new_shipping_name) {
978 component => "shopadmin:orders:saveorder",
980 msg => "New shippping method set: '" . $order->shipping_name . "/" . $order->shipping_method . "'",
985 $order->new_stage(scalar $req->user, $stage, $stage_note);
989 $req->flash("msg:bse/admin/shop/saveorder/saved");
992 $req->flash("msg:bse/admin/shop/saveorder/nochanges");
995 my $url = $cgi->param("r") || $req->url("shopadmin", { a_order_detail => 1, id => $order->id });
997 return $req->get_refresh($url);
1000 #####################
1002 # perhaps some of these belong in a class...
1004 # format an ANSI SQL date for display
1008 if ( my ($year, $month, $day) =
1009 ($date =~ /^(\d+)-(\d+)-(\d+)/)) {
1010 return sprintf("%02d/%02d/%04d", $day, $month, $year);
1015 sub money_to_cents {
1018 $$money =~ /^\s*(\d+(\.\d*)|\.\d+)/
1020 return $$money = sprintf("%.0f ", $$money * 100);
1023 # convert an epoch time to sql format
1025 use POSIX 'strftime';
1028 return strftime('%Y-%m-%d', localtime $time);
1031 # convert an epoch time to sql format
1032 sub epoch_to_sql_datetime {
1033 use POSIX 'strftime';
1036 return strftime('%Y-%m-%d %H:%M', localtime $time);
1045 shopadmin.pl - administration for the online-store tables
1049 (This is a CGI script.)
1053 shopadmin.pl gives a UI to edit the product table, and view the orders and
1058 shopadmin.pl uses a few templates from the templates/admin directory.
1060 =head2 product_list.tmpl
1064 =item product I<name>
1066 Access to product fields.
1070 Formats the I<name> field of the product as a date.
1074 Formats the I<name> integer field as a 2 decimal place money value.
1076 =item iterator ... products
1078 Iterates over the products database in reverse expire order.
1082 The name of the current script for use in URLs.
1086 An error message that may have been passed in the 'message' parameter.
1090 'Deleted' if the expire date of the current product has passed.
1094 =head2 add_product.tmpl
1095 =head2 edit_product.tmpl
1096 =head2 product_detail.tmpl
1098 These use the same tags.
1102 =item product I<name>
1104 The specified field of the product.
1108 Formats the given field of the product as a date.
1112 Formats the given integer field of the product as money.
1116 Either 'Add New' or 'Edit'.
1120 The message parameter passed into the script.
1124 The name of the script, for use in urls.
1128 Conditional, true if the product has an image.
1132 "Hidden" if the product is hidden.
1136 =head2 order_list.tmpl
1138 Used to display the list of orders. You can also specify a template
1139 parameter to the order_list target, and perform filtering and sorting
1140 within the template.
1146 The given field of the order.
1148 =item iterator ... orders [filter-sort-spec]
1150 Iterates over the orders in reverse orderDate order.
1152 The [filter-sort-spec] can contain none, either or both of the following:
1156 =item filter= field op value, ...
1158 filter the data by checking the given expression.
1160 eg. filter= filled == 0
1162 =item sort= [+|-] keyword, ...
1164 Sorts the result by the specified fields, in reverse if preceded by '-'.
1170 The given field of the current order formatted as money.
1174 The given field of the current order formatted as a date.
1178 The name of the script, for use in urls.
1182 =head2 order_detail.tmpl
1184 Used to display the details for an order.
1190 Displays the given field of a line item
1192 =item iterator ... items
1194 Iterates over the line items in the order.
1198 The given field of the order.
1200 =item money I<func> I<args>
1202 Formats the given functions return value as money.
1204 =item date I<func> I<args>
1206 Formats the given function return value as a date.
1208 =item extension I<name>
1210 Takes the given field for the current item multiplied by the units column.
1212 =item product I<name>
1214 The given product field of the product for the current item.
1218 The name of the current script (for use in urls).
1220 =item iterator ... options
1222 Iterates over the options set for the current order item.
1224 =item option I<field>
1226 Access to a field of the option, any of id, value, desc or label.
1230 Conditional tag, true if the current product has any options.
1234 A laid-out list of the options set for the current order item.