Skip to content

Commit

Permalink
acquisition: fix receipt issues
Browse files Browse the repository at this point in the history
* Adds serializer for `AcqReceiptLine` search results.
* Adds a `Document` dumper for acquisition resources.
* Fixes `AcqReceiptLines` JSON schema for editor display.
* Adds `label` optional field for `AcqReceipt` resource.
* Fixes acquisition indexing chains for `AcqReceiptLine` resource.

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed Dec 15, 2021
1 parent c2693fb commit 22af9d7
Show file tree
Hide file tree
Showing 36 changed files with 466 additions and 121 deletions.
22 changes: 21 additions & 1 deletion rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,13 @@ def _(x):
'application/json': (
'rero_ils.modules.serializers:json_v1_search'
),
'application/rero+json': (
'rero_ils.modules.acq_receipt_lines.serializers:json_acrl_search'
)
},
search_serializers_aliases={
'json': 'application/json',
'rero': 'application/rero+json'
},
record_loaders={
'application/json': lambda: AcqReceiptLine(request.get_json()),
Expand Down Expand Up @@ -2107,6 +2114,13 @@ def _(x):
format='yyyy-MM-dd'
)
),
receipt_date=dict(
date_histogram=dict(
field='receipts.receipt_date',
calendar_interval='1d',
format='yyyy-MM-dd'
)
),
account=dict(
terms=dict(
field='order_lines.account.pid',
Expand All @@ -2126,6 +2140,12 @@ def _(x):
format='epoch_millis',
start_date_math='/d',
end_date_math='/d'
),
_('receipt_date'): range_filter(
'receipts.receipt_date',
format='epoch_millis',
start_date_math='/d',
end_date_math='/d'
)
},
),
Expand Down Expand Up @@ -2362,7 +2382,7 @@ def _(x):

# ------ ACQUISITION ORDERS SORT
RECORDS_REST_SORT_OPTIONS['acq_orders']['receipt_date'] = dict(
fields=['-order_lines.receipt_date'], title='Receipt date',
fields=['-receipts.receipt_date'], title='Receipt date',
default_order='desc'
)
RECORDS_REST_DEFAULT_SORT['acq_orders'] = dict(
Expand Down
25 changes: 22 additions & 3 deletions rero_ils/modules/acq_order_lines/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"""API for manipulating Acquisition Order Line."""

from copy import deepcopy
from datetime import datetime
from functools import partial

from flask_babelex import gettext as _
from werkzeug.utils import cached_property

from .extensions import AcqOrderLineValidationExtension
from .models import AcqOrderLineIdentifier, AcqOrderLineMetadata, \
Expand Down Expand Up @@ -178,6 +180,11 @@ def account(self):
"""Shortcut to the account object related to this order line."""
return extracted_data_from_ref(self.get('acq_account'), data='record')

@property
def document_pid(self):
"""Shortcut to the document pid related to this order line."""
return extracted_data_from_ref(self.get('document'))

@property
def document(self):
"""Shortcut to the document object related to this order line."""
Expand All @@ -199,10 +206,10 @@ def quantity(self):

@property
def received_quantity(self):
"""Get quantity of received ordered_items for a line order.
"""Get quantity of received ordered_items for a order line.
The received quantitiy is number of quantity received for the resource
acq_receipt_line and for the correspoding acq_line_order.
The received quantity is number of quantity received for the resource
acq_receipt_line and for the corresponding acq_line_order.
"""
from rero_ils.modules.acq_receipt_lines.api import \
AcqReceiptLinesSearch
Expand All @@ -217,6 +224,18 @@ def unreceived_quantity(self):
"""Get quantity of unreceived ordered_items for a line order."""
return self.quantity - self.received_quantity

@cached_property
def receipt_date(self):
"""Get the first reception date for one item of this order line."""
from rero_ils.modules.acq_receipt_lines.api import \
AcqReceiptLinesSearch
search = AcqReceiptLinesSearch() \
.filter('term', acq_order_line__pid=self.pid)
search.aggs.metric('min_receipt_date', 'min', field='receipt_date')
results = search.execute()
epoch = results.aggregations.min_receipt_date.value / 1000
return datetime.fromtimestamp(epoch)

@property
def status(self):
"""Calculate the order line status.
Expand Down
2 changes: 1 addition & 1 deletion rero_ils/modules/acq_order_lines/dumpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def dump(self, record, data):
:param data: The initial dump data passed in by ``record.dumps()``.
"""
# Keep only some attributes from AcqOrderLine object initial dump.
for attr in ['status', 'order_date', 'receipt_date', 'quantity']:
for attr in ['pid', 'status', 'order_date', 'quantity']:
value = record.get(attr)
if value:
data.update({attr: value})
Expand Down
96 changes: 66 additions & 30 deletions rero_ils/modules/acq_orders/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from .models import AcqOrderIdentifier, AcqOrderMetadata, AcqOrderStatus
from ..acq_order_lines.api import AcqOrderLine, AcqOrderLinesSearch
from ..acq_order_lines.models import AcqOrderLineStatus
from ..acq_receipts.api import AcqReceiptsSearch
from ..acq_receipts.api import AcqReceipt, AcqReceiptsSearch
from ..api import IlsRecord, IlsRecordsIndexer, IlsRecordsSearch
from ..fetchers import id_fetcher
from ..minters import id_minter
Expand Down Expand Up @@ -226,6 +226,20 @@ def get_note(self, note_type):
]
return next(iter(note), None)

def get_receipts(self, output=None):
"""Get AcqReceipts related to this AcqOrder.
:param output: output method. 'count', 'query' or None.
:return a generator of related AcqReceipts.
"""
query = AcqReceiptsSearch().filter('term', acq_order__pid=self.pid)
if output == 'count':
return query.count()
elif output == 'query':
return query
else:
return self._list_object_by_id(AcqReceipt, query)

def get_related_notes(self, resource_filters=None):
"""Get all notes from resource relates to this `AcqOrder`.
Expand Down Expand Up @@ -261,69 +275,91 @@ def get_related_notes(self, resource_filters=None):
def get_order_lines(self, output=None, includes=None):
"""Get order lines related to this order.
:param output: output method. 'count' or None.
:param output: output method. 'count', 'query' or None.
:param includes: a list of statuses to include order lines.
:return a generator of related order lines (or length).
"""
query = AcqOrderLinesSearch()\
.filter('term', acq_order__pid=self.pid)
query = AcqOrderLinesSearch().filter('term', acq_order__pid=self.pid)
if includes:
query = query.filter('terms', status=includes)

if output == 'count':
return query.count()
return self._list_object_by_id(AcqOrderLine, query)
elif output == 'query':
return query
else:
return self._list_object_by_id(AcqOrderLine, query)

def get_order_total_amount(self):
"""Get total amount of order."""
def get_order_provisional_total_amount(self):
"""Get provisional total amount of this order."""
search = AcqOrderLinesSearch()\
.filter('term', acq_order__pid=self.pid) \
.exclude('term', status=AcqOrderLineStatus.CANCELLED)
search.aggs.metric(
'order_total_amount',
'sum',
field='total_amount',
script={
'source': 'Math.round(_value*100)/100.00'
}
script={'source': 'Math.round(_value*100)/100.00'}
)
results = search.execute()
return results.aggregations.order_total_amount.value
return round(results.aggregations.order_total_amount.value, 2)

def get_order_expenditure_total_amount(self):
"""Get total amount of known expenditures of this order."""
search = AcqReceiptsSearch().filter('term', acq_order__pid=self.pid)
search.aggs.metric(
'receipt_total_amount',
'sum',
field='total_amount',
script={'source': 'Math.round(_value*100)/100.00'}
)
results = search.execute()
return round(results.aggregations.receipt_total_amount.value, 2)

def get_account_statement(self):
"""Get account statement for this order.
Accounting informations contains data about encumbrance and
expenditure. For each section, we can find the total amount and the
related item quantity.
"""
return {
'provisional': {
'total_amount': self.get_order_provisional_total_amount(),
'quantity': self.item_quantity,
},
'expenditure': {
'total_amount': self.get_order_expenditure_total_amount(),
'quantity': self.item_received_quantity
}
}

def get_links_to_me(self, get_pids=False):
"""Record links.
"""Get related record links.
:param get_pids: if True list of linked pids
if False count of linked records
:param get_pids: if True list of related record pids, if False count
of related records.
"""
links = {}
order_lines_query = AcqOrderLinesSearch()\
.filter('term', acq_order__pid=self.pid)
receipts_query = AcqReceiptsSearch()\
.filter('term', acq_order__pid=self.pid)

if get_pids:
order_lines = sorted_pids(order_lines_query)
receipts = sorted_pids(receipts_query)
links['order_lines'] = sorted_pids(
self.get_order_lines(output='query'))
links['receipts'] = sorted_pids(self.get_receipts(output='query'))
else:
order_lines = order_lines_query.count()
receipts = receipts_query.count()

if order_lines:
links['acq_order_lines'] = order_lines
if receipts:
links['acq_receipts'] = receipts
links['order_lines'] = self.get_order_lines(output='count')
links['receipts'] = self.get_receipts(output='count')
links = {k: v for k, v in links.items() if v}
return links

def reasons_not_to_delete(self):
"""Get reasons not to delete record."""
cannot_delete = {}
links = self.get_links_to_me()
# The link with AcqOrderLine ressources isn't a reason to not delete
# The link with AcqOrderLine resources isn't a reason to not delete
# an AcqOrder. Indeed, when we delete an AcqOrder, we also delete all
# related AcqOrderLines (cascade delete). Check the extension
# ``pre_delete`` hook.
links.pop('acq_order_lines', None)
links.pop('order_lines', None)
if self.status != AcqOrderStatus.PENDING:
cannot_delete['others'] = {
_(f'Order status is {self.status}'): True
Expand Down
12 changes: 4 additions & 8 deletions rero_ils/modules/acq_orders/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,27 @@ def pre_dump(self, record, dumper=None):
"""
if record.order_date:
record['order_date'] = record.order_date
record['total_amount'] = record.get_order_total_amount()
record['account_statement'] = \
record.get_account_statement()
record['status'] = record.status
record['item_quantity'] = {
'ordered': record.item_quantity,
'received': record.item_received_quantity
}

def pre_load(self, data, loader=None):
"""Called before a record is loaded.
:param data: the data to load.
:param loader: the record loader.
"""
data.pop('total_amount', None)
data.pop('account_statement', None)
data.pop('status', None)
data.pop('order_date', None)
data.pop('item_quantity', None)

def pre_delete(self, record, force=False):
"""Called before a record is deleted.
:param record: the record metadata.
"""
# For pending orders, we are allowed to delete all of its
# line orders without futher checks.
# line orders without further checks.
# there is no need to check if it is a pending order or not because
# the can_delete is already execute in the method self.delete
for line in record.get_order_lines():
Expand Down
15 changes: 10 additions & 5 deletions rero_ils/modules/acq_orders/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from .api import AcqOrdersSearch
from ..acq_order_lines.dumpers import AcqOrderLineESDumper
from ..acq_receipts.dumpers import AcqReceiptESDumper


def enrich_acq_order_data(sender, json=None, record=None, index=None,
Expand Down Expand Up @@ -60,18 +61,22 @@ def enrich_acq_order_data(sender, json=None, record=None, index=None,
})

# RELATED ORDER LINES METADATA ----------------------------------------
order_line_dumper = AcqOrderLineESDumper()
json['order_lines'] = [
order_line.dumps(dumper=AcqOrderLineESDumper())
order_line.dumps(dumper=order_line_dumper)
for order_line in record.get_order_lines()
]

# RELATED RECEIPTS ----------------------------------------------------
receipt_dumper = AcqReceiptESDumper()
json['receipts'] = [
receipt.dumps(dumper=receipt_dumper)
for receipt in record.get_receipts()
]

# ADD OTHERS DYNAMIC KEYS ---------------------------------------------
json.update({
'status': record.status,
'item_quantity': {
'ordered': record.item_quantity,
'received': record.item_received_quantity
},
'organisation': {
'pid': record.organisation_pid,
'type': 'org',
Expand Down

0 comments on commit 22af9d7

Please sign in to comment.