59aaf0b687420a4a6f587dff4cd166573f712508
[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.009";
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 *
97
98 C<< tier_price_I<tier_id> >> - set the product price for the specified
99 tier.
100
101 =back
102
103 =head1 METHODS
104
105 =over
106
107 =item new()
108
109 Create a new article import target.  Follows the protocol specified by
110 L<BSE::Importer::Target::Base>.
111
112 =cut
113
114 sub 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');
124   $self->{prodopt_value_sep} = $importer->cfg_entry("prodopt_separator", "|");
125   $self->{reset_prodopts} = $importer->cfg_entry("reset_prodopts", 1);
126
127   my $map = $importer->maps;
128   unless ($importer->update_only) {
129     defined $map->{retailPrice}
130       or die "No retailPrice mapping found\n";
131   }
132
133   $self->{price_tiers} = +{ map { $_->id => $_ } BSE::TB::PriceTiers->all };
134
135   return $self;
136 }
137
138 =item xform_entry()
139
140 Called by row() to perform an extra data transformation needed.
141
142 Currently this forces non-blank code fields if C<codes> is set,
143 removes the dollar sign if any from the retail prices, transforms the
144 retail price from dollars to cents if C<price_dollar> is configured
145 and warns if no price is set.
146
147 =cut
148
149 sub xform_entry {
150   my ($self, $importer, $entry) = @_;
151
152   $self->SUPER::xform_entry($importer, $entry);
153
154   if (defined $entry->{product_code}) {
155     $entry->{product_code} =~ s/\A\s+//;
156     $entry->{product_code} =~ s/\s+\z//;
157   }
158
159   if ($self->{use_codes}) {
160     $entry->{$self->{code_field}} =~ /\S/
161       or die "$self->{code_field} blank with use_codes\n";
162   }
163
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     }
175   }
176 }
177
178 =item children_of()
179
180 Returns catalogs that are a child of the specified article.
181
182 sub children_of {
183   my ($self, $parent) = @_;
184
185   return grep $_->{generator} eq 'BSE::Generate::Catalog',
186     BSE::TB::Articles->children($parent);
187 }
188
189 =item make_parent()
190
191 Create a catalog.
192
193 =cut
194
195 sub make_parent {
196   my ($self, $importer, %entry) = @_;
197
198   return bse_make_catalog(%entry);
199 }
200
201 =item find_leaf()
202
203 Find an existing product matching the code.
204
205 =cut
206
207 sub find_leaf {
208   my ($self, $leaf_id, $importer) = @_;
209
210   my $leaf;
211   if ($self->{code_field} eq "id") {
212     $leaf = BSE::TB::Products->getByPkey($leaf_id);
213   }
214   else {
215     ($leaf) = BSE::TB::Products->getBy($self->{code_field}, $leaf_id)
216       or return;
217   }
218
219   $importer->event(find_leaf => { id => $leaf_id, leaf => $leaf });
220
221   if ($self->{reset_prodopts}) {
222     my @options = $leaf->db_options;
223     for my $option (@options) {
224       $option->remove;
225     }
226   }
227
228   return $leaf;
229 }
230
231 =item make_leaf()
232
233 Make a new product.
234
235 =cut
236
237 sub make_leaf {
238   my ($self, $importer, %entry) = @_;
239
240   my $leaf = bse_make_product(%entry);
241
242   $importer->event(make_leaf => { leaf => $leaf });
243
244   return $leaf;
245 }
246
247 =item fill_leaf()
248
249 Fill in the product with the new data.
250
251 =cut
252
253 sub fill_leaf {
254   my ($self, $importer, $leaf, %entry) = @_;
255
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"};
260
261     defined $name && $name =~ /\S/ && $values =~ /\S/
262       or next;
263     my @values = split /\Q$self->{prodopt_value_sep}/, $values
264       or next;
265
266     my $option = BSE::TB::ProductOptions->make
267       (
268        product_id => $leaf->id,
269        name => $name,
270        display_order => $ordering++,
271       );
272
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     }
281   }
282
283   my %prices = map { $_->tier_id => $_->retailPrice } $leaf->prices;
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
296   return $self->SUPER::fill_leaf($importer, $leaf, %entry);
297 }
298
299 =item default_parent()
300
301 Overrides the default parent.
302
303 =cut
304
305 sub default_parent { 3 }
306
307 =item default_code_field()
308
309 Overrides the default code field.
310
311 =cut
312
313 sub default_code_field { "product_code" }
314
315 =item key_fields
316
317 Fields that can act as key fields.
318
319 =cut
320
321 sub key_fields {
322   my ($class) = @_;
323
324   return ( $class->SUPER::key_fields(), "product_code" );
325 }
326
327 =item validate_make_leaf
328
329 =cut
330
331 sub validate_make_leaf {
332   my ($self, $importer, $entry) = @_;
333
334   if (defined $entry->{product_code} && $entry->{product_code} ne '') {
335     my $other = BSE::TB::Products->getBy(product_code => $entry->{product_code});
336     $other
337       and die "Duplicate product_code with product ", $other->id, "\n";
338   }
339 }
340
341 1;
342
343 =back
344
345 =head1 AUTHOR
346
347 Tony Cook <tony@develop-help.com>
348
349 =cut