source: rattail/rattail/importing/rattail.py

Last change on this file was c52617c, checked in by Lance Edgar <lance@…>, 3 weeks ago

Remove TPR, sale price refs from *simple* Product importer fields

for sake of rattail <-> rattail

  • Property mode set to 100644
File size: 14.3 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2019 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Rattail -> Rattail data import
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import sqlalchemy as sa
30
31from rattail.db import Session
32from rattail.importing import model
33from rattail.importing.handlers import FromSQLAlchemyHandler, ToSQLAlchemyHandler
34from rattail.importing.sqlalchemy import FromSQLAlchemySameToSame
35from rattail.util import OrderedDict
36
37
38class FromRattailHandler(FromSQLAlchemyHandler):
39    """
40    Base class for import handlers which target a Rattail database on the local side.
41    """
42    host_title = "Rattail"
43
44    def make_host_session(self):
45        return Session()
46
47
48class ToRattailHandler(ToSQLAlchemyHandler):
49    """
50    Base class for import handlers which target a Rattail database on the local side.
51    """
52    local_title = "Rattail"
53
54    def make_session(self):
55        kwargs = {}
56        if hasattr(self, 'runas_user'):
57            kwargs['continuum_user'] = self.runas_user
58        return Session(**kwargs)
59
60    def begin_local_transaction(self):
61        self.session = self.make_session()
62        if hasattr(self, 'runas_username') and self.runas_username:
63            self.session.set_continuum_user(self.runas_username)
64
65
66class FromRattailToRattailBase(object):
67    """
68    Common base class for Rattail -> Rattail data import/export handlers.
69    """
70
71    def get_importers(self):
72        importers = OrderedDict()
73        importers['Person'] = PersonImporter
74        importers['GlobalPerson'] = GlobalPersonImporter
75        importers['PersonEmailAddress'] = PersonEmailAddressImporter
76        importers['PersonPhoneNumber'] = PersonPhoneNumberImporter
77        importers['PersonMailingAddress'] = PersonMailingAddressImporter
78        importers['User'] = UserImporter
79        importers['AdminUser'] = AdminUserImporter
80        importers['GlobalUser'] = GlobalUserImporter
81        importers['Message'] = MessageImporter
82        importers['MessageRecipient'] = MessageRecipientImporter
83        importers['Store'] = StoreImporter
84        importers['StorePhoneNumber'] = StorePhoneNumberImporter
85        importers['Employee'] = EmployeeImporter
86        importers['EmployeeStore'] = EmployeeStoreImporter
87        importers['EmployeeEmailAddress'] = EmployeeEmailAddressImporter
88        importers['EmployeePhoneNumber'] = EmployeePhoneNumberImporter
89        importers['ScheduledShift'] = ScheduledShiftImporter
90        importers['WorkedShift'] = WorkedShiftImporter
91        importers['Customer'] = CustomerImporter
92        importers['CustomerGroup'] = CustomerGroupImporter
93        importers['CustomerGroupAssignment'] = CustomerGroupAssignmentImporter
94        importers['CustomerPerson'] = CustomerPersonImporter
95        importers['CustomerEmailAddress'] = CustomerEmailAddressImporter
96        importers['CustomerPhoneNumber'] = CustomerPhoneNumberImporter
97        importers['Member'] = MemberImporter
98        importers['MemberEmailAddress'] = MemberEmailAddressImporter
99        importers['MemberPhoneNumber'] = MemberPhoneNumberImporter
100        importers['Vendor'] = VendorImporter
101        importers['VendorEmailAddress'] = VendorEmailAddressImporter
102        importers['VendorPhoneNumber'] = VendorPhoneNumberImporter
103        importers['VendorContact'] = VendorContactImporter
104        importers['Department'] = DepartmentImporter
105        importers['EmployeeDepartment'] = EmployeeDepartmentImporter
106        importers['Subdepartment'] = SubdepartmentImporter
107        importers['Category'] = CategoryImporter
108        importers['Family'] = FamilyImporter
109        importers['ReportCode'] = ReportCodeImporter
110        importers['DepositLink'] = DepositLinkImporter
111        importers['Tax'] = TaxImporter
112        importers['InventoryAdjustmentReason'] = InventoryAdjustmentReasonImporter
113        importers['Brand'] = BrandImporter
114        importers['Product'] = ProductImporter
115        importers['ProductCode'] = ProductCodeImporter
116        importers['ProductCost'] = ProductCostImporter
117        importers['ProductPrice'] = ProductPriceImporter
118        importers['ProductStoreInfo'] = ProductStoreInfoImporter
119        importers['ProductVolatile'] = ProductVolatileImporter
120        importers['ProductImage'] = ProductImageImporter
121        importers['LabelProfile'] = LabelProfileImporter
122        return importers
123
124    def get_default_keys(self):
125        keys = self.get_importer_keys()
126
127        avoid_by_default = [
128            'GlobalPerson',
129            'AdminUser',
130            'GlobalUser',
131            'ProductImage',
132        ]
133
134        for key in avoid_by_default:
135            if key in keys:
136                keys.remove(key)
137
138        return keys
139
140
141class FromRattailToRattailImport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
142    """
143    Handler for Rattail (other) -> Rattail (local) data import.
144
145    .. attribute:: direction
146
147       Value is ``'import'`` - see also
148       :attr:`rattail.importing.handlers.ImportHandler.direction`.
149    """
150    dbkey = 'host'
151
152    @property
153    def host_title(self):
154        return "{} ({})".format(self.config.app_title(default="Rattail"), self.dbkey)
155
156    @property
157    def local_title(self):
158        return self.config.node_title(default="Rattail (local)")
159
160    def make_host_session(self):
161        return Session(bind=self.config.rattail_engines[self.dbkey])
162
163
164class FromRattailToRattailExport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
165    """
166    Handler for Rattail (local) -> Rattail (other) data export.
167
168    .. attribute:: direction
169
170       Value is ``'export'`` - see also
171       :attr:`rattail.importing.handlers.ImportHandler.direction`.
172    """
173    direction = 'export'
174
175    @property
176    def host_title(self):
177        return self.config.node_title()
178
179    @property
180    def local_title(self):
181        return "{} ({})".format(self.config.app_title(default="Rattail"), self.dbkey)
182
183    def make_session(self):
184        return Session(bind=self.config.rattail_engines[self.dbkey])
185
186
187class FromRattail(FromSQLAlchemySameToSame):
188    """
189    Base class for Rattail -> Rattail data importers.
190    """
191
192
193class PersonImporter(FromRattail, model.PersonImporter):
194    pass
195
196
197class GlobalPersonImporter(FromRattail, model.GlobalPersonImporter):
198    """
199    This is a customized version of the :class:`PersonImporter`, which simply
200    avoids "local only" person accounts.
201    """
202
203    def query(self):
204        query = super(GlobalPersonImporter, self).query()
205
206        # never include "local only" people
207        query = query.filter(sa.or_(
208            self.host_model_class.local_only == False,
209            self.host_model_class.local_only == None))
210
211        return query
212
213    def normalize_host_object(self, person):
214
215        # must check this here for sake of datasync
216        if person.local_only:
217            return
218
219        data = super(GlobalPersonImporter, self).normalize_host_object(person)
220        return data
221
222
223class PersonEmailAddressImporter(FromRattail, model.PersonEmailAddressImporter):
224    pass
225
226class PersonPhoneNumberImporter(FromRattail, model.PersonPhoneNumberImporter):
227    pass
228
229class PersonMailingAddressImporter(FromRattail, model.PersonMailingAddressImporter):
230    pass
231
232class UserImporter(FromRattail, model.UserImporter):
233    pass
234
235
236class GlobalUserImporter(FromRattail, model.GlobalUserImporter):
237    """
238    This is a customized version of the :class:`UserImporter`, which simply
239    avoids "local only" user accounts.
240    """
241
242    def query(self):
243        query = super(GlobalUserImporter, self).query()
244
245        # never include "local only" users
246        query = query.filter(sa.or_(
247            self.host_model_class.local_only == False,
248            self.host_model_class.local_only == None))
249
250        return query
251
252    def normalize_host_object(self, user):
253
254        # must check this here for sake of datasync
255        if user.local_only:
256            return
257
258        data = super(GlobalUserImporter, self).normalize_host_object(user)
259        return data
260
261
262class AdminUserImporter(FromRattail, model.AdminUserImporter):
263
264    @property
265    def supported_fields(self):
266        return super(AdminUserImporter, self).supported_fields + [
267            'admin',
268        ]
269
270    def normalize_host_object(self, user):
271        data = super(AdminUserImporter, self).normalize_local_object(user) # sic
272        if 'admin' in self.fields: # TODO: do we really need this, after the above?
273            data['admin'] = self.admin_uuid in [r.role_uuid for r in user._roles]
274        return data
275
276
277class MessageImporter(FromRattail, model.MessageImporter):
278    pass
279
280class MessageRecipientImporter(FromRattail, model.MessageRecipientImporter):
281    pass
282
283class StoreImporter(FromRattail, model.StoreImporter):
284    pass
285
286class StorePhoneNumberImporter(FromRattail, model.StorePhoneNumberImporter):
287    pass
288
289class EmployeeImporter(FromRattail, model.EmployeeImporter):
290    pass
291
292class EmployeeStoreImporter(FromRattail, model.EmployeeStoreImporter):
293    pass
294
295class EmployeeDepartmentImporter(FromRattail, model.EmployeeDepartmentImporter):
296    pass
297
298class EmployeeEmailAddressImporter(FromRattail, model.EmployeeEmailAddressImporter):
299    pass
300
301class EmployeePhoneNumberImporter(FromRattail, model.EmployeePhoneNumberImporter):
302    pass
303
304class ScheduledShiftImporter(FromRattail, model.ScheduledShiftImporter):
305    pass
306
307class WorkedShiftImporter(FromRattail, model.WorkedShiftImporter):
308    pass
309
310class CustomerImporter(FromRattail, model.CustomerImporter):
311    pass
312
313class CustomerGroupImporter(FromRattail, model.CustomerGroupImporter):
314    pass
315
316class CustomerGroupAssignmentImporter(FromRattail, model.CustomerGroupAssignmentImporter):
317    pass
318
319class CustomerPersonImporter(FromRattail, model.CustomerPersonImporter):
320    pass
321
322class CustomerEmailAddressImporter(FromRattail, model.CustomerEmailAddressImporter):
323    pass
324
325class CustomerPhoneNumberImporter(FromRattail, model.CustomerPhoneNumberImporter):
326    pass
327
328class MemberImporter(FromRattail, model.MemberImporter):
329    pass
330
331class MemberEmailAddressImporter(FromRattail, model.MemberEmailAddressImporter):
332    pass
333
334class MemberPhoneNumberImporter(FromRattail, model.MemberPhoneNumberImporter):
335    pass
336
337class VendorImporter(FromRattail, model.VendorImporter):
338    pass
339
340class VendorEmailAddressImporter(FromRattail, model.VendorEmailAddressImporter):
341    pass
342
343class VendorPhoneNumberImporter(FromRattail, model.VendorPhoneNumberImporter):
344    pass
345
346class VendorContactImporter(FromRattail, model.VendorContactImporter):
347    pass
348
349class DepartmentImporter(FromRattail, model.DepartmentImporter):
350    pass
351
352class SubdepartmentImporter(FromRattail, model.SubdepartmentImporter):
353    pass
354
355class CategoryImporter(FromRattail, model.CategoryImporter):
356    pass
357
358class FamilyImporter(FromRattail, model.FamilyImporter):
359    pass
360
361class ReportCodeImporter(FromRattail, model.ReportCodeImporter):
362    pass
363
364class DepositLinkImporter(FromRattail, model.DepositLinkImporter):
365    pass
366
367class TaxImporter(FromRattail, model.TaxImporter):
368    pass
369
370class InventoryAdjustmentReasonImporter(FromRattail, model.InventoryAdjustmentReasonImporter):
371    pass
372
373class BrandImporter(FromRattail, model.BrandImporter):
374    pass
375
376class ProductImporter(FromRattail, model.ProductImporter):
377
378    @property
379    def simple_fields(self):
380        fields = super(ProductImporter, self).simple_fields
381        # NOTE: it seems we can't consider these "simple" due to the
382        # self-referencing foreign key situation.  an importer can still
383        # "support" these fields, but they're excluded from the simple set for
384        # sake of rattail <-> rattail
385        fields.remove('regular_price_uuid')
386        fields.remove('tpr_price_uuid')
387        fields.remove('sale_price_uuid')
388        fields.remove('current_price_uuid')
389        return fields
390
391    def query(self):
392        query = super(ProductImporter, self).query()
393
394        # make sure potential unit items (i.e. rows with NULL unit_uuid) come
395        # first, so they will be created before pack items reference them
396        # cf. https://www.postgresql.org/docs/current/static/queries-order.html
397        # cf. https://stackoverflow.com/a/7622046
398        query = query.order_by(self.host_model_class.unit_uuid.desc())
399
400        return query
401
402
403class ProductCodeImporter(FromRattail, model.ProductCodeImporter):
404    pass
405
406class ProductCostImporter(FromRattail, model.ProductCostImporter):
407    pass
408
409class ProductPriceImporter(FromRattail, model.ProductPriceImporter):
410
411    @property
412    def supported_fields(self):
413        return super(ProductPriceImporter, self).supported_fields + self.product_reference_fields
414
415
416class ProductStoreInfoImporter(FromRattail, model.ProductStoreInfoImporter):
417    pass
418
419class ProductVolatileImporter(FromRattail, model.ProductVolatileImporter):
420    pass
421
422
423class ProductImageImporter(FromRattail, model.ProductImageImporter):
424    """
425    Importer for product images.  Note that this uses the "batch" approach
426    because fetching all data up front is not performant when the host/local
427    systems are on different machines etc.
428    """
429
430    def query(self):
431        query = self.host_session.query(self.model_class)\
432                                 .order_by(self.model_class.uuid)
433        return query[self.host_index:self.host_index + self.batch_size]
434
435
436class LabelProfileImporter(FromRattail, model.LabelProfileImporter):
437
438    def query(self):
439        query = super(LabelProfileImporter, self).query()
440
441        if not self.config.getbool('rattail', 'labels.sync_all_profiles', default=False):
442            # only fetch labels from host which are marked as "sync me"
443            query = query .filter(self.model_class.sync_me == True)
444
445        return query.order_by(self.model_class.ordinal)
Note: See TracBrowser for help on using the repository browser.