Merge branch 'nabinhait-billing_status' into develop

This commit is contained in:
Anand Doshi 2016-01-06 16:32:19 +05:30
commit a0693983cc
21 changed files with 3123 additions and 2687 deletions

View File

@ -11,6 +11,7 @@ import frappe.defaults
from erpnext.controllers.buying_controller import BuyingController
from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.accounts.utils import get_account_currency
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billed_amount_based_on_po
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@ -150,14 +151,14 @@ class PurchaseInvoice(BuyingController):
against_accounts = []
stock_items = self.get_stock_items()
for item in self.get("items"):
# in case of auto inventory accounting,
# in case of auto inventory accounting,
# against expense account is always "Stock Received But Not Billed"
# for a stock item and if not epening entry and not drop-ship entry
if auto_accounting_for_stock and item.item_code in stock_items \
and self.is_opening == 'No' and (not item.po_detail or
and self.is_opening == 'No' and (not item.po_detail or
not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")):
item.expense_account = stock_not_billed_account
item.cost_center = None
@ -246,6 +247,7 @@ class PurchaseInvoice(BuyingController):
self.update_against_document_in_jv()
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr()
self.update_project()
@ -407,6 +409,8 @@ class PurchaseInvoice(BuyingController):
self.update_prevdoc_status()
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.update_billing_status_in_pr()
self.make_gl_entries_on_cancel()
self.update_project()
@ -431,6 +435,21 @@ class PurchaseInvoice(BuyingController):
if pi:
frappe.throw("Supplier Invoice No exists in Purchase Invoice {0}".format(pi))
def update_billing_status_in_pr(self, update_modified=True):
updated_pr = []
for d in self.get("items"):
if d.pr_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""", d.pr_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Purchase Receipt Item", d.pr_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_pr.append(d.purchase_receipt)
elif d.po_detail:
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified)
for pr in set(updated_pr):
frappe.get_doc("Purchase Receipt", pr).update_billing_percentage(update_modified=update_modified)
@frappe.whitelist()
def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond

View File

@ -12,6 +12,7 @@ from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@ -98,6 +99,7 @@ class SalesInvoice(SellingController):
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_in_dn()
# this sequence because outstanding may get -ve
self.make_gl_entries()
@ -111,6 +113,7 @@ class SalesInvoice(SellingController):
self.update_time_log_batch(self.name)
def before_cancel(self):
self.update_time_log_batch(None)
@ -129,6 +132,7 @@ class SalesInvoice(SellingController):
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_in_dn()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
@ -381,7 +385,7 @@ class SalesInvoice(SellingController):
def validate_warehouse(self):
super(SalesInvoice, self).validate_warehouse()
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}").format(d.idx))
@ -410,7 +414,7 @@ class SalesInvoice(SellingController):
if self.c_form_applicable == 'Yes' and self.c_form_no:
msgprint(_("Please remove this Invoice {0} from C-Form {1}")
.format(self.name, self.c_form_no), raise_exception = 1)
def validate_dropship_item(self):
for item in self.items:
if item.sales_order:
@ -630,6 +634,21 @@ class SalesInvoice(SellingController):
}, write_off_account_currency)
)
def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = []
for d in self.get("items"):
if d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)

View File

@ -0,0 +1,4 @@
- Billing Status in Delivery Note (DN) and Purchase Receipt (PR)
- If Invoice made directly against DN/PR, calculate based on actual billed amount
- If Invoice made via Sales Order/Purchase Order, allocate billed amount in DN/PR based on FIFO
- Billing status updated in existing DN/PR

View File

@ -52,13 +52,15 @@ status_map = {
],
"Delivery Note": [
["Draft", None],
["Submitted", "eval:self.docstatus==1"],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
],
"Purchase Receipt": [
["Draft", None],
["Submitted", "eval:self.docstatus==1"],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
]
@ -79,7 +81,7 @@ class StatusUpdater(Document):
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
return
if self.doctype in status_map:
_status = self.status
@ -102,9 +104,9 @@ class StatusUpdater(Document):
if self.status != _status and self.status not in ("Submitted", "Cancelled"):
self.add_comment("Label", _(self.status))
if update:
frappe.db.set_value(self.doctype, self.name, "status", self.status,
frappe.db.set_value(self.doctype, self.name, "status", self.status,
update_modified=update_modified)
def validate_qty(self):
@ -153,7 +155,6 @@ class StatusUpdater(Document):
# check if overflow is within tolerance
tolerance, self.tolerance, self.global_tolerance = get_tolerance_for(item['item_code'],
self.tolerance, self.global_tolerance)
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
@ -166,10 +167,10 @@ class StatusUpdater(Document):
throw(_("{0} must be reduced by {1} or you should increase overflow tolerance")
.format(_(item["target_ref_field"].title()), item["reduce_by"]))
def update_qty(self, change_modified=True):
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level
:param change_modified: If true, updates `modified` and `modified_by` for target parent doc
:param update_modified: If true, updates `modified` and `modified_by` for target parent doc
"""
for args in self.status_updater:
# condition to include current record (if submit or no if cancel)
@ -178,22 +179,19 @@ class StatusUpdater(Document):
else:
args['cond'] = ' and parent!="%s"' % self.name.replace('"', '\"')
args['set_modified'] = ''
if change_modified:
args['set_modified'] = ', modified = now(), modified_by = "{0}"'\
.format(frappe.db.escape(frappe.session.user))
self._update_children(args)
self._update_children(args, update_modified)
if "percent_join_field" in args:
self._update_percent_field(args)
self._update_percent_field_in_targets(args, update_modified)
def _update_children(self, args):
def _update_children(self, args, update_modified):
"""Update quantities or amount in child table"""
for d in self.get_all_children():
if d.doctype != args['source_dt']:
continue
self._update_modified(args, update_modified)
# updates qty in the child table
args['detail_id'] = d.get(args['join_field'])
@ -212,44 +210,58 @@ class StatusUpdater(Document):
if not args.get("extra_cond"): args["extra_cond"] = ""
frappe.db.sql("""update `tab%(target_dt)s`
set %(target_field)s = (select ifnull(sum(%(source_field)s), 0)
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
and (docstatus=1 %(cond)s) %(extra_cond)s) %(second_source_condition)s
set %(target_field)s = (
(select ifnull(sum(%(source_field)s), 0)
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
and (docstatus=1 %(cond)s) %(extra_cond)s)
%(second_source_condition)s
)
%(update_modified)s
where name='%(detail_id)s'""" % args)
def _update_percent_field(self, args):
def _update_percent_field_in_targets(self, args, update_modified=True):
"""Update percent field in parent transaction"""
unique_transactions = set([d.get(args['percent_join_field']) for d in self.get_all_children(args['source_dt'])])
distinct_transactions = set([d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt'])])
for name in unique_transactions:
if not name:
continue
for name in distinct_transactions:
if name:
args['name'] = name
self._update_percent_field(args, update_modified)
args['name'] = name
def _update_percent_field(self, args, update_modified=True):
"""Update percent field in parent transaction"""
# update percent complete in the parent table
if args.get('target_parent_field'):
frappe.db.sql("""update `tab%(target_parent_dt)s`
set %(target_parent_field)s = round(
ifnull((select
ifnull(sum(if(%(target_ref_field)s > %(target_field)s, %(target_field)s, %(target_ref_field)s)), 0)
/ sum(%(target_ref_field)s) * 100
from `tab%(target_dt)s` where parent="%(name)s"), 0), 2)
%(set_modified)s
where name='%(name)s'""" % args)
self._update_modified(args, update_modified)
# update field
if args.get('status_field'):
frappe.db.sql("""update `tab%(target_parent_dt)s`
set %(status_field)s = if(%(target_parent_field)s<0.001,
'Not %(keyword)s', if(%(target_parent_field)s>=99.99,
'Fully %(keyword)s', 'Partly %(keyword)s'))
where name='%(name)s'""" % args)
if args.get('target_parent_field'):
frappe.db.sql("""update `tab%(target_parent_dt)s`
set %(target_parent_field)s = round(
ifnull((select
ifnull(sum(if(%(target_ref_field)s > %(target_field)s, %(target_field)s, %(target_ref_field)s)), 0)
/ sum(%(target_ref_field)s) * 100
from `tab%(target_dt)s` where parent="%(name)s"), 0), 2)
%(update_modified)s
where name='%(name)s'""" % args)
if args.get("set_modified"):
target = frappe.get_doc(args["target_parent_dt"], name)
target.set_status(update=True)
target.notify_update()
# update field
if args.get('status_field'):
frappe.db.sql("""update `tab%(target_parent_dt)s`
set %(status_field)s = if(%(target_parent_field)s<0.001,
'Not %(keyword)s', if(%(target_parent_field)s>=99.99,
'Fully %(keyword)s', 'Partly %(keyword)s'))
where name='%(name)s'""" % args)
if update_modified:
target = frappe.get_doc(args["target_parent_dt"], args["name"])
target.set_status(update=True)
target.notify_update()
def _update_modified(self, args, update_modified):
args['update_modified'] = ''
if update_modified:
args['update_modified'] = ', modified = now(), modified_by = "{0}"'\
.format(frappe.db.escape(frappe.session.user))
def update_billing_status_for_zero_amount_refdoc(self, ref_dt):
ref_fieldname = ref_dt.lower().replace(" ", "_")

View File

@ -312,6 +312,16 @@ class StockController(AccountsController):
for w in warehouses:
validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True):
self._update_percent_field({
"target_dt": self.doctype + " Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_billed",
"target_ref_field": "amount",
"target_field": "billed_amt",
"name": self.name,
}, update_modified)
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None):

View File

@ -240,3 +240,4 @@ erpnext.patches.v6_10.fix_billed_amount_in_drop_ship_po
erpnext.patches.v6_10.fix_delivery_status_of_drop_ship_item #2015-12-08
erpnext.patches.v5_8.tax_rule #2015-12-08
erpnext.patches.v6_12.set_overdue_tasks
erpnext.patches.v6_16.update_billing_status_in_dn_and_pr

View File

@ -13,6 +13,6 @@ def execute():
where purchase_order=%s and docstatus=1""", po[0])
if invoices:
for inv in invoices:
frappe.get_doc("Purchase Invoice", inv[0]).update_qty(change_modified=False)
frappe.get_doc("Purchase Invoice", inv[0]).update_qty(update_modified=False)
else:
frappe.db.sql("""update `tabPurchase Order` set per_billed=0 where name=%s""", po[0])

View File

@ -14,4 +14,4 @@ def execute():
{"patch_date": not_null_patch_date}):
doc = frappe.get_doc(doctype, name)
doc.update_qty(change_modified=False)
doc.update_qty(update_modified=False)

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@ -0,0 +1,33 @@
# Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
for dt in ("Delivery Note", "Purchase Receipt"):
frappe.reload_doctype(dt)
frappe.reload_doctype(dt + " Item")
# Update billed_amt in DN and PR which are not against any order
for d in frappe.db.sql("""select name from `tabDelivery Note Item`
where (so_detail is null or so_detail = '') and docstatus=1""", as_dict=1):
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.name)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.name, "billed_amt", billed_amt, update_modified=False)
# Update billed_amt in DN and PR which are not against any order
for d in frappe.db.sql("""select name from `tabPurchase Receipt Item`
where (prevdoc_detail_docname is null or prevdoc_detail_docname = '') and docstatus=1""", as_dict=1):
billed_amt = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""", d.name)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Purchase Receipt Item", d.name, "billed_amt", billed_amt, update_modified=False)
# Update billed amt which are against order or invoice
# Update billing status for all
for d in frappe.db.sql("select name from `tab{0}` where docstatus=1".format(dt), as_dict=1):
doc = frappe.get_doc(dt, d.name)
doc.update_billing_status(update_modified=False)
doc.set_status(update=True, update_modified=False)

View File

@ -52,16 +52,8 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
}
}
if(doc.__onload && !doc.__onload.billing_complete && doc.docstatus==1
&& !doc.is_return && doc.status!="Closed") {
// show Make Invoice button only if Delivery Note is not created from Sales Invoice
var from_sales_invoice = false;
from_sales_invoice = cur_frm.doc.items.some(function(item) {
return item.against_sales_invoice ? true : false;
});
if(!from_sales_invoice)
cur_frm.add_custom_button(__('Invoice'), this.make_sales_invoice).addClass("btn-primary");
if(doc.docstatus==1 && !doc.is_return && doc.status!="Closed" && flt(doc.per_billed, 2) < 100) {
cur_frm.add_custom_button(__('Invoice'), this.make_sales_invoice).addClass("btn-primary");
}
if(doc.docstatus==1 && doc.status === "Closed" && this.frm.has_perm("submit")) {
@ -285,6 +277,3 @@ if (sys_defaults.auto_accounting_for_stock) {
}
}
}

View File

@ -2154,6 +2154,30 @@
"unique": 0,
"width": "150px"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "per_billed",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "% Amount Billed",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -2317,7 +2341,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nCancelled\nClosed",
"options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
@ -2672,7 +2696,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2015-12-17 16:19:27.004718",
"modified": "2015-12-25 16:20:39.014291",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@ -48,7 +48,8 @@ class DeliveryNote(SellingController):
'target_ref_field': 'qty',
'source_field': 'qty',
'percent_join_field': 'against_sales_invoice',
'overflow_type': 'delivery'
'overflow_type': 'delivery',
'no_tolerance': 1
},
{
'source_dt': 'Delivery Note Item',
@ -56,22 +57,12 @@ class DeliveryNote(SellingController):
'join_field': 'so_detail',
'target_field': 'returned_qty',
'target_parent_dt': 'Sales Order',
# 'target_parent_field': 'per_delivered',
# 'target_ref_field': 'qty',
'source_field': '-1 * qty',
# 'percent_join_field': 'against_sales_order',
# 'overflow_type': 'delivery',
'extra_cond': """ and exists (select name from `tabDelivery Note` where name=`tabDelivery Note Item`.parent and is_return=1)"""
}]
def onload(self):
billed_qty = frappe.db.sql("""select sum(qty) from `tabSales Invoice Item`
where docstatus=1 and delivery_note=%s""", self.name)
if billed_qty:
total_qty = sum((item.qty for item in self.get("items")))
self.set_onload("billing_complete", (billed_qty[0][0] == total_qty))
self.set_onload("has_return_entry", len(frappe.db.exists({"doctype": "Delivery Note",
self.set_onload("has_return_entry", len(frappe.db.exists({"doctype": "Delivery Note",
"is_return": 1, "return_against": self.name, "docstatus": 1})))
def before_print(self):
@ -171,7 +162,7 @@ class DeliveryNote(SellingController):
def validate_warehouse(self):
super(DeliveryNote, self).validate_warehouse()
for d in self.get_item_list():
if frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1:
if not d['warehouse']:
@ -199,6 +190,7 @@ class DeliveryNote(SellingController):
# update delivered qty in sales order
self.update_prevdoc_status()
self.update_billing_status()
if not self.is_return:
self.check_credit_limit()
@ -206,18 +198,15 @@ class DeliveryNote(SellingController):
self.update_stock_ledger()
self.make_gl_entries()
frappe.db.set(self, 'status', 'Submitted')
def on_cancel(self):
self.check_stop_or_close_sales_order("against_sales_order")
self.check_next_docstatus()
self.update_prevdoc_status()
self.update_billing_status()
self.update_stock_ledger()
frappe.db.set(self, 'status', 'Cancelled')
self.cancel_packing_slips()
self.make_gl_entries_on_cancel()
@ -280,6 +269,63 @@ class DeliveryNote(SellingController):
self.notify_update()
clear_doctype_notifications(self)
def update_billing_status(self, update_modified=True):
updated_delivery_notes = [self.name]
for d in self.get("items"):
if d.si_detail and not d.so_detail:
d.db_set('billed_amt', d.amount, update_modified=update_modified)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes):
dn_doc = self if (dn == self.name) else frappe.get_doc("Delivery Note", dn)
dn_doc.update_billing_percentage(update_modified=update_modified)
self.load_from_db()
def update_billed_amount_based_on_so(so_detail, update_modified=True):
# Billed against Sales Order directly
billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail)
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row
dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
where dn.name=dn_item.parent and dn_item.so_detail=%s
and dn.docstatus=1 and dn.is_return = 0
order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1)
updated_dn = []
for dnd in dn_details:
billed_amt_agianst_dn = 0
# If delivered against Sales Invoice
if dnd.si_detail:
billed_amt_agianst_dn = flt(dnd.amount)
billed_against_so -= billed_amt_agianst_dn
else:
# Get billed amount directly against Delivery Note
billed_amt_agianst_dn = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", dnd.name)
billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0
# Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_agianst_dn < dnd.amount:
pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
if pending_to_bill <= billed_against_so:
billed_amt_agianst_dn += pending_to_bill
billed_against_so -= pending_to_bill
else:
billed_amt_agianst_dn += billed_against_so
billed_against_so = 0
frappe.db.set_value("Delivery Note Item", dnd.name, "billed_amt", billed_amt_agianst_dn, update_modified=update_modified)
updated_dn.append(dnd.parent)
return updated_dn
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)

View File

@ -1,11 +1,15 @@
frappe.listview_settings['Delivery Note'] = {
add_fields: ["customer", "customer_name", "base_grand_total", "per_installed",
add_fields: ["customer", "customer_name", "base_grand_total", "per_installed", "per_billed",
"transporter_name", "grand_total", "is_return", "status"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if(doc.status==="Closed") {
return [__("Closed"), "green", "status,=,Closed"];
}
} else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"];
} else if (flt(doc.per_billed, 2) == 100) {
return [__("Completed"), "green", "per_billed,=,100"];
}
}
};

View File

@ -18,6 +18,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry \
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWarehouseError
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
import create_stock_reconciliation, set_valuation_method
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so
class TestDeliveryNote(unittest.TestCase):
def test_over_billing_against_dn(self):
@ -405,6 +406,112 @@ class TestDeliveryNote(unittest.TestCase):
update_delivery_note_status(dn.name, "Closed")
self.assertEquals(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order()
dn = create_dn_against_so(so.name, delivered_qty=2)
self.assertEqual(dn.status, "To Bill")
self.assertEqual(dn.per_billed, 0)
si = make_sales_invoice(dn.name)
si.submit()
dn.load_from_db()
self.assertEqual(dn.get("items")[0].billed_amt, 200)
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.status, "Completed")
def test_dn_billing_status_case2(self):
# SO -> SI and SO -> DN1, DN2
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
so = make_sales_order()
si = make_sales_invoice(so.name)
si.get("items")[0].qty = 5
si.insert()
si.submit()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
dn1 = make_delivery_note(so.name)
dn1.posting_time = "10:00"
dn1.get("items")[0].qty = 2
dn1.submit()
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
self.assertEqual(dn1.per_billed, 100)
self.assertEqual(dn1.status, "Completed")
dn2 = make_delivery_note(so.name)
dn2.posting_time = "08:00"
dn2.get("items")[0].qty = 4
dn2.submit()
dn1.load_from_db()
self.assertEqual(dn1.get("items")[0].billed_amt, 100)
self.assertEqual(dn1.per_billed, 50)
self.assertEqual(dn1.status, "To Bill")
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
self.assertEqual(dn2.per_billed, 100)
self.assertEqual(dn2.status, "Completed")
def test_dn_billing_status_case3(self):
# SO -> DN1 -> SI and SO -> SI and SO -> DN2
from erpnext.selling.doctype.sales_order.sales_order \
import make_delivery_note, make_sales_invoice as make_sales_invoice_from_so
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
so = make_sales_order()
dn1 = make_delivery_note(so.name)
dn1.posting_time = "10:00"
dn1.get("items")[0].qty = 2
dn1.submit()
si1 = make_sales_invoice(dn1.name)
si1.submit()
dn1.load_from_db()
self.assertEqual(dn1.per_billed, 100)
si2 = make_sales_invoice_from_so(so.name)
si2.get("items")[0].qty = 4
si2.submit()
dn2 = make_delivery_note(so.name)
dn2.posting_time = "08:00"
dn2.get("items")[0].qty = 5
dn2.submit()
dn1.load_from_db()
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
self.assertEqual(dn1.per_billed, 100)
self.assertEqual(dn1.status, "Completed")
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
self.assertEqual(dn2.per_billed, 80)
self.assertEqual(dn2.status, "To Bill")
def test_dn_billing_status_case4(self):
# SO -> SI -> DN
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
so = make_sales_order()
si = make_sales_invoice(so.name)
si.submit()
dn = make_delivery_note(si.name)
dn.submit()
self.assertEqual(dn.get("items")[0].billed_amt, 1000)
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.status, "Completed")
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@ -1812,7 +1812,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nCancelled\nClosed",
"options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
@ -1930,6 +1930,30 @@
"unique": 0,
"width": "50%"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "per_billed",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "% Amount Billed",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -2278,7 +2302,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2015-12-17 16:18:48.969643",
"modified": "2015-12-30 18:15:06.678001",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@ -243,6 +243,8 @@ class PurchaseReceipt(BuyingController):
self.update_prevdoc_status()
self.update_ordered_qty()
self.update_billing_status()
if not self.is_return:
purchase_controller.update_last_purchase_rate(self, 1)
@ -281,6 +283,8 @@ class PurchaseReceipt(BuyingController):
# Must be called after updating received qty in PO
self.update_ordered_qty()
self.update_billing_status()
if not self.is_return:
pc_obj.update_last_purchase_rate(self, 0)
@ -436,6 +440,54 @@ class PurchaseReceipt(BuyingController):
self.notify_update()
clear_doctype_notifications(self)
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
for d in self.get("items"):
if d.prevdoc_detail_docname:
updated_pr += update_billed_amount_based_on_po(d.prevdoc_detail_docname, update_modified)
for pr in set(updated_pr):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
pr_doc.update_billing_percentage(update_modified=update_modified)
self.load_from_db()
def update_billed_amount_based_on_po(po_detail, update_modified=True):
# Billed against Sales Order directly
billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail)
billed_against_po = billed_against_po and billed_against_po[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row
pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name=pr_item.parent and pr_item.prevdoc_detail_docname=%s
and pr.docstatus=1 and pr.is_return = 0
order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", po_detail, as_dict=1)
updated_pr = []
for pr_item in pr_details:
# Get billed amount directly against Purchase Receipt
billed_amt_agianst_pr = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""", pr_item.name)
billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0
# Distribute billed amount directly against PO between PRs based on FIFO
if billed_against_po and billed_amt_agianst_pr < pr_item.amount:
pending_to_bill = flt(pr_item.amount) - billed_amt_agianst_pr
if pending_to_bill <= billed_against_po:
billed_amt_agianst_pr += pending_to_bill
billed_against_po -= pending_to_bill
else:
billed_amt_agianst_pr += billed_against_po
billed_against_po = 0
frappe.db.set_value("Purchase Receipt Item", pr_item.name, "billed_amt", billed_amt_agianst_pr, update_modified=update_modified)
updated_pr.append(pr_item.parent)
return updated_pr
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc

View File

@ -1,11 +1,15 @@
frappe.listview_settings['Purchase Receipt'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "is_subcontracted",
"transporter_name", "is_return", "status"],
"transporter_name", "is_return", "status", "per_billed"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if(doc.status==="Closed") {
return [__("Closed"), "green", "status,=,Closed"];
}
} else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"];
} else if (flt(doc.per_billed, 2) == 100) {
return [__("Completed"), "green", "per_billed,=,100"];
}
}
};

View File

@ -7,11 +7,10 @@ import unittest
import frappe
import frappe.defaults
from frappe.utils import cint, flt, cstr
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
class TestPurchaseReceipt(unittest.TestCase):
def test_make_purchase_invoice(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
pr = make_purchase_receipt(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name)
pr.submit()
@ -185,6 +184,45 @@ class TestPurchaseReceipt(unittest.TestCase):
update_purchase_receipt_status(pr.name, "Closed")
self.assertEquals(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
def test_pr_billing_status(self):
# PO -> PR1 -> PI and PO -> PI and PO -> PR2
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.purchase_order.purchase_order \
import make_purchase_receipt, make_purchase_invoice as make_purchase_invoice_from_po
po = create_purchase_order()
pr1 = make_purchase_receipt(po.name)
pr1.posting_time = "10:00"
pr1.get("items")[0].received_qty = 2
pr1.get("items")[0].qty = 2
pr1.submit()
pi1 = make_purchase_invoice(pr1.name)
pi1.submit()
pr1.load_from_db()
self.assertEqual(pr1.per_billed, 100)
pi2 = make_purchase_invoice_from_po(po.name)
pi2.get("items")[0].qty = 4
pi2.submit()
pr2 = make_purchase_receipt(po.name)
pr2.posting_time = "08:00"
pr2.get("items")[0].received_qty = 5
pr2.get("items")[0].qty = 5
pr2.submit()
pr1.load_from_db()
self.assertEqual(pr1.get("items")[0].billed_amt, 1000)
self.assertEqual(pr1.per_billed, 100)
self.assertEqual(pr1.status, "Completed")
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill")
def get_gl_entries(voucher_type, voucher_no):
return frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type=%s and voucher_no=%s