allow importing custom fields for product option values
[bse.git] / site / cgi-bin / modules / BSE / Importer / Target / Product.pm
1 package BSE::Importer::Target::Product;
2 use strict;
3 use base 'BSE::Importer::Target::Article';
4 use BSE::API qw(bse_make_product bse_make_catalog bse_add_image);
5 use BSE::TB::Articles;
6 use BSE::TB::Products;
7 use BSE::TB::ProductOptions;
8 use BSE::TB::ProductOptionValues;
9 use BSE::TB::PriceTiers;
10
11 our $VERSION = "1.012";
12
13 =head1 NAME
14
15 BSE::Importer::Target::Product - import target for products
16
17 =head1 SYNOPSIS
18
19   [import profile foo]
20   ...
21   ; these are the defaults
22   codes=0
23   code_field=product_code
24   parent=3
25   ignore_missing=1
26   reset_images=0
27   reset_steps=0
28   price_dollar=0
29   prodopt_value_sep=|
30   reset_prodopts=1
31
32   # done by the importer
33   my $target = BSE::Importer::Target::BSE::TB::Product->new
34      (importer => $importer, opts => \%opts)
35   ...
36   $target->start($imp);
37   # for each row:
38   $target->row($imp, \%entry, \@parents);
39
40 =head1 DESCRIPTION
41
42 Provides a target for importing BSE products.
43
44 The import profile must provide C<title> and C<retailPrice> mappings.
45
46 =head1 CONFIGURATION
47
48 This is in addition to the configuration in
49 L<BSE::Importer::Target::Article/CONFIGURATION>.
50
51 =over
52
53 =item *
54
55 C<code_field> - the default changes to C<product_code>
56
57 =item *
58
59 C<parent> - the default changes to the id of the shop article.
60
61 =item *
62
63 C<price_dollar> - if true, the C<retailPrice> field and tier prices
64 are treated as dollar amounts rather than cents.  Default: 0.
65
66 =item *
67
68 C<prodopt_value_sep> - the separator between product options.
69 Default: C<|>.
70
71 =item *
72
73 C<reset_prodopts> - if true, product options are reset when updating a
74 product.  Default: 1.
75
76 =back
77
78 =head1 SPECIAL FIELDS
79
80 In addition to those in L<BSE::Importer::Target::Article/SPECIAL
81 FIELDS>, the following fields are used to import extra information
82 into products:
83
84 =over
85
86 =item *
87
88 C<< prodoptI<index>_name >> - define the name of a product option.
89 C<index> can be from 1 to 10.
90
91 =item *
92
93 C<< prodoptI<index>_values >> - define the values for a product
94 option, separated by the configured C<prodop_value_sep>.
95
96 =item * C<< prodoptI<index>_custom.I<name> >> - define custom values
97 called I<name> for each value specified by C<< prodoptI<index>_values
98 >>, separated by the configured C<prodop_value_sep>.
99
100 =item *
101
102 C<< tier_price_I<tier_id> >> - set the product price for the specified
103 tier.
104
105 =back
106
107 =head1 METHODS
108
109 =over
110
111 =item new()
112
113 Create a new article import target.  Follows the protocol specified by
114 L<BSE::Importer::Target::Base>.
115
116 =cut
117
118 sub new {
119   my ($class, %opts) = @_;
120
121   my $self = $class->SUPER::new(%opts);
122
123   my $importer = delete $opts{importer};
124
125   $self->{price_dollar} = $importer->cfg_entry('price_dollar', 0);
126   $self->{product_template} = $importer->cfg_entry('product_template');
127   $self->{catalog_template} = $importer->cfg_entry('catalog_template');
128   $self->{prodopt_value_sep} = $importer->cfg_entry("prodopt_separator", "|");
129   $self->{reset_prodopts} = $importer->cfg_entry("reset_prodopts", 1);
130
131   my $map = $importer->maps;
132   unless ($importer->update_only) {
133     defined $map->{retailPrice}
134       or die "No retailPrice mapping found\n";
135   }
136
137   $self->{price_tiers} = +{ map { $_->id => $_ } BSE::TB::PriceTiers->all };
138
139   return $self;
140 }
141
142 =item xform_entry()
143
144 Called by row() to perform an extra data transformation needed.
145
146 Currently this forces non-blank code fields if C<codes> is set,
147 removes the dollar sign if any from the retail prices, transforms the
148 retail price from dollars to cents if C<price_dollar> is configured
149 and warns if no price is set.
150
151 =cut
152
153 sub xform_entry {
154   my ($self, $importer, $entry) = @_;
155
156   $self->SUPER::xform_entry($importer, $entry);
157
158   if (defined $entry->{product_code}) {
159     $entry->{product_code} =~ s/\A\s+//;
160     $entry->{product_code} =~ s/\s+\z//;
161   }
162
163   if ($self->{use_codes}) {
164     $entry->{$self->{code_field}} =~ /\S/
165       or die "$self->{code_field} blank with use_codes\n";
166   }
167
168   if (exists $entry->{retailPrice}) {
169     $entry->{retailPrice} =~ s/\$//; # in case
170
171     if ($entry->{retailPrice} =~ /\d/) {
172       $self->{price_dollar}
173         and $entry->{retailPrice} *= 100;
174     }
175     else {
176       $importer->warn("Warning: no price");
177       $entry->{retailPrice} = 0;
178     }
179   }
180 }
181
182 =item children_of()
183
184 Returns catalogs that are a child of the specified article.
185
186 sub children_of {
187   my ($self, $parent) = @_;
188
189   return grep $_->{generator} eq 'BSE::Generate::Catalog',
190     BSE::TB::Articles->children($parent);
191 }
192
193 =item make_parent()
194
195 Create a catalog.
196
197 =cut
198
199 sub make_parent {
200   my ($self, $importer, %entry) = @_;
201
202   return bse_make_catalog(%entry);
203 }
204
205 =item find_leaf()
206
207 Find an existing product matching the code.
208
209 =cut
210
211 sub find_leaf {
212   my ($self, $leaf_id, $importer) = @_;
213
214   my $leaf;
215   if ($self->{code_field} eq "id") {
216     $leaf = BSE::TB::Products->getByPkey($leaf_id);
217   }
218   else {
219     ($leaf) = BSE::TB::Products->getBy($self->{code_field}, $leaf_id)
220       or return;
221   }
222
223   $importer->event(find_leaf => { id => $leaf_id, leaf => $leaf });
224
225   if ($self->{reset_prodopts}) {
226     my @options = $leaf->db_options;
227     for my $option (@options) {
228       $option->remove;
229     }
230   }
231
232   return $leaf;
233 }
234
235 =item make_leaf()
236
237 Make a new product.
238
239 =cut
240
241 sub make_leaf {
242   my ($self, $importer, %entry) = @_;
243
244   my $leaf = bse_make_product(%entry);
245
246   $importer->event(make_leaf => { leaf => $leaf });
247
248   return $leaf;
249 }
250
251 =item fill_leaf()
252
253 Fill in the product with the new data.
254
255 =cut
256
257 sub fill_leaf {
258   my ($self, $importer, $leaf, %entry) = @_;
259
260   my $ordering = time;
261   for my $opt_num (1 .. 10) {
262     my $name = $entry{"prodopt${opt_num}_name"};
263     my $values = $entry{"prodopt${opt_num}_values"};
264
265     defined $name && $name =~ /\S/ && $values =~ /\S/
266       or next;
267     my @values = split /\Q$self->{prodopt_value_sep}/, $values
268       or next;
269     my @custom = grep /^prodopt${opt_num}_custom\./, keys %entry;
270
271     my %custom_values;
272     for my $custom_name (@custom) {
273       (my $name = $custom_name) =~ s/^prodopt${opt_num}_custom\.//;
274       $custom_values{$name} = [ split /\Q$self->{prodopt_value_sep}/, $entry{$custom_name} ];
275     }
276
277     my $option = BSE::TB::ProductOptions->make
278       (
279        product_id => $leaf->id,
280        name => $name,
281        display_order => $ordering++,
282       );
283
284     my $index = 0;
285     for my $value (@values) {
286       my $entry = BSE::TB::ProductOptionValues->make
287         (
288          product_option_id => $option->id,
289          value => $value,
290          display_order => $ordering++,
291         );
292
293       for my $name (keys %custom_values) {
294         if ($index < @{$custom_values{$name}}) {
295           $entry->set_custom($name => $custom_values{$name}[$index]);
296         }
297       }
298       $entry->save;
299
300       ++$index;
301     }
302   }
303
304   my %prices = map { $_->tier_id => $_->retailPrice } $leaf->prices;
305   for my $tier_id (keys %{$self->{price_tiers}}) {
306     my $price = $entry{"tier_price_$tier_id"};
307     if (defined $price && $price =~ /\d/) {
308       $price =~ s/\$//; # in case
309       $price *= 100 if $self->{price_dollar};
310
311       $prices{$tier_id} = $price;
312     }
313   }
314
315   $leaf->set_prices(\%prices);
316
317   return $self->SUPER::fill_leaf($importer, $leaf, %entry);
318 }
319
320 =item default_parent()
321
322 Overrides the default parent.
323
324 =cut
325
326 sub default_parent { 3 }
327
328 =item default_code_field()
329
330 Overrides the default code field.
331
332 =cut
333
334 sub default_code_field { "product_code" }
335
336 =item key_fields
337
338 Fields that can act as key fields.
339
340 =cut
341
342 sub key_fields {
343   my ($class) = @_;
344
345   return ( $class->SUPER::key_fields(), "product_code" );
346 }
347
348 =item validate_make_leaf
349
350 =cut
351
352 sub validate_make_leaf {
353   my ($self, $importer, $entry) = @_;
354
355   if (defined $entry->{product_code} && $entry->{product_code} ne '') {
356     my $other = BSE::TB::Products->getBy(product_code => $entry->{product_code});
357     $other
358       and die "Duplicate product_code with product ", $other->id, "\n";
359   }
360 }
361
362 =item primary_key_fields
363
364 Fields we can't modify (or initialize) since the database (or database
365 interface) generates them.
366
367 =cut
368
369 sub primary_key_fields {
370   my ($class) = @_;
371
372   return ( $class->SUPER::primary_key_fields(), "articleId" );
373 }
374
375 1;
376
377 =back
378
379 =head1 AUTHOR
380
381 Tony Cook <tony@develop-help.com>
382
383 =cut