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

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

Expose new "calculated" invoice totals for receiving batch, rows

  • Property mode set to 100644
File size: 35.5 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_received', 'quantity')
617        g.set_type('units_received', 'quantity')
618        g.set_type('po_total', 'currency')
619        g.set_type('credits', 'boolean')
620
621        # we only want the grid column to have abbreviated label, but *not* the filter
622        # TODO: would be nice to somehow make this simpler
623        g.set_label('cases_ordered', "Cases Ord.")
624        g.filters['cases_ordered'].label = "Cases Ordered"
625        g.set_label('units_ordered', "Units Ord.")
626        g.filters['units_ordered'].label = "Units Ordered"
627        g.set_label('cases_received', "Cases Rec.")
628        g.filters['cases_received'].label = "Cases Received"
629        g.set_label('units_received', "Units Rec.")
630        g.filters['units_received'].label = "Units Received"
631
632        # invoice_total
633        g.set_type('invoice_total', 'currency')
634        g.set_label('invoice_total', "Total")
635
636        # invoice_total_calculated
637        g.set_type('invoice_total_calculated', 'currency')
638        g.set_label('invoice_total_calculated', "Total")
639
640        g.set_label('po_total', "Total")
641        g.set_label('credits', "Credits?")
642
643    def make_row_grid_tools(self, batch):
644        return self.make_default_row_grid_tools(batch)
645
646    def row_grid_extra_class(self, row, i):
647        if row.status_code in (row.STATUS_PRODUCT_NOT_FOUND,
648                               row.STATUS_COST_NOT_FOUND):
649            return 'warning'
650        if row.status_code in (row.STATUS_INCOMPLETE,
651                               row.STATUS_CASE_QUANTITY_DIFFERS,
652                               row.STATUS_ORDERED_RECEIVED_DIFFER,
653                               row.STATUS_TRUCKDUMP_UNCLAIMED,
654                               row.STATUS_TRUCKDUMP_PARTCLAIMED,
655                               row.STATUS_OUT_OF_STOCK):
656            return 'notice'
657
658    def configure_row_form(self, f):
659        super(PurchasingBatchView, self).configure_row_form(f)
660        row = f.model_instance
661        if self.creating:
662            batch = self.get_instance()
663        else:
664            batch = self.get_parent(row)
665
666        # readonly fields
667        f.set_readonly('case_quantity')
668
669        # quantity fields
670        f.set_type('case_quantity', 'quantity')
671        f.set_type('cases_ordered', 'quantity')
672        f.set_type('units_ordered', 'quantity')
673        f.set_type('cases_received', 'quantity')
674        f.set_type('units_received', 'quantity')
675        f.set_type('cases_damaged', 'quantity')
676        f.set_type('units_damaged', 'quantity')
677        f.set_type('cases_expired', 'quantity')
678        f.set_type('units_expired', 'quantity')
679        f.set_type('cases_mispick', 'quantity')
680        f.set_type('units_mispick', 'quantity')
681
682        # currency fields
683        f.set_type('po_unit_cost', 'currency')
684        f.set_type('po_total', 'currency')
685        f.set_type('invoice_unit_cost', 'currency')
686
687        # upc
688        f.set_type('upc', 'gpc')
689
690        # invoice total
691        f.set_readonly('invoice_total')
692        f.set_type('invoice_total', 'currency')
693        f.set_label('invoice_total', "Invoice Total (Orig.)")
694
695        # invoice total_calculated
696        f.set_readonly('invoice_total_calculated')
697        f.set_type('invoice_total_calculated', 'currency')
698        f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
699
700        # credits
701        f.set_readonly('credits')
702        f.set_renderer('credits', self.render_row_credits)
703
704        if self.creating:
705            f.remove_fields(
706                'upc',
707                'product',
708                'po_total',
709                'invoice_total',
710            )
711            if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
712                f.remove_fields('cases_received',
713                                'units_received')
714            elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
715                f.remove_fields('cases_ordered',
716                                'units_ordered')
717
718        elif self.editing:
719            f.set_readonly('upc')
720            f.set_readonly('item_id')
721            f.set_readonly('product')
722            f.set_renderer('product', self.render_product)
723
724            # TODO: what's up with this again?
725            # f.remove_fields('po_total',
726            #                 'invoice_total',
727            #                 'status_code')
728
729        elif self.viewing:
730            if row.product:
731                f.remove_fields('brand_name',
732                                'description',
733                                'size')
734                f.set_renderer('product', self.render_product)
735            else:
736                f.remove_field('product')
737
738    def render_row_credits(self, row, field):
739        if not row.credits:
740            return ""
741
742        route_prefix = self.get_route_prefix()
743        columns = [
744            'credit_type',
745            'cases_shorted',
746            'units_shorted',
747            'credit_total',
748        ]
749        g = grids.Grid(
750            key='{}.row_credits'.format(route_prefix),
751            data=row.credits,
752            columns=columns,
753            labels={'credit_type': "Type",
754                    'cases_shorted': "Cases",
755                    'units_shorted': "Units"})
756        g.set_type('cases_shorted', 'quantity')
757        g.set_type('units_shorted', 'quantity')
758        g.set_type('credit_total', 'currency')
759        return HTML.literal(g.render_grid())
760
761    def configure_mobile_row_form(self, f):
762        super(PurchasingBatchView, self).configure_mobile_row_form(f)
763        # row = f.model_instance
764        # if self.creating:
765        #     batch = self.get_instance()
766        # else:
767        #     batch = self.get_parent(row)
768
769        # # readonly fields
770        # f.set_readonly('case_quantity')
771        # f.set_readonly('credits')
772
773        # quantity fields
774        f.set_type('case_quantity', 'quantity')
775        f.set_type('cases_ordered', 'quantity')
776        f.set_type('units_ordered', 'quantity')
777        f.set_type('cases_received', 'quantity')
778        f.set_type('units_received', 'quantity')
779        f.set_type('cases_damaged', 'quantity')
780        f.set_type('units_damaged', 'quantity')
781        f.set_type('cases_expired', 'quantity')
782        f.set_type('units_expired', 'quantity')
783        f.set_type('cases_mispick', 'quantity')
784        f.set_type('units_mispick', 'quantity')
785
786        # currency fields
787        f.set_type('po_unit_cost', 'currency')
788        f.set_type('po_total', 'currency')
789        f.set_type('invoice_unit_cost', 'currency')
790        f.set_type('invoice_total', 'currency')
791
792        # if self.creating:
793        #     f.remove_fields(
794        #         'upc',
795        #         'product',
796        #         'po_total',
797        #         'invoice_total',
798        #     )
799        #     if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
800        #         f.remove_fields('cases_received',
801        #                         'units_received')
802        #     elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
803        #         f.remove_fields('cases_ordered',
804        #                         'units_ordered')
805
806        # elif self.editing:
807        #     f.set_readonly('upc')
808        #     f.set_readonly('product')
809        #     f.remove_fields('po_total',
810        #                     'invoice_total',
811        #                     'status_code')
812
813        # elif self.viewing:
814        #     if row.product:
815        #         f.remove_fields('brand_name',
816        #                         'description',
817        #                         'size')
818        #     else:
819        #         f.remove_field('product')
820
821    def mobile_new_product(self):
822        """
823        View which allows user to create a new Product and add a row for it to
824        the Purchasing Batch.
825        """
826        batch = self.get_instance()
827        batch_url = self.get_action_url('view', batch, mobile=True)
828        form = forms.Form(schema=self.make_new_product_schema(),
829                          request=self.request,
830                          mobile=True,
831                          cancel_url=batch_url)
832
833        if form.validate(newstyle=True):
834            product = model.Product()
835            product.item_id = form.validated['item_id']
836            product.description = form.validated['description']
837            row = self.model_row_class()
838            row.product = product
839            self.handler.add_row(batch, row)
840            self.Session.flush()
841            return self.redirect(self.get_row_action_url('edit', row, mobile=True))
842
843        return self.render_to_response('new_product', {
844            'form': form,
845            'dform': form.make_deform_form(),
846            'instance_title': self.get_instance_title(batch),
847            'instance_url': batch_url,
848        }, mobile=True)
849
850    def make_new_product_schema(self):
851        """
852        Must return a ``colander.Schema`` instance for use with the form in the
853        :meth:`mobile_new_product()` view.
854        """
855        return NewProduct()
856
857#     def item_lookup(self, value, field=None):
858#         """
859#         Try to locate a single product using ``value`` as a lookup code.
860#         """
861#         batch = self.get_instance()
862#         product = api.get_product_by_vendor_code(Session(), value, vendor=batch.vendor)
863#         if product:
864#             return product.uuid
865#         if value.isdigit():
866#             product = api.get_product_by_upc(Session(), GPC(value))
867#             if not product:
868#                 product = api.get_product_by_upc(Session(), GPC(value, calc_check_digit='upc'))
869#             if product:
870#                 if not product.cost_for_vendor(batch.vendor):
871#                     raise fa.ValidationError("Product {} exists but has no cost for vendor {}".format(
872#                         product.upc.pretty(), batch.vendor))
873#                 return product.uuid
874#         raise fa.ValidationError("Product not found")
875
876#     def before_create_row(self, form):
877#         row = form.fieldset.model
878#         batch = self.get_instance()
879#         batch.add_row(row)
880#         # TODO: this seems heavy-handed but works..
881#         row.product_uuid = self.item_lookup(form.fieldset.item_lookup.value)
882
883#     def after_create_row(self, row):
884#         self.handler.refresh_row(row)
885
886    def save_edit_row_form(self, form):
887        row = form.model_instance
888        batch = row.batch
889
890        # first undo any totals previously in effect for the row
891        if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING and row.po_total:
892            batch.po_total -= row.po_total
893        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING and row.invoice_total:
894            batch.invoice_total -= row.invoice_total
895
896        row = super(PurchasingBatchView, self).save_edit_row_form(form)
897        # TODO: is this needed?
898        # self.handler.refresh_row(row)
899        return row
900
901#     def redirect_after_create_row(self, row):
902#         self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product))
903#         return self.redirect(self.request.current_route_url())
904
905    # TODO: seems like this should be master behavior, controlled by setting?
906    def redirect_after_edit_row(self, row, mobile=False):
907        parent = self.get_parent(row)
908        return self.redirect(self.get_action_url('view', parent, mobile=mobile))
909
910    def delete_row(self):
911        """
912        Update the batch totals in addition to marking row as removed.
913        """
914        row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid'])
915        if not row:
916            raise self.notfound()
917        batch = row.batch
918        if row.po_total:
919            batch.po_total -= row.po_total
920        if row.invoice_total:
921            batch.invoice_total -= row.invoice_total
922        return super(PurchasingBatchView, self).delete_row()
923
924#     def get_execute_success_url(self, batch, result, **kwargs):
925#         # if batch execution yielded a Purchase, redirect to it
926#         if isinstance(result, model.Purchase):
927#             return self.request.route_url('purchases.view', uuid=result.uuid)
928
929#         # otherwise just view batch again
930#         return self.get_action_url('view', batch)
931
932
933    @classmethod
934    def _purchasing_defaults(cls, config):
935        route_prefix = cls.get_route_prefix()
936        url_prefix = cls.get_url_prefix()
937        permission_prefix = cls.get_permission_prefix()
938        model_key = cls.get_model_key()
939        model_title = cls.get_model_title()
940
941        # eligible purchases (AJAX)
942        config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix))
943        config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix),
944                        renderer='json', permission='{}.view'.format(permission_prefix))
945
946        # add new product
947        if cls.supports_new_product:
948            config.add_tailbone_permission(permission_prefix, '{}.new_product'.format(permission_prefix),
949                                           "Create new Product when adding row to {}".format(model_title))
950            config.add_route('mobile.{}.new_product'.format(route_prefix), '{}/{{{}}}/new-product'.format(url_prefix, model_key))
951            config.add_view(cls, attr='mobile_new_product', route_name='mobile.{}.new_product'.format(route_prefix),
952                            permission='{}.new_product'.format(permission_prefix))
953
954
955    @classmethod
956    def defaults(cls, config):
957        cls._purchasing_defaults(config)
958        cls._batch_defaults(config)
959        cls._defaults(config)
960
961
962class NewProduct(colander.Schema):
963
964    item_id = colander.SchemaNode(colander.String())
965
966    description = colander.SchemaNode(colander.String())
Note: See TracBrowser for help on using the repository browser.