source: rattail/rattail/db/model/batch/purchase.py @ a60b4f3

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

Add "truck dump status" fields for purchase batch, row

this is a separate concern really, from common "status code" fields. although
we can keep using existing enum values

  • Property mode set to 100644
File size: 14.4 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2018 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"""
24Models for purchase order batches
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import six
30import sqlalchemy as sa
31from sqlalchemy import orm
32from sqlalchemy.ext.declarative import declared_attr
33
34from rattail.db.model import (Base, BatchMixin, BatchRowMixin,
35                              PurchaseBase, PurchaseItemBase, PurchaseCreditBase,
36                              Purchase, PurchaseItem)
37from rattail.db.core import uuid_column, filename_column
38from rattail.util import pretty_quantity
39
40
41class PurchaseBatch(BatchMixin, PurchaseBase, Base):
42    """
43    Hopefully generic batch used for entering new purchases into the system, etc.?
44    """
45    batch_key = 'purchase'
46    __tablename__ = 'purchase_batch'
47    __batchrow_class__ = 'PurchaseBatchRow'
48    model_title = "Purchasing Batch"
49    model_title_plural = "Purchasing Batches"
50
51    @declared_attr
52    def __table_args__(cls):
53        return cls.__batch_table_args__() + cls.__purchase_table_args__() + (
54            sa.ForeignKeyConstraint(['purchase_uuid'], ['purchase.uuid'], name='purchase_batch_fk_purchase'),
55            sa.ForeignKeyConstraint(['truck_dump_batch_uuid'], ['purchase_batch.uuid'], name='purchase_batch_fk_truck_dump_batch', use_alter=True),
56        )
57
58    STATUS_OK                   = 1
59    STATUS_UNKNOWN_PRODUCT      = 2
60    STATUS_TRUCKDUMP_UNCLAIMED  = 3
61    STATUS_TRUCKDUMP_CLAIMED    = 4
62    STATUS_UNKNOWN_COSTS        = 5
63
64    STATUS = {
65        STATUS_OK                       : "ok",
66        STATUS_UNKNOWN_PRODUCT          : "has unknown product(s)",
67        STATUS_UNKNOWN_COSTS            : "has unknown product cost(s)",
68        STATUS_TRUCKDUMP_UNCLAIMED      : "not yet fully claimed",
69        STATUS_TRUCKDUMP_CLAIMED        : "fully claimed by child(ren)",
70    }
71
72    purchase_uuid = sa.Column(sa.String(length=32), nullable=True)
73
74    purchase = orm.relationship(
75        Purchase,
76        doc="""
77        Reference to the purchase with which the batch is associated.  May be
78        null, e.g. in the case of a "new purchase" batch.
79        """,
80        backref=orm.backref(
81            'batches',
82            order_by='PurchaseBatch.id',
83            doc="""
84            List of batches associated with the purchase.
85            """))
86
87    mode = sa.Column(sa.Integer(), nullable=False, doc="""
88    Numeric "mode" for the purchase batch, to indicate new/receiving etc.
89    """)
90
91    invoice_file = filename_column(doc="Base name for the associated invoice file, if any.")
92
93    invoice_parser_key = sa.Column(sa.String(length=100), nullable=True, doc="""
94    The key of the parser used to read the contents of the invoice file.
95    """)
96
97    order_quantities_known = sa.Column(sa.Boolean(), nullable=True, doc="""
98    Flag indicating whether the order quantities were known at time of batch
99    creation / population.  Really this is only used for batches of 'receiving'
100    mode, to present a slightly different UI if order quantities were (not) known.
101    """)
102
103    truck_dump = sa.Column(sa.Boolean(), nullable=True, default=False, doc="""
104    Flag indicating whether a "receiving" batch is of the "truck dump"
105    persuasion, i.e.  it does not correspond to a single purchase order but
106    rather is assumed to represent multiple orders.
107    """)
108
109    truck_dump_children_first = sa.Column(sa.Boolean(), nullable=True, default=False, doc="""
110    If batch is a "truck dump parent", this flag indicates whether its
111    "children" are to be attached *first* as opposed to *last*.  If the flag is
112    true, all children must be attached prior to the receiving process.  If
113    flag is false, receiving must happen first, and then all children attached
114    at the end.
115    """)
116
117    truck_dump_ready = sa.Column(sa.Boolean(), nullable=True, default=False, doc="""
118    If batch is a "truck dump parent", this flag indicates whether it is
119    "ready" for the actual receiving process.  If children are to be attached
120    first, this flag should not be set until all children are attached.  If
121    children are last, this flag should be set immediately upon batch creation.
122    """)
123
124    truck_dump_status = sa.Column(sa.Integer(), nullable=True, doc="""
125    Truck dump status code for the row.  Only relevant for truck dump parent
126    batches.  Indicates whether this parent has been fully claimed by children
127    yet, etc.
128    """)
129
130    truck_dump_batch_uuid = sa.Column(sa.String(length=32), nullable=True)
131    truck_dump_batch = orm.relationship(
132        'PurchaseBatch',
133        remote_side='PurchaseBatch.uuid',
134        doc="""
135        Reference to the "truck dump" receiving batch, for which the current
136        batch represents a single invoice which partially "consumes" the truck
137        dump.
138        """,
139        backref=orm.backref(
140            'truck_dump_children',
141            order_by='PurchaseBatch.id',
142            doc="""
143            List of batches which are "children" of the current batch, which is
144            assumed to be a truck dump.
145            """))
146
147    def is_truck_dump_parent(self):
148        """
149        Returns boolean indicating whether or not the batch is a "truck dump"
150        parent.
151        """
152        if self.truck_dump:
153            return True
154        return False
155
156    def is_truck_dump_child(self):
157        """
158        Returns boolean indicating whether or not the batch is a "truck dump"
159        child.
160        """
161        if self.truck_dump_batch:
162            return True
163        return False
164
165    def is_truck_dump_related(self):
166        """
167        Returns boolean indicating whether or not the batch is associated with
168        a "truck dump" in any way, i.e. is a parent or child of such.
169        """
170        if self.is_truck_dump_parent():
171            return True
172        if self.is_truck_dump_child():
173            return True
174        return False
175
176
177class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
178    """
179    Row of data within a purchase batch.
180    """
181    __tablename__ = 'purchase_batch_row'
182    __batch_class__ = PurchaseBatch
183
184    @declared_attr
185    def __table_args__(cls):
186        return cls.__batchrow_table_args__() + cls.__purchaseitem_table_args__() + (
187            sa.ForeignKeyConstraint(['item_uuid'], ['purchase_item.uuid'], name='purchase_batch_row_fk_item'),
188        )
189
190    STATUS_OK                           = 1
191    STATUS_PRODUCT_NOT_FOUND            = 2
192    STATUS_COST_NOT_FOUND               = 3
193    STATUS_CASE_QUANTITY_UNKNOWN        = 4
194    STATUS_INCOMPLETE                   = 5
195    STATUS_ORDERED_RECEIVED_DIFFER      = 6
196    STATUS_TRUCKDUMP_UNCLAIMED          = 7
197    STATUS_TRUCKDUMP_CLAIMED            = 8
198    STATUS_TRUCKDUMP_OVERCLAIMED        = 9
199    STATUS_CASE_QUANTITY_DIFFERS        = 10
200    STATUS_TRUCKDUMP_PARTCLAIMED        = 11
201    STATUS_OUT_OF_STOCK                 = 12
202
203    STATUS = {
204        STATUS_OK                       : "ok",
205        STATUS_PRODUCT_NOT_FOUND        : "product not found",
206        STATUS_COST_NOT_FOUND           : "product found but not cost",
207        STATUS_CASE_QUANTITY_UNKNOWN    : "case quantity not known",
208        STATUS_CASE_QUANTITY_DIFFERS    : "case quantity differs",
209        STATUS_INCOMPLETE               : "incomplete",
210        STATUS_ORDERED_RECEIVED_DIFFER  : "ordered / received differ",
211        STATUS_TRUCKDUMP_UNCLAIMED      : "not claimed by any child(ren)",
212        STATUS_TRUCKDUMP_PARTCLAIMED    : "partially claimed by child(ren)",
213        STATUS_TRUCKDUMP_CLAIMED        : "fully claimed by child(ren)",
214        STATUS_TRUCKDUMP_OVERCLAIMED    : "OVER claimed by child(ren)",
215        STATUS_OUT_OF_STOCK             : "out of stock",
216    }
217
218    item_entry = sa.Column(sa.String(length=20), nullable=True, doc="""
219    Raw entry value, as obtained from the initial data source, and which is
220    used to locate the product within the system.  This raw value is preserved
221    in case the initial lookup fails and a refresh must attempt further
222    lookup(s) later.  Only used by certain batch handlers in practice.
223    """)
224
225    item_uuid = sa.Column(sa.String(length=32), nullable=True)
226
227    item = orm.relationship(
228        PurchaseItem,
229        doc="""
230        Reference to the purchase item with which the batch row is associated.
231        May be null, e.g. in the case of a "new purchase" batch.
232        """)
233
234    truck_dump_status = sa.Column(sa.Integer(), nullable=True, doc="""
235    Truck dump status code for the row.  Only relevant for truck dump parent
236    batches.  Indicates whether this parent row has been fully claimed by
237    children yet, etc.
238    """)
239
240
241class PurchaseBatchRowClaim(Base):
242    """
243    Represents the connection between a row(s) from a truck dump batch, and the
244    corresponding "child" batch row which claims it, as well as the claimed
245    quantities etc.
246    """
247    __tablename__ = 'purchase_batch_row_claim'
248    __table_args__ = (
249        sa.ForeignKeyConstraint(['claiming_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claiming_row'),
250        sa.ForeignKeyConstraint(['claimed_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claimed_row'),
251    )
252
253    uuid = uuid_column()
254
255    claiming_row_uuid = sa.Column(sa.String(length=32), nullable=False)
256    claiming_row = orm.relationship(
257        PurchaseBatchRow,
258        foreign_keys='PurchaseBatchRowClaim.claiming_row_uuid',
259        doc="""
260        Reference to the "child" row which is claiming some row from a truck
261        dump batch.
262        """,
263        backref=orm.backref(
264            'truck_dump_claims',
265            cascade='all, delete-orphan',
266            doc="""
267            List of claims which this "child" row makes against rows within a
268            truck dump batch.
269            """))
270
271    claimed_row_uuid = sa.Column(sa.String(length=32), nullable=False)
272    claimed_row = orm.relationship(
273        PurchaseBatchRow,
274        foreign_keys='PurchaseBatchRowClaim.claimed_row_uuid',
275        doc="""
276        Reference to the truck dump batch row which is claimed by the "child" row.
277        """,
278        backref=orm.backref(
279            'claims',
280            cascade='all, delete-orphan',
281            doc="""
282            List of claims made by "child" rows against this truck dump batch row.
283            """))
284
285    cases_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
286    Number of cases of product which were ultimately received, and are involved in the claim.
287    """)
288
289    units_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
290    Number of units of product which were ultimately received, and are involved in the claim.
291    """)
292
293    cases_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
294    Number of cases of product which were shipped damaged, and are involved in the claim.
295    """)
296
297    units_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
298    Number of units of product which were shipped damaged, and are involved in the claim.
299    """)
300
301    cases_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
302    Number of cases of product which were shipped expired, and are involved in the claim.
303    """)
304
305    units_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
306    Number of units of product which were shipped expired, and are involved in the claim.
307    """)
308
309    # TODO: should have fields for mispick here too, right?
310
311    def is_empty(self):
312        """
313        Returns boolean indicating whether the claim is "empty" - i.e. if it
314        has zero or null for all of its quantity fields.
315        """
316        if self.cases_received:
317            return False
318        if self.units_received:
319            return False
320
321        if self.cases_damaged:
322            return False
323        if self.units_damaged:
324            return False
325
326        if self.cases_expired:
327            return False
328        if self.units_expired:
329            return False
330
331        return True
332
333
334@six.python_2_unicode_compatible
335class PurchaseBatchCredit(PurchaseCreditBase, Base):
336    """
337    Represents a working copy of purchase credit tied to a batch row.
338    """
339    __tablename__ = 'purchase_batch_credit'
340
341    @declared_attr
342    def __table_args__(cls):
343        return cls.__purchasecredit_table_args__() + (
344            sa.ForeignKeyConstraint(['row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_credit_fk_row'),
345        )
346
347    uuid = uuid_column()
348
349    row_uuid = sa.Column(sa.String(length=32), nullable=True)
350
351    row = orm.relationship(
352        PurchaseBatchRow,
353        doc="""
354        Reference to the batch row with which the credit is associated.
355        """,
356        backref=orm.backref(
357            'credits',
358            doc="""
359            List of :class:`PurchaseBatchCredit` instances for the row.
360            """))
361
362    def __str__(self):
363        if self.cases_shorted is not None and self.units_shorted is not None:
364            qty = "{} cases, {} units".format(
365                pretty_quantity(self.cases_shorted),
366                pretty_quantity(self.units_shorted))
367        elif self.cases_shorted is not None:
368            qty = "{} cases".format(pretty_quantity(self.cases_shorted))
369        elif self.units_shorted is not None:
370            qty = "{} units".format(pretty_quantity(self.units_shorted))
371        else:
372            qty = "??"
373        qty += " {}".format(self.credit_type)
374        if self.credit_type == 'expired' and self.expiration_date:
375            qty += " ({})".format(self.expiration_date)
376        if self.credit_type == 'mispick' and self.mispick_product:
377            qty += " ({})".format(self.mispick_product)
378        if self.invoice_total:
379            return "{} = ${:0.2f}".format(qty, self.invoice_total)
380        return qty
Note: See TracBrowser for help on using the repository browser.