source: rattail/rattail/importing/rattail.py @ 874cbc44

Last change on this file since 874cbc44 was 874cbc44, checked in by Lance Edgar <ledgar@…>, 8 months ago

Clean up Rattail <-> Rattail import/export handlers a bit

  • Property mode set to 100644
File size: 12.7 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
29from rattail.db import Session
30from rattail.importing import model
31from rattail.importing.handlers import FromSQLAlchemyHandler, ToSQLAlchemyHandler
32from rattail.importing.sqlalchemy import FromSQLAlchemy
33from rattail.util import OrderedDict
34
35
36class FromRattailHandler(FromSQLAlchemyHandler):
37    """
38    Base class for import handlers which target a Rattail database on the local side.
39    """
40    host_title = "Rattail"
41
42    def make_host_session(self):
43        return Session()
44
45
46class ToRattailHandler(ToSQLAlchemyHandler):
47    """
48    Base class for import handlers which target a Rattail database on the local side.
49    """
50    local_title = "Rattail"
51
52    def make_session(self):
53        kwargs = {}
54        if hasattr(self, 'runas_user'):
55            kwargs['continuum_user'] = self.runas_user
56        return Session(**kwargs)
57
58    def begin_local_transaction(self):
59        self.session = self.make_session()
60        if hasattr(self, 'runas_username') and self.runas_username:
61            self.session.set_continuum_user(self.runas_username)
62
63
64class FromRattailToRattailBase(object):
65    """
66    Common base class for Rattail -> Rattail data import/export handlers.
67    """
68
69    def get_importers(self):
70        importers = OrderedDict()
71        importers['Person'] = PersonImporter
72        importers['PersonEmailAddress'] = PersonEmailAddressImporter
73        importers['PersonPhoneNumber'] = PersonPhoneNumberImporter
74        importers['PersonMailingAddress'] = PersonMailingAddressImporter
75        importers['User'] = UserImporter
76        importers['AdminUser'] = AdminUserImporter
77        importers['Message'] = MessageImporter
78        importers['MessageRecipient'] = MessageRecipientImporter
79        importers['Store'] = StoreImporter
80        importers['StorePhoneNumber'] = StorePhoneNumberImporter
81        importers['Employee'] = EmployeeImporter
82        importers['EmployeeStore'] = EmployeeStoreImporter
83        importers['EmployeeEmailAddress'] = EmployeeEmailAddressImporter
84        importers['EmployeePhoneNumber'] = EmployeePhoneNumberImporter
85        importers['ScheduledShift'] = ScheduledShiftImporter
86        importers['WorkedShift'] = WorkedShiftImporter
87        importers['Customer'] = CustomerImporter
88        importers['CustomerGroup'] = CustomerGroupImporter
89        importers['CustomerGroupAssignment'] = CustomerGroupAssignmentImporter
90        importers['CustomerPerson'] = CustomerPersonImporter
91        importers['CustomerEmailAddress'] = CustomerEmailAddressImporter
92        importers['CustomerPhoneNumber'] = CustomerPhoneNumberImporter
93        importers['Member'] = MemberImporter
94        importers['MemberEmailAddress'] = MemberEmailAddressImporter
95        importers['MemberPhoneNumber'] = MemberPhoneNumberImporter
96        importers['Vendor'] = VendorImporter
97        importers['VendorEmailAddress'] = VendorEmailAddressImporter
98        importers['VendorPhoneNumber'] = VendorPhoneNumberImporter
99        importers['VendorContact'] = VendorContactImporter
100        importers['Department'] = DepartmentImporter
101        importers['EmployeeDepartment'] = EmployeeDepartmentImporter
102        importers['Subdepartment'] = SubdepartmentImporter
103        importers['Category'] = CategoryImporter
104        importers['Family'] = FamilyImporter
105        importers['ReportCode'] = ReportCodeImporter
106        importers['DepositLink'] = DepositLinkImporter
107        importers['Tax'] = TaxImporter
108        importers['InventoryAdjustmentReason'] = InventoryAdjustmentReasonImporter
109        importers['Brand'] = BrandImporter
110        importers['Product'] = ProductImporter
111        importers['ProductCode'] = ProductCodeImporter
112        importers['ProductCost'] = ProductCostImporter
113        importers['ProductPrice'] = ProductPriceImporter
114        importers['ProductStoreInfo'] = ProductStoreInfoImporter
115        importers['ProductImage'] = ProductImageImporter
116        importers['LabelProfile'] = LabelProfileImporter
117        return importers
118
119    def get_default_keys(self):
120        keys = self.get_importer_keys()
121        if 'AdminUser' in keys:
122            keys.remove('AdminUser')
123        if 'ProductImage' in keys:
124            keys.remove('ProductImage')
125        return keys
126
127
128class FromRattailToRattailImport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
129    """
130    Handler for Rattail (other) -> Rattail (local) data import.
131    """
132    dbkey = 'host'
133
134    @property
135    def host_title(self):
136        return "{} ({})".format(self.config.app_title(default="Rattail"), self.dbkey)
137
138    @property
139    def local_title(self):
140        return self.config.node_title()
141
142    def make_host_session(self):
143        return Session(bind=self.config.rattail_engines[self.dbkey])
144
145
146class FromRattailToRattailExport(FromRattailToRattailBase, FromRattailHandler, ToRattailHandler):
147    """
148    Handler for Rattail (local) -> Rattail (other) data export.
149    """
150
151    @property
152    def host_title(self):
153        return self.config.node_title()
154
155    @property
156    def local_title(self):
157        return "{} ({})".format(self.config.app_title(default="Rattail"), self.dbkey)
158
159    def make_session(self):
160        return Session(bind=self.config.rattail_engines[self.dbkey])
161
162
163class FromRattail(FromSQLAlchemy):
164    """
165    Base class for Rattail -> Rattail data importers.
166    """
167
168    @property
169    def host_model_class(self):
170        return self.model_class
171
172    @property
173    def supported_fields(self):
174        """
175        We only need to support the simple fields in a Rattail->Rattail import,
176        since all relevant tables should be covered and therefore no need to do
177        crazy foreign key acrobatics etc.
178        """
179        return self.simple_fields
180
181    def query(self):
182        """
183        Leverage the same caching optimizations on both sides, if applicable.
184        """
185        query = super(FromRattail, self).query()
186        if hasattr(self, 'cache_query_options'):
187            options = self.cache_query_options()
188            if options:
189                for option in options:
190                    query = query.options(option)
191        return query
192
193    def normalize_host_object(self, obj):
194        """
195        Normalization should work the same for both sides.
196        """
197        return self.normalize_local_object(obj)
198
199
200class PersonImporter(FromRattail, model.PersonImporter):
201    pass
202
203class PersonEmailAddressImporter(FromRattail, model.PersonEmailAddressImporter):
204    pass
205
206class PersonPhoneNumberImporter(FromRattail, model.PersonPhoneNumberImporter):
207    pass
208
209class PersonMailingAddressImporter(FromRattail, model.PersonMailingAddressImporter):
210    pass
211
212class UserImporter(FromRattail, model.UserImporter):
213    pass
214
215class AdminUserImporter(FromRattail, model.AdminUserImporter):
216
217    @property
218    def supported_fields(self):
219        return super(AdminUserImporter, self).supported_fields + [
220            'admin',
221        ]
222
223    def normalize_host_object(self, user):
224        data = super(AdminUserImporter, self).normalize_local_object(user) # sic
225        if 'admin' in self.fields: # TODO: do we really need this, after the above?
226            data['admin'] = self.admin_uuid in [r.role_uuid for r in user._roles]
227        return data
228
229
230class MessageImporter(FromRattail, model.MessageImporter):
231    pass
232
233class MessageRecipientImporter(FromRattail, model.MessageRecipientImporter):
234    pass
235
236class StoreImporter(FromRattail, model.StoreImporter):
237    pass
238
239class StorePhoneNumberImporter(FromRattail, model.StorePhoneNumberImporter):
240    pass
241
242class EmployeeImporter(FromRattail, model.EmployeeImporter):
243    pass
244
245class EmployeeStoreImporter(FromRattail, model.EmployeeStoreImporter):
246    pass
247
248class EmployeeDepartmentImporter(FromRattail, model.EmployeeDepartmentImporter):
249    pass
250
251class EmployeeEmailAddressImporter(FromRattail, model.EmployeeEmailAddressImporter):
252    pass
253
254class EmployeePhoneNumberImporter(FromRattail, model.EmployeePhoneNumberImporter):
255    pass
256
257class ScheduledShiftImporter(FromRattail, model.ScheduledShiftImporter):
258    pass
259
260class WorkedShiftImporter(FromRattail, model.WorkedShiftImporter):
261    pass
262
263class CustomerImporter(FromRattail, model.CustomerImporter):
264    pass
265
266class CustomerGroupImporter(FromRattail, model.CustomerGroupImporter):
267    pass
268
269class CustomerGroupAssignmentImporter(FromRattail, model.CustomerGroupAssignmentImporter):
270    pass
271
272class CustomerPersonImporter(FromRattail, model.CustomerPersonImporter):
273    pass
274
275class CustomerEmailAddressImporter(FromRattail, model.CustomerEmailAddressImporter):
276    pass
277
278class CustomerPhoneNumberImporter(FromRattail, model.CustomerPhoneNumberImporter):
279    pass
280
281class MemberImporter(FromRattail, model.MemberImporter):
282    pass
283
284class MemberEmailAddressImporter(FromRattail, model.MemberEmailAddressImporter):
285    pass
286
287class MemberPhoneNumberImporter(FromRattail, model.MemberPhoneNumberImporter):
288    pass
289
290class VendorImporter(FromRattail, model.VendorImporter):
291    pass
292
293class VendorEmailAddressImporter(FromRattail, model.VendorEmailAddressImporter):
294    pass
295
296class VendorPhoneNumberImporter(FromRattail, model.VendorPhoneNumberImporter):
297    pass
298
299class VendorContactImporter(FromRattail, model.VendorContactImporter):
300    pass
301
302class DepartmentImporter(FromRattail, model.DepartmentImporter):
303    pass
304
305class SubdepartmentImporter(FromRattail, model.SubdepartmentImporter):
306    pass
307
308class CategoryImporter(FromRattail, model.CategoryImporter):
309    pass
310
311class FamilyImporter(FromRattail, model.FamilyImporter):
312    pass
313
314class ReportCodeImporter(FromRattail, model.ReportCodeImporter):
315    pass
316
317class DepositLinkImporter(FromRattail, model.DepositLinkImporter):
318    pass
319
320class TaxImporter(FromRattail, model.TaxImporter):
321    pass
322
323class InventoryAdjustmentReasonImporter(FromRattail, model.InventoryAdjustmentReasonImporter):
324    pass
325
326class BrandImporter(FromRattail, model.BrandImporter):
327    pass
328
329class ProductImporter(FromRattail, model.ProductImporter):
330
331    # TODO...
332    @property
333    def simple_fields(self):
334        fields = super(ProductImporter, self).simple_fields
335        fields.remove('regular_price_uuid')
336        fields.remove('current_price_uuid')
337        return fields
338
339    def query(self):
340        query = super(ProductImporter, self).query()
341
342        # make sure potential unit items (i.e. rows with NULL unit_uuid) come
343        # first, so they will be created before pack items reference them
344        # cf. https://www.postgresql.org/docs/current/static/queries-order.html
345        # cf. https://stackoverflow.com/a/7622046
346        query = query.order_by(self.host_model_class.unit_uuid.desc())
347
348        return query
349
350
351class ProductCodeImporter(FromRattail, model.ProductCodeImporter):
352    pass
353
354class ProductCostImporter(FromRattail, model.ProductCostImporter):
355    pass
356
357class ProductPriceImporter(FromRattail, model.ProductPriceImporter):
358
359    @property
360    def supported_fields(self):
361        return super(ProductPriceImporter, self).supported_fields + self.product_reference_fields
362
363
364class ProductStoreInfoImporter(FromRattail, model.ProductStoreInfoImporter):
365    pass
366
367
368class ProductImageImporter(FromRattail, model.ProductImageImporter):
369    """
370    Importer for product images.  Note that this uses the "batch" approach
371    because fetching all data up front is not performant when the host/local
372    systems are on different machines etc.
373    """
374
375    def query(self):
376        query = self.host_session.query(self.model_class)\
377                                 .order_by(self.model_class.uuid)
378        return query[self.host_index:self.host_index + self.batch_size]
379
380
381class LabelProfileImporter(FromRattail, model.LabelProfileImporter):
382
383    def query(self):
384        query = super(LabelProfileImporter, self).query()
385
386        if not self.config.getbool('rattail', 'labels.sync_all_profiles', default=False):
387            # only fetch labels from host which are marked as "sync me"
388            query = query .filter(self.model_class.sync_me == True)
389
390        return query.order_by(self.model_class.ordinal)
Note: See TracBrowser for help on using the repository browser.