# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2013-2015 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
"""Work order implementation and utils"""
# pylint: enable=E1101
import logging
from kiwi.currency import currency
from storm.expr import (Count, Join, LeftJoin, Alias, Select, Sum, Coalesce,
In, And, Or, Eq, Not, Cast)
from storm.info import ClassAlias
from storm.references import Reference, ReferenceSet
from zope.interface import implementer
from stoqlib.database.expr import Field, NullIf, Concat
from stoqlib.database.properties import (IntCol, DateTimeCol, UnicodeCol,
PriceCol, DecimalCol, QuantityCol,
IdentifierCol, IdCol, BoolCol, EnumCol)
from stoqlib.database.runtime import get_current_branch, get_current_user
from stoqlib.database.viewable import Viewable
from stoqlib.exceptions import InvalidStatus, NeedReason
from stoqlib.domain.base import Domain
from stoqlib.domain.events import (SaleStatusChangedEvent,
SaleItemBeforeDecreaseStockEvent,
SaleItemBeforeIncreaseStockEvent,
SaleItemAfterSetBatchesEvent)
from stoqlib.domain.interfaces import IDescribable, IContainer
from stoqlib.domain.person import (Branch, Client, Person, SalesPerson,
Company, LoginUser, Employee)
from stoqlib.domain.product import Product, StockTransactionHistory
from stoqlib.domain.sale import Sale
from stoqlib.domain.sellable import Sellable
from stoqlib.lib.dateutils import localnow, localtoday
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
log = logging.getLogger(__name__)
def _validate_package_branch(obj, attr, value):
other_dict = {
'destination_branch_id': obj.source_branch_id,
'source_branch_id': obj.destination_branch_id}
if other_dict[attr] == value:
raise ValueError(
_("The source branch and destination branch can't be equal"))
return value
[docs]class WorkOrderPackageItem(Domain):
"""A |workorderpackage| item
This is a representation of a |workorder| inside a
|workorderpackage|. This is used instead of the work
order directly so we can keep a history of sent and
received packages.
See also:
`schema <http://doc.stoq.com.br/schema/tables/work_order_item.html>`__
"""
__storm_table__ = 'work_order_package_item'
package_id = IdCol(allow_none=False)
#: the |workorderpackage| this item is transported in
package = Reference(package_id, 'WorkOrderPackage.id')
order_id = IdCol(allow_none=False)
#: the |workorder| this item represents
order = Reference(order_id, 'WorkOrder.id')
#
# Public API
#
[docs] def send(self):
"""Send the item to the :attr:`WorkOrderPackage.destination_branch`
This will mark the package as sent. Note that it's only possible
to call this on the same branch as :attr:`.source_branch`.
When calling this, the work orders' :attr:`WorkOrder.current_branch`
will be ``None``, since they are on a package and not on any branch.
"""
if self.package.destination_branch != self.order.branch:
old_execution_branch = self.order.execution_branch
self.order.execution_branch = self.package.destination_branch
WorkOrderHistory.add_entry(
self.store, self.order, _(u"Execution branch"),
old_value=(old_execution_branch and
old_execution_branch.get_description()),
new_value=self.package.destination_branch.get_description())
[docs] def receive(self):
"""Receive this item on the :attr:`WorkOrderPackage.destination_branch`
This will mark the package as received in the branch
to receive it there. Note that it's only possible to call this
on the same branch as :attr:`.destination_branch`.
When calling this, the work orders' :attr:`WorkOrder.current_branch`
will be set to :attr:`WorkOrderPackage.destination_branch`, since
receiving means they got to their destination.
"""
#FIXME: For unknown reason some of W.O is not setted as None, so we
#are disabling this check for now
#assert self.order.current_branch is None
if self.order.current_branch is not None: # pragma nocoverage
log.warning('Work order with wrong current branch %r' % self.order)
# The order is in destination branch now
self.order.current_branch = self.package.destination_branch
WorkOrderHistory.add_entry(
self.store, self.order, _(u"Current branch"),
old_value=_(u"Package %s") % self.package.identifier,
new_value=self.package.destination_branch.get_description())
[docs]class WorkOrderPackage(Domain):
"""A package of |workorders|
This is a package (called 'malote' on Brazil) that will be used to
send workorder(s) to another branch for the task execution.
.. graphviz::
digraph work_order_package_status {
STATUS_OPENED -> STATUS_SENT;
STATUS_SENT -> STATUS_RECEIVED;
}
See also:
`schema <http://doc.stoq.com.br/schema/tables/work_order_package.html>`__
"""
__storm_table__ = 'work_order_package'
#: package is opened, waiting to be sent
STATUS_OPENED = u'opened'
#: package was sent to the :attr:`.destination_branch`
STATUS_SENT = u'sent'
#: package was received by the :attr:`.destination_branch`
STATUS_RECEIVED = u'received'
statuses = {
STATUS_OPENED: _(u'Opened'),
STATUS_SENT: _(u'Sent'),
STATUS_RECEIVED: _(u'Received')}
status = EnumCol(allow_none=False, default=STATUS_OPENED)
# FIXME: Change identifier to another name, to avoid
# confusions with IdentifierCol used elsewhere
#: the packages's identifier
identifier = UnicodeCol()
#: when the package was sent from the :attr:`.source_branch`
send_date = DateTimeCol()
#: when the package was received by the :attr:`.destination_branch`
receive_date = DateTimeCol()
send_responsible_id = IdCol(default=None)
#: the |loginuser| responsible for sending the package
send_responsible = Reference(send_responsible_id, 'LoginUser.id')
receive_responsible_id = IdCol(default=None)
#: the |loginuser| responsible for receiving the package
receive_responsible = Reference(receive_responsible_id, 'LoginUser.id')
destination_branch_id = IdCol(validator=_validate_package_branch)
#: the destination branch, that is, the branch where
#: the package is going to be sent to
destination_branch = Reference(destination_branch_id, 'Branch.id')
source_branch_id = IdCol(allow_none=False,
validator=_validate_package_branch)
#: the source branch, that is, the branch where
#: the package is leaving
source_branch = Reference(source_branch_id, 'Branch.id')
#: the |workorderpackageitems| inside this package
package_items = ReferenceSet('id', 'WorkOrderPackageItem.package_id')
@property
def quantity(self):
"""The quantity of |workorderpackageitems| inside this package"""
return self.package_items.count()
#
# Public API
#
[docs] def add_order(self, workorder, notes=None):
"""Add a |workorder| on this package
Note that this will set the :attr:`WorkOrder.current_branch`
to ``None`` (since it's now on the package).
:param notes: some notes that will be used when adding
an entry on :class:`WorkOrderHistory`
:returns: the created |workorderpackageitem|
"""
if not self.package_items.find(order=workorder).is_empty():
raise ValueError(
_("The order %s is already on the package %s") % (
workorder, self))
if workorder.current_branch != self.source_branch:
raise ValueError(
_("The order %s is not in the source branch") % (
workorder, ))
# The order is going to leave the current_branch
workorder.current_branch = None
WorkOrderHistory.add_entry(
self.store, workorder, _(u"Current branch"),
old_value=self.source_branch.get_description(),
new_value=_(u"Package %s") % self.identifier,
notes=notes)
return WorkOrderPackageItem(store=self.store,
order=workorder, package=self)
[docs] def can_send(self):
"""If we can send this package to the :attr:`.destination_branch`"""
return self.status == self.STATUS_OPENED
[docs] def can_receive(self):
"""If we can receive this package in the :attr:`.destination_branch`"""
return self.status == self.STATUS_SENT
[docs] def send(self):
"""Send the package to the :attr:`.destination_branch`
This will mark the package as sent. Note that it's only possible
to call this on the same branch as :attr:`.source_branch`.
Each :obj:`.package_items` will have it's
:meth:`WorkOrderPackageItem.send` method called
"""
assert self.can_send()
if self.source_branch != get_current_branch(self.store):
fmt = _("This package's source branch is %s and you are in %s. "
"It's not possible to send a package outside the "
"source branch")
raise ValueError(fmt % (self.source_branch,
get_current_branch(self.store)))
package_items = list(self.package_items)
if not len(package_items):
raise ValueError(_("There're no orders to send"))
for package_item in package_items:
package_item.send()
self.send_date = localnow()
self.status = self.STATUS_SENT
[docs] def receive(self):
"""Receive the package on the :attr:`.destination_branch`
This will mark the package as received in the branch
to receive it there. Note that it's only possible to call this
on the same branch as :attr:`.destination_branch`.
Each :obj:`.package_items` will have it's
:meth:`WorkOrderPackageItem.receive` method called
"""
assert self.can_receive()
if self.destination_branch != get_current_branch(self.store):
fmt = _("This package's destination branch is %s and you are in %s. "
"It's not possible to receive a package outside the "
"destination branch")
raise ValueError(fmt % (self.destination_branch,
get_current_branch(self.store)))
for package_item in self.package_items:
package_item.receive()
self.receive_date = localnow()
self.status = self.STATUS_RECEIVED
@implementer(IDescribable)
[docs]class WorkOrderCategory(Domain):
"""A |workorder|'s category
Used to categorize a |workorder|. It can be differentiate
mainly by the :attr:`.name`, but one can use :obj:`color`
in a gui to make the differentiation better.
See also:
`schema <http://doc.stoq.com.br/schema/tables/work_order_category.html>`__
"""
__storm_table__ = 'work_order_category'
#: category's name
name = UnicodeCol()
#: category's color (e.g. #ff0000 for red)
color = UnicodeCol()
#
# IDescribable
#
def get_description(self):
return self.name
[docs]class WorkOrderItem(Domain):
"""A |workorder| item
This is an item in a |workorder|. That is, a |product| or a |service|
(here referenced by their respective |sellable|) used on the work
and that will be after used to compose the |saleitem| of the |sale|.
Note that objects of this type should not be created manually, only by
calling :meth:`WorkOrder.add_sellable`
See also:
`schema <http://doc.stoq.com.br/schema/tables/work_order_item.html>`__
"""
__storm_table__ = 'work_order_item'
#: |sellable|'s quantity used on the |workorder|
quantity = QuantityCol(default=0)
#: the quantity of |sellable| consumed (i.e. decreased from the stock).
#: This needs to be equal to :obj:`.quantity` for the work order
#: to be finished
quantity_decreased = QuantityCol(default=0)
#: price of the |sellable|, this is how much the |client| is going
#: to be charged for the sellable. This includes discounts and markup.
price = PriceCol()
sellable_id = IdCol()
#: the |sellable| of this item, either a |service| or a |product|
sellable = Reference(sellable_id, 'Sellable.id')
batch_id = IdCol()
#: If the sellable is a storable, the |batch| that it was removed from
batch = Reference(batch_id, 'StorableBatch.id')
order_id = IdCol()
#: |workorder| this item belongs
order = Reference(order_id, 'WorkOrder.id')
sale_item_id = IdCol()
#: the corresponding |saleitem| for this item
sale_item = Reference(sale_item_id, 'SaleItem.id')
@property
def total(self):
"""The total value for this item
Note that this is the same as :obj:`.quantity` * :obj:`.price`
"""
return currency(self.price * self.quantity)
#
# Public API
#
[docs] def reserve(self, quantity):
"""Reserve some quantity of this item
Reserving some quantity of items means decreasing them from
the stock. All :obj:`.quantity` needs to be reserved for a
|workorder| to be finished. The already reserved quantity
will be stored at :obj:`.quantity_decreased`
:param quantity: the quantity to consume
:raises: :exc:`ValueError` if the quantity to reserve is
greater than the unreserved quantity
(:obj:`.quantity` - :obj:`.quantity_decreased`
"""
if quantity > self.quantity - self.quantity_decreased:
raise ValueError(
"Trying to reserve more than unreserved quantity")
storable = self.sellable.product_storable
if storable:
storable.decrease_stock(
quantity, self.order.branch,
StockTransactionHistory.TYPE_WORK_ORDER_USED, self.id,
batch=self.batch)
self.quantity_decreased += quantity
if self.sale_item:
# Keep the sale_item in sync, so the stock is not increased twice
self.sale_item.quantity_decreased += quantity
[docs] def return_to_stock(self, quantity):
"""Return some quantity of this item to stock
Returning some quantity of items to the stock means increasing
the stock back.
:param quantity: the quantity to return to the stock
:raises: :exc:`ValueError` if the quantity to return to the stock
greater than the :obj:`.quantity_decreased`
"""
if quantity > self.quantity_decreased:
raise ValueError(
"Trying to return more quantity than reserved")
# TODO: Implement a way to say that this quantity was lost
# (probably by receiving an extra kwarg here). Then we would still
# remove the quantity from quantity_decreased, but not reincrease stock
storable = self.sellable.product_storable
if storable:
storable.increase_stock(
quantity, self.order.branch,
StockTransactionHistory.TYPE_WORK_ORDER_RETURN_TO_STOCK, self.id,
batch=self.batch)
self.quantity_decreased -= quantity
if self.sale_item:
# Keep the sale_item in sync, so the stock is not decreased twice
self.sale_item.quantity_decreased -= quantity
#
# Classmethods
#
@classmethod
[docs] def get_from_sale_item(cls, store, sale_item):
"""Get the |workorderitem| given one |saleitem|
:param store: a store
:param sale_item: a |saleitem|
:returns: The |workorderitem| related to the |saleitem|
:rtype: |workorderitem|
"""
return store.find(cls, cls.sale_item_id == sale_item.id).one()
#
# Events
#
@SaleItemBeforeIncreaseStockEvent.connect
@classmethod
def _on_sale_item_before_increase_stock(cls, sale_item):
self = cls.get_from_sale_item(sale_item.store, sale_item)
if self is None:
return
assert sale_item.quantity == self.quantity
# When a sale item has an corresponding work order item, they need to
# be in sync all the time, but the stock management
# (increasing/decreasing) must be done only once.
sale_item.quantity_decreased = max(sale_item.quantity_decreased,
self.quantity_decreased)
# This is why when a sale item is canceled (ie, the stock is returned),
# we must also inform that the quantity decreased for the work order
# item was also returned.
self.quantity_decreased = 0
@SaleItemBeforeDecreaseStockEvent.connect
@classmethod
def _on_sale_item_before_decrease_stock(cls, sale_item):
self = cls.get_from_sale_item(sale_item.store, sale_item)
if self is None:
return
assert sale_item.quantity == self.quantity
# When a sale item has an corresponding work order item, they need to
# be in sync all the time, but the stock management
# (increasing/decreasing) must be done only once.
sale_item.quantity_decreased = max(sale_item.quantity_decreased,
self.quantity_decreased)
# sale_item will decrease everything that was missing, so there's
# nothing more to decrease here, that's why we are setting
# quantity_decreased = quantity
self.quantity_decreased = self.quantity
@SaleItemAfterSetBatchesEvent.connect
@classmethod
def _on_sale_item_after_set_batches(cls, sale_item, new_sale_items):
self = cls.get_from_sale_item(sale_item.store, sale_item)
if self is None:
return
self.quantity = sale_item.quantity
self.batch = sale_item.batch
for sale_item in new_sale_items:
cls(store=sale_item.store,
quantity=sale_item.quantity,
price=sale_item.price,
batch=sale_item.batch,
order=self.order,
sellable=self.sellable,
sale_item=sale_item)
@implementer(IContainer)
[docs]class WorkOrder(Domain):
"""Represents a work order
Normally, this is a maintenance task, like:
* The |client| reports a defect on an equipment.
* The responsible for doing the quote analyzes the equipment
and detects the real defect.
* The |client| then approves the quote and the work begins.
* After it's finished, a |sale| is created for it, the
|client| pays and gets it's equipment back.
.. graphviz::
digraph work_order_status {
STATUS_OPENED -> STATUS_WORK_WAITING;
STATUS_OPENED -> STATUS_CANCELLED;
STATUS_WORK_WAITING -> STATUS_WORK_IN_PROGRESS;
STATUS_WORK_WAITING -> STATUS_CANCELLED;
STATUS_WORK_IN_PROGRESS -> STATUS_WORK_FINISHED;
STATUS_WORK_IN_PROGRESS -> STATUS_WORK_WAITING;
STATUS_WORK_IN_PROGRESS -> STATUS_CANCELLED;
STATUS_WORK_FINISHED -> STATUS_DELIVERED;
STATUS_WORK_FINISHED -> STATUS_WORK_IN_PROGRESS;
}
See also:
`schema <http://doc.stoq.com.br/schema/tables/work_order.html>`__
"""
__storm_table__ = 'work_order'
#: a request for an order has been created, the order has not yet
#: been approved the |client|
STATUS_OPENED = u'opened'
#: for some reason it was cancelled
STATUS_CANCELLED = u'cancelled'
#: this is the initial status after the order gets approved by the
#: |client| and also a helper state for :attr:`.STATUS_WORK_IN_PROGRESS`
#: since when there, the order can come back here to be explicit that
#: it's waiting (for material, for labor, etc) to continue the work
STATUS_WORK_WAITING = u'waiting'
#: work is currently in progress. Note that if at any time we need
#: to wait for more material to continue the work, the status can
#: go back to :attr:`.STATUS_WORK_WAITING` and then come back
#: here when we have it and the work is going to be continued
STATUS_WORK_IN_PROGRESS = u'in-progress'
#: work has been finished, but no |sale| has been created yet.
#: Work orders with this status will be displayed in the till/pos
#: applications and it's possible to create a |sale| from them.
STATUS_WORK_FINISHED = u'finished'
# FIXME: This is not really delivered, it used to be closed, but
# closed/finished are not ideal. This probably needs to be
# renamed to something else in the future when we have a better name
#: a |sale| has been created, delivery and payment handled there
STATUS_DELIVERED = u'delivered'
statuses = {
STATUS_OPENED: _(u'Opened'),
STATUS_CANCELLED: _(u'Cancelled'),
STATUS_WORK_WAITING: _(u'Waiting'),
STATUS_WORK_IN_PROGRESS: _(u'In progress'),
STATUS_WORK_FINISHED: _(u'Finished'),
STATUS_DELIVERED: _(u'Delivered')}
status = EnumCol(default=STATUS_OPENED)
#: A numeric identifier for this object. This value should be used instead of
#: :obj:`Domain.id` when displaying a numerical representation of this object to
#: the user, in dialogs, lists, reports and such.
identifier = IdentifierCol()
sellable_id = IdCol()
#: a corresponding sellable for this equipament. Can be `None` if it is
#: something that this shop does not sell
sellable = Reference(sellable_id, 'Sellable.id')
#: If a sellable is specified, the number of items of this sellable this
#: workorder is for
quantity = IntCol()
#: description of the specific item brought by the client. This can be used
#: to describe the equipament, if it is not one of the sellables available,
#: or even to describe the serial number of the object.
description = UnicodeCol()
#: defect reported by the |client|
defect_reported = UnicodeCol(default=u'')
#: defect detected by the :obj:`.quote_responsible`
defect_detected = UnicodeCol(default=u'')
#: estimated hours needed to complete the work
estimated_hours = DecimalCol(default=None)
#: estimated cost of the work
estimated_cost = PriceCol(default=None)
#: estimated date the work will start
estimated_start = DateTimeCol(default=None)
#: estimated date the work will finish
estimated_finish = DateTimeCol(default=None)
#: date this work was opened
open_date = DateTimeCol(default_factory=localnow)
#: date this work was approved (set by :obj:`.approve`)
approve_date = DateTimeCol(default=None)
#: date this work was finished (set by :obj:`.finish`)
finish_date = DateTimeCol(default=None)
#: if the order was rejected by the other |branch|, e.g. when one
#: branch sends the order in a |workorderpackage| to another branch
#: for execution and it sends it back because of something
is_rejected = BoolCol(allow_none=False, default=False)
branch_id = IdCol()
#: the |branch| where this order was created and responsible for it
branch = Reference(branch_id, 'Branch.id')
current_branch_id = IdCol()
#: the actual branch where the order is. Can differ from
# :attr:`.branch` if the order was sent in a |workorderpackage|
#: to another |branch| for execution
current_branch = Reference(current_branch_id, 'Branch.id')
execution_branch_id = IdCol()
#: the branch where the work's execution was made. It's automatically
#: set by sending the order on a |workorderpackage|
execution_branch = Reference(execution_branch_id, 'Branch.id')
quote_responsible_id = IdCol(default=None)
#: the |employee| responsible for the :obj:`.defect_detected`
quote_responsible = Reference(quote_responsible_id, 'Employee.id')
execution_responsible_id = IdCol(default=None)
#: the |employee| responsible for the execution of the work
execution_responsible = Reference(execution_responsible_id, 'Employee.id')
client_id = IdCol(default=None)
#: the |client|, owner of the equipment
client = Reference(client_id, 'Client.id')
category_id = IdCol(default=None)
#: the |workordercategory| this work belongs
category = Reference(category_id, 'WorkOrderCategory.id')
sale_id = IdCol(default=None)
#: the |sale| created after this work is finished
sale = Reference(sale_id, 'Sale.id')
order_items = ReferenceSet('id', 'WorkOrderItem.order_id')
history_entries = ReferenceSet('id', 'WorkOrderHistory.work_order_id')
#: Number of supplier order.
supplier_order = UnicodeCol()
@property
def status_str(self):
return self.statuses[self.status]
def __init__(self, *args, **kwargs):
super(WorkOrder, self).__init__(*args, **kwargs)
if self.current_branch is None:
self.current_branch = self.branch
#
# IContainer implementation
#
def add_item(self, item):
assert item.order is None
item.order = self
def get_items(self):
return self.order_items
def remove_item(self, item):
assert item.order is self
if item.quantity_decreased > 0:
item.return_to_stock(item.quantity_decreased)
item.order = None
self.store.maybe_remove(item)
#
# Public API
#
[docs] def get_total_amount(self):
"""Returns the total amount of this work order
This is the same as::
sum(item.total for item in :obj:`.order_items`)
"""
items = self.order_items.find()
return (items.sum(WorkOrderItem.price * WorkOrderItem.quantity) or
currency(0))
[docs] def add_sellable(self, sellable, price=None, quantity=1, batch=None):
"""Adds a sellable to this work order
:param sellable: the |sellable| being added
:param price: the price the sellable will be sold when
finishing this work order
:param quantity: the sellable's quantity
:param batch: the |batch| this sellable comes from, if the sellable is a
storable. Should be ``None`` if it is not a storable or if the storable
does not have batches.
:returns: the created |workorderitem|
"""
# We only allow a batch to not be specified if we already have a sale
# with it (meaning this work order comes from a work order quote)
if not self.sale:
self.validate_batch(batch, sellable=sellable)
if price is None:
price = sellable.base_price
item = WorkOrderItem(store=self.store,
sellable=sellable,
batch=batch,
price=price,
quantity=quantity,
order=self)
return item
[docs] def is_items_totally_reserved(self):
"""Check if this work order item's are fully reserved
For a |workorderitem| to be fully synchronized, it's
:attr:`WorkOrderItem.quantity` should be equal to it's
:attr:`WorkOrderItem.quantity_decreased`
:returns: ``True`` if all is synchronized, ``False`` otherwise
"""
tables = [WorkOrderItem,
Join(Sellable, WorkOrderItem.sellable_id == Sellable.id),
LeftJoin(Product, Product.id == Sellable.id)]
# Only products that manage stock should be checked for quantity_decreased
return self.store.using(*tables).find(
WorkOrderItem,
And(WorkOrderItem.order_id == self.id,
WorkOrderItem.quantity_decreased != WorkOrderItem.quantity,
Eq(Product.manage_stock, True))).is_empty()
[docs] def is_in_transport(self):
"""Checks if this work order is in transport
A work order is in transport if it's :attr:`.current_branch`
is ``None``. The transportation of the work order is done in
a |workorderpackage|
:returns: ``True`` if in transport, ``False`` otherwise
"""
return self.current_branch is None
[docs] def is_approved(self):
"""Checks if this work order is approved
If the order is paused or in progress, it's
considered to be approved (obviously, the same applies to
status after them, like finished and delivered).
:returns: ``True`` if the order is considered as approved,
``False`` otherwise.
"""
return self.status not in [self.STATUS_OPENED,
self.STATUS_CANCELLED]
[docs] def is_finished(self):
"""Checks if this work order is finished
A work order is finished when the work that needs to be done
on it finished, so this will be ``True`` when :obj:`WorkOrder.status` is
:obj:`.STATUS_WORK_FINISHED` and :obj:`.STATUS_DELIVERED`
"""
return self.status in [self.STATUS_WORK_FINISHED, self.STATUS_DELIVERED]
[docs] def is_late(self):
"""Checks if this work order is late
Being late means we set an
:obj:`estimated finish date <.estimated_finish>` and that
date has already passed.
"""
if self.is_finished():
return False
if not self.estimated_finish:
# No estimated_finish means we are not late
return False
today = localtoday().date()
return self.estimated_finish.date() < today
[docs] def can_cancel(self, ignore_sale=False):
"""Checks if this work order can be cancelled
The order can be cancelled at any point, once it's not
finished (this is done by checking :meth:`.is_finished`) or its already
cancelled
If the work order is related to a sale, the user cannot cancel it, and
should cancel the sale instead.
:param ignore_sale: Dont consider the related sale. This should only be
used when the sale is being canceled
:returns: ``True`` if can be cancelled, ``False`` otherwise
"""
if self.status == self.STATUS_CANCELLED:
return False
if not ignore_sale and self.sale_id:
return False
return not self.is_finished()
[docs] def can_approve(self):
"""Checks if this work order can be approved
:returns: ``True`` if can be approved, ``False`` otherwise
"""
return self.status == self.STATUS_OPENED
[docs] def can_pause(self):
"""Checks if we can put the order on "waiting" state
Only orders with work in progress be put in that state.
:returns: ``True`` if can work, ``False`` otherwise
"""
if self.is_rejected or self.is_in_transport():
return False
return self.status == self.STATUS_WORK_IN_PROGRESS
[docs] def can_work(self):
"""Checks if this order's task can be worked
Note that the work needs to be approved before it's task
can be started to be worked.
:returns: ``True`` if can work, ``False`` otherwise
"""
if self.is_rejected or self.is_in_transport():
return False
# FIXME: We should not be calling get_current_branch on domain
if self.current_branch != get_current_branch(self.store):
return False
return self.status == self.STATUS_WORK_WAITING
[docs] def can_edit(self):
"""Check if this work order can be edited
:returns: ``True`` if can edit, ``False`` otherwise
"""
return not self.is_finished()
[docs] def can_finish(self):
"""Checks if this work order can finish
Note that the work needs to be started before you can finish.
:returns: ``True`` if can finish, ``False`` otherwise
"""
if self.is_rejected or self.is_in_transport():
return False
# FIXME: We should not be calling get_current_branch on domain
if self.current_branch != get_current_branch(self.store):
return False
return self.status in [self.STATUS_WORK_IN_PROGRESS,
self.STATUS_WORK_WAITING]
[docs] def can_close(self):
"""Checks if this work order can delivery
Note that the work needs to be finished before you can deliver.
Also, all of it's items need to be already decreased from
the stock, that is, :attr:`WorkOrderItem.quantity` needs to
be equal to :attr:`WorkOrderItem.quantity`.
:returns: ``True`` if can deliver, ``False`` otherwise
"""
# Because the way pre-sales are implemented, if we have a sale,
# we don't need to reserve all quantities here (since the sale will
# decrease the rest for us).
if self.sale is None and not self.is_items_totally_reserved():
return False
if self.is_rejected or self.is_in_transport():
return False
# FIXME: We should not be calling get_current_branch on domain
if self.current_branch != get_current_branch(self.store):
return False
return self.status == self.STATUS_WORK_FINISHED
[docs] def can_reopen(self):
"""Checks if this work order can be re-opened
A finished or delivered order can be reopened.
:returns: ``True`` if it can, ``False`` otherwise
"""
return self.is_finished()
[docs] def can_reject(self):
"""Checks if the :obj:`.is_rejected` flag can be set
:returns: ``True`` if it can, ``False`` otherwise
"""
if self.is_rejected or self.is_in_transport():
return False
return self.status in [self.STATUS_WORK_WAITING,
self.STATUS_WORK_IN_PROGRESS,
self.STATUS_WORK_FINISHED]
[docs] def can_undo_rejection(self):
"""Checks if the :obj:`.is_rejected` flag can be unset
:returns: ``True`` if it can, ``False`` otherwise
"""
return self.is_rejected and not self.is_in_transport()
[docs] def reject(self, reason):
"""Setter for the :obj:`.is_rejected` flag
When setting the is_rejected flag to ``True``,
it should be done here since some additional logic
(e.g. Registering a :class:`WorkOrderHistory`) will be
made together.
:param reason: the explanation to why we are setting this flag
"""
assert self.can_reject()
self.is_rejected = True
WorkOrderHistory.add_entry(
self.store, self, what=_(u"Rejected"),
old_value=_(u"No"), new_value=_(u"Yes"), notes=reason)
[docs] def undo_rejection(self, reason):
"""Unsetter for the :obj:`.is_rejected` flag
When setting the is_rejected flag to ``False``,
it should be done here since some additional logic
(e.g. Registering a :class:`WorkOrderHistory`) will be
made together.
:param reason: an explanation to what was done to make this
order not rejected anymore
"""
assert self.can_undo_rejection()
self.is_rejected = False
WorkOrderHistory.add_entry(
self.store, self, what=_(u"Rejected"),
old_value=_(u"Yes"), new_value=_(u"No"), notes=reason)
[docs] def cancel(self, reason=None, ignore_sale=False):
"""Cancels this work order
Cancel the work order, probably because the |client|
didn't approve it or simply gave up of doing it.
All reserved items (the ones with
:attr:`WorkOrderItem.quantity_decreased` > 0) will be
returned to stock.
:param reason: an explanation to why this order was cancelled
"""
assert self.can_cancel(ignore_sale)
for item in self.order_items:
if item.quantity_decreased > 0:
item.return_to_stock(item.quantity_decreased)
self._change_status(self.STATUS_CANCELLED, notes=reason)
[docs] def approve(self):
"""Approves this work order
Approving means that the |client| has accepted the
work's quote and it's cost and it can now start.
"""
assert self.can_approve()
self.approve_date = localnow()
WorkOrderHistory.add_entry(
self.store, self, what=_(u"Approved"),
old_value=_(u"No"), new_value=_(u"Yes"))
self._change_status(self.STATUS_WORK_WAITING)
[docs] def work(self):
"""Set this orders state as "work in progress"
The :obj:`.execution_responsible` started working on
this order's task and will finish sometime in the future.
Note that if the work has to stop for a while for some reason
(e.g. lack of material, lack of labor, etc), one can call
:meth:`.pause` to set the state properly and then call this
again when the work can continue.
"""
assert self.can_work()
self._change_status(self.STATUS_WORK_IN_PROGRESS)
[docs] def pause(self, reason):
"""Set this orders state as "waiting"
This is used to indicate that the work has stopped for a while
for a reason (e.g. lack of material, lack of labor, etc). When the
work can continue call :meth:`.work`
Note: When comming from :attr:`.STATUS_OPENED`, :meth:`.approve` must
be used instead.
:param reason: the reason explaining why this order was paused
"""
assert self.can_pause()
self._change_status(self.STATUS_WORK_WAITING, notes=reason)
[docs] def finish(self):
"""Finishes this work order's task
The :obj:`.execution_responsible` has finished working on
this order's task. It's possible now to give the equipment
back to the |client| and create a |sale| so we are able
to :meth:`deliver <.deliver>` this order.
"""
assert self.can_finish()
self.finish_date = localnow()
# Make sure we are not overwriting this value, since we can reopen the
# order and finish again
if not self.execution_branch:
branch = get_current_branch(self.store)
self.execution_branch = branch
self._change_status(self.STATUS_WORK_FINISHED)
[docs] def reopen(self, reason):
"""Reopens the work order
This is useful if the order was finished but needs to be reopened
for some reason. The state will be back to
:attr:`.STATUS_WORK_IN_PROGRESS`
:param reason: the reason explaining why this order was reopened
"""
assert self.can_reopen()
self.finish_date = None
self._change_status(self.STATUS_WORK_IN_PROGRESS, notes=reason)
[docs] def close(self):
"""Delivers this work order
This order's task is done, the |client| got the equipment
back and a |sale| was created for the |workorderitems|
Nothing more needs to be done.
"""
assert self.can_close()
self._change_status(self.STATUS_DELIVERED)
[docs] def change_status(self, new_status, reason=None):
"""
Change the status of this work order
Using this function you can change the status is several steps.
:param new_status: the new status
:param reason: a reason for that status change. Only needed
by some changes
:returns: if the status was changed
:raises: :exc:`stoqlib.exceptions.InvalidStatus` if the status cannot be changed
:raises: :exc:`stoqlib.exceptions.NeedReason` if the change
needs a reason to happen
"""
# This is the logic order of status changes, this is the flow/ordering
# of the status that should be used
status_order = [WorkOrder.STATUS_OPENED,
WorkOrder.STATUS_WORK_WAITING,
WorkOrder.STATUS_WORK_IN_PROGRESS,
WorkOrder.STATUS_WORK_FINISHED]
old_index = status_order.index(self.status)
new_index = status_order.index(new_status)
direction = cmp(new_index, old_index)
next_status = self.status
while True:
# Calculate what's the next status we should set in order to reach
# our goal (new_status). Note that this can go either forward or backward
# depending on the direction
next_status = status_order[status_order.index(next_status) + direction]
if next_status == WorkOrder.STATUS_WORK_IN_PROGRESS:
if self.can_reopen():
if reason is not None:
self.reopen(reason=reason)
else:
raise NeedReason(_("A reason is needed to reopen "
"the work order"))
elif self.can_work():
self.work()
else:
raise InvalidStatus(
_("This work order cannot be worked on"))
if next_status == WorkOrder.STATUS_WORK_FINISHED:
if not self.can_finish():
raise InvalidStatus(
_('This work order cannot be finished'))
self.finish()
if next_status == WorkOrder.STATUS_WORK_WAITING:
if self.can_approve():
self.approve()
elif self.can_pause():
if reason is not None:
self.pause(reason=reason)
else:
raise NeedReason(_("A reason is needed to pause "
"the work order"))
else:
raise InvalidStatus(
_("This work order cannot wait for material"))
if next_status == WorkOrder.STATUS_OPENED:
raise InvalidStatus(_("This work order cannot be re-opened"))
# We've reached our goal, bail out
if next_status == new_status:
break
#
# Private
#
def _change_status(self, new_status, notes=None):
old_status = self.status
self.status = new_status
WorkOrderHistory.add_entry(self.store, self, what=_(u"Status"),
old_value=self.statuses[old_status],
new_value=self.statuses[new_status],
notes=notes)
#
# Classmethods
#
@classmethod
[docs] def find_by_sale(cls, store, sale):
"""Returns all |workorders| associated with the given |sale|.
:param sale: The |sale| used to filter the existing |workorders|
:resturn: An iterable with all work orders:
:rtype: resultset
"""
return store.find(cls, sale=sale)
#
# Events
#
@SaleStatusChangedEvent.connect
@classmethod
def _on_sale_status_changed(cls, sale, old_status):
if sale.status == Sale.STATUS_CANCELLED:
for self in cls.find_by_sale(sale.store, sale):
#FIXME: this is sort of hack, currently can not cancel a
# finished work order. Maybe we should allow it.
if self.is_finished():
self.reopen(reason=_(u"Reopening work order to "
"cancel the sale"))
self.cancel(reason=_(u"The sale was cancelled"),
ignore_sale=True)
# TODO: Maybe this can be moved to a generic 'DomainHistory' (or something
# like that) so that we can have the same api for logging domain activities
[docs]class WorkOrderHistory(Domain):
"""Holds information about changes for |workorders|
Every time something happens to a |workorder|, it should be logged
here, e.g. When it is opened, when it is approved, when it sent
in a |workorderpackage| to another branch, etc.
"""
__storm_table__ = 'work_order_history'
#: the date and time that this event happened
date = DateTimeCol(default_factory=localnow)
#: the "what has changed". e.g. "Status", "Current branch"
what = UnicodeCol(allow_none=False)
#: the old value for the :attr:`.what`
old_value = UnicodeCol()
#: the new value for the :attr:`.what`
new_value = UnicodeCol()
#: some notes about the change. Usually used for a more detailed
#: explanation about the :attr:`.what`
notes = UnicodeCol()
user_id = IdCol(allow_none=False)
#: the |loginuser| that made this change
user = Reference(user_id, 'LoginUser.id')
work_order_id = IdCol(allow_none=False)
#: the |workorder| where this change happened
work_order = Reference(work_order_id, 'WorkOrder.id')
#
# Classmethods
#
@classmethod
[docs] def add_entry(cls, store, workorder, what,
old_value=None, new_value=None, notes=None):
"""Add an entry to the history
:param store: a store
:param workorder: the |workorder| where this change happened
:param what: the description of what has changed. See
:attr:`.what` for more information
:param old_value: the *what's* old value. See
:attr:`.old_value` for more information
:param new_value: the *what's* new value. See
:attr:`.new_value` for more information
:returns: the newly created :class:`WorkOrderHistory`
"""
user = get_current_user(store)
return cls(store=store, work_order=workorder, user=user, what=what,
old_value=old_value, new_value=new_value, notes=notes)
_WorkOrderItemsSummary = Alias(Select(
columns=[
WorkOrderItem.order_id,
Alias(Sum(WorkOrderItem.quantity), 'quantity'),
Alias(Sum(WorkOrderItem.quantity * WorkOrderItem.price), 'total')],
tables=[WorkOrderItem],
group_by=[WorkOrderItem.order_id]),
'_work_order_items')
[docs]class WorkOrderView(Viewable):
"""A view for |workorders|
This is used to get the most information of a |workorder|
without doing lots of database queries.
"""
# TODO: Maybe we should have a cache for branches, to avoid all this
# joins just to get the company name.
_BranchOriginalBranch = ClassAlias(Branch, "branch_original_branch")
_BranchCurrentBranch = ClassAlias(Branch, "branch_current_branch")
_BranchExecutionBranch = ClassAlias(Branch, "branch_execution_branch")
_PersonOriginalBranch = ClassAlias(Person, "person_original_branch")
_PersonCurrentBranch = ClassAlias(Person, "person_current_branch")
_PersonExecutionBranch = ClassAlias(Person, "person_execution_branch")
_CompanyOriginalBranch = ClassAlias(Company, "company_original_branch")
_CompanyCurrentBranch = ClassAlias(Company, "company_current_branch")
_CompanyExecutionBranch = ClassAlias(Company, "company_execution_branch")
_PersonClient = ClassAlias(Person, "person_client")
_PersonSalesPerson = ClassAlias(Person, "person_salesperson")
_PersonEmployee = ClassAlias(Person, "person_employee")
#: the |workorder| object
work_order = WorkOrder
#: the |workordercategory| object
category = WorkOrderCategory
#: the |client| object
client = Client
#: the |sale| associated with this workorder
sale = Sale
# WorkOrder
id = WorkOrder.id
identifier = WorkOrder.identifier
identifier_str = Cast(WorkOrder.identifier, 'text')
status = WorkOrder.status
description = WorkOrder.description
open_date = WorkOrder.open_date
approve_date = WorkOrder.approve_date
estimated_start = WorkOrder.estimated_start
estimated_finish = WorkOrder.estimated_finish
finish_date = WorkOrder.finish_date
is_rejected = WorkOrder.is_rejected
equipment = Coalesce(Concat(Sellable.description, u" - ", WorkOrder.description),
WorkOrder.description)
supplier_order = WorkOrder.supplier_order
# WorkOrderCategory
category_id = WorkOrderCategory.id
category_name = WorkOrderCategory.name
category_color = WorkOrderCategory.color
# Client
client_name = _PersonClient.name
# SalesPerson
salesperson_name = _PersonSalesPerson.name
# Employee
employee_name = _PersonEmployee.name
# Branch
branch_id = WorkOrder.branch_id
branch_name = Coalesce(NullIf(_CompanyOriginalBranch.fancy_name, u''),
_PersonOriginalBranch.name)
current_branch_name = Coalesce(NullIf(_CompanyCurrentBranch.fancy_name, u''),
_PersonCurrentBranch.name)
execution_branch_name = Coalesce(NullIf(_CompanyExecutionBranch.fancy_name, u''),
_PersonExecutionBranch.name)
# Sale
sale_id = Sale.id
sale_identifier = Sale.identifier
sale_identifier_str = Cast(Sale.identifier, 'text')
# Sellable
sellable = Sellable.description
# WorkOrderItem
quantity = Coalesce(Field('_work_order_items', 'quantity'), 0)
total = Coalesce(Field('_work_order_items', 'total'), 0)
tables = [
WorkOrder,
LeftJoin(Client, WorkOrder.client_id == Client.id),
LeftJoin(_PersonClient, Client.person_id == _PersonClient.id),
LeftJoin(Sale, WorkOrder.sale_id == Sale.id),
LeftJoin(SalesPerson, Sale.salesperson_id == SalesPerson.id),
LeftJoin(_PersonSalesPerson,
SalesPerson.person_id == _PersonSalesPerson.id),
LeftJoin(Employee, WorkOrder.quote_responsible_id == Employee.id),
LeftJoin(_PersonEmployee,
Employee.person_id == _PersonEmployee.id),
LeftJoin(Sellable, WorkOrder.sellable_id == Sellable.id),
LeftJoin(_BranchOriginalBranch,
WorkOrder.branch_id == _BranchOriginalBranch.id),
LeftJoin(_PersonOriginalBranch,
_BranchOriginalBranch.person_id == _PersonOriginalBranch.id),
LeftJoin(_CompanyOriginalBranch,
_CompanyOriginalBranch.person_id == _PersonOriginalBranch.id),
LeftJoin(_BranchCurrentBranch,
WorkOrder.current_branch_id == _BranchCurrentBranch.id),
LeftJoin(_PersonCurrentBranch,
_BranchCurrentBranch.person_id == _PersonCurrentBranch.id),
LeftJoin(_CompanyCurrentBranch,
_CompanyCurrentBranch.person_id == _PersonCurrentBranch.id),
LeftJoin(_BranchExecutionBranch,
WorkOrder.execution_branch_id == _BranchExecutionBranch.id),
LeftJoin(_PersonExecutionBranch,
_BranchExecutionBranch.person_id == _PersonExecutionBranch.id),
LeftJoin(_CompanyExecutionBranch,
_CompanyExecutionBranch.person_id == _PersonExecutionBranch.id),
LeftJoin(WorkOrderCategory,
WorkOrder.category_id == WorkOrderCategory.id),
LeftJoin(_WorkOrderItemsSummary,
Field('_work_order_items', 'order_id') == WorkOrder.id),
]
@property
def branch(self):
return self.store.get(Branch, self.branch_id)
@property
def status_str(self):
return self.work_order.status_str
@classmethod
def post_search_callback(cls, sresults):
select = sresults.get_select_expr(Count(1), Sum(cls.total))
return ('count', 'sum'), select
@classmethod
def find_by_current_branch(cls, store, branch):
return store.find(cls, WorkOrder.current_branch_id == branch.id)
@classmethod
def find_by_can_send_to_branch(cls, store, current_branch,
destination_branch):
if destination_branch.can_execute_foreign_work_orders:
# When the destination can execute foreign work orders, we can send
# orders that are originally from it, waiting and any rejected
query = Or(cls.branch_id == destination_branch.id,
cls.status == WorkOrder.STATUS_WORK_WAITING,
Eq(cls.is_rejected, True))
else:
# When the destination branch can't execute foreign work orders,
# it just can receive it's own orders back, and those orders needs
# to be finished or rejected
query = And(cls.branch_id == destination_branch.id,
Or(cls.status == WorkOrder.STATUS_WORK_FINISHED,
Eq(cls.is_rejected, True)))
results = cls.find_by_current_branch(store, current_branch)
return results.find(query)
@classmethod
[docs] def find_pending(cls, store, start_date=None, end_date=None):
"""Find results for this view that are pending (not delivered yet)
:param store: the store that will be used to find the results
:param start_date: if not ``None``, the results will be filtered
to show only the ones with :attr:`.estimated_finish` greater
than it
:param end_date: if not ``None``, the results will be filtered
to show only the ones with :attr:`.estimated_finish` lesser
than it
:returns: the matching views
:rtype: a sequence of :class:`WorkOrderWithPackageView`
"""
query = Not(In(WorkOrder.status,
[WorkOrder.STATUS_DELIVERED,
WorkOrder.STATUS_CANCELLED]))
if start_date:
query = And(query, WorkOrder.estimated_finish >= start_date)
if end_date:
query = And(query, WorkOrder.estimated_finish <= end_date)
return store.find(cls, query)
[docs]class WorkOrderWithPackageView(WorkOrderView):
"""A view for |workorders| in a |workorderpackage|
This is the same as :class:`.WorkOrderView`, but package
information is joined together
"""
_BranchSource = ClassAlias(Branch, "branch_source")
_BranchDestination = ClassAlias(Branch, "branch_destination")
_PersonSource = ClassAlias(Person, "person_source")
_PersonDestination = ClassAlias(Person, "person_destination")
_CompanySource = ClassAlias(Company, "company_source")
_CompanyDestination = ClassAlias(Company, "company_destination")
# WorkOrderPackage
package_id = WorkOrderPackage.id
package_identifier = WorkOrderPackage.identifier
package_send_date = WorkOrderPackage.send_date
package_receive_date = WorkOrderPackage.receive_date
# WorkOrderPackageItem
package_item_id = WorkOrderPackageItem.id
# Branch
source_branch_name = Coalesce(_CompanySource.fancy_name,
_PersonSource.name)
destination_branch_name = Coalesce(_CompanyDestination.fancy_name,
_PersonDestination.name)
tables = WorkOrderView.tables[:]
tables.extend([
LeftJoin(WorkOrderPackageItem,
WorkOrderPackageItem.order_id == WorkOrder.id),
LeftJoin(WorkOrderPackage,
WorkOrderPackageItem.package_id == WorkOrderPackage.id),
LeftJoin(_BranchSource,
WorkOrderPackage.source_branch_id == _BranchSource.id),
LeftJoin(_PersonSource,
_BranchSource.person_id == _PersonSource.id),
LeftJoin(_CompanySource,
_CompanySource.person_id == _PersonSource.id),
LeftJoin(_BranchDestination,
WorkOrderPackage.destination_branch_id == _BranchDestination.id),
LeftJoin(_PersonDestination,
_BranchDestination.person_id == _PersonDestination.id),
LeftJoin(_CompanyDestination,
_CompanyDestination.person_id == _PersonDestination.id),
])
@classmethod
[docs] def find_by_package(cls, store, package):
"""Find results for this view that are in the *package*
:param store: the store that will be used to find the
results
:param package: the |workorderpackage| used to filter
the results
:returns: the matching views
:rtype: a sequence of :class:`WorkOrderWithPackageView`
"""
return store.find(cls, package_id=package.id)
[docs]class WorkOrderApprovedAndFinishedView(WorkOrderView):
"""A view for approved and finished |workorders|
This is the same as :class:`.WorkOrderView`, but only
approved and finished orders are showed here.
"""
clause = In(WorkOrder.status, [WorkOrder.STATUS_WORK_WAITING,
WorkOrder.STATUS_WORK_FINISHED])
[docs]class WorkOrderFinishedView(WorkOrderView):
"""A view for finished |workorders| that still dont have a |sale|
This viewable should be used only to find what workorders still dont have a
sale and can be delivered (ie, they can have the sale created).
This is the same as :class:`.WorkOrderView`, but only finished
orders are showed here.
"""
clause = And(WorkOrder.status == WorkOrder.STATUS_WORK_FINISHED,
Eq(Sale.id, None))
_WorkOrderPackageItemsSummary = Alias(Select(
columns=[
WorkOrderPackageItem.package_id,
Alias(Count(WorkOrderPackageItem.id), 'quantity')],
tables=[WorkOrderPackageItem],
group_by=[WorkOrderPackageItem.package_id]),
'_package_items')
[docs]class WorkOrderPackageView(Viewable):
"""A view for |workorderpackages|
This is used to get the most information of a |workorderpackage|
without doing lots of database queries.
"""
_BranchSource = ClassAlias(Branch, "branch_source")
_BranchDestination = ClassAlias(Branch, "branch_destination")
_PersonSource = ClassAlias(Person, "person_source")
_PersonDestination = ClassAlias(Person, "person_destination")
_CompanySource = ClassAlias(Company, "company_source")
_CompanyDestination = ClassAlias(Company, "company_destination")
#: the |workorderpackage| object
package = WorkOrderPackage
# WorkOrderPackage
id = WorkOrderPackage.id
identifier = WorkOrderPackage.identifier
send_date = WorkOrderPackage.send_date
receive_date = WorkOrderPackage.receive_date
# Branch
source_branch_name = Coalesce(NullIf(_CompanySource.fancy_name, u''),
_PersonSource.name)
destination_branch_name = Coalesce(NullIf(_CompanyDestination.fancy_name, u''),
_PersonDestination.name)
# WorkOrder
quantity = Coalesce(Field('_package_items', 'quantity'), 0)
tables = [
WorkOrderPackage,
LeftJoin(_BranchSource,
WorkOrderPackage.source_branch_id == _BranchSource.id),
LeftJoin(_PersonSource,
_BranchSource.person_id == _PersonSource.id),
LeftJoin(_CompanySource,
_CompanySource.person_id == _PersonSource.id),
LeftJoin(_BranchDestination,
WorkOrderPackage.destination_branch_id == _BranchDestination.id),
LeftJoin(_PersonDestination,
_BranchDestination.person_id == _PersonDestination.id),
LeftJoin(_CompanyDestination,
_CompanyDestination.person_id == _PersonDestination.id),
LeftJoin(_WorkOrderPackageItemsSummary,
Field('_package_items', 'package_id') == WorkOrderPackage.id),
]
@classmethod
def find_by_destination_branch(cls, store, branch):
return store.find(cls,
WorkOrderPackage.destination_branch_id == branch.id)
[docs]class WorkOrderPackageSentView(WorkOrderPackageView):
"""A view for sent |workorderpackages|
This is the same as :class:`.WorkOrderPackageView`, but only
sent orders are showed here.
"""
clause = WorkOrderPackage.status == WorkOrderPackage.STATUS_SENT
[docs]class WorkOrderHistoryView(Viewable):
"""A view for :class:`WorkOrderHistoryView`"""
#: the :class:`WorkOrderHistory` object
history = WorkOrderHistory
# WorkOrderHistory
id = WorkOrderHistory.id
date = WorkOrderHistory.date
what = WorkOrderHistory.what
old_value = WorkOrderHistory.old_value
new_value = WorkOrderHistory.new_value
notes = WorkOrderHistory.notes
# LoginUser
user_name = Person.name
tables = [
WorkOrderHistory,
# LoginUser
Join(LoginUser, WorkOrderHistory.user_id == LoginUser.id),
Join(Person, LoginUser.person_id == Person.id),
]
#
# Classmethods
#
@classmethod
[docs] def find_by_work_order(cls, store, workorder):
"""Find results for this view that references *workorder*
:param store: the store that will be used to find the results
:param package: the |workorder| used to filter the results
:returns: the matching views
:rtype: a sequence of :class:`WorkOrderHistoryView`
"""
return store.find(cls, WorkOrderHistory.work_order_id == workorder.id)