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