source: rattail/rattail/db/model/batch/purchase.py @ 5e58644

Last change on this file since 5e58644 was 5e58644, checked in by Lance Edgar <lance@…>, 20 months ago

Move the filename_column() function to rattail.db.core module

this seems to be generally useful, might as well standardize some more

  • Property mode set to 100644
File size: 13.1 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_batch_uuid = sa.Column(sa.String(length=32), nullable=True)
110    truck_dump_batch = orm.relationship(
111        'PurchaseBatch',
112        remote_side='PurchaseBatch.uuid',
113        doc="""
114        Reference to the "truck dump" receiving batch, for which the current
115        batch represents a single invoice which partially "consumes" the truck
116        dump.
117        """,
118        backref=orm.backref(
119            'truck_dump_children',
120            order_by='PurchaseBatch.id',
121            doc="""
122            List of batches which are "children" of the current batch, which is
123            assumed to be a truck dump.
124            """))
125
126    def is_truck_dump_parent(self):
127        """
128        Returns boolean indicating whether or not the batch is a "truck dump"
129        parent.
130        """
131        if self.truck_dump:
132            return True
133        return False
134
135    def is_truck_dump_child(self):
136        """
137        Returns boolean indicating whether or not the batch is a "truck dump"
138        child.
139        """
140        if self.truck_dump_batch:
141            return True
142        return False
143
144    def is_truck_dump_related(self):
145        """
146        Returns boolean indicating whether or not the batch is associated with
147        a "truck dump" in any way, i.e. is a parent or child of such.
148        """
149        if self.is_truck_dump_parent():
150            return True
151        if self.is_truck_dump_child():
152            return True
153        return False
154
155
156class PurchaseBatchRow(BatchRowMixin, PurchaseItemBase, Base):
157    """
158    Row of data within a purchase batch.
159    """
160    __tablename__ = 'purchase_batch_row'
161    __batch_class__ = PurchaseBatch
162
163    @declared_attr
164    def __table_args__(cls):
165        return cls.__batchrow_table_args__() + cls.__purchaseitem_table_args__() + (
166            sa.ForeignKeyConstraint(['item_uuid'], ['purchase_item.uuid'], name='purchase_batch_row_fk_item'),
167        )
168
169    STATUS_OK                           = 1
170    STATUS_PRODUCT_NOT_FOUND            = 2
171    STATUS_COST_NOT_FOUND               = 3
172    STATUS_CASE_QUANTITY_UNKNOWN        = 4
173    STATUS_INCOMPLETE                   = 5
174    STATUS_ORDERED_RECEIVED_DIFFER      = 6
175    STATUS_TRUCKDUMP_UNCLAIMED          = 7
176    STATUS_TRUCKDUMP_CLAIMED            = 8
177    STATUS_TRUCKDUMP_OVERCLAIMED        = 9
178    STATUS_CASE_QUANTITY_DIFFERS        = 10
179    STATUS_TRUCKDUMP_PARTCLAIMED        = 11
180    STATUS_OUT_OF_STOCK                 = 12
181
182    STATUS = {
183        STATUS_OK                       : "ok",
184        STATUS_PRODUCT_NOT_FOUND        : "product not found",
185        STATUS_COST_NOT_FOUND           : "product found but not cost",
186        STATUS_CASE_QUANTITY_UNKNOWN    : "case quantity not known",
187        STATUS_CASE_QUANTITY_DIFFERS    : "case quantity differs",
188        STATUS_INCOMPLETE               : "incomplete",
189        STATUS_ORDERED_RECEIVED_DIFFER  : "ordered / received differ",
190        STATUS_TRUCKDUMP_UNCLAIMED      : "not claimed by any child(ren)",
191        STATUS_TRUCKDUMP_PARTCLAIMED    : "partially claimed by child(ren)",
192        STATUS_TRUCKDUMP_CLAIMED        : "fully claimed by child(ren)",
193        STATUS_TRUCKDUMP_OVERCLAIMED    : "OVER claimed by child(ren)",
194        STATUS_OUT_OF_STOCK             : "out of stock",
195    }
196
197    item_entry = sa.Column(sa.String(length=20), nullable=True, doc="""
198    Raw entry value, as obtained from the initial data source, and which is
199    used to locate the product within the system.  This raw value is preserved
200    in case the initial lookup fails and a refresh must attempt further
201    lookup(s) later.  Only used by certain batch handlers in practice.
202    """)
203
204    item_uuid = sa.Column(sa.String(length=32), nullable=True)
205
206    item = orm.relationship(
207        PurchaseItem,
208        doc="""
209        Reference to the purchase item with which the batch row is associated.
210        May be null, e.g. in the case of a "new purchase" batch.
211        """)
212
213
214class PurchaseBatchRowClaim(Base):
215    """
216    Represents the connection between a row(s) from a truck dump batch, and the
217    corresponding "child" batch row which claims it, as well as the claimed
218    quantities etc.
219    """
220    __tablename__ = 'purchase_batch_row_claim'
221    __table_args__ = (
222        sa.ForeignKeyConstraint(['claiming_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claiming_row'),
223        sa.ForeignKeyConstraint(['claimed_row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_row_claim_fk_claimed_row'),
224    )
225
226    uuid = uuid_column()
227
228    claiming_row_uuid = sa.Column(sa.String(length=32), nullable=False)
229    claiming_row = orm.relationship(
230        PurchaseBatchRow,
231        foreign_keys='PurchaseBatchRowClaim.claiming_row_uuid',
232        doc="""
233        Reference to the "child" row which is claiming some row from a truck
234        dump batch.
235        """,
236        backref=orm.backref(
237            'truck_dump_claims',
238            cascade='all, delete-orphan',
239            doc="""
240            List of claims which this "child" row makes against rows within a
241            truck dump batch.
242            """))
243
244    claimed_row_uuid = sa.Column(sa.String(length=32), nullable=False)
245    claimed_row = orm.relationship(
246        PurchaseBatchRow,
247        foreign_keys='PurchaseBatchRowClaim.claimed_row_uuid',
248        doc="""
249        Reference to the truck dump batch row which is claimed by the "child" row.
250        """,
251        backref=orm.backref(
252            'claims',
253            cascade='all, delete-orphan',
254            doc="""
255            List of claims made by "child" rows against this truck dump batch row.
256            """))
257
258    cases_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
259    Number of cases of product which were ultimately received, and are involved in the claim.
260    """)
261
262    units_received = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
263    Number of units of product which were ultimately received, and are involved in the claim.
264    """)
265
266    cases_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
267    Number of cases of product which were shipped damaged, and are involved in the claim.
268    """)
269
270    units_damaged = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
271    Number of units of product which were shipped damaged, and are involved in the claim.
272    """)
273
274    cases_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
275    Number of cases of product which were shipped expired, and are involved in the claim.
276    """)
277
278    units_expired = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
279    Number of units of product which were shipped expired, and are involved in the claim.
280    """)
281
282    # TODO: should have fields for mispick here too, right?
283
284    def is_empty(self):
285        """
286        Returns boolean indicating whether the claim is "empty" - i.e. if it
287        has zero or null for all of its quantity fields.
288        """
289        if self.cases_received:
290            return False
291        if self.units_received:
292            return False
293
294        if self.cases_damaged:
295            return False
296        if self.units_damaged:
297            return False
298
299        if self.cases_expired:
300            return False
301        if self.units_expired:
302            return False
303
304        return True
305
306
307@six.python_2_unicode_compatible
308class PurchaseBatchCredit(PurchaseCreditBase, Base):
309    """
310    Represents a working copy of purchase credit tied to a batch row.
311    """
312    __tablename__ = 'purchase_batch_credit'
313
314    @declared_attr
315    def __table_args__(cls):
316        return cls.__purchasecredit_table_args__() + (
317            sa.ForeignKeyConstraint(['row_uuid'], ['purchase_batch_row.uuid'], name='purchase_batch_credit_fk_row'),
318        )
319
320    uuid = uuid_column()
321
322    row_uuid = sa.Column(sa.String(length=32), nullable=True)
323
324    row = orm.relationship(
325        PurchaseBatchRow,
326        doc="""
327        Reference to the batch row with which the credit is associated.
328        """,
329        backref=orm.backref(
330            'credits',
331            doc="""
332            List of :class:`PurchaseBatchCredit` instances for the row.
333            """))
334
335    def __str__(self):
336        if self.cases_shorted is not None and self.units_shorted is not None:
337            qty = "{} cases, {} units".format(
338                pretty_quantity(self.cases_shorted),
339                pretty_quantity(self.units_shorted))
340        elif self.cases_shorted is not None:
341            qty = "{} cases".format(pretty_quantity(self.cases_shorted))
342        elif self.units_shorted is not None:
343            qty = "{} units".format(pretty_quantity(self.units_shorted))
344        else:
345            qty = "??"
346        qty += " {}".format(self.credit_type)
347        if self.credit_type == 'expired' and self.expiration_date:
348            qty += " ({})".format(self.expiration_date)
349        if self.credit_type == 'mispick' and self.mispick_product:
350            qty += " ({})".format(self.mispick_product)
351        if self.invoice_total:
352            return "{} = ${:0.2f}".format(qty, self.invoice_total)
353        return qty
Note: See TracBrowser for help on using the repository browser.