source: tailbone/tailbone/views/purchasing/batch.py @ 3760c32

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

Improve display of purchase credit data

esp. within a receiving batch row

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