prevent the importer overwriting primary key fields
[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
f5203422 11our $VERSION = "1.010";
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
96=item *
97
f41558fd 98C<< tier_price_I<tier_id> >> - set the product price for the specified
d415d0ba
TC
99tier.
100
101=back
102
103=head1 METHODS
104
105=over
106
107=item new()
108
109Create a new article import target. Follows the protocol specified by
110L<BSE::Importer::Target::Base>.
111
112=cut
cb7fd78d 113
3709451d
TC
114sub new {
115 my ($class, %opts) = @_;
116
117 my $self = $class->SUPER::new(%opts);
118
119 my $importer = delete $opts{importer};
120
121 $self->{price_dollar} = $importer->cfg_entry('price_dollar', 0);
122 $self->{product_template} = $importer->cfg_entry('product_template');
123 $self->{catalog_template} = $importer->cfg_entry('catalog_template');
0cca6ce6
TC
124 $self->{prodopt_value_sep} = $importer->cfg_entry("prodopt_separator", "|");
125 $self->{reset_prodopts} = $importer->cfg_entry("reset_prodopts", 1);
3709451d 126
0cca6ce6 127 my $map = $importer->maps;
57e4a9c7
TC
128 unless ($importer->update_only) {
129 defined $map->{retailPrice}
130 or die "No retailPrice mapping found\n";
131 }
3709451d 132
df2663f0
TC
133 $self->{price_tiers} = +{ map { $_->id => $_ } BSE::TB::PriceTiers->all };
134
3709451d
TC
135 return $self;
136}
137
d415d0ba
TC
138=item xform_entry()
139
140Called by row() to perform an extra data transformation needed.
141
142Currently this forces non-blank code fields if C<codes> is set,
143removes the dollar sign if any from the retail prices, transforms the
144retail price from dollars to cents if C<price_dollar> is configured
145and warns if no price is set.
146
147=cut
148
3709451d
TC
149sub xform_entry {
150 my ($self, $importer, $entry) = @_;
151
152 $self->SUPER::xform_entry($importer, $entry);
153
1455f602
TC
154 if (defined $entry->{product_code}) {
155 $entry->{product_code} =~ s/\A\s+//;
156 $entry->{product_code} =~ s/\s+\z//;
157 }
158
3709451d 159 if ($self->{use_codes}) {
57e4a9c7
TC
160 $entry->{$self->{code_field}} =~ /\S/
161 or die "$self->{code_field} blank with use_codes\n";
3709451d 162 }
3709451d 163
57e4a9c7
TC
164 if (exists $entry->{retailPrice}) {
165 $entry->{retailPrice} =~ s/\$//; # in case
166
167 if ($entry->{retailPrice} =~ /\d/) {
168 $self->{price_dollar}
169 and $entry->{retailPrice} *= 100;
170 }
171 else {
172 $importer->warn("Warning: no price");
173 $entry->{retailPrice} = 0;
174 }
3709451d
TC
175 }
176}
177
d415d0ba
TC
178=item children_of()
179
180Returns catalogs that are a child of the specified article.
181
3709451d
TC
182sub children_of {
183 my ($self, $parent) = @_;
184
46541e94 185 return grep $_->{generator} eq 'BSE::Generate::Catalog',
e0ed81d7 186 BSE::TB::Articles->children($parent);
3709451d
TC
187}
188
d415d0ba
TC
189=item make_parent()
190
191Create a catalog.
192
193=cut
194
3709451d
TC
195sub make_parent {
196 my ($self, $importer, %entry) = @_;
197
3709451d
TC
198 return bse_make_catalog(%entry);
199}
200
d415d0ba
TC
201=item find_leaf()
202
203Find an existing product matching the code.
204
205=cut
206
3709451d 207sub find_leaf {
57e4a9c7 208 my ($self, $leaf_id, $importer) = @_;
3709451d 209
1455f602
TC
210 my $leaf;
211 if ($self->{code_field} eq "id") {
10dd37f9 212 $leaf = BSE::TB::Products->getByPkey($leaf_id);
1455f602
TC
213 }
214 else {
10dd37f9 215 ($leaf) = BSE::TB::Products->getBy($self->{code_field}, $leaf_id)
1455f602
TC
216 or return;
217 }
3709451d 218
57e4a9c7
TC
219 $importer->event(find_leaf => { id => $leaf_id, leaf => $leaf });
220
0cca6ce6
TC
221 if ($self->{reset_prodopts}) {
222 my @options = $leaf->db_options;
223 for my $option (@options) {
224 $option->remove;
225 }
226 }
227
3709451d
TC
228 return $leaf;
229}
230
d415d0ba
TC
231=item make_leaf()
232
233Make a new product.
234
235=cut
236
3709451d
TC
237sub make_leaf {
238 my ($self, $importer, %entry) = @_;
239
57e4a9c7
TC
240 my $leaf = bse_make_product(%entry);
241
242 $importer->event(make_leaf => { leaf => $leaf });
243
244 return $leaf;
3709451d
TC
245}
246
d415d0ba
TC
247=item fill_leaf()
248
249Fill in the product with the new data.
250
251=cut
252
0cca6ce6
TC
253sub fill_leaf {
254 my ($self, $importer, $leaf, %entry) = @_;
3709451d 255
0cca6ce6
TC
256 my $ordering = time;
257 for my $opt_num (1 .. 5) {
258 my $name = $entry{"prodopt${opt_num}_name"};
259 my $values = $entry{"prodopt${opt_num}_values"};
3709451d 260
0cca6ce6
TC
261 defined $name && $name =~ /\S/ && $values =~ /\S/
262 or next;
263 my @values = split /\Q$self->{prodopt_value_sep}/, $values
3709451d 264 or next;
3709451d 265
0cca6ce6 266 my $option = BSE::TB::ProductOptions->make
3709451d 267 (
0cca6ce6
TC
268 product_id => $leaf->id,
269 name => $name,
270 display_order => $ordering++,
3709451d 271 );
3709451d 272
0cca6ce6
TC
273 for my $value (@values) {
274 my $entry = BSE::TB::ProductOptionValues->make
275 (
276 product_option_id => $option->id,
277 value => $value,
278 display_order => $ordering++,
279 );
280 }
3709451d 281 }
df2663f0 282
a7c8c69d 283 my %prices = map { $_->tier_id => $_->retailPrice } $leaf->prices;
df2663f0
TC
284 for my $tier_id (keys %{$self->{price_tiers}}) {
285 my $price = $entry{"tier_price_$tier_id"};
286 if (defined $price && $price =~ /\d/) {
287 $price =~ s/\$//; # in case
288 $price *= 100 if $self->{price_dollar};
289
290 $prices{$tier_id} = $price;
291 }
292 }
293
294 $leaf->set_prices(\%prices);
295
0cca6ce6 296 return $self->SUPER::fill_leaf($importer, $leaf, %entry);
3709451d
TC
297}
298
d415d0ba
TC
299=item default_parent()
300
301Overrides the default parent.
302
303=cut
304
3709451d
TC
305sub default_parent { 3 }
306
d415d0ba
TC
307=item default_code_field()
308
309Overrides the default code field.
310
311=cut
312
3709451d
TC
313sub default_code_field { "product_code" }
314
57e4a9c7
TC
315=item key_fields
316
317Fields that can act as key fields.
318
319=cut
320
321sub key_fields {
322 my ($class) = @_;
323
324 return ( $class->SUPER::key_fields(), "product_code" );
325}
326
1455f602
TC
327=item validate_make_leaf
328
329=cut
330
331sub validate_make_leaf {
332 my ($self, $importer, $entry) = @_;
333
334 if (defined $entry->{product_code} && $entry->{product_code} ne '') {
10dd37f9 335 my $other = BSE::TB::Products->getBy(product_code => $entry->{product_code});
1455f602
TC
336 $other
337 and die "Duplicate product_code with product ", $other->id, "\n";
338 }
339}
340
f5203422
TC
341=item primary_key_fields
342
343Fields we can't modify (or initialize) since the database (or database
344interface) generates them.
345
346=cut
347
348sub primary_key_fields {
349 my ($class) = @_;
350
351 return ( $class->SUPER::primary_key_fields(), "articleId" );
352}
353
3709451d 3541;
d415d0ba
TC
355
356=back
357
358=head1 AUTHOR
359
360Tony Cook <tony@develop-help.com>
361
362=cut