Commit | Line | Data |
---|---|---|
d415d0ba | 1 | package BSE::Importer::Target::Product; |
3709451d | 2 | use strict; |
d415d0ba | 3 | use base 'BSE::Importer::Target::Article'; |
3709451d | 4 | use BSE::API qw(bse_make_product bse_make_catalog bse_add_image); |
e0ed81d7 | 5 | use BSE::TB::Articles; |
3709451d | 6 | use Products; |
0cca6ce6 TC |
7 | use BSE::TB::ProductOptions; |
8 | use BSE::TB::ProductOptionValues; | |
df2663f0 | 9 | use BSE::TB::PriceTiers; |
3709451d | 10 | |
e0ed81d7 | 11 | our $VERSION = "1.007"; |
d415d0ba TC |
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::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_proce_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 | |
cb7fd78d | 113 | |
3709451d TC |
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'); | |
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 | ||
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 | ||
3709451d TC |
149 | sub 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 | ||
180 | Returns catalogs that are a child of the specified article. | |
181 | ||
3709451d TC |
182 | sub 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 | ||
191 | Create a catalog. | |
192 | ||
193 | =cut | |
194 | ||
3709451d TC |
195 | sub 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 | ||
203 | Find an existing product matching the code. | |
204 | ||
205 | =cut | |
206 | ||
3709451d | 207 | sub find_leaf { |
57e4a9c7 | 208 | my ($self, $leaf_id, $importer) = @_; |
3709451d | 209 | |
1455f602 TC |
210 | my $leaf; |
211 | if ($self->{code_field} eq "id") { | |
212 | $leaf = Products->getByPkey($leaf_id); | |
213 | } | |
214 | else { | |
215 | ($leaf) = Products->getBy($self->{code_field}, $leaf_id) | |
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 | ||
233 | Make a new product. | |
234 | ||
235 | =cut | |
236 | ||
3709451d TC |
237 | sub 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 | ||
249 | Fill in the product with the new data. | |
250 | ||
251 | =cut | |
252 | ||
0cca6ce6 TC |
253 | sub 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 | ||
301 | Overrides the default parent. | |
302 | ||
303 | =cut | |
304 | ||
3709451d TC |
305 | sub default_parent { 3 } |
306 | ||
d415d0ba TC |
307 | =item default_code_field() |
308 | ||
309 | Overrides the default code field. | |
310 | ||
311 | =cut | |
312 | ||
3709451d TC |
313 | sub default_code_field { "product_code" } |
314 | ||
57e4a9c7 TC |
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 | ||
1455f602 TC |
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 = Products->getBy(product_code => $entry->{product_code}); | |
336 | $other | |
337 | and die "Duplicate product_code with product ", $other->id, "\n"; | |
338 | } | |
339 | } | |
340 | ||
3709451d | 341 | 1; |
d415d0ba TC |
342 | |
343 | =back | |
344 | ||
345 | =head1 AUTHOR | |
346 | ||
347 | Tony Cook <tony@develop-help.com> | |
348 | ||
349 | =cut |