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

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

Introduce some new logic for "children first" truck dump receiving

needs more testing yet to see what's left...

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