diff --git a/tailbone_corepos/templates/purchases/view.mako b/tailbone_corepos/templates/purchases/view.mako
new file mode 100644
index 0000000..6c8d882
--- /dev/null
+++ b/tailbone_corepos/templates/purchases/view.mako
@@ -0,0 +1,104 @@
+## -*- coding: utf-8; -*-
+<%inherit file="tailbone:templates/purchases/view.mako" />
+
+<%def name="object_helpers()">
+ ${parent.object_helpers()}
+ % if master.has_perm('download_for_corepos'):
+
+ % endif
+%def>
+
+<%def name="render_this_page_template()">
+ ${parent.render_this_page_template()}
+ % if master.has_perm('download_for_corepos'):
+
+ % endif
+%def>
+
+<%def name="finalize_this_page_vars()">
+ ${parent.finalize_this_page_vars()}
+ % if master.has_perm('download_for_corepos'):
+
+ % endif
+%def>
+
+
+${parent.body()}
diff --git a/tailbone_corepos/views/purchases.py b/tailbone_corepos/views/purchases.py
new file mode 100644
index 0000000..d9fe1f4
--- /dev/null
+++ b/tailbone_corepos/views/purchases.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2021 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Rattail. If not, see .
+#
+################################################################################
+"""
+Purchase views
+"""
+
+import csv
+import os
+
+from rattail.util import pretty_quantity
+from rattail_corepos.config import core_office_url
+
+from tailbone.views.purchases import core as base
+
+
+class PurchaseView(base.PurchaseView):
+ """
+ Expose some extra features per CORE-POS integration.
+ """
+
+ def template_kwargs_view(self, **kwargs):
+ kwargs = super(PurchaseView, self).template_kwargs_view(**kwargs)
+
+ url = core_office_url(self.rattail_config)
+ if url:
+ url = '{}/purchasing/ImportPurchaseOrder.php'.format(url)
+ kwargs['corepos_import_url'] = url
+
+ return kwargs
+
+ def download_for_corepos(self):
+ """
+ View for downloading a Purchase data file for import to CORE-POS.
+ """
+ purchase = self.get_instance()
+ app = self.get_rattail_app()
+ tmpdir = app.make_temp_dir()
+ path = os.path.join(tmpdir, 'Purchase {}.csv'.format(purchase.id_str))
+
+ fields = [
+ 'sku',
+ 'cost_total',
+ 'quantity_units',
+ 'quantity_cases',
+ 'units_per_case',
+ 'unit_size',
+ 'brand',
+ 'description',
+ 'upc_without_check',
+ 'upc_with_check',
+ ]
+
+ rows = []
+
+ def collect(item, i):
+ upc = item.upc
+ if upc:
+ # TODO: i am still confused by what really is expected for the
+ # 'Unit Size' field here. CORE *behavior* seems to prefer what
+ # logically maps to Rattail `Product.unit_size` (e.g. 12) but
+ # the data type for CORE `PurchaseOrderItems.unitSize` is varchar
+ # and code comments imply e.g. '12 OZ' would also be expected.
+ # for now i am being "conservative" and mimicking CORE *behavior*.
+ unit_size = None
+ if item.product:
+ unit_size = pretty_quantity(item.product.unit_size)
+ rows.append({
+ 'sku': item.vendor_code,
+ 'cost_total': item.po_total,
+ 'quantity_units': pretty_quantity(item.units_ordered),
+ 'quantity_cases': pretty_quantity(item.cases_ordered),
+ 'units_per_case': pretty_quantity(item.case_quantity),
+ 'unit_size': unit_size,
+ 'brand': item.brand_name,
+ 'description': item.description,
+ 'upc_without_check': str(upc)[:-1],
+ 'upc_with_check': str(upc),
+ })
+
+ self.progress_loop(collect, purchase.items, None, # TODO
+ message="Converting data to CSV")
+
+ with open(path, 'w') as f:
+ writer = csv.DictWriter(f, fields)
+ # note, the CORE importer does not really need or expect a header
+ # TODO: seems like it should? b/c would be more helpful for humans
+ #writer.writeheader()
+ writer.writerows(rows)
+
+ return self.file_response(path)
+
+ @classmethod
+ def defaults(cls, config):
+ cls._corepos_purchase_defaults(config)
+ cls._purchase_defaults(config)
+ cls._defaults(config)
+
+ @classmethod
+ def _corepos_purchase_defaults(cls, config):
+ route_prefix = cls.get_route_prefix()
+ instance_url_prefix = cls.get_instance_url_prefix()
+ permission_prefix = cls.get_permission_prefix()
+ model_title = cls.get_model_title()
+
+ # download for core-pos
+ config.add_route('{}.download_for_corepos'.format(route_prefix), '{}/download-for-corepos'.format(instance_url_prefix))
+ config.add_view(cls, attr='download_for_corepos', route_name='{}.download_for_corepos'.format(route_prefix),
+ permission='{}.download_for_corepos'.format(permission_prefix))
+ config.add_tailbone_permission(permission_prefix, '{}.download'.format(permission_prefix),
+ "Download {} for import to CORE-POS".format(model_title))
+
+
+def includeme(config):
+ PurchaseView.defaults(config)