]>
Commit | Line | Data |
---|---|---|
0ec4ac8a TC |
1 | package BSE::TB::Order; |
2 | use strict; | |
3 | # represents an order from the database | |
4 | use Squirrel::Row; | |
5 | use vars qw/@ISA/; | |
6 | @ISA = qw/Squirrel::Row/; | |
5d88571c | 7 | use Carp 'confess'; |
0ec4ac8a | 8 | |
1546e1f0 | 9 | our $VERSION = "1.012"; |
cb7fd78d | 10 | |
0ec4ac8a TC |
11 | sub columns { |
12 | return qw/id | |
13 | delivFirstName delivLastName delivStreet delivSuburb delivState | |
14 | delivPostCode delivCountry | |
15 | billFirstName billLastName billStreet billSuburb billState | |
16 | billPostCode billCountry | |
17 | telephone facsimile emailAddress | |
18 | total wholesaleTotal gst orderDate | |
19 | ccNumberHash ccName ccExpiryHash ccType | |
20 | filled whenFilled whoFilled paidFor paymentReceipt | |
21 | randomId cancelled userId paymentType | |
22 | customInt1 customInt2 customInt3 customInt4 customInt5 | |
23 | customStr1 customStr2 customStr3 customStr4 customStr5 | |
24 | instructions billTelephone billFacsimile billEmail | |
e3d242f7 | 25 | siteuser_id affiliate_code shipping_cost |
41e7c841 TC |
26 | delivMobile billMobile |
27 | ccOnline ccSuccess ccReceipt ccStatus ccStatusText | |
37dd20ad | 28 | ccStatus2 ccTranId complete delivOrganization billOrganization |
d9803c26 | 29 | delivStreet2 billStreet2 purchase_order shipping_method |
13a986ee | 30 | shipping_name shipping_trace |
f0722dd2 TC |
31 | paypal_token paypal_tran_id freight_tracking stage/; |
32 | } | |
33 | ||
34 | sub table { | |
35 | return "orders"; | |
0ec4ac8a TC |
36 | } |
37 | ||
14604ada TC |
38 | sub defaults { |
39 | require BSE::Util::SQL; | |
40 | require Digest::MD5; | |
41 | return | |
42 | ( | |
f0722dd2 TC |
43 | billFirstName => "", |
44 | billLastName => "", | |
45 | billStreet => "", | |
46 | billSuburb => "", | |
47 | billState => "", | |
48 | billPostCode => "", | |
49 | billCountry => "", | |
14604ada TC |
50 | total => 0, |
51 | wholesaleTotal => 0, | |
52 | gst => 0, | |
53 | orderDate => BSE::Util::SQL::now_datetime(), | |
54 | filled => 0, | |
55 | whenFilled => undef, | |
56 | whoFilled => '', | |
57 | paidFor => 0, | |
58 | paymentReceipt => '', | |
59 | randomId => Digest::MD5::md5_hex(time().rand().{}.$$), | |
60 | ccNumberHash => '', | |
61 | ccName => '', | |
62 | ccExpiryHash => '', | |
63 | ccType => '', | |
64 | randomId => '', | |
65 | cancelled => 0, | |
66 | userId => '', | |
67 | paymentType => 0, | |
68 | customInt1 => undef, | |
69 | customInt2 => undef, | |
70 | customInt3 => undef, | |
71 | customInt4 => undef, | |
72 | customInt5 => undef, | |
73 | customStr1 => undef, | |
74 | customStr2 => undef, | |
75 | customStr3 => undef, | |
76 | customStr4 => undef, | |
77 | customStr5 => undef, | |
78 | instructions => '', | |
79 | siteuser_id => undef, | |
80 | affiliate_code => '', | |
81 | shipping_cost => 0, | |
82 | ccOnline => 0, | |
83 | ccSuccess => 0, | |
84 | ccReceipt => '', | |
85 | ccStatus => 0, | |
86 | ccStatusText => '', | |
87 | ccStatus2 => '', | |
88 | ccTranId => '', | |
89 | complete => 0, | |
90 | purchase_order => '', | |
91 | shipping_method => '', | |
92 | shipping_name => '', | |
93 | shipping_trace => undef, | |
f0722dd2 TC |
94 | paypal_token => "", |
95 | paypal_tran_id => "", | |
080fc207 | 96 | freight_tracking => "", |
f0722dd2 | 97 | stage => "incomplete", |
14604ada TC |
98 | ); |
99 | } | |
100 | ||
101 | sub address_columns { | |
102 | return qw/ | |
103 | delivFirstName delivLastName delivStreet delivSuburb delivState | |
104 | delivPostCode delivCountry | |
105 | billFirstName billLastName billStreet billSuburb billState | |
106 | billPostCode billCountry | |
107 | telephone facsimile emailAddress | |
108 | instructions billTelephone billFacsimile billEmail | |
109 | delivMobile billMobile | |
110 | delivOrganization billOrganization | |
111 | delivStreet2 billStreet2/; | |
112 | } | |
113 | ||
114 | sub user_columns { | |
115 | return qw/userId siteuser_id/; | |
116 | } | |
117 | ||
118 | sub payment_columns { | |
119 | return qw/ccNumberHash ccName ccExpiryHash ccType | |
120 | paidFor paymentReceipt paymentType | |
121 | ccOnline ccSuccess ccReceipt ccStatus ccStatusText | |
122 | ccStatus2 ccTranId/; | |
123 | } | |
124 | ||
c4f18087 TC |
125 | =item billing_to_delivery_map |
126 | ||
127 | Return a hashref where the key is a billing field and the value is the | |
128 | corresponding delivery field. | |
129 | ||
130 | =cut | |
131 | ||
132 | { | |
133 | my %billing_to_delivery = | |
134 | ( | |
135 | billEmail => "emailAddress", | |
136 | billFirstName => "delivFirstName", | |
137 | billLastName => "delivLastName", | |
138 | billStreet => "delivStreet", | |
139 | billStreet2 => "delivStreet2", | |
140 | billSuburb => "delivSuburb", | |
141 | billState => "delivState", | |
142 | billPostCode => "delivPostCode", | |
143 | billCountry => "delivCountry", | |
144 | billTelephone => "telephone", | |
a964e89d | 145 | billMobile => "delivMobile", |
c4f18087 TC |
146 | billFacsimile => "facsimile", |
147 | billOrganization => "delivOrganization", | |
148 | ); | |
149 | ||
150 | sub billing_to_delivery_map { | |
151 | return \%billing_to_delivery; | |
152 | } | |
153 | } | |
154 | ||
0ec4ac8a TC |
155 | =item siteuser |
156 | ||
157 | returns the SiteUser object of the user who made this order. | |
158 | ||
159 | =cut | |
160 | ||
161 | sub siteuser { | |
162 | my ($self) = @_; | |
163 | ||
f0722dd2 TC |
164 | if ($self->siteuser_id) { |
165 | require SiteUsers; | |
166 | my $user = SiteUsers->getByPkey($self->siteuser_id); | |
167 | $user and return $user; | |
168 | } | |
169 | ||
0ec4ac8a TC |
170 | $self->{userId} or return; |
171 | ||
172 | require SiteUsers; | |
173 | ||
174 | return ( SiteUsers->getBy(userId=>$self->{userId}) )[0]; | |
175 | } | |
176 | ||
177 | sub items { | |
178 | my ($self) = @_; | |
179 | ||
180 | require BSE::TB::OrderItems; | |
181 | return BSE::TB::OrderItems->getBy(orderId => $self->{id}); | |
182 | } | |
183 | ||
ab2cd916 TC |
184 | sub files { |
185 | my ($self) = @_; | |
186 | ||
7c6f563b TC |
187 | require BSE::TB::ArticleFiles; |
188 | return BSE::TB::ArticleFiles->getSpecial(orderFiles=>$self->{id}); | |
ab2cd916 TC |
189 | } |
190 | ||
eb9d306d TC |
191 | sub paid_files { |
192 | my ($self) = @_; | |
193 | ||
194 | $self->paidFor | |
195 | or return; | |
196 | ||
197 | require BSE::TB::ArticleFiles; | |
198 | return BSE::TB::ArticleFiles->getSpecial(orderPaidFor => $self->id); | |
199 | } | |
200 | ||
ab2cd916 TC |
201 | sub products { |
202 | my ($self) = @_; | |
203 | ||
204 | require Products; | |
205 | Products->getSpecial(orderProducts=>$self->{id}); | |
206 | } | |
207 | ||
41e7c841 TC |
208 | sub valid_fields { |
209 | my ($class, $cfg) = @_; | |
210 | ||
211 | my %fields = | |
212 | ( | |
37dd20ad TC |
213 | delivFirstName => { description=>'Delivery First Name', |
214 | rules=>'dh_one_line' }, | |
215 | delivLastName => { description => 'Delivery Last Name', | |
216 | rules=>'dh_one_line' }, | |
217 | delivOrganization => { description => 'Delivery Organization', | |
218 | rules=>'dh_one_line' }, | |
219 | delivStreet => { description => 'Delivery Street', | |
220 | rules=>'dh_one_line' }, | |
221 | delivStreet2 => { description => 'Delivery Street 2', | |
222 | rules=>'dh_one_line' }, | |
223 | delivState => { description => 'Delivery State', | |
224 | rules=>'dh_one_line' }, | |
225 | delivSuburb => { description => 'Delivery Suburb', | |
226 | rules=>'dh_one_line' }, | |
227 | delivPostCode => { description => 'Delivery Post Code', | |
9074efa2 | 228 | rules=>'dh_one_line;dh_int_postcode' }, |
37dd20ad TC |
229 | delivCountry => { description => 'Delivery Country', |
230 | rules=>'dh_one_line' }, | |
231 | billFirstName => { description => 'Billing First Name', | |
232 | rules=>'dh_one_line' }, | |
233 | billLastName => { description => 'Billing Last Name', | |
234 | rules=>'dh_one_line' }, | |
235 | billOrganization => { description => 'Billing Organization', | |
236 | rules=>'dh_one_line' }, | |
237 | billStreet => { description => 'Billing Street', | |
238 | rules=>'dh_one_line' }, | |
239 | billStreet2 => { description => 'Billing Street 2', | |
240 | rules=>'dh_one_line' }, | |
241 | billSuburb => { description => 'Billing Suburb', | |
242 | rules=>'dh_one_line' }, | |
243 | billState => { description => 'Billing State', | |
244 | rules=>'dh_one_line' }, | |
245 | billPostCode => { description => 'Billing Post Code', | |
9074efa2 | 246 | rules=>'dh_one_line;dh_int_postcode' }, |
37dd20ad TC |
247 | billCountry => { description => 'Billing First Name', |
248 | rules=>'dh_one_line' }, | |
41e7c841 TC |
249 | telephone => { description => 'Telephone Number', |
250 | rules => "phone" }, | |
251 | facsimile => { description => 'Facsimile Number', | |
252 | rules => 'phone' }, | |
253 | emailAddress => { description => 'Email Address', | |
c4f18087 | 254 | rules=>'email' }, |
41e7c841 TC |
255 | instructions => { description => 'Instructions' }, |
256 | billTelephone => { description => 'Billing Telephone Number', | |
257 | rules=>'phone' }, | |
258 | billFacsimile => { description => 'Billing Facsimile Number', | |
259 | rules=>'phone' }, | |
260 | billEmail => { description => 'Billing Email Address', | |
c4f18087 | 261 | rules => 'email;required' }, |
41e7c841 TC |
262 | delivMobile => { description => 'Delivery Mobile Number', |
263 | rules => 'phone' }, | |
264 | billMobile => { description => 'Billing Mobile Number', | |
265 | rules=>'phone' }, | |
266 | instructions => { description => 'Instructions' }, | |
74b21f6d | 267 | purchase_order => { description => 'Purchase Order No' }, |
d8674b8b AMS |
268 | shipping_cost => { description => 'Shipping charges' }, |
269 | shipping_method => { description => 'Shipping method' }, | |
41e7c841 TC |
270 | ); |
271 | ||
272 | for my $field (keys %fields) { | |
273 | my $display = $cfg->entry('shop', "display_$field"); | |
274 | $display and $fields{$field}{description} = $display; | |
275 | } | |
276 | ||
277 | return %fields; | |
278 | } | |
279 | ||
280 | sub valid_rules { | |
281 | my ($class, $cfg) = @_; | |
282 | ||
283 | return; | |
284 | } | |
285 | ||
286 | sub valid_payment_fields { | |
287 | my ($class, $cfg) = @_; | |
288 | ||
289 | my %fields = | |
290 | ( | |
291 | cardNumber => | |
292 | { | |
293 | description => "Credit Card Number", | |
294 | rules=>"creditcardnumber", | |
295 | }, | |
296 | cardExpiry => | |
297 | { | |
298 | description => "Credit Card Expiry Date", | |
299 | rules => 'creditcardexpirysingle', | |
300 | }, | |
301 | cardHolder => { description => "Credit Card Holder" }, | |
1546e1f0 | 302 | ccType => { description => "Credit Card Type" }, |
41e7c841 TC |
303 | cardVerify => |
304 | { | |
305 | description => 'Card Verification Value', | |
306 | rules => 'creditcardcvv', | |
307 | }, | |
308 | ); | |
309 | ||
310 | for my $field (keys %fields) { | |
311 | my $display = $cfg->entry('shop', "display_$field"); | |
312 | $display and $fields{$field}{description} = $display; | |
313 | } | |
314 | ||
315 | return %fields; | |
316 | } | |
317 | ||
318 | sub valid_payment_rules { | |
319 | return; | |
320 | } | |
321 | ||
5d88571c TC |
322 | sub clear_items { |
323 | my ($self) = @_; | |
324 | ||
325 | confess "Attempt to clear items on completed order $self->{id}" | |
326 | if $self->{complete}; | |
327 | ||
328 | BSE::DB->run(deleteOrdersItems => $self->{id}); | |
329 | } | |
330 | ||
14604ada TC |
331 | sub add_item { |
332 | my ($self, %opts) = @_; | |
333 | ||
334 | my $prod = delete $opts{product} | |
335 | or confess "Missing product option"; | |
336 | my $units = delete $opts{units} || 1; | |
337 | ||
338 | my $options = ''; | |
339 | my @dboptions; | |
340 | if ($opts{options}) { | |
341 | if (ref $opts{options}) { | |
342 | @dboptions = @{delete $opts{options}}; | |
343 | } | |
344 | else { | |
345 | $options = delete $opts{options}; | |
346 | } | |
347 | } | |
348 | ||
349 | require BSE::TB::OrderItems; | |
350 | my %item = | |
351 | ( | |
352 | productId => $prod->id, | |
353 | orderId => $self->id, | |
354 | units => $units, | |
355 | price => $prod->retailPrice, | |
356 | options => $options, | |
357 | max_lapsed => 0, | |
358 | session_id => 0, | |
359 | ( map { $_ => $prod->{$_} } | |
360 | qw/wholesalePrice gst customInt1 customInt2 customInt3 customStr1 customStr2 customStr3 title description subscription_id subscription_period product_code/ | |
361 | ), | |
362 | ); | |
363 | ||
364 | $self->set_total($self->total + $prod->retailPrice * $units); | |
365 | ||
366 | return BSE::TB::OrderItems->make(%item); | |
367 | } | |
368 | ||
13a986ee TC |
369 | sub deliv_country_code { |
370 | my ($self) = @_; | |
371 | ||
372 | my $use_codes = BSE::Cfg->single->entry("shop", "country_code", 0); | |
373 | if ($use_codes) { | |
374 | return $self->delivCountry; | |
375 | } | |
376 | else { | |
377 | require BSE::Countries; | |
378 | return BSE::Countries::bse_country_code($self->delivCountry); | |
379 | } | |
380 | } | |
381 | ||
f0722dd2 TC |
382 | =item stage |
383 | ||
384 | Return the order stage. | |
385 | ||
386 | If the stage is empty, guess from the order flags. | |
387 | ||
388 | =cut | |
389 | ||
390 | sub stage { | |
391 | my ($self) = @_; | |
392 | ||
393 | if ($self->{stage} ne "") { | |
394 | return $self->{stage}; | |
395 | } | |
396 | ||
397 | if (!$self->complete) { | |
398 | return "incomplete"; | |
399 | } | |
400 | elsif ($self->filled) { | |
401 | return "shipped"; | |
402 | } | |
403 | else { | |
404 | return "unprocessed"; | |
405 | } | |
406 | } | |
407 | ||
408 | sub stage_description { | |
409 | my ($self, $lang) = @_; | |
410 | ||
411 | return BSE::TB::Orders->stage_label($self->stage, $lang); | |
412 | } | |
413 | ||
f55be9df TC |
414 | sub stage_description_id { |
415 | my ($self) = @_; | |
416 | ||
417 | return BSE::TB::Orders->stage_label_id($self->stage); | |
418 | } | |
419 | ||
c4f18087 | 420 | =item delivery_mail_recipient |
f0722dd2 | 421 | |
c4f18087 TC |
422 | Return a value suitable for BSE::ComposeMail's to parameter for the |
423 | shipping email address. | |
f0722dd2 TC |
424 | |
425 | =cut | |
426 | ||
c4f18087 | 427 | sub delivery_mail_recipient { |
f0722dd2 TC |
428 | my ($self) = @_; |
429 | ||
430 | my $user = $self->siteuser; | |
c4f18087 | 431 | my $email = $self->emailAddress || $self->billEmail; |
f0722dd2 | 432 | |
c4f18087 | 433 | if ($user && $user->email eq $email) { |
f0722dd2 TC |
434 | return $user; |
435 | } | |
436 | ||
c4f18087 | 437 | return $email; |
f0722dd2 TC |
438 | } |
439 | ||
768dccf0 | 440 | =item _tags |
8d8895b4 | 441 | |
768dccf0 | 442 | Internal method with the common code between tags() and mail_tags(). |
8d8895b4 TC |
443 | |
444 | =cut | |
445 | ||
768dccf0 TC |
446 | sub _tags { |
447 | my ($self, $escape) = @_; | |
8d8895b4 TC |
448 | |
449 | require BSE::Util::Tags; | |
8d8895b4 | 450 | require BSE::TB::OrderItems; |
768dccf0 TC |
451 | require BSE::Util::Iterate; |
452 | my $it; | |
453 | my $art; | |
454 | my $esc; | |
455 | my $obj; | |
456 | if ($escape) { | |
457 | require BSE::Util::HTML; | |
458 | $it = BSE::Util::Iterate::Objects->new; | |
459 | $art = \&BSE::Util::Tags::tag_article; | |
460 | $obj = \&BSE::Util::Tags::tag_object; | |
461 | $esc = \&BSE::Util::HTML::escape_html; | |
462 | } | |
463 | else { | |
464 | $it = BSE::Util::Iterate::Objects::Text->new; | |
465 | $art = \&BSE::Util::Tags::tag_article_plain; | |
466 | $obj = \&BSE::Util::Tags::tag_object_plain; | |
467 | $esc = sub { return $_[0] }; | |
468 | } | |
469 | ||
470 | my $cfg = BSE::Cfg->single; | |
471 | my $must_be_paid = $cfg->entryBool('downloads', 'must_be_paid', 0); | |
472 | my $must_be_filled = $cfg->entryBool('downloads', 'must_be_filled', 0); | |
473 | ||
8d8895b4 TC |
474 | my %item_cols = map { $_ => 1 } BSE::TB::OrderItem->columns; |
475 | my %products; | |
476 | my $current_item; | |
768dccf0 | 477 | my $current_file; |
8d8895b4 TC |
478 | return |
479 | ( | |
768dccf0 | 480 | order => [ $obj, $self ], |
8d8895b4 TC |
481 | $it->make |
482 | ( | |
483 | single => "item", | |
484 | plural => "items", | |
485 | code => [ items => $self ], | |
486 | store => \$current_item, | |
487 | ), | |
488 | extended => sub { | |
489 | my ($args) = @_; | |
490 | ||
491 | $current_item | |
492 | or return '* only usable in items iterator *'; | |
493 | ||
494 | $item_cols{$args} | |
495 | or return "* unknown item column $args *"; | |
496 | ||
497 | return $current_item->$args() * $current_item->units; | |
498 | }, | |
499 | $it->make | |
500 | ( | |
501 | single => "option", | |
502 | plural => "options", | |
503 | code => sub { | |
504 | $current_item | |
505 | or return; | |
506 | return $current_item->option_hashes | |
507 | }, | |
508 | nocache => 1, | |
509 | ), | |
510 | options => sub { | |
511 | $current_item | |
512 | or return '* only in the items iterator *'; | |
768dccf0 | 513 | return $esc->($current_item->nice_options); |
8d8895b4 TC |
514 | }, |
515 | product => sub { | |
516 | $current_item | |
517 | or return '* only usable in items *'; | |
518 | ||
519 | require Products; | |
520 | my $id = $current_item->productId; | |
521 | $products{$id} ||= Products->getByPkey($id); | |
522 | ||
523 | my $product = $products{$id} | |
524 | or return ''; | |
525 | ||
768dccf0 TC |
526 | return $art->($product, $cfg, $_[0]); |
527 | }, | |
7c6f563b | 528 | $it->make |
768dccf0 TC |
529 | ( |
530 | single => 'orderfile', | |
531 | plural => 'orderfiles', | |
532 | code => [ files => $self ], | |
533 | store => \$current_file, | |
534 | ), | |
7c6f563b | 535 | $it->make |
768dccf0 TC |
536 | ( |
537 | single => "prodfile", | |
538 | plural => "prodfiles", | |
539 | code => sub { | |
540 | $current_item | |
541 | or return '* only usable in items *'; | |
542 | ||
543 | require Products; | |
544 | my $id = $current_item->productId; | |
545 | $products{$id} ||= Products->getByPkey($id); | |
546 | ||
547 | my $product = $products{$id} | |
548 | or return ''; | |
549 | ||
550 | return $product->files; | |
551 | }, | |
552 | store => \$current_file, | |
553 | ), | |
554 | ifFileAvail => sub { | |
555 | $current_file or return 0; | |
556 | $current_file->{forSale} or return 1; | |
557 | ||
558 | return 0 if $must_be_paid && !$self->{paidFor}; | |
559 | return 0 if $must_be_filled && !$self->{filled}; | |
560 | ||
561 | return 1; | |
8d8895b4 TC |
562 | }, |
563 | ); | |
564 | } | |
565 | ||
768dccf0 TC |
566 | =item mail_tags |
567 | ||
568 | =cut | |
569 | ||
570 | sub mail_tags { | |
571 | my ($self) = @_; | |
572 | ||
573 | return $self->_tags(0); | |
574 | } | |
575 | ||
576 | =item tags | |
577 | ||
578 | Return template tags suitable for an order (non-mail) | |
579 | ||
580 | =cut | |
581 | ||
582 | sub tags { | |
583 | my ($self) = @_; | |
584 | ||
585 | return $self->_tags(1); | |
586 | } | |
587 | ||
f0722dd2 TC |
588 | sub send_shipped_email { |
589 | my ($self) = @_; | |
590 | ||
c4f18087 | 591 | my $to = $self->delivery_mail_recipient; |
f0722dd2 TC |
592 | require BSE::ComposeMail; |
593 | my $mailer = BSE::ComposeMail->new(cfg => BSE::Cfg->single); | |
594 | require BSE::Util::Tags; | |
f0722dd2 TC |
595 | my %acts = |
596 | ( | |
597 | BSE::Util::Tags->mail_tags(), | |
8d8895b4 | 598 | $self->mail_tags, |
f0722dd2 | 599 | ); |
c4f18087 | 600 | my %opts = |
f0722dd2 TC |
601 | ( |
602 | to => $to, | |
603 | subject => "Your order has shipped", | |
604 | template => "email/ordershipped", | |
605 | acts => \%acts, | |
606 | log_msg => "Notify customer order has shipped", | |
607 | log_object => $self, | |
608 | log_component => "shopadmin:orders:saveorder", | |
609 | ); | |
c4f18087 TC |
610 | if ($self->emailAddress && $self->billEmail |
611 | && lc $self->emailAddress ne $self->billEmail) { | |
612 | $opts{cc} = $self->billEmail; | |
613 | } | |
614 | ||
615 | $mailer->send(%opts); | |
f0722dd2 TC |
616 | } |
617 | ||
618 | sub new_stage { | |
619 | my ($self, $who, $stage, $stage_note) = @_; | |
620 | ||
621 | unless ($stage ne $self->stage | |
622 | || defined $stage_note && $stage_note =~ /\S/) { | |
623 | return; | |
624 | } | |
625 | ||
626 | my $old_stage = $self->stage; | |
627 | my $msg = "Set to stage '$stage'"; | |
628 | if (defined $stage_note && $stage_note =~ /\S/) { | |
629 | $msg .= ": $stage_note"; | |
630 | } | |
631 | require BSE::TB::AuditLog; | |
632 | BSE::TB::AuditLog->log | |
633 | ( | |
634 | component => "shopadmin:orders:saveorder", | |
635 | object => $self, | |
636 | msg => $msg, | |
637 | level => "info", | |
638 | actor => $who || "U" | |
639 | ); | |
640 | ||
641 | if ($stage ne $old_stage) { | |
642 | $self->set_stage($stage); | |
643 | if ($stage eq "shipped") { | |
644 | $self->send_shipped_email(); | |
645 | $self->set_filled(1); | |
646 | } | |
647 | else { | |
648 | $self->set_filled(0); | |
649 | } | |
650 | } | |
651 | } | |
652 | ||
0ec4ac8a | 653 | 1; |