source: tailbone/tailbone/views/purchasing/batch.py @ bf189bb7

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

Use shipped instead of ordered, for receiving authority

i.e. compare receiving quantities to shipped quantities instead of ordered

  • Property mode set to 100644
File size: 35.4 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"""
24Base views for purchasing batches
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import six
30
31from rattail.db import model, api
32from rattail.time import localtime
33
34import colander
35from deform import widget as dfwidget
36from pyramid import httpexceptions
37from webhelpers2.html import tags, HTML
38
39from tailbone import forms, grids
40from tailbone.views.batch import BatchMasterView
41
42
43class PurchasingBatchView(BatchMasterView):
44    """
45    Master view for purchase order batches.
46    """
47    model_class = model.PurchaseBatch
48    model_row_class = model.PurchaseBatchRow
49    default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
50    supports_new_product = False
51    cloneable = True
52
53    labels = {
54        'po_total': "PO Total",
55    }
56
57    grid_columns = [
58        'id',
59        'vendor',
60        'department',
61        'buyer',
62        'date_ordered',
63        'created',
64        'created_by',
65        'rowcount',
66        'status_code',
67        'executed',
68    ]
69
70    form_fields = [
71        'id',
72        'store',
73        'buyer',
74        'vendor',
75        'department',
76        'purchase',
77        'vendor_email',
78        'vendor_fax',
79        'vendor_contact',
80        'vendor_phone',
81        'date_ordered',
82        'date_received',
83        'po_number',
84        'po_total',
85        'invoice_date',
86        'invoice_number',
87        'invoice_total',
88        'notes',
89        'created',
90        'created_by',
91        'status_code',
92        'complete',
93        'executed',
94        'executed_by',
95    ]
96
97    row_labels = {
98        'upc': "UPC",
99        'item_id': "Item ID",
100        'brand_name': "Brand",
101        'po_line_number': "PO Line Number",
102        'po_unit_cost': "PO Unit Cost",
103        'po_total': "PO Total",
104    }
105
106    # row_grid_columns = [
107    #     'sequence',
108    #     'upc',
109    #     # 'item_id',
110    #     'brand_name',
111    #     'description',
112    #     'size',
113    #     'cases_ordered',
114    #     'units_ordered',
115    #     'cases_received',
116    #     'units_received',
117    #     'po_total',
118    #     'invoice_total',
119    #     'credits',
120    #     'status_code',
121    # ]
122
123    row_form_fields = [
124        'upc',
125        'item_id',
126        'product',
127        'brand_name',
128        'description',
129        'size',
130        'case_quantity',
131        'cases_ordered',
132        'units_ordered',
133        'cases_received',
134        'units_received',
135        'cases_damaged',
136        'units_damaged',
137        'cases_expired',
138        'units_expired',
139        'cases_mispick',
140        'units_mispick',
141        'po_line_number',
142        'po_unit_cost',
143        'po_total',
144        'invoice_line_number',
145        'invoice_unit_cost',
146        'invoice_total',
147        'invoice_total_calculated',
148        'status_code',
149        'credits',
150    ]
151
152    mobile_row_form_fields = [
153        'upc',
154        'item_id',
155        'product',
156        'brand_name',
157        'description',
158        'size',
159        'case_quantity',
160        'cases_ordered',
161        'units_ordered',
162        'cases_received',
163        'units_received',
164        'cases_damaged',
165        'units_damaged',
166        'cases_expired',
167        'units_expired',
168        'cases_mispick',
169        'units_mispick',
170        # 'po_line_number',
171        'po_unit_cost',
172        'po_total',
173        # 'invoice_line_number',
174        'invoice_unit_cost',
175        'invoice_total',
176        'status_code',
177        # 'credits',
178    ]
179
180    @property
181    def batch_mode(self):
182        raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
183
184    def query(self, session):
185        return session.query(model.PurchaseBatch)\
186                      .filter(model.PurchaseBatch.mode == self.batch_mode)
187
188    def configure_grid(self, g):
189        super(PurchasingBatchView, self).configure_grid(g)
190
191        g.joiners['vendor'] = lambda q: q.join(model.Vendor)
192        g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
193                                            default_active=True, default_verb='contains')
194        g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
195
196        g.joiners['department'] = lambda q: q.join(model.Department)
197        g.filters['department'] = g.make_filter('department', model.Department.name)
198        g.sorters['department'] = g.make_sorter(model.Department.name)
199
200        g.joiners['buyer'] = lambda q: q.join(model.Employee).join(model.Person)
201        g.filters['buyer'] = g.make_filter('buyer', model.Person.display_name,
202                                           default_active=True, default_verb='contains')
203        g.sorters['buyer'] = g.make_sorter(model.Person.display_name)
204
205        if self.request.has_perm('{}.execute'.format(self.get_permission_prefix())):
206            g.filters['complete'].default_active = True
207            g.filters['complete'].default_verb = 'is_true'
208
209        # invoice_total
210        g.set_type('invoice_total', 'currency')
211        g.set_label('invoice_total', "Total")
212
213        # invoice_total_calculated
214        g.set_type('invoice_total_calculated', 'currency')
215        g.set_label('invoice_total_calculated', "Total")
216
217        g.set_label('date_ordered', "Ordered")
218        g.set_label('date_received', "Received")
219
220    def grid_extra_class(self, batch, i):
221        if batch.status_code == batch.STATUS_UNKNOWN_PRODUCT:
222            return 'notice'
223
224#     def make_form(self, batch, **kwargs):
225#         if self.creating:
226#             kwargs.setdefault('id', 'new-purchase-form')
227#         form = super(PurchasingBatchView, self).make_form(batch, **kwargs)
228#         return form
229
230    def configure_common_form(self, f):
231        super(PurchasingBatchView, self).configure_common_form(f)
232
233        # po_total
234        if self.creating:
235            f.remove_field('po_total')
236        else:
237            f.set_readonly('po_total')
238            f.set_type('po_total', 'currency')
239
240    def configure_form(self, f):
241        super(PurchasingBatchView, self).configure_form(f)
242        batch = f.model_instance
243        today = localtime(self.rattail_config).date()
244
245        # mode
246        f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
247
248        # store
249        single_store = self.rattail_config.single_store()
250        if self.creating:
251            f.replace('store', 'store_uuid')
252            if single_store:
253                store = self.rattail_config.get_store(self.Session())
254                f.set_widget('store_uuid', forms.widgets.ReadonlyWidget())
255                f.set_default('store_uuid', store.uuid)
256                f.set_hidden('store_uuid')
257            else:
258                f.set_widget('store_uuid', dfwidget.SelectWidget(values=self.get_store_values()))
259                f.set_label('store_uuid', "Store")
260        else:
261            if single_store:
262                f.remove_field('store')
263            else:
264                f.set_readonly('store')
265                f.set_renderer('store', self.render_store)
266
267        # purchase
268        f.set_renderer('purchase', self.render_purchase)
269        if self.editing:
270            f.set_readonly('purchase')
271
272        # vendor
273        # fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer,
274        #               attrs={'selected': 'vendor_selected',
275        #                      'cleared': 'vendor_cleared'})
276        f.set_renderer('vendor', self.render_vendor)
277        if self.creating:
278            f.replace('vendor', 'vendor_uuid')
279            f.set_label('vendor_uuid', "Vendor")
280            widget_type = self.rattail_config.get('tailbone', 'default_widget.vendor',
281                                                  default='autocomplete')
282            if widget_type == 'autocomplete':
283                vendor_display = ""
284                if self.request.method == 'POST':
285                    if self.request.POST.get('vendor_uuid'):
286                        vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor_uuid'])
287                        if vendor:
288                            vendor_display = six.text_type(vendor)
289                vendors_url = self.request.route_url('vendors.autocomplete')
290                f.set_widget('vendor_uuid', forms.widgets.JQueryAutocompleteWidget(
291                    field_display=vendor_display, service_url=vendors_url))
292            elif widget_type == 'dropdown':
293                vendors = self.Session.query(model.Vendor)\
294                                      .order_by(model.Vendor.id)
295                vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name))
296                                 for vendor in vendors]
297                f.set_widget('vendor_uuid', dfwidget.SelectWidget(values=vendor_values))
298            else:
299                raise NotImplementedError("Unsupported vendor widget type: {}".format(widget_type))
300        elif self.editing:
301            f.set_readonly('vendor')
302
303        # department
304        f.set_renderer('department', self.render_department)
305        if self.creating:
306            if 'department' in f.fields:
307                f.replace('department', 'department_uuid')
308                f.set_node('department_uuid', colander.String())
309                dept_options = self.get_department_options()
310                dept_values = [(v, k) for k, v in dept_options]
311                f.set_widget('department_uuid', dfwidget.SelectWidget(values=dept_values))
312                f.set_label('department_uuid', "Department")
313        else:
314            f.set_readonly('department')
315
316        # buyer
317        if 'buyer' in f:
318            f.set_renderer('buyer', self.render_buyer)
319            if self.creating or self.editing:
320                f.replace('buyer', 'buyer_uuid')
321                f.set_node('buyer_uuid', colander.String(), missing=colander.null)
322                buyer_display = ""
323                if self.request.method == 'POST':
324                    if self.request.POST.get('buyer_uuid'):
325                        buyer = self.Session.query(model.Employee).get(self.request.POST['buyer_uuid'])
326                        if buyer:
327                            buyer_display = six.text_type(buyer)
328                elif self.creating:
329                    buyer = self.request.user.employee
330                    buyer_display = six.text_type(buyer)
331                    f.set_default('buyer_uuid', buyer.uuid)
332                elif self.editing:
333                    buyer_display = six.text_type(batch.buyer or '')
334                buyers_url = self.request.route_url('employees.autocomplete')
335                f.set_widget('buyer_uuid', forms.widgets.JQueryAutocompleteWidget(
336                    field_display=buyer_display, service_url=buyers_url))
337                f.set_label('buyer_uuid', "Buyer")
338
339        # date_ordered
340        f.set_type('date_ordered', 'date_jquery')
341        if self.creating:
342            f.set_default('date_ordered', today)
343
344        # date_received
345        f.set_type('date_received', 'date_jquery')
346        if self.creating:
347            f.set_default('date_received', today)
348
349        # invoice_date
350        f.set_type('invoice_date', 'date_jquery')
351
352        # po_number
353        f.set_label('po_number', "PO Number")
354
355        # invoice_total
356        f.set_readonly('invoice_total')
357        f.set_type('invoice_total', 'currency')
358
359        # invoice_total_calculated
360        f.set_readonly('invoice_total_calculated')
361        f.set_type('invoice_total_calculated', 'currency')
362
363        # vendor_email
364        f.set_readonly('vendor_email')
365        f.set_renderer('vendor_email', self.render_vendor_email)
366
367        # vendor_fax
368        f.set_readonly('vendor_fax')
369        f.set_renderer('vendor_fax', self.render_vendor_fax)
370
371        # vendor_contact
372        f.set_readonly('vendor_contact')
373        f.set_renderer('vendor_contact', self.render_vendor_contact)
374
375        # vendor_phone
376        f.set_readonly('vendor_phone')
377        f.set_renderer('vendor_phone', self.render_vendor_phone)
378
379        if self.creating:
380            f.remove_fields('po_total',
381                            'invoice_total',
382                            'complete',
383                            'vendor_email',
384                            'vendor_fax',
385                            'vendor_phone',
386                            'vendor_contact',
387                            'status_code')
388
389    def render_store(self, batch, field):
390        store = batch.store
391        if not store:
392            return ""
393        text = "({}) {}".format(store.id, store.name)
394        url = self.request.route_url('stores.view', uuid=store.uuid)
395        return tags.link_to(text, url)
396
397    def render_purchase(self, batch, field):
398        purchase = batch.purchase
399        if not purchase:
400            return ""
401        text = six.text_type(purchase)
402        url = self.request.route_url('purchases.view', uuid=purchase.uuid)
403        return tags.link_to(text, url)
404
405    def render_vendor(self, batch, field):
406        vendor = batch.vendor
407        if not vendor:
408            return ""
409        text = "({}) {}".format(vendor.id, vendor.name)
410        url = self.request.route_url('vendors.view', uuid=vendor.uuid)
411        return tags.link_to(text, url)
412
413    def render_vendor_email(self, batch, field):
414        if batch.vendor.email:
415            return batch.vendor.email.address
416
417    def render_vendor_fax(self, batch, field):
418        return self.get_vendor_fax_number(batch)
419
420    def render_vendor_contact(self, batch, field):
421        if batch.vendor.contact:
422            return six.text_type(batch.vendor.contact)
423
424    def render_vendor_phone(self, batch, field):
425        return self.get_vendor_phone_number(batch)
426
427    def render_department(self, batch, field):
428        department = batch.department
429        if not department:
430            return ""
431        if department.number:
432            text = "({}) {}".format(department.number, department.name)
433        else:
434            text = department.name
435        url = self.request.route_url('departments.view', uuid=department.uuid)
436        return tags.link_to(text, url)
437
438    def render_buyer(self, batch, field):
439        employee = batch.buyer
440        if not employee:
441            return ""
442        text = six.text_type(employee)
443        if self.request.has_perm('employees.view'):
444            url = self.request.route_url('employees.view', uuid=employee.uuid)
445            return tags.link_to(text, url)
446        return text
447
448    def get_store_values(self):
449        stores = self.Session.query(model.Store)\
450                             .order_by(model.Store.id)
451        return [(s.uuid, "({}) {}".format(s.id, s.name))
452                for s in stores]
453
454    def get_vendors(self):
455        return self.Session.query(model.Vendor)\
456                           .order_by(model.Vendor.name)
457
458    def get_vendor_values(self):
459        vendors = self.get_vendors()
460        return [(v.uuid, "({}) {}".format(v.id, v.name))
461                for v in vendors]
462
463    def get_vendor_values(self):
464        vendors = self.get_vendors()
465        return [(v.uuid, "({}) {}".format(v.id, v.name))
466                for v in vendors]
467
468    def get_buyers(self):
469        return self.Session.query(model.Employee)\
470                           .join(model.Person)\
471                           .filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)\
472                           .order_by(model.Person.display_name)
473
474    def get_buyer_values(self):
475        buyers = self.get_buyers()
476        return [(b.uuid, six.text_type(b))
477                for b in buyers]
478
479    def get_department_options(self):
480        departments = self.Session.query(model.Department).order_by(model.Department.number)
481        return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments]
482
483    def get_vendor_phone_number(self, batch):
484        for phone in batch.vendor.phones:
485            if phone.type == 'Voice':
486                return phone.number
487
488    def get_vendor_fax_number(self, batch):
489        for phone in batch.vendor.phones:
490            if phone.type == 'Fax':
491                return phone.number
492
493    def eligible_purchases(self, vendor_uuid=None, mode=None):
494        if not vendor_uuid:
495            vendor_uuid = self.request.GET.get('vendor_uuid')
496        vendor = self.Session.query(model.Vendor).get(vendor_uuid) if vendor_uuid else None
497        if not vendor:
498            return {'error': "Must specify a vendor."}
499
500        if mode is None:
501            mode = self.request.GET.get('mode')
502            mode = int(mode) if mode and mode.isdigit() else None
503        if not mode or mode not in self.enum.PURCHASE_BATCH_MODE:
504            return {'error': "Unknown mode: {}".format(mode)}
505
506        purchases = self.Session.query(model.Purchase)\
507                                .filter(model.Purchase.vendor == vendor)
508        if mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
509            purchases = purchases.filter(model.Purchase.status == self.enum.PURCHASE_STATUS_ORDERED)\
510                                 .order_by(model.Purchase.date_ordered, model.Purchase.created)
511        elif mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
512            purchases = purchases.filter(model.Purchase.status == self.enum.PURCHASE_STATUS_RECEIVED)\
513                                 .order_by(model.Purchase.date_received, model.Purchase.created)
514
515        return {'purchases': [{'key': p.uuid,
516                               'department_uuid': p.department_uuid or '',
517                               'display': self.render_eligible_purchase(p)}
518                              for p in purchases]}
519
520    def render_eligible_purchase(self, purchase):
521        if purchase.status == self.enum.PURCHASE_STATUS_ORDERED:
522            date = purchase.date_ordered
523            total = purchase.po_total
524        elif purchase.status == self.enum.PURCHASE_STATUS_RECEIVED:
525            date = purchase.date_received
526            total = purchase.invoice_total
527        return '{} for ${:0,.2f} ({})'.format(date, total, purchase.department or purchase.buyer)
528
529    def get_batch_kwargs(self, batch, mobile=False):
530        kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
531        kwargs['mode'] = self.batch_mode
532        kwargs['truck_dump'] = batch.truck_dump
533        kwargs['invoice_parser_key'] = batch.invoice_parser_key
534
535        if batch.store:
536            kwargs['store'] = batch.store
537        elif batch.store_uuid:
538            kwargs['store_uuid'] = batch.store_uuid
539
540        if batch.truck_dump_batch:
541            kwargs['truck_dump_batch'] = batch.truck_dump_batch
542        elif batch.truck_dump_batch_uuid:
543            kwargs['truck_dump_batch_uuid'] = batch.truck_dump_batch_uuid
544
545        if batch.vendor:
546            kwargs['vendor'] = batch.vendor
547        elif batch.vendor_uuid:
548            kwargs['vendor_uuid'] = batch.vendor_uuid
549
550        if batch.department:
551            kwargs['department'] = batch.department
552        elif batch.department_uuid:
553            kwargs['department_uuid'] = batch.department_uuid
554
555        if batch.buyer:
556            kwargs['buyer'] = batch.buyer
557        elif batch.buyer_uuid:
558            kwargs['buyer_uuid'] = batch.buyer_uuid
559
560        kwargs['po_number'] = batch.po_number
561        kwargs['po_total'] = batch.po_total
562
563        # TODO: should these always get set?
564        if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
565            kwargs['date_ordered'] = batch.date_ordered
566        elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
567            kwargs['date_ordered'] = batch.date_ordered
568            kwargs['date_received'] = batch.date_received
569            kwargs['invoice_number'] = batch.invoice_number
570        elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
571            kwargs['invoice_date'] = batch.invoice_date
572            kwargs['invoice_number'] = batch.invoice_number
573
574        if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
575                               self.enum.PURCHASE_BATCH_MODE_COSTING):
576            purchase = batch.purchase
577            if not purchase and batch.purchase_uuid:
578                purchase = self.Session.query(model.Purchase).get(batch.purchase_uuid)
579                assert purchase
580            if purchase:
581                kwargs['purchase'] = purchase
582                kwargs['buyer'] = purchase.buyer
583                kwargs['buyer_uuid'] = purchase.buyer_uuid
584                kwargs['date_ordered'] = purchase.date_ordered
585                kwargs['po_total'] = purchase.po_total
586
587        return kwargs
588
589#     def template_kwargs_view(self, **kwargs):
590#         kwargs = super(PurchasingBatchView, self).template_kwargs_view(**kwargs)
591#         vendor = kwargs['batch'].vendor
592#         kwargs['vendor_cost_count'] = Session.query(model.ProductCost)\
593#                                              .filter(model.ProductCost.vendor == vendor)\
594#                                              .count()
595#         kwargs['vendor_cost_threshold'] = self.rattail_config.getint(
596#             'tailbone', 'purchases.order_form.vendor_cost_warning_threshold', default=699)
597#         return kwargs
598
599    def template_kwargs_create(self, **kwargs):
600        kwargs['purchases_field'] = 'purchase_uuid'
601        return kwargs
602
603#     def get_row_data(self, batch):
604#         query = super(PurchasingBatchView, self).get_row_data(batch)
605#         return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
606
607    def sort_mobile_row_data(self, query):
608        return query.order_by(model.PurchaseBatchRow.modified.desc())
609
610    def configure_row_grid(self, g):
611        super(PurchasingBatchView, self).configure_row_grid(g)
612
613        g.set_type('upc', 'gpc')
614        g.set_type('cases_ordered', 'quantity')
615        g.set_type('units_ordered', 'quantity')
616        g.set_type('cases_shipped', 'quantity')
617        g.set_type('units_shipped', 'quantity')
618        g.set_type('cases_received', 'quantity')
619        g.set_type('units_received', 'quantity')
620        g.set_type('po_total', 'currency')
621        g.set_type('credits', 'boolean')
622
623        # we only want the grid column to have abbreviated label, but *not* the filter
624        # TODO: would be nice to somehow make this simpler
625        g.set_label('cases_ordered', "Cases Ord.")
626        g.filters['cases_ordered'].label = "Cases Ordered"
627        g.set_label('units_ordered', "Units Ord.")
628        g.filters['units_ordered'].label = "Units Ordered"
629        g.set_label('cases_shipped', "Cases Shp.")
630        g.filters['cases_shipped'].label = "Cases Shipped"
631        g.set_label('units_shipped', "Units Shp.")
632        g.filters['units_shipped'].label = "Units Shipped"
633        g.set_label('cases_received', "Cases Rec.")
634        g.filters['cases_received'].label = "Cases Received"
635        g.set_label('units_received', "Units Rec.")
636        g.filters['units_received'].label = "Units Received"
637
638        # invoice_total
639        g.set_type('invoice_total', 'currency')
640        g.set_label('invoice_total', "Total")
641
642        # invoice_total_calculated
643        g.set_type('invoice_total_calculated', 'currency')
644        g.set_label('invoice_total_calculated', "Total")
645
646        g.set_label('po_total', "Total")
647        g.set_label('credits', "Credits?")
648
649    def make_row_grid_tools(self, batch):
650        return self.make_default_row_grid_tools(batch)
651
652    def row_grid_extra_class(self, row, i):
653        if row.status_code in (row.STATUS_PRODUCT_NOT_FOUND,
654                               row.STATUS_COST_NOT_FOUND):
655            return 'warning'
656        if row.status_code in (row.STATUS_INCOMPLETE,
657                               row.STATUS_CASE_QUANTITY_DIFFERS,
658                               row.STATUS_ORDERED_RECEIVED_DIFFER,
659                               row.STATUS_TRUCKDUMP_UNCLAIMED,
660                               row.STATUS_TRUCKDUMP_PARTCLAIMED,
661                               row.STATUS_OUT_OF_STOCK):
662            return 'notice'
663
664    def configure_row_form(self, f):
665        super(PurchasingBatchView, self).configure_row_form(f)
666        row = f.model_instance
667        if self.creating:
668            batch = self.get_instance()
669        else:
670            batch = self.get_parent(row)
671
672        # readonly fields
673        f.set_readonly('case_quantity')
674
675        # quantity fields
676        f.set_type('case_quantity', 'quantity')
677        f.set_type('cases_ordered', 'quantity')
678        f.set_type('units_ordered', 'quantity')
679        f.set_type('cases_shipped', 'quantity')
680        f.set_type('units_shipped', 'quantity')
681        f.set_type('cases_received', 'quantity')
682        f.set_type('units_received', 'quantity')
683        f.set_type('cases_damaged', 'quantity')
684        f.set_type('units_damaged', 'quantity')
685        f.set_type('cases_expired', 'quantity')
686        f.set_type('units_expired', 'quantity')
687        f.set_type('cases_mispick', 'quantity')
688        f.set_type('units_mispick', 'quantity')
689
690        # currency fields
691        f.set_type('po_unit_cost', 'currency')
692        f.set_type('po_total', 'currency')
693        f.set_type('invoice_unit_cost', 'currency')
694
695        # upc
696        f.set_type('upc', 'gpc')
697
698        # invoice total
699        f.set_readonly('invoice_total')
700        f.set_type('invoice_total', 'currency')
701        f.set_label('invoice_total', "Invoice Total (Orig.)")
702
703        # invoice total_calculated
704        f.set_readonly('invoice_total_calculated')
705        f.set_type('invoice_total_calculated', 'currency')
706        f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
707
708        # credits
709        f.set_readonly('credits')
710        f.set_renderer('credits', self.render_row_credits)
711
712        if self.creating:
713            f.remove_fields(
714                'upc',
715                'product',
716                'po_total',
717                'invoice_total',
718            )
719            if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
720                f.remove_fields('cases_received',
721                                'units_received')
722            elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
723                f.remove_fields('cases_ordered',
724                                'units_ordered')
725
726        elif self.editing:
727            f.set_readonly('upc')
728            f.set_readonly('item_id')
729            f.set_readonly('product')
730            f.set_renderer('product', self.render_product)
731
732            # TODO: what's up with this again?
733            # f.remove_fields('po_total',
734            #                 'invoice_total',
735            #                 'status_code')
736
737        elif self.viewing:
738            if row.product:
739                f.remove_fields('brand_name',
740                                'description',
741                                'size')
742                f.set_renderer('product', self.render_product)
743            else:
744                f.remove_field('product')
745
746    def render_row_credits(self, row, field):
747        if not row.credits:
748            return ""
749
750        route_prefix = self.get_route_prefix()
751        columns = [
752            'credit_type',
753            'cases_shorted',
754            'units_shorted',
755            'credit_total',
756        ]
757        g = grids.Grid(
758            key='{}.row_credits'.format(route_prefix),
759            data=row.credits,
760            columns=columns,
761            labels={'credit_type': "Type",
762                    'cases_shorted': "Cases",
763                    'units_shorted': "Units"})
764        g.set_type('cases_shorted', 'quantity')
765        g.set_type('units_shorted', 'quantity')
766        g.set_type('credit_total', 'currency')
767        return HTML.literal(g.render_grid())
768
769    def configure_mobile_row_form(self, f):
770        super(PurchasingBatchView, self).configure_mobile_row_form(f)
771        # row = f.model_instance
772        # if self.creating:
773        #     batch = self.get_instance()
774        # else:
775        #     batch = self.get_parent(row)
776
777        # # readonly fields
778        # f.set_readonly('case_quantity')
779        # f.set_readonly('credits')
780
781        # quantity fields
782        f.set_type('case_quantity', 'quantity')
783        f.set_type('cases_ordered', 'quantity')
784        f.set_type('units_ordered', 'quantity')
785        f.set_type('cases_received', 'quantity')
786        f.set_type('units_received', 'quantity')
787        f.set_type('cases_damaged', 'quantity')
788        f.set_type('units_damaged', 'quantity')
789        f.set_type('cases_expired', 'quantity')
790        f.set_type('units_expired', 'quantity')
791        f.set_type('cases_mispick', 'quantity')
792        f.set_type('units_mispick', 'quantity')
793
794        # currency fields
795        f.set_type('po_unit_cost', 'currency')
796        f.set_type('po_total', 'currency')
797        f.set_type('invoice_unit_cost', 'currency')
798        f.set_type('invoice_total', 'currency')
799
800        # if self.creating:
801        #     f.remove_fields(
802        #         'upc',
803        #         'product',
804        #         'po_total',
805        #         'invoice_total',
806        #     )
807        #     if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
808        #         f.remove_fields('cases_received',
809        #                         'units_received')
810        #     elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
811        #         f.remove_fields('cases_ordered',
812        #                         'units_ordered')
813
814        # elif self.editing:
815        #     f.set_readonly('upc')
816        #     f.set_readonly('product')
817        #     f.remove_fields('po_total',
818        #                     'invoice_total',
819        #                     'status_code')
820
821        # elif self.viewing:
822        #     if row.product:
823        #         f.remove_fields('brand_name',
824        #                         'description',
825        #                         'size')
826        #     else:
827        #         f.remove_field('product')
828
829    def mobile_new_product(self):
830        """
831        View which allows user to create a new Product and add a row for it to
832        the Purchasing Batch.
833        """
834        batch = self.get_instance()
835        batch_url = self.get_action_url('view', batch, mobile=True)
836        form = forms.Form(schema=self.make_new_product_schema(),
837                          request=self.request,
838                          mobile=True,
839                          cancel_url=batch_url)
840
841        if form.validate(newstyle=True):
842            product = model.Product()
843            product.item_id = form.validated['item_id']
844            product.description = form.validated['description']
845            row = self.model_row_class()
846            row.product = product
847            self.handler.add_row(batch, row)
848            self.Session.flush()
849            return self.redirect(self.get_row_action_url('edit', row, mobile=True))
850
851        return self.render_to_response('new_product', {
852            'form': form,
853            'dform': form.make_deform_form(),
854            'instance_title': self.get_instance_title(batch),
855            'instance_url': batch_url,
856        }, mobile=True)
857
858    def make_new_product_schema(self):
859        """
860        Must return a ``colander.Schema`` instance for use with the form in the
861        :meth:`mobile_new_product()` view.
862        """
863        return NewProduct()
864
865#     def item_lookup(self, value, field=None):
866#         """
867#         Try to locate a single product using ``value`` as a lookup code.
868#         """
869#         batch = self.get_instance()
870#         product = api.get_product_by_vendor_code(Session(), value, vendor=batch.vendor)
871#         if product:
872#             return product.uuid
873#         if value.isdigit():
874#             product = api.get_product_by_upc(Session(), GPC(value))
875#             if not product:
876#                 product = api.get_product_by_upc(Session(), GPC(value, calc_check_digit='upc'))
877#             if product:
878#                 if not product.cost_for_vendor(batch.vendor):
879#                     raise fa.ValidationError("Product {} exists but has no cost for vendor {}".format(
880#                         product.upc.pretty(), batch.vendor))
881#                 return product.uuid
882#         raise fa.ValidationError("Product not found")
883
884#     def before_create_row(self, form):
885#         row = form.fieldset.model
886#         batch = self.get_instance()
887#         batch.add_row(row)
888#         # TODO: this seems heavy-handed but works..
889#         row.product_uuid = self.item_lookup(form.fieldset.item_lookup.value)
890
891#     def after_create_row(self, row):
892#         self.handler.refresh_row(row)
893
894    def save_edit_row_form(self, form):
895        row = form.model_instance
896        batch = row.batch
897
898        # first undo any totals previously in effect for the row
899        if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING and row.po_total:
900            batch.po_total -= row.po_total
901        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING and row.invoice_total:
902            batch.invoice_total -= row.invoice_total
903
904        row = super(PurchasingBatchView, self).save_edit_row_form(form)
905        # TODO: is this needed?
906        # self.handler.refresh_row(row)
907        return row
908
909#     def redirect_after_create_row(self, row):
910#         self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product))
911#         return self.redirect(self.request.current_route_url())
912
913    # TODO: seems like this should be master behavior, controlled by setting?
914    def redirect_after_edit_row(self, row, mobile=False):
915        parent = self.get_parent(row)
916        return self.redirect(self.get_action_url('view', parent, mobile=mobile))
917
918#     def get_execute_success_url(self, batch, result, **kwargs):
919#         # if batch execution yielded a Purchase, redirect to it
920#         if isinstance(result, model.Purchase):
921#             return self.request.route_url('purchases.view', uuid=result.uuid)
922
923#         # otherwise just view batch again
924#         return self.get_action_url('view', batch)
925
926
927    @classmethod
928    def _purchasing_defaults(cls, config):
929        route_prefix = cls.get_route_prefix()
930        url_prefix = cls.get_url_prefix()
931        permission_prefix = cls.get_permission_prefix()
932        model_key = cls.get_model_key()
933        model_title = cls.get_model_title()
934
935        # eligible purchases (AJAX)
936        config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix))
937        config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix),
938                        renderer='json', permission='{}.view'.format(permission_prefix))
939
940        # add new product
941        if cls.supports_new_product:
942            config.add_tailbone_permission(permission_prefix, '{}.new_product'.format(permission_prefix),
943                                           "Create new Product when adding row to {}".format(model_title))
944            config.add_route('mobile.{}.new_product'.format(route_prefix), '{}/{{{}}}/new-product'.format(url_prefix, model_key))
945            config.add_view(cls, attr='mobile_new_product', route_name='mobile.{}.new_product'.format(route_prefix),
946                            permission='{}.new_product'.format(permission_prefix))
947
948
949    @classmethod
950    def defaults(cls, config):
951        cls._purchasing_defaults(config)
952        cls._batch_defaults(config)
953        cls._defaults(config)
954
955
956class NewProduct(colander.Schema):
957
958    item_id = colander.SchemaNode(colander.String())
959
960    description = colander.SchemaNode(colander.String())
Note: See TracBrowser for help on using the repository browser.