" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "
"
+ message += "
" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "
"
+
+ frappe.throw(message, title=_("Account Missing"))
+
def validate_party(self):
party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party)
From 73d944da21045d1f6387b5fc583e37e37850c30d Mon Sep 17 00:00:00 2001
From: Anupam
Date: Tue, 13 Oct 2020 18:11:05 +0530
Subject: [PATCH 014/286] fix: review changes
---
erpnext/selling/doctype/quotation/quotation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 7c55d7742f8..3157982d528 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -62,7 +62,7 @@ class Quotation(SellingController):
opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity)
- opp.status = status
+ opp.set_status(status=status)
opp.set_status(update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
From 359778e2357997aaeac126c37bdcb34e8efa7ed3 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 16 Oct 2020 16:47:23 +0530
Subject: [PATCH 015/286] chore: Code cleanup, reduce redundancy
---
.../request_for_quotation.js | 2 +-
.../supplier_quotation/supplier_quotation.js | 2 +-
erpnext/public/js/controllers/buying.js | 78 +++++++++----------
3 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 660af965052..1ebd21a17be 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -326,7 +326,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
d.show();
}, __("Get items from"));
- //Link Material Requests
+ // Link Material Requests
this.frm.add_custom_button(__('Link to Material Requests'),
function() {
erpnext.buying.link_to_mrs(me.frm);
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
index c146f13dfe8..934d71c3b38 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
@@ -55,7 +55,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
})
}, __("Get items from"));
- //Link Material Requests
+ // Link Material Requests
this.frm.add_custom_button(__('Link to Material Requests'),
function() {
erpnext.buying.link_to_mrs(me.frm);
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 62da7f5c5bc..8cae7a5b3d3 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -363,54 +363,54 @@ erpnext.buying.link_to_mrs = function(frm) {
frappe.call({
method: "erpnext.buying.utils.get_linked_material_requests",
args:{
- items: frm.doc.items.map((item) => {return item.item_code;})
+ items: frm.doc.items.map((item) => item.item_code)
},
callback: function(r) {
- if(!r.message || r.message.length == 0) {
- frappe.throw({message: __("No pending Material Requests found to link for the given items."), title: __("Note")});
+ if (!r.message || r.message.length == 0) {
+ frappe.throw({
+ message: __("No pending Material Requests found to link for the given items."),
+ title: __("Note")
+ });
}
- else {
- var i = 0;
- var item_length = frm.doc.items.length;
- while (i < item_length) {
- var qty = frm.doc.items[i].qty;
- (r.message[0] || []).forEach(function(d) {
- if (d.qty > 0 && qty > 0 && frm.doc.items[i].item_code == d.item_code && !frm.doc.items[i].material_request_item)
+
+ var item_length = frm.doc.items.length;
+ for (let item of frm.doc.items) {
+ var qty = item.qty;
+ (r.message[0] || []).forEach(function(d) {
+ if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item)
+ {
+ item.material_request = d.mr_name;
+ item.material_request_item = d.mr_item;
+ var my_qty = Math.min(qty, d.qty);
+ qty = qty - my_qty;
+ d.qty = d.qty - my_qty;
+ item.stock_qty = my_qty*item.conversion_factor;
+ item.qty = my_qty;
+
+ frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")");
+ if (qty > 0)
{
- frm.doc.items[i].material_request = d.mr_name;
- frm.doc.items[i].material_request_item = d.mr_item;
- var my_qty = Math.min(qty, d.qty);
- qty = qty - my_qty;
- d.qty = d.qty - my_qty;
- frm.doc.items[i].stock_qty = my_qty*frm.doc.items[i].conversion_factor;
- frm.doc.items[i].qty = my_qty;
+ frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
+ var newrow = frappe.model.add_child(frm.doc, item.doctype, "items");
+ item_length++;
- frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + frm.doc.items[i].idx + ")");
- if (qty > 0)
+ for (var key in item)
{
- frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
- var newrow = frappe.model.add_child(frm.doc, frm.doc.items[i].doctype, "items");
- item_length++;
-
- for (var key in frm.doc.items[i])
- {
- newrow[key] = frm.doc.items[i][key];
- }
-
- newrow.idx = item_length;
- newrow["stock_qty"] = newrow.conversion_factor*qty;
- newrow["qty"] = qty;
-
- newrow["material_request"] = "";
- newrow["material_request_item"] = "";
-
+ newrow[key] = item[key];
}
+
+ newrow.idx = item_length;
+ newrow["stock_qty"] = newrow.conversion_factor*qty;
+ newrow["qty"] = qty;
+
+ newrow["material_request"] = "";
+ newrow["material_request_item"] = "";
+
}
- });
- i++;
- }
- refresh_field("items");
+ }
+ });
}
+ refresh_field("items");
}
});
}
From c70cc0d95080d3337b4613c56983e5fbaad47426 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 29 Sep 2020 21:37:08 +0530
Subject: [PATCH 016/286] fix: tds calculation, skip invoices with "Apply Tax
Withholding Amount" has disabled
---
.../tax_withholding_category.py | 6 ++---
.../test_tax_withholding_category.py | 25 ++++++++++++++++++-
2 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 8b5e68b359b..32ad4cb03ab 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -140,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
else:
tds_amount = _get_tds(net_total, tax_details.rate)
else:
- supplier_credit_amount = frappe.get_all('Purchase Invoice Item',
- fields = ['sum(net_amount)'],
- filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1)
+ supplier_credit_amount = frappe.get_all('Purchase Invoice',
+ fields = ['sum(net_total)'],
+ filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index b1468999fc1..a0b0cbb9956 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
+ def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
+ invoices = []
+ frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ pi.submit()
+ invoices.append(pi)
+
+ # TDS not applied
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
+ pi.submit()
+ invoices.append(pi)
+
+ pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ pi.submit()
+ invoices.append(pi)
+
+ self.assertEqual(pi.taxes_and_charges_deducted, 2000)
+ self.assertEqual(pi.grand_total, 8000)
+
+ # delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
def create_purchase_invoice(**args):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
@@ -109,7 +132,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc({
"doctype": "Purchase Invoice",
"posting_date": today(),
- "apply_tds": 1,
+ "apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier,
"company": '_Test Company',
"taxes_and_charges": "",
From d6596a169cd2de4a4dfc74cf68d454cec290a30d Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 2 Nov 2020 15:07:48 +0530
Subject: [PATCH 017/286] fix: Billing % Logic and Map Pending Qty only in PR
and DN
- Billing % should consider unreturned amount as total
- While mapping to return doc, map unreturned amount
- Added field Received Qty in Stock UOM, to tally against Returned Qty in PR
- PR billing percentage updation custom function
- In patch set received qty in stock uom first, then update returned qty and billing
---
.../purchase_invoice/purchase_invoice.py | 4 +-
erpnext/controllers/buying_controller.py | 4 ++
.../controllers/sales_and_purchase_return.py | 58 ++++++++++++++++---
erpnext/controllers/stock_controller.py | 6 +-
erpnext/patches.txt | 2 +-
.../v13_0/update_returned_qty_in_pr_dn.py | 7 +++
erpnext/public/js/controllers/buying.js | 1 +
.../purchase_receipt/purchase_receipt.py | 40 ++++++++++++-
.../purchase_receipt_item.json | 9 ++-
9 files changed, 117 insertions(+), 14 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 91c4dfb5877..014f05c4c1f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1032,7 +1032,9 @@ class PurchaseInvoice(BuyingController):
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)
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
+ pr_doc = frappe.get_doc("Purchase Receipt", pr)
+ update_billing_percentage(pr_doc, update_modified=update_modified)
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index f376836f7b8..af2474d3dee 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -497,6 +497,10 @@ class BuyingController(StockController):
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
+ if self.doctype=="Purchase Receipt" and d.meta.get_field("received_stock_qty"):
+ # Set Received Qty in Stock UOM
+ d.received_stock_qty = flt(d.received_qty) * flt(d.conversion_factor, d.precision("conversion_factor"))
+
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index b4da5fa3e79..e11289d79ea 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -203,6 +203,41 @@ def get_already_returned_items(doc):
return items
+def get_returned_qty_map_for_row(row_name, doctype):
+ child_doctype = doctype + " Item"
+ reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail"
+ reference_field = "child." + reference_field
+ columns = ""
+
+ if doctype == "Purchase Receipt":
+ columns += ", sum(abs(child.rejected_qty)) as rejected_qty, \
+ sum(abs(child.received_qty)) as received_qty, \
+ sum(abs(child.received_stock_qty)) as received_stock_qty"
+
+ data = frappe.db.sql("""
+ select
+ sum(abs(child.qty)) as qty,
+ sum(abs(child.stock_qty)) as stock_qty,
+ %(columns)s
+ from
+ `tab{0}` child, `tab{1}` parent
+ where
+ child.parent = parent.name
+ and parent.docstatus = 1
+ and parent.is_return = 1
+ and {2} = %(row_name)s
+ """.format(child_doctype, doctype, reference_field),
+ {
+ "row_name": row_name,
+ "columns": columns,
+ "child_doctype": child_doctype,
+ "doctype": doctype,
+ "reference_field": reference_field
+ },
+ as_dict=1)
+
+ return data[0]
+
def make_return_doc(doctype, source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
company = frappe.db.get_value("Delivery Note", source_name, "company")
@@ -262,20 +297,25 @@ def make_return_doc(doctype, source_name, target_doc=None):
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
- target_doc.qty = -1* source_doc.qty
+ target_doc.qty = -1 * source_doc.qty
+
if doctype == "Purchase Receipt":
- target_doc.received_qty = -1* source_doc.received_qty
- target_doc.rejected_qty = -1* source_doc.rejected_qty
- target_doc.qty = -1* source_doc.qty
- target_doc.stock_qty = -1 * source_doc.stock_qty
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
+ target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
+ target_doc.received_stock_qty = -1 * flt(source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0))
+
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_order_item = source_doc.purchase_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
- target_doc.received_qty = -1* source_doc.received_qty
- target_doc.rejected_qty = -1* source_doc.rejected_qty
+ target_doc.received_qty = -1 * source_doc.received_qty
+ target_doc.rejected_qty = -1 * source_doc.rejected_qty
target_doc.qty = -1* source_doc.qty
target_doc.stock_qty = -1 * source_doc.stock_qty
target_doc.purchase_order = source_doc.purchase_order
@@ -286,6 +326,10 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
+
target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice
target_doc.so_detail = source_doc.so_detail
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index f743d707f75..196279fa5c8 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -338,11 +338,15 @@ class StockController(AccountsController):
validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True):
+ target_ref_field = "amount"
+ if self.doctype == "Delivery Note":
+ target_ref_field = "amount - (returned_qty * rate)"
+
self._update_percent_field({
"target_dt": self.doctype + " Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_billed",
- "target_ref_field": "amount",
+ "target_ref_field": target_ref_field,
"target_field": "billed_amt",
"name": self.name,
}, update_modified)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 6dfa085b588..dc7e99bcde1 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -732,4 +732,4 @@ erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
-erpnext.patches.v13_0.update_returned_qty_in_pr_dn
+erpnext.patches.v13_0.update_returned_qty_in_pr_dn #12am
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py
index a13640e1b04..7f42cd92e3c 100644
--- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py
+++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py
@@ -15,6 +15,13 @@ def execute():
# Update original receipt/delivery document from return
return_doc = frappe.get_cached_doc(doctype, return_doc.name)
return_doc.update_prevdoc_status()
+ return_against = frappe.get_doc(doctype, return_doc.return_against)
+ return_against.update_billing_status()
+
+ # Set received qty in stock uom in PR, as returned qty is checked against it
+ frappe.db.sql(""" update `tabPurchase Receipt Item`
+ set received_stock_qty = received_qty * conversion_factor
+ where docstatus = 1 """)
for doctype in ('Purchase Receipt', 'Delivery Note'):
update_from_return_docs(doctype)
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index cb76c87b625..11f70f7f59f 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -189,6 +189,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
frappe.model.round_floats_in(item, ["qty", "received_qty"]);
item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
+ item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
}
this._super(doc, cdt, cdn);
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 551f3777a53..be3ff5e5c2d 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -77,8 +77,8 @@ class PurchaseReceipt(BuyingController):
'target_field': 'returned_qty',
'target_parent_dt': 'Purchase Receipt',
'target_parent_field': 'per_returned',
- 'target_ref_field': 'stock_qty',
- 'source_field': '-1 * stock_qty',
+ 'target_ref_field': 'received_stock_qty',
+ 'source_field': '-1 * received_stock_qty',
'percent_join_field_parent': 'return_against'
}
])
@@ -503,7 +503,7 @@ class PurchaseReceipt(BuyingController):
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)
+ update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db()
@@ -543,6 +543,39 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
return updated_pr
+def update_billing_percentage(pr_doc, update_modified=True):
+ # Update Billing % based on pending accepted qty
+ total_amount, total_billed_amount = 0, 0
+ for item in pr_doc.items:
+ returned_qty = frappe.db.sql("""
+ select sum(abs(child.qty)) as qty
+ from
+ `tabPurchase Receipt Item` child,
+ `tabPurchase Receipt` parent
+ where
+ child.parent = parent.name
+ and parent.docstatus = 1
+ and parent.is_return = 1
+ and child.purchase_receipt_item = %(row_name)s
+ """, {"row_name": item.name})
+ returned_qty = returned_qty[0][0] if returned_qty else 0
+
+ returned_amount = flt(returned_qty) * flt(item.rate)
+ pending_amount = flt(item.amount) - returned_amount
+ total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
+
+ total_amount += total_billable_amount
+ total_billed_amount += flt(item.billed_amt)
+
+ print(total_billed_amount, total_amount)
+ percent_billed = round(100 * (total_billed_amount / total_amount), 6)
+ pr_doc.db_set("per_billed", percent_billed)
+ pr_doc.load_from_db()
+
+ if update_modified:
+ pr_doc.set_status(update=True)
+ pr_doc.notify_update()
+
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
@@ -562,6 +595,7 @@ def make_purchase_invoice(source_name, target_doc=None):
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 20ae56feeb3..84c64aa8f85 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -31,6 +31,7 @@
"retain_sample",
"sample_quantity",
"tracking_section",
+ "received_stock_qty",
"stock_qty",
"col_break_tracking_section",
"returned_qty",
@@ -854,12 +855,18 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "received_stock_qty",
+ "fieldtype": "Float",
+ "label": "Received Qty in Stock UOM",
+ "print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-09 13:39:46.452817",
+ "modified": "2020-11-02 10:00:38.204294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
From 3991b84b2bf26d30402d33ef479ba8af5843c220 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 2 Nov 2020 15:23:41 +0530
Subject: [PATCH 018/286] chore: Avoid multiline string in Translation & remove
print statement
---
erpnext/controllers/stock_controller.py | 4 ++--
erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 196279fa5c8..4436ab07e56 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -229,8 +229,8 @@ class StockController(AccountsController):
def check_expense_account(self, item):
if not item.get("expense_account"):
- frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
- Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
+ frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense Account in the Items table")
+ .format(item.idx, frappe.bold(item.item_code)),
title=_("Expense Account Missing"))
else:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index be3ff5e5c2d..c37740cc7d3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -567,7 +567,6 @@ def update_billing_percentage(pr_doc, update_modified=True):
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
- print(total_billed_amount, total_amount)
percent_billed = round(100 * (total_billed_amount / total_amount), 6)
pr_doc.db_set("per_billed", percent_billed)
pr_doc.load_from_db()
From f21e3fbf04c157ff74d40b88cb7bd4bc7d7578ca Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 3 Nov 2020 12:01:56 +0530
Subject: [PATCH 019/286] chore: Tests
- Added test for mapping secnd return doc
- Added test for billing % of partially returned doc
- Handled PR with 0 billing amount
---
.../delivery_note/test_delivery_note.py | 26 ++++++++++++
.../purchase_receipt/purchase_receipt.py | 7 +++-
.../purchase_receipt/test_purchase_receipt.py | 41 +++++++++++++++++--
3 files changed, 68 insertions(+), 6 deletions(-)
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 9f273d7959d..fa07a2510ca 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -255,6 +255,32 @@ class TestDeliveryNote(unittest.TestCase):
self.assertEqual(dn.items[0].returned_qty, 2)
self.assertEqual(dn.per_returned, 40)
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ return_dn_2 = make_return_doc("Delivery Note", dn.name)
+
+ # Check if unreturned amount is mapped in 2nd return
+ self.assertEqual(return_dn_2.items[0].qty, -3)
+
+ si = make_sales_invoice(dn.name)
+ si.submit()
+
+ self.assertEqual(si.items[0].qty, 3)
+
+ dn.load_from_db()
+ # DN should be completed on billing all unreturned amount
+ self.assertEqual(dn.items[0].billed_amt, 1500)
+ self.assertEqual(dn.per_billed, 100)
+ self.assertEqual(dn.status, 'Completed')
+
+ si.load_from_db()
+ si.cancel()
+
+ dn.load_from_db()
+ self.assertEqual(dn.per_billed, 0)
+
+ dn1.cancel()
+ dn.cancel()
+
def test_sales_return_for_non_bundled_items_full(self):
from erpnext.stock.doctype.item.test_item import make_item
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index c37740cc7d3..1852985b1de 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -513,7 +513,7 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
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
+ # Get all Purchase Receipt Item rows against the Purchase 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.purchase_order_item=%s
@@ -544,6 +544,9 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
return updated_pr
def update_billing_percentage(pr_doc, update_modified=True):
+ # Reload as billed amount was set in db directly
+ pr_doc.load_from_db()
+
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
for item in pr_doc.items:
@@ -567,7 +570,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
- percent_billed = round(100 * (total_billed_amount / total_amount), 6)
+ percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
pr_doc.load_from_db()
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index aef5bf3959c..c23d6c2b53d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -100,7 +100,10 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no}))
def test_purchase_receipt_gl_entry(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, get_taxes_and_charges = True)
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
+ get_multiple_items = True, get_taxes_and_charges = True)
+
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -245,10 +248,12 @@ class TestPurchaseReceipt(unittest.TestCase):
pr.get("items")[0].rejected_warehouse)
def test_purchase_return_partial(self):
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
-
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, do_not_submit=1)
+ return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1, return_against=pr.name, qty=-2, do_not_submit=1)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
@@ -283,6 +288,33 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr.items[0].returned_qty, 2)
self.assertEqual(pr.per_returned, 40)
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ return_pr_2 = make_return_doc("Purchase Receipt", pr.name)
+
+ # Check if unreturned amount is mapped in 2nd return
+ self.assertEqual(return_pr_2.items[0].qty, -3)
+
+ # Make PI against unreturned amount
+ pi = make_purchase_invoice(pr.name)
+ pi.submit()
+
+ self.assertEqual(pi.items[0].qty, 3)
+
+ pr.load_from_db()
+ # PR should be completed on billing all unreturned amount
+ self.assertEqual(pr.items[0].billed_amt, 150)
+ self.assertEqual(pr.per_billed, 100)
+ self.assertEqual(pr.status, 'Completed')
+
+ pi.load_from_db()
+ pi.cancel()
+
+ pr.load_from_db()
+ self.assertEqual(pr.per_billed, 0)
+
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_full(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
@@ -406,6 +438,7 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr1.per_billed, 100)
self.assertEqual(pr1.status, "Completed")
+ pr2.load_from_db()
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill")
From 53b1a9a40bb792614324316f8a933c72a92beac3 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 3 Nov 2020 15:45:25 +0530
Subject: [PATCH 020/286] chore: Add Test for missing debit account
---
.../test_opening_invoice_creation_tool.py | 53 +++++++++++++++++--
erpnext/controllers/accounts_controller.py | 6 ++-
2 files changed, 54 insertions(+), 5 deletions(-)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 54229f52470..329d84bdb7a 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -7,16 +7,18 @@ import frappe
import unittest
test_dependencies = ["Customer", "Supplier"]
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def make_invoices(self, invoice_type="Sales"):
+ def make_invoices(self, invoice_type="Sales", company=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
- args = get_opening_invoice_creation_dict(invoice_type=invoice_type)
+ args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
+ property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices()
self.assertEqual(len(invoices), 2)
@@ -27,6 +29,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
}
self.check_expected_values(invoices, expected_value)
+ si = frappe.get_doc("Sales Invoice", invoices[0])
+
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
+
+ property_setter.delete()
+
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -46,6 +55,32 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
}
self.check_expected_values(invoices, expected_value, "Purchase")
+ def test_opening_sales_invoice_creation_with_missing_debit_account(self):
+ company = make_company()
+ old_default_receivable_account = frappe.db.get_value("Company", company.name, "default_receivable_account")
+ frappe.db.set_value("Company", company.name, "default_receivable_account", "")
+
+ if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
+ cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
+ "is_group": 1, "company": "_Test Opening Invoice Company"})
+ cc.insert(ignore_mandatory=True)
+ cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
+ "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
+ cc2.insert()
+
+ frappe.db.set_value("Company", company.name, "cost_center", "Main - _TOIC")
+
+ self.make_invoices(company="_Test Opening Invoice Company")
+
+ # Check if missing debit account error raised
+ error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
+ self.assertTrue(error_log)
+
+ # teardown
+ frappe.db.set_value("Company", company.name, "default_receivable_account", old_default_receivable_account)
+ company.delete()
+ frappe.get_doc("Error Log", error_log).delete()
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@@ -76,4 +111,16 @@ def get_opening_invoice_creation_dict(**args):
})
invoice_dict.update(args)
- return invoice_dict
\ No newline at end of file
+ return invoice_dict
+
+def make_company():
+ if frappe.db.exists("Company", "_Test Opening Invoice Company"):
+ return frappe.get_doc("Company", "_Test Opening Invoice Company")
+
+ company = frappe.new_doc("Company")
+ company.company_name = "_Test Opening Invoice Company"
+ company.abbr = "_TOIC"
+ company.default_currency = "INR"
+ company.country = "India"
+ company.insert()
+ return company
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 28c73a39e91..93a79ec934e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+class AccountMissingError(frappe.ValidationError): pass
+
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
class AccountsController(TransactionBase):
@@ -736,7 +738,7 @@ class AccountsController(TransactionBase):
return self._abbr
def raise_missing_debit_credit_account_error(self, party_type, party):
- """Raise an error if debit to/credit to account does not exist"""
+ """Raise an error if debit to/credit to account does not exist."""
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
@@ -748,7 +750,7 @@ class AccountsController(TransactionBase):
message += "
" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "
"
message += "
" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "
"
- frappe.throw(message, title=_("Account Missing"))
+ frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)
def validate_party(self):
party_type, party = self.get_party()
From 7837161a3fbf52d384896b146ae1491447045078 Mon Sep 17 00:00:00 2001
From: marination
Date: Tue, 3 Nov 2020 22:09:42 +0530
Subject: [PATCH 021/286] fix: Sider
---
.../stock/doctype/purchase_receipt/test_purchase_receipt.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index c23d6c2b53d..722b2c9aead 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -316,9 +316,11 @@ class TestPurchaseReceipt(unittest.TestCase):
pr.cancel()
def test_purchase_return_full(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1")
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1)
+ return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
From aa08fb971659ffd3e801db2954d06277238a6c25 Mon Sep 17 00:00:00 2001
From: igormbq
Date: Wed, 4 Nov 2020 11:40:57 -0300
Subject: [PATCH 022/286] Add location on Asset to use make_demo
---
erpnext/demo/data/asset.json | 21 ++++++++++++++-------
erpnext/demo/data/location.json | 22 ++++++++++++++++++++++
erpnext/demo/setup/manufacture.py | 1 +
erpnext/demo/user/stock.py | 2 +-
4 files changed, 38 insertions(+), 8 deletions(-)
create mode 100644 erpnext/demo/data/location.json
diff --git a/erpnext/demo/data/asset.json b/erpnext/demo/data/asset.json
index 23029ca5e36..44db2ae9e1b 100644
--- a/erpnext/demo/data/asset.json
+++ b/erpnext/demo/data/asset.json
@@ -4,48 +4,55 @@
"item_code": "Computer",
"gross_purchase_amount": 100000,
"asset_owner": "Company",
- "available_for_use_date": "2017-01-02"
+ "available_for_use_date": "2017-01-02",
+ "location": "Main Location"
},
{
"asset_name": "Macbook Air - 1",
"item_code": "Computer",
"gross_purchase_amount": 60000,
"asset_owner": "Company",
- "available_for_use_date": "2017-10-02"
+ "available_for_use_date": "2017-10-02",
+ "location": "Avg Location"
},
{
"asset_name": "Conferrence Table",
"item_code": "Table",
"gross_purchase_amount": 30000,
"asset_owner": "Company",
- "available_for_use_date": "2018-10-02"
+ "available_for_use_date": "2018-10-02",
+ "location": "Zany Location"
},
{
"asset_name": "Lunch Table",
"item_code": "Table",
"gross_purchase_amount": 20000,
"asset_owner": "Company",
- "available_for_use_date": "2018-06-02"
+ "available_for_use_date": "2018-06-02",
+ "location": "Fletcher Location"
},
{
"asset_name": "ERPNext",
"item_code": "ERP",
"gross_purchase_amount": 100000,
"asset_owner": "Company",
- "available_for_use_date": "2018-09-02"
+ "available_for_use_date": "2018-09-02",
+ "location":"Main Location"
},
{
"asset_name": "Chair 1",
"item_code": "Chair",
"gross_purchase_amount": 10000,
"asset_owner": "Company",
- "available_for_use_date": "2018-07-02"
+ "available_for_use_date": "2018-07-02",
+ "location": "Zany Location"
},
{
"asset_name": "Chair 2",
"item_code": "Chair",
"gross_purchase_amount": 10000,
"asset_owner": "Company",
- "available_for_use_date": "2018-07-02"
+ "available_for_use_date": "2018-07-02",
+ "location": "Avg Location"
}
]
diff --git a/erpnext/demo/data/location.json b/erpnext/demo/data/location.json
new file mode 100644
index 00000000000..b521aa08c48
--- /dev/null
+++ b/erpnext/demo/data/location.json
@@ -0,0 +1,22 @@
+[
+ {
+ "location_name": "Main Location",
+ "latitude": 40.0,
+ "longitude": 20.0
+ },
+ {
+ "location_name": "Avg Location",
+ "latitude": 63.0,
+ "longitude": 99.3
+ },
+ {
+ "location_name": "Zany Location",
+ "latitude": 47.5,
+ "longitude": 10.0
+ },
+ {
+ "location_name": "Fletcher Location",
+ "latitude": 100.90,
+ "longitude": 80
+ }
+]
\ No newline at end of file
diff --git a/erpnext/demo/setup/manufacture.py b/erpnext/demo/setup/manufacture.py
index d3846369cd0..7d6b5012ea6 100644
--- a/erpnext/demo/setup/manufacture.py
+++ b/erpnext/demo/setup/manufacture.py
@@ -9,6 +9,7 @@ from erpnext.demo.domains import data
from six import iteritems
def setup_data():
+ import_json("Location")
import_json("Asset Category")
setup_item()
setup_workstation()
diff --git a/erpnext/demo/user/stock.py b/erpnext/demo/user/stock.py
index f95a6b83315..d44da7d127e 100644
--- a/erpnext/demo/user/stock.py
+++ b/erpnext/demo/user/stock.py
@@ -79,7 +79,7 @@ def make_stock_reconciliation():
if item.qty:
item.qty = item.qty - round(random.randint(1, item.qty))
try:
- stock_reco.insert(ignore_permissions=True)
+ stock_reco.insert(ignore_permissions=True, ignore_mandatory=True)
stock_reco.submit()
frappe.db.commit()
except OpeningEntryAccountError:
From 4f2a64479dfcaf6a4bc164315abb4865723f75fe Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 9 Nov 2020 17:00:09 +0530
Subject: [PATCH 023/286] fix: Patch for old loans
---
erpnext/patches/v13_0/update_old_loans.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 77239429c51..eaeda093f5f 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -70,7 +70,7 @@ def execute():
payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date
FROM `tabJournal Entry` j, `tabJournal Entry Account` a
WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s
- and account = %s
+ and a.account = %s and j.docstatus = 1
''', (loan.name, loan.loan_account), as_dict=1)
for payment in payments:
From 928dc432aba2cde284072b958c2ba3d801e4aeee Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 9 Nov 2020 17:17:12 +0530
Subject: [PATCH 024/286] fix: Reload journal entry account doc
---
erpnext/patches/v13_0/update_old_loans.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index eaeda093f5f..dd15f10e097 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -18,6 +18,7 @@ def execute():
frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail')
frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual')
frappe.reload_doc('accounts', 'doctype', 'gl_entry')
+ frappe.reload_doc('accounts', 'doctype', 'journal_entry_account')
updated_loan_types = []
From 1c969d64a2f928f78470c4dd42899d2393c6291f Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 10 Nov 2020 20:25:35 +0530
Subject: [PATCH 025/286] fix: Handle cases where same loan type is used for
multiple companies
---
erpnext/patches/v13_0/update_old_loans.py | 35 +++++++++++++++++++++--
1 file changed, 33 insertions(+), 2 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index dd15f10e097..2925f0a5bcb 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -23,7 +23,8 @@ def execute():
updated_loan_types = []
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
- 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'])
+ 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
+ filters={'docstatus': 1})
for loan in loans:
# Update details in Loan Types and Loan
@@ -39,7 +40,26 @@ def execute():
penalty_account = create_account(company=loan.company, account_type='Income Account',
account_name='Penalty Account', parent_account=group_income_account)
- if not loan_type_company:
+ # Same loan type used for multiple companies
+ if loan_type_company and loan_type_company != loan.company:
+ # get loan type for appropriate company
+ loan_type_name = frappe.get_value('Loan Type', {'company': loan.company,
+ 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account,
+ 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account,
+ 'penalty_income_account': loan.penalty_income_account}, 'name')
+
+ if not loan_type_name:
+ loan_type_name = loan.loan_type + " - " + ''.join([c[0] for c in loan.company.split()]).upper()
+ create_loan_type(loan, loan_type_name, penalty_account)
+
+ # update loan type in loan
+ frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name,
+ loan.name))
+
+ if loan_type_name not in updated_loan_types:
+ updated_loan_types.append(loan_type_name)
+
+ elif not loan_type_company:
loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type)
loan_type_doc.is_term_loan = 1
loan_type_doc.company = loan.company
@@ -87,3 +107,14 @@ def execute():
jv.flags.ignore_links = True
jv.cancel()
+def create_loan_type(loan, loan_type_name, penalty_account):
+ loan_type_doc = frappe.new_doc('Loan Type')
+ loan_type_doc.loan_name = loan_type_name
+ loan_type_doc.is_term_loan = 1
+ loan_type_doc.company = loan.company
+ loan_type_doc.mode_of_payment = loan.mode_of_payment
+ loan_type_doc.payment_account = loan.payment_account
+ loan_type_doc.loan_account = loan.loan_account
+ loan_type_doc.interest_income_account = loan.interest_income_account
+ loan_type_doc.penalty_income_account = penalty_account
+ loan_type_doc.submit()
From 73bde45bc5f488906f911cef57e6035712b38cb0 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 10 Nov 2020 22:08:02 +0530
Subject: [PATCH 026/286] fix: Pass updated loan type
---
erpnext/patches/v13_0/update_old_loans.py | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 2925f0a5bcb..c16c2c81b86 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -29,6 +29,7 @@ def execute():
for loan in loans:
# Update details in Loan Types and Loan
loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company')
+ loan_type = loan.loan_type
group_income_account = frappe.get_value('Account', {'company': loan.company,
'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')})
@@ -56,6 +57,7 @@ def execute():
frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name,
loan.name))
+ loan_type = loan_type_name
if loan_type_name not in updated_loan_types:
updated_loan_types.append(loan_type_name)
@@ -70,8 +72,9 @@ def execute():
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
updated_loan_types.append(loan.loan_type)
+ loan_type = loan.loan_type
- if loan.loan_type in updated_loan_types:
+ if loan_type in updated_loan_types:
if loan.status == 'Fully Disbursed':
status = 'Disbursed'
elif loan.status == 'Repaid/Closed':
@@ -85,7 +88,7 @@ def execute():
'status': status
})
- process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type,
+ process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type,
loan=loan.name)
payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date
@@ -96,7 +99,7 @@ def execute():
for payment in payments:
repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant,
- loan.loan_type, loan.company)
+ loan_type, loan.company)
repayment_entry.amount_paid = payment.debit_in_account_currency
repayment_entry.posting_date = payment.posting_date
From 13d1dda74b88c8c46d0e3adf618433e687c6cda2 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 11 Nov 2020 11:07:17 +0530
Subject: [PATCH 027/286] fix: Handle loan type naming collisions
---
erpnext/patches/v13_0/update_old_loans.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index c16c2c81b86..70c1b7eb39f 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -51,7 +51,7 @@ def execute():
if not loan_type_name:
loan_type_name = loan.loan_type + " - " + ''.join([c[0] for c in loan.company.split()]).upper()
- create_loan_type(loan, loan_type_name, penalty_account)
+ loan_type_name = create_loan_type(loan, loan_type_name, penalty_account)
# update loan type in loan
frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name,
@@ -111,6 +111,10 @@ def execute():
jv.cancel()
def create_loan_type(loan, loan_type_name, penalty_account):
+
+ if frappe.db.get_value('Loan Type', loan_type_name):
+ loan_type_name = loan_type_name + '-1'
+
loan_type_doc = frappe.new_doc('Loan Type')
loan_type_doc.loan_name = loan_type_name
loan_type_doc.is_term_loan = 1
From 0dc052e635d5b9846265807af29f704105c9afc4 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 11 Nov 2020 12:57:16 +0530
Subject: [PATCH 028/286] fix: Return loan type name
---
erpnext/patches/v13_0/update_old_loans.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 70c1b7eb39f..fcadc6273e3 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -125,3 +125,5 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
+
+ return loan_type_name
From a2dc1740df6d4dea70d76d19fabadbd2dc885c2e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 11 Nov 2020 13:57:10 +0530
Subject: [PATCH 029/286] fix: Use autoname for loan creation
---
erpnext/patches/v13_0/update_old_loans.py | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index fcadc6273e3..23e4803029a 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -5,6 +5,7 @@ from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
+from frappe.model.naming import make_autoname
def execute():
@@ -50,7 +51,6 @@ def execute():
'penalty_income_account': loan.penalty_income_account}, 'name')
if not loan_type_name:
- loan_type_name = loan.loan_type + " - " + ''.join([c[0] for c in loan.company.split()]).upper()
loan_type_name = create_loan_type(loan, loan_type_name, penalty_account)
# update loan type in loan
@@ -111,12 +111,8 @@ def execute():
jv.cancel()
def create_loan_type(loan, loan_type_name, penalty_account):
-
- if frappe.db.get_value('Loan Type', loan_type_name):
- loan_type_name = loan_type_name + '-1'
-
loan_type_doc = frappe.new_doc('Loan Type')
- loan_type_doc.loan_name = loan_type_name
+ loan_type_doc.loan_name = make_autoname("Loan Type-.####")
loan_type_doc.is_term_loan = 1
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
@@ -126,4 +122,4 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
- return loan_type_name
+ return loan_type_doc.name
From b58dca8d942c18bc3ab0e1afa7ca7e744967c5c8 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 12 Nov 2020 13:37:11 +0530
Subject: [PATCH 030/286] fix: Only update open loans
---
erpnext/patches/v13_0/update_old_loans.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 23e4803029a..8ed789cf451 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -25,7 +25,7 @@ def execute():
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
- filters={'docstatus': 1})
+ filters={'docstatus': 1, 'status': ('!=', 'Closed')})
for loan in loans:
# Update details in Loan Types and Loan
From c51b340ddf46486cc98d92af03a9332c3e899517 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 12 Nov 2020 18:43:43 +0530
Subject: [PATCH 031/286] fix: Update closed loans
---
erpnext/patches/v13_0/update_old_loans.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 8ed789cf451..3042db331a9 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -23,6 +23,14 @@ def execute():
updated_loan_types = []
+ # Update old loan status as closed
+ loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule`
+ where paid = 0 and docstatus = 1""", as_dict=1)
+
+ loans_to_close = [d.parent for d in loans_list]
+
+ frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
+
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
filters={'docstatus': 1, 'status': ('!=', 'Closed')})
@@ -91,7 +99,7 @@ def execute():
process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type,
loan=loan.name)
- payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date
+ payments = frappe.db.sql(''' SELECT j.name, a.credit, a.credit_in_account_currency, j.posting_date
FROM `tabJournal Entry` j, `tabJournal Entry Account` a
WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s
and a.account = %s and j.docstatus = 1
From 78690af440ca67b3dd9de60b585522f172dfc423 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 12 Nov 2020 18:47:34 +0530
Subject: [PATCH 032/286] fix: Update only if loans to close
---
erpnext/patches/v13_0/update_old_loans.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 3042db331a9..c7f372e26f8 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -29,7 +29,8 @@ def execute():
loans_to_close = [d.parent for d in loans_list]
- frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
+ if loans_to_close:
+ frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
From a862eb25e6c51b31eafe96f0bbfdb5f20d9d3cf2 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 13 Nov 2020 17:57:57 +0530
Subject: [PATCH 033/286] fix: Make repayment entry only if amount exists
---
erpnext/patches/v13_0/update_old_loans.py | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index c7f372e26f8..c4d9bdb7af7 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -107,17 +107,18 @@ def execute():
''', (loan.name, loan.loan_account), as_dict=1)
for payment in payments:
- repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant,
- loan_type, loan.company)
+ if payment.credit_in_account_currency:
+ repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant,
+ loan_type, loan.company)
- repayment_entry.amount_paid = payment.debit_in_account_currency
- repayment_entry.posting_date = payment.posting_date
- repayment_entry.save()
- repayment_entry.submit()
+ repayment_entry.amount_paid = payment.credit_in_account_currency
+ repayment_entry.posting_date = payment.posting_date
+ repayment_entry.save()
+ repayment_entry.submit()
- jv = frappe.get_doc('Journal Entry', payment.name)
- jv.flags.ignore_links = True
- jv.cancel()
+ jv = frappe.get_doc('Journal Entry', payment.name)
+ jv.flags.ignore_links = True
+ jv.cancel()
def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc = frappe.new_doc('Loan Type')
From 642819b955205e9faa64ae1e72c697c2a7950305 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 17 Nov 2020 09:47:10 +0530
Subject: [PATCH 034/286] fix: place of supply change when address changes
---
erpnext/public/js/utils.js | 15 +++++++++++++++
erpnext/regional/india/utils.py | 1 +
erpnext/selling/sales_common.js | 1 +
3 files changed, 17 insertions(+)
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index ea2093eee10..b4fe412fe94 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -304,6 +304,21 @@ $.extend(erpnext.utils, {
}
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
+ },
+
+ set_place_of_supply: function(frm){
+ frappe.call({
+ method: "erpnext.regional.india.utils.get_place_of_supply",
+ args: {
+ "party_details": frm.doc,
+ "doctype": frm.doc.doctype
+ },
+ callback: function(r){
+ if(r.message){
+ frm.set_value("place_of_supply", r.message)
+ }
+ }
+ })
}
});
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index dd87f0f6601..c774cb03be7 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -135,6 +135,7 @@ def test_method():
'''test function'''
return 'overridden'
+@frappe.whitelist()
def get_place_of_supply(party_details, doctype):
if not frappe.get_meta('Address').has_field('gst_state'): return
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 002cfe41e18..77bdf2912f3 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -117,6 +117,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
customer_address: function() {
erpnext.utils.get_address_display(this.frm, "customer_address");
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
+ erpnext.utils.set_place_of_supply(this.frm)
},
shipping_address_name: function() {
From 6b10d87d468acdf4e810d5a4b4da4b389def6a06 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 17 Nov 2020 20:34:51 +0530
Subject: [PATCH 035/286] fix: place of supply change on address change
---
erpnext/public/js/controllers/buying.js | 1 +
erpnext/public/js/utils.js | 28 ++++++++++++-------------
erpnext/regional/india/utils.py | 3 +++
3 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index cb76c87b625..cd5cc9282b3 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -135,6 +135,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
supplier_address: function() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
+ erpnext.utils.set_place_of_supply(this.frm)
},
buying_price_list: function() {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index b4fe412fe94..1555896eac8 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -116,6 +116,19 @@ $.extend(erpnext.utils, {
}
},
+ set_place_of_supply: function(frm){
+ frappe.call({
+ method: "erpnext.regional.india.utils.get_place_of_supply",
+ args: {
+ "party_details": frm.doc,
+ "doctype": frm.doc.doctype
+ },
+ callback: function(r){
+ frm.set_value("place_of_supply", r.message)
+ }
+ })
+ },
+
add_indicator_for_multicompany: function(frm, info) {
frm.dashboard.stats_area.removeClass('hidden');
frm.dashboard.stats_area_row.addClass('flex');
@@ -304,21 +317,6 @@ $.extend(erpnext.utils, {
}
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
- },
-
- set_place_of_supply: function(frm){
- frappe.call({
- method: "erpnext.regional.india.utils.get_place_of_supply",
- args: {
- "party_details": frm.doc,
- "doctype": frm.doc.doctype
- },
- callback: function(r){
- if(r.message){
- frm.set_value("place_of_supply", r.message)
- }
- }
- })
}
});
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index c774cb03be7..7ad1c07f93a 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -138,6 +138,9 @@ def test_method():
@frappe.whitelist()
def get_place_of_supply(party_details, doctype):
if not frappe.get_meta('Address').has_field('gst_state'): return
+ if isinstance(party_details, string_types):
+ party_details = json.loads(party_details)
+ party_details = frappe._dict(party_details)
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
address_name = party_details.customer_address or party_details.shipping_address_name
From 8c9b60edfec53a8a58ace5e5a3284efbdae7b724 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Wed, 18 Nov 2020 12:51:13 +0530
Subject: [PATCH 036/286] fix: reversing previous commits and adding condition
in regional controller
---
erpnext/public/js/controllers/buying.js | 1 -
erpnext/public/js/utils.js | 13 -------------
erpnext/regional/india/taxes.js | 1 +
erpnext/regional/india/utils.py | 23 +++++++++--------------
erpnext/selling/sales_common.js | 1 -
5 files changed, 10 insertions(+), 29 deletions(-)
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index cd5cc9282b3..cb76c87b625 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -135,7 +135,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
supplier_address: function() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
- erpnext.utils.set_place_of_supply(this.frm)
},
buying_price_list: function() {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 1555896eac8..ea2093eee10 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -116,19 +116,6 @@ $.extend(erpnext.utils, {
}
},
- set_place_of_supply: function(frm){
- frappe.call({
- method: "erpnext.regional.india.utils.get_place_of_supply",
- args: {
- "party_details": frm.doc,
- "doctype": frm.doc.doctype
- },
- callback: function(r){
- frm.set_value("place_of_supply", r.message)
- }
- })
- },
-
add_indicator_for_multicompany: function(frm, info) {
frm.dashboard.stats_area.removeClass('hidden');
frm.dashboard.stats_area_row.addClass('flex');
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 3b6a28f52c0..ecfa9b7cdf5 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -37,6 +37,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
callback: function(r) {
if(r.message) {
frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
+ frm.set_value('place_of_supply', r.message.place_of_supply);
} else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) {
frm.set_value('taxes_and_charges', '');
frm.set_value('taxes', []);
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 7ad1c07f93a..54083dea847 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -135,12 +135,8 @@ def test_method():
'''test function'''
return 'overridden'
-@frappe.whitelist()
def get_place_of_supply(party_details, doctype):
if not frappe.get_meta('Address').has_field('gst_state'): return
- if isinstance(party_details, string_types):
- party_details = json.loads(party_details)
- party_details = frappe._dict(party_details)
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
address_name = party_details.customer_address or party_details.shipping_address_name
@@ -164,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
party_details.taxes = ''
- return
+ return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
@@ -172,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges') and return_taxes:
+ if party_details.get('taxes_and_charges'):
return party_details
if not party_details.company_gstin:
- return
+ return party_details
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges') and return_taxes:
+ if party_details.get('taxes_and_charges'):
return party_details
if not party_details.supplier_gstin:
- return
+ return party_details
- if not party_details.place_of_supply: return
+ if not party_details.place_of_supply: return party_details
- if not party_details.company_gstin: return
+ if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
@@ -201,12 +197,11 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
if not default_tax:
- return
+ return party_details
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
- if return_taxes:
- return party_details
+ return party_details
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 77bdf2912f3..002cfe41e18 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -117,7 +117,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
customer_address: function() {
erpnext.utils.get_address_display(this.frm, "customer_address");
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
- erpnext.utils.set_place_of_supply(this.frm)
},
shipping_address_name: function() {
From 410db04b48291e310b981b541279d0930dcf4eed Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Wed, 18 Nov 2020 15:57:16 +0530
Subject: [PATCH 037/286] fix: linter issue for translation syntax
---
erpnext/regional/india/utils.py | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 227af9cdebf..e189da7b11c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -86,7 +86,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
factor = 2 if factor == 1 else 1
if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
frappe.throw(_("""Invalid {0}! The check digit validation has failed.
- Please ensure you've typed the {0} correctly.""".format(label)))
+ Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
@@ -160,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
party_details.taxes = ''
- return party_details
+ return
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
@@ -168,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges'):
+ if party_details.get('taxes_and_charges') and return_taxes:
return party_details
if not party_details.company_gstin:
- return party_details
+ return
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges'):
+ if party_details.get('taxes_and_charges') and return_taxes:
return party_details
if not party_details.supplier_gstin:
- return party_details
+ return
- if not party_details.place_of_supply: return party_details
+ if not party_details.place_of_supply: return
- if not party_details.company_gstin: return party_details
+ if not party_details.company_gstin: return
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
@@ -197,11 +197,12 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
if not default_tax:
- return party_details
+ return
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
- return party_details
+ if return_taxes:
+ return party_details
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
@@ -235,7 +236,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
if tax_category.gst_state == number_state_mapping[state_code] or \
(not default_tax and not tax_category.gst_state):
default_tax = frappe.db.get_value(master_doctype,
- {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
+ {'disabled': 0, 'tax_category': tax_category.name}, 'name')
return default_tax
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
From cd05b34691d7ef50d06791820afddb184259e1b3 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 19 Nov 2020 11:37:08 +0530
Subject: [PATCH 038/286] fix: company filter added again
---
erpnext/regional/india/utils.py | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index e189da7b11c..c6620aa92b8 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -160,7 +160,7 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
party_details.taxes = ''
- return
+ return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
@@ -168,26 +168,26 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges') and return_taxes:
+ if party_details.get('taxes_and_charges'):
return party_details
if not party_details.company_gstin:
- return
+ return party_details
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges') and return_taxes:
+ if party_details.get('taxes_and_charges'):
return party_details
if not party_details.supplier_gstin:
- return
+ return party_details
- if not party_details.place_of_supply: return
+ if not party_details.place_of_supply: return party_details
- if not party_details.company_gstin: return
+ if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
@@ -197,12 +197,11 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N
default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
if not default_tax:
- return
+ return party_details
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
- if return_taxes:
- return party_details
+ return party_details
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
@@ -236,7 +235,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
if tax_category.gst_state == number_state_mapping[state_code] or \
(not default_tax and not tax_category.gst_state):
default_tax = frappe.db.get_value(master_doctype,
- {'disabled': 0, 'tax_category': tax_category.name}, 'name')
+ {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
return default_tax
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
From c1187bc1d598d6d771f2e0379b654a63adddb84b Mon Sep 17 00:00:00 2001
From: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
Date: Thu, 19 Nov 2020 13:10:24 +0530
Subject: [PATCH 039/286] fix: duplicate items validation for POS Invoice when
allow multiple items is disabled (#23896)
* fix: duplicate items validation for POS when allow multiple items in disabled
* fix: variable name change for duplicate item validation
Co-authored-by: pateljannat
---
erpnext/controllers/selling_controller.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 7504746e078..515239a982f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -416,26 +416,26 @@ class SellingController(StockController):
return
for d in self.get('items'):
- if self.doctype == "Sales Invoice":
- e = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
- f = [d.item_code, d.description, d.sales_order or d.delivery_note]
+ if self.doctype in ["POS Invoice","Sales Invoice"]:
+ stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
+ non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
- e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
- f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
+ stock_items = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
+ non_stock_items = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
elif self.doctype in ["Sales Order", "Quotation"]:
- e = [d.item_code, d.description, d.warehouse, '']
- f = [d.item_code, d.description]
+ stock_items = [d.item_code, d.description, d.warehouse, '']
+ non_stock_items = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
- if e in check_list:
+ if stock_items in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
- check_list.append(e)
+ check_list.append(stock_items)
else:
- if f in chk_dupl_itm:
+ if non_stock_items in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
- chk_dupl_itm.append(f)
+ chk_dupl_itm.append(non_stock_items)
def validate_target_warehouse(self):
items = self.get("items") + (self.get("packed_items") or [])
From f0b1670abc331bf7aad8eb7d746482648b710e65 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 19 Nov 2020 18:40:13 +0530
Subject: [PATCH 040/286] fix: tds test case
---
.../test_tax_withholding_category.py | 12 ++++++++----
.../buying/doctype/supplier/test_supplier.py | 17 +++++++++++++++++
2 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index a0b0cbb9956..ef77674372b 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -7,6 +7,7 @@ import frappe
import unittest
from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"]
@@ -103,17 +104,20 @@ class TestTaxWithholdingCategory(unittest.TestCase):
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
invoices = []
- frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
+ tax_withholding_category="Single Threshold TDS")
+ supplier = doc.name
+
+ pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
# TDS not applied
- pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
+ pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
pi.submit()
invoices.append(pi)
- pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+ pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index a377ec90f8b..f9c8d35518d 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -120,3 +120,20 @@ class TestSupplier(unittest.TestCase):
# Rollback
address.delete()
+
+def create_supplier(**args):
+ args = frappe._dict(args)
+
+ try:
+ doc = frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": args.supplier_name,
+ "supplier_group": args.supplier_group or "Services",
+ "supplier_type": args.supplier_type or "Company",
+ "tax_withholding_category": args.tax_withholding_category
+ }).insert()
+
+ return doc
+
+ except frappe.DuplicateEntryError:
+ return frappe.get_doc("Supplier", args.supplier_name)
\ No newline at end of file
From 8aeb340dc8bd87651a73bbfdef9e3d2c681de878 Mon Sep 17 00:00:00 2001
From: Smit Vora
Date: Thu, 19 Nov 2020 19:18:48 +0530
Subject: [PATCH 041/286] fix: add remarks to sales invoice
---
.../doctype/sales_invoice/sales_invoice.py | 9 ++++--
erpnext/patches.txt | 1 +
.../v12_0/update_sales_invoice_remarks.py | 32 +++++++++++++++++++
3 files changed, 40 insertions(+), 2 deletions(-)
create mode 100644 erpnext/patches/v12_0/update_sales_invoice_remarks.py
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index af6c6968dc1..0530aa2d234 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
import frappe.defaults
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form
+from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc
@@ -535,7 +535,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc)
def add_remarks(self):
- if not self.remarks: self.remarks = 'No Remarks'
+ if not self.remarks:
+ if self.po_no and self.po_date:
+ self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
+ formatdate(self.po_date))
+ else:
+ self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 25be8841174..4a38cb3ab80 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
erpnext.patches.v13_0.update_custom_fields_for_shopify
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
+erpnext.patches.v12_0.update_sales_invoice_remarks
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/update_sales_invoice_remarks.py b/erpnext/patches/v12_0/update_sales_invoice_remarks.py
new file mode 100644
index 00000000000..7e8feaaca6c
--- /dev/null
+++ b/erpnext/patches/v12_0/update_sales_invoice_remarks.py
@@ -0,0 +1,32 @@
+from __future__ import unicode_literals
+import frappe
+
+from frappe import _
+from frappe.utils import formatdate
+
+def execute():
+ si_list = frappe.db.get_all('Sales Invoice', filters = {
+ 'docstatus': 1,
+ 'remarks': 'No Remarks',
+ 'po_no' : ['!=', ''],
+ 'po_date' : ['!=', '']
+ },
+ fields = ['name', 'po_no', 'po_date']
+ )
+
+ for doc in si_list:
+ remarks = _("Against Customer Order {0} dated {1}").format(doc.po_no,
+ formatdate(doc.po_date))
+
+ frappe.db.set_value('Sales Invoice', doc.name, 'remarks', remarks)
+
+ gl_entry_list = frappe.db.get_all('GL Entry', filters = {
+ 'voucher_type': 'Sales Invoice',
+ 'remarks': 'No Remarks',
+ 'voucher_no' : doc.name
+ },
+ fields = ['name']
+ )
+
+ for entry in gl_entry_list:
+ frappe.db.set_value('GL Entry', entry.name, 'remarks', remarks)
\ No newline at end of file
From 1d5d863e9a7e71c540e9ba032ef042e218667a56 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 19 Nov 2020 20:11:45 +0530
Subject: [PATCH 042/286] fix: removing return_taxes condition
---
erpnext/regional/india/taxes.js | 3 +--
erpnext/regional/india/utils.py | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index ecfa9b7cdf5..3c156479c58 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -31,8 +31,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
args: {
party_details: JSON.stringify(party_details),
doctype: frm.doc.doctype,
- company: frm.doc.company,
- return_taxes: 1
+ company: frm.doc.company
},
callback: function(r) {
if(r.message) {
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index c6620aa92b8..8d89335717a 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -150,7 +150,7 @@ def get_place_of_supply(party_details, doctype):
return cstr(address.gst_state_number) + "-" + cstr(address.gst_state)
@frappe.whitelist()
-def get_regional_address_details(party_details, doctype, company, return_taxes=None):
+def get_regional_address_details(party_details, doctype, company):
if isinstance(party_details, string_types):
party_details = json.loads(party_details)
party_details = frappe._dict(party_details)
From ceab692f7313acfb11704dd50a72738a9c3be9c0 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Wed, 18 Nov 2020 17:57:35 +0530
Subject: [PATCH 043/286] fix: incorrect delink serial no and batch
---
erpnext/controllers/stock_controller.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index f743d707f75..2d2fff8fd54 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -229,9 +229,9 @@ class StockController(AccountsController):
def check_expense_account(self, item):
if not item.get("expense_account"):
- frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
- Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
- title=_("Expense Account Missing"))
+ msg = _("Please set an Expense Account in the Items table")
+ frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}")
+ .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else:
is_expense_account = frappe.db.get_value("Account",
@@ -247,7 +247,9 @@ class StockController(AccountsController):
for d in self.items:
if not d.batch_no: continue
- serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})]
+ serial_nos = [sr.name for sr in frappe.get_all("Serial No",
+ {'batch_no': d.batch_no, 'status': 'Inactive'})]
+
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
From 34d07b630669a6d18e3289df9561a50dcc3371fa Mon Sep 17 00:00:00 2001
From: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
Date: Fri, 20 Nov 2020 19:22:35 +0530
Subject: [PATCH 044/286] fix: purchase receipt to purchase invoice bill date
mapping (#23967)
Co-authored-by: pateljannat
---
erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index d9646698307..2cc4679c8c6 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -572,7 +572,8 @@ def make_purchase_invoice(source_name, target_doc=None):
"doctype": "Purchase Invoice",
"field_map": {
"supplier_warehouse":"supplier_warehouse",
- "is_return": "is_return"
+ "is_return": "is_return",
+ "bill_date": "bill_date"
},
"validation": {
"docstatus": ["=", 1],
From 20d6143c382a677db5464a0b383962e746c4b3a5 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 21 Nov 2020 12:09:11 +0530
Subject: [PATCH 045/286] fix: Validation for journal entry with 0 debit and
credit values
---
erpnext/accounts/doctype/journal_entry/journal_entry.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d8394785c6b..0b3205523f7 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -34,6 +34,7 @@ class JournalEntry(AccountsController):
self.validate_entries_for_advance()
self.validate_multi_currency()
self.set_amounts_in_company_currency()
+ self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
@@ -369,6 +370,11 @@ class JournalEntry(AccountsController):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
+ def validate_debit_credit_amount(self):
+ for d in self.get('accounts'):
+ if not flt(d.debit) and not flt(d.credit):
+ frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
+
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
From 610d9ca64937366da56ad2ef7610def4c5d36b57 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Mon, 23 Nov 2020 13:47:49 +0530
Subject: [PATCH 046/286] fix: bom stock report color issue
---
.../report/bom_stock_report/bom_stock_report.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 2ac6fa073bf..45331c6af82 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -25,11 +25,11 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (column.id == "Item"){
+ if (column.id == "item"){
if (data["Enough Parts to Build"] > 0){
- value = `${data['Item']}`
+ value = `${data['item']}`;
} else {
- value = `${data['Item']}`
+ value = `${data['item']}`;
}
}
return value
From 34f381df172f7c40db4f51317a22d8f29868b0d3 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Mon, 23 Nov 2020 15:31:08 +0530
Subject: [PATCH 047/286] fix: enabling track changes for stock settings
---
erpnext/stock/doctype/stock_settings/stock_settings.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 067659f64a1..a1666579d12 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -217,7 +217,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 10:33:29.147682",
+ "modified": "2020-11-23 15:26:54.225608",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
@@ -235,5 +235,6 @@
],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
From f9a44000d9b06517dc2fd10916eac3f8d0c02b8e Mon Sep 17 00:00:00 2001
From: rohitwaghchaure
Date: Mon, 23 Nov 2020 15:40:06 +0530
Subject: [PATCH 048/286] Update bom_stock_report.js
---
.../manufacturing/report/bom_stock_report/bom_stock_report.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 45331c6af82..84f5c346ca3 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -25,8 +25,8 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (column.id == "item"){
- if (data["Enough Parts to Build"] > 0){
+ if (column.id == "item") {
+ if (data["Enough Parts to Build"] > 0) {
value = `${data['item']}`;
} else {
value = `${data['item']}`;
From 6aa6ec1832d9c4caf00e4f113845b47f80dd92bb Mon Sep 17 00:00:00 2001
From: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Date: Tue, 24 Nov 2020 08:01:19 +0530
Subject: [PATCH 049/286] fix: clear error message when approval not available
(#23971)
---
.../department_approver/department_approver.py | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py
index 9b2de0e1cbc..d337959d534 100644
--- a/erpnext/hr/doctype/department_approver/department_approver.py
+++ b/erpnext/hr/doctype/department_approver/department_approver.py
@@ -20,7 +20,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = []
department_details = {}
department_list = []
- employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
+ employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department
if employee_department:
@@ -59,11 +59,9 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
if len(approvers) == 0:
- frappe.throw(_("Please set {0} for the Employee or for Department: {1}").
- format(
- field_name, frappe.bold(employee_department),
- frappe.bold(employee.name)
- ),
- title=_(field_name + " Missing"))
+ error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
+ if department_list:
+ error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
+ frappe.throw(error_msg, title=_(field_name + " Missing"))
return set(tuple(approver) for approver in approvers)
From d07447aa5fbeed93e72e882230e6b571a47b6611 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Tue, 24 Nov 2020 08:09:17 +0530
Subject: [PATCH 050/286] fix: Validation for duplicate Tax Category (#23978)
* fix: Validation for duplicate Tax Category
* Update utils.py
Co-authored-by: Nabin Hait
---
erpnext/hooks.py | 3 +++
erpnext/regional/india/utils.py | 14 ++++++++++----
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index b4c57d7c915..741176f33f4 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -237,6 +237,9 @@ doc_events = {
"Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
},
+ "Tax Category": {
+ "validate": "erpnext.regional.india.utils.validate_tax_category"
+ },
"Sales Invoice": {
"on_submit": [
"erpnext.regional.create_transaction_log",
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index fc38ed0972e..62487ba2aa0 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -51,6 +51,13 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
+def validate_tax_category(doc, method):
+ if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
+ if doc.is_inter_state:
+ frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
+ else:
+ frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
+
def update_gst_category(doc, method):
for link in doc.links:
if link.link_doctype in ['Customer', 'Supplier']:
@@ -85,8 +92,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
total += digit
factor = 2 if factor == 1 else 1
if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
- frappe.throw(_("""Invalid {0}! The check digit validation has failed.
- Please ensure you've typed the {0} correctly.""").format(label))
+ frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
@@ -515,7 +521,7 @@ def get_address_details(data, doc, company_address, billing_address):
data.transType = 1
data.actualToStateCode = data.toStateCode
shipping_address = billing_address
-
+
if doc.gst_category == 'SEZ':
data.toStateCode = 99
@@ -754,4 +760,4 @@ def make_regional_gl_entries(gl_entries, doc):
}, account_currency, item=tax)
)
- return gl_entries
\ No newline at end of file
+ return gl_entries
From e09037ed2c1e5cf634fbd993e1d135cae84b0673 Mon Sep 17 00:00:00 2001
From: Krushnal Patel
Date: Tue, 24 Nov 2020 12:53:30 +0530
Subject: [PATCH 051/286] docs: README build status badge (#23933)
* fixed build status badge
* changed build branch from `master` to `develop`
* updated build status badge url
* Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0f6a52142bf..15782a2e0c4 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
ERP made simple
-[](https://travis-ci.com/frappe/erpnext)
+[](https://travis-ci.com/frappe/erpnext)
[](https://www.codetriage.com/frappe/erpnext)
[](https://coveralls.io/github/frappe/erpnext?branch=develop)
From 927106f5528bfebe8d43ae24ac627ae9965720d5 Mon Sep 17 00:00:00 2001
From: Anurag Mishra
Date: Tue, 24 Nov 2020 15:02:52 +0530
Subject: [PATCH 052/286] fix: maintain stock can't be changed it there is
product bundle
---
erpnext/stock/doctype/item/item.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 3b62c38b866..be845d9d9d5 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -977,15 +977,20 @@ class Item(WebsiteGenerator):
# For "Is Stock Item", following doctypes is important
# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
if field == "is_stock_item":
- linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item"]
+ linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"]
for doctype in linked_doctypes:
+ filters={"item_code": self.name, "docstatus": 1}
+
+ if doctype == "Product Bundle":
+ filters={"new_item_code": self.name}
+
if doctype in ("Purchase Invoice Item", "Sales Invoice Item",):
# If Invoice has Stock impact, only then consider it.
if self.stock_ledger_created():
return True
- elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}):
+ elif frappe.db.get_value(doctype, filters):
return True
def validate_auto_reorder_enabled_in_stock_settings(self):
From 43a830f3f593f463f695c7419e1b7961ff96d79d Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 24 Nov 2020 15:10:36 +0530
Subject: [PATCH 053/286] fix: Old shopify order syncing date
---
.../connectors/shopify_connection.py | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py
index 8aa7453bd6b..efbaa71924e 100644
--- a/erpnext/erpnext_integrations/connectors/shopify_connection.py
+++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py
@@ -149,26 +149,28 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal
si.shopify_order_number = shopify_order.get("name")
si.set_posting_time = 1
si.posting_date = posting_date
+ si.due_date = posting_date
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
si.flags.ignore_mandatory = True
set_cost_center(si.items, shopify_settings.cost_center)
si.insert(ignore_mandatory=True)
si.submit()
- make_payament_entry_against_sales_invoice(si, shopify_settings)
+ make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date)
frappe.db.commit()
def set_cost_center(items, cost_center):
for item in items:
item.cost_center = cost_center
-def make_payament_entry_against_sales_invoice(doc, shopify_settings):
+def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
- payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
- payemnt_entry.flags.ignore_mandatory = True
- payemnt_entry.reference_no = doc.name
- payemnt_entry.reference_date = nowdate()
- payemnt_entry.insert(ignore_permissions=True)
- payemnt_entry.submit()
+ payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
+ payment_entry.flags.ignore_mandatory = True
+ payment_entry.reference_no = doc.name
+ payment_entry.posting_date = posting_date or nowdate()
+ payment_entry.reference_date = posting_date or nowdate()
+ payment_entry.insert(ignore_permissions=True)
+ payment_entry.submit()
def create_delivery_note(shopify_order, shopify_settings, so):
if not cint(shopify_settings.sync_delivery_note):
From b67ebc7636187f1e4ee508579a77cbb6a7e223d1 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 24 Nov 2020 15:37:30 +0530
Subject: [PATCH 054/286] fix: job card error handling for operations field
---
.../doctype/job_card/job_card.py | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 4dfa78bf217..d15d81ed93d 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -353,17 +353,19 @@ def get_operation_details(work_order, operation):
@frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters):
- if filters.get("work_order"):
- args = {"parent": filters.get("work_order")}
- if txt:
- args["operation"] = ("like", "%{0}%".format(txt))
+ if not filters.get("work_order"):
+ frappe.msgprint(_("Please select a Work Order first."))
+ return []
+ args = {"parent": filters.get("work_order")}
+ if txt:
+ args["operation"] = ("like", "%{0}%".format(txt))
- return frappe.get_all("Work Order Operation",
- filters = args,
- fields = ["distinct operation as operation"],
- limit_start = start,
- limit_page_length = page_len,
- order_by="idx asc", as_list=1)
+ return frappe.get_all("Work Order Operation",
+ filters = args,
+ fields = ["distinct operation as operation"],
+ limit_start = start,
+ limit_page_length = page_len,
+ order_by="idx asc", as_list=1)
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
From 5a33f2c394aceb03be6b00e142e6dea25695d1d3 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 24 Nov 2020 16:59:36 +0530
Subject: [PATCH 055/286] fix: bom stock report color showing always red
---
.../manufacturing/report/bom_stock_report/bom_stock_report.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 84f5c346ca3..8cd016461cc 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -26,7 +26,7 @@ frappe.query_reports["BOM Stock Report"] = {
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.id == "item") {
- if (data["Enough Parts to Build"] > 0) {
+ if (data["enough_parts_to_build"] > 0) {
value = `${data['item']}`;
} else {
value = `${data['item']}`;
From e4755828c4690404939ad7a5cc53a0d6ca988e18 Mon Sep 17 00:00:00 2001
From: Mohammad Hasnain Mohsin Rajan
Date: Tue, 24 Nov 2020 23:31:06 +0530
Subject: [PATCH 056/286] fix: template errors in pricing rule (#23999)
* fix: solve microtemplating errors
---
.../doctype/pricing_rule/pricing_rule.js | 30 +++++++++----------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
index c92b58b5809..d79ad5f528f 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
@@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {
- ${__('Notes')}
+ {{__('Notes')}}
- ${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
+ {{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
- ${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}
+ {{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
- ${__('Discount Percentage can be applied either against a Price List or for all Price List.')}
+ {{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
- ${__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}
+ {{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
- ${__('How Pricing Rule is applied?')}
+ {{__('How Pricing Rule is applied?')}}
- ${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}
+ {{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
- ${__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}
+ {{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
- ${__('Pricing Rules are further filtered based on quantity.')}
+ {{__('Pricing Rules are further filtered based on quantity.')}}
- ${__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}
+ {{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
- ${__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}
+ {{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
- ${__('Item Code > Item Group > Brand')}
+ {{__('Item Code > Item Group > Brand')}}
- ${__('Customer > Customer Group > Territory')}
+ {{__('Customer > Customer Group > Territory')}}
- ${__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}
+ {{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
From a339752ba4419a5a00530286499282ebb0cb4216 Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Wed, 25 Nov 2020 08:54:51 +0530
Subject: [PATCH 057/286] fix: Loan disbursement amount validation (#24000)
---
erpnext/loan_management/doctype/loan/loan.json | 3 ++-
.../doctype/loan_disbursement/loan_disbursement.py | 13 ++++++++-----
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index e8ecf015c37..d468f52bc0f 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -332,6 +332,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.is_secured_loan",
"fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
@@ -352,7 +353,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-05 10:04:00.762975",
+ "modified": "2020-11-24 12:27:23.208240",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 233862bcfe0..f341e81065f 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -171,10 +171,10 @@ def get_total_pledged_security_value(loan):
return security_value
@frappe.whitelist()
-def get_disbursal_amount(loan):
- loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment",
- "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"],
- filters= { "name": loan })[0]
+def get_disbursal_amount(loan, on_current_security_price=0):
+ loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
+ "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
+ "maximum_loan_amount"], as_dict=1)
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
@@ -188,9 +188,12 @@ def get_disbursal_amount(loan):
- flt(loan_details.total_principal_paid)
security_value = 0.0
- if loan_details.is_secured_loan:
+ if loan_details.is_secured_loan and on_current_security_price:
security_value = get_total_pledged_security_value(loan)
+ if loan_details.is_secured_loan and not on_current_security_price:
+ security_value = flt(loan_details.maximum_loan_amount)
+
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
From c66bd45ba46b9e7c6ebd54eda42db4e6fb57761a Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 25 Nov 2020 09:09:40 +0530
Subject: [PATCH 058/286] feat: Inpatient Medication Orders Script Report
(#23984)
* feat: Inpatient Medication Orders Script Report
* feat: add chart for Inpatient Medication Order Report
* feat: add report to Desk Page
* feat: added filters for dates and healthcare service unit
* test: Inpatient Medication Orders report
---
.../desk_page/healthcare/healthcare.json | 4 +-
.../inpatient_medication_entry.py | 4 +-
.../inpatient_medication_orders/__init__.py | 0
.../inpatient_medication_orders.js | 57 +++++
.../inpatient_medication_orders.json | 36 ++++
.../inpatient_medication_orders.py | 198 ++++++++++++++++++
.../test_inpatient_medication_orders.py | 128 +++++++++++
7 files changed, 424 insertions(+), 3 deletions(-)
create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/__init__.py
create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js
create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json
create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py
create mode 100644 erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json
index 6546b08db99..81d60481ce6 100644
--- a/erpnext/healthcare/desk_page/healthcare/healthcare.json
+++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json
@@ -43,7 +43,7 @@
{
"hidden": 0,
"label": "Reports",
- "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]"
+ "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Inpatient Medication Orders\",\n\t\t\"doctype\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Orders\"\n\t}\n]"
}
],
"category": "Domains",
@@ -64,7 +64,7 @@
"idx": 0,
"is_standard": 1,
"label": "Healthcare",
- "modified": "2020-06-25 23:50:56.951698",
+ "modified": "2020-11-23 23:00:48.764377",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
index 23e75196ee1..5dac23abd90 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -274,4 +274,6 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
- return ip_record.inpatient_occupancies[-1].service_unit
\ No newline at end of file
+ if ip_record.inpatient_occupancies:
+ return ip_record.inpatient_occupancies[-1].service_unit
+ return
\ No newline at end of file
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/__init__.py b/erpnext/healthcare/report/inpatient_medication_orders/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js
new file mode 100644
index 00000000000..a10f83760fa
--- /dev/null
+++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.js
@@ -0,0 +1,57 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Inpatient Medication Orders"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ fieldname: "to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.now_date(),
+ reqd: 1
+ },
+ {
+ fieldname: "patient",
+ label: __("Patient"),
+ fieldtype: "Link",
+ options: "Patient"
+ },
+ {
+ fieldname: "service_unit",
+ label: __("Healthcare Service Unit"),
+ fieldtype: "Link",
+ options: "Healthcare Service Unit",
+ get_query: () => {
+ var company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: {
+ 'company': company,
+ 'is_group': 0
+ }
+ }
+ }
+ },
+ {
+ fieldname: "show_completed_orders",
+ label: __("Show Completed Orders"),
+ fieldtype: "Check",
+ default: 1
+ }
+ ]
+};
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json
new file mode 100644
index 00000000000..9217fa18919
--- /dev/null
+++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-11-23 17:25:58.802949",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2020-11-23 19:40:20.227591",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Inpatient Medication Orders",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Inpatient Medication Order",
+ "report_name": "Inpatient Medication Orders",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Healthcare Administrator"
+ },
+ {
+ "role": "Nursing User"
+ },
+ {
+ "role": "Physician"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py
new file mode 100644
index 00000000000..b9077301bad
--- /dev/null
+++ b/erpnext/healthcare/report/inpatient_medication_orders/inpatient_medication_orders.py
@@ -0,0 +1,198 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ chart = get_chart_data(data)
+
+ return columns, data, None, chart
+
+def get_columns():
+ return [
+ {
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "label": "Patient",
+ "options": "Patient",
+ "width": 200
+ },
+ {
+ "fieldname": "healthcare_service_unit",
+ "fieldtype": "Link",
+ "label": "Healthcare Service Unit",
+ "options": "Healthcare Service Unit",
+ "width": 150
+ },
+ {
+ "fieldname": "drug",
+ "fieldtype": "Link",
+ "label": "Drug Code",
+ "options": "Item",
+ "width": 150
+ },
+ {
+ "fieldname": "drug_name",
+ "fieldtype": "Data",
+ "label": "Drug Name",
+ "width": 150
+ },
+ {
+ "fieldname": "dosage",
+ "fieldtype": "Link",
+ "label": "Dosage",
+ "options": "Prescription Dosage",
+ "width": 80
+ },
+ {
+ "fieldname": "dosage_form",
+ "fieldtype": "Link",
+ "label": "Dosage Form",
+ "options": "Dosage Form",
+ "width": 100
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "time",
+ "fieldtype": "Time",
+ "label": "Time",
+ "width": 100
+ },
+ {
+ "fieldname": "is_completed",
+ "fieldtype": "Check",
+ "label": "Is Order Completed",
+ "width": 100
+ },
+ {
+ "fieldname": "healthcare_practitioner",
+ "fieldtype": "Link",
+ "label": "Healthcare Practitioner",
+ "options": "Healthcare Practitioner",
+ "width": 200
+ },
+ {
+ "fieldname": "inpatient_medication_entry",
+ "fieldtype": "Link",
+ "label": "Inpatient Medication Entry",
+ "options": "Inpatient Medication Entry",
+ "width": 200
+ },
+ {
+ "fieldname": "inpatient_record",
+ "fieldtype": "Link",
+ "label": "Inpatient Record",
+ "options": "Inpatient Record",
+ "width": 200
+ }
+ ]
+
+def get_data(filters):
+ conditions, values = get_conditions(filters)
+
+ data = frappe.db.sql("""
+ SELECT
+ parent.patient, parent.inpatient_record, parent.practitioner,
+ child.drug, child.drug_name, child.dosage, child.dosage_form,
+ child.date, child.time, child.is_completed, child.name
+ FROM `tabInpatient Medication Order` parent
+ INNER JOIN `tabInpatient Medication Order Entry` child
+ ON child.parent = parent.name
+ WHERE
+ parent.docstatus = 1
+ {conditions}
+ ORDER BY date, time
+ """.format(conditions=conditions), values, as_dict=1)
+
+ data = get_inpatient_details(data, filters.get("service_unit"))
+
+ return data
+
+def get_conditions(filters):
+ conditions = ""
+ values = dict()
+
+ if filters.get("company"):
+ conditions += " AND parent.company = %(company)s"
+ values["company"] = filters.get("company")
+
+ if filters.get("from_date") and filters.get("to_date"):
+ conditions += " AND child.date BETWEEN %(from_date)s and %(to_date)s"
+ values["from_date"] = filters.get("from_date")
+ values["to_date"] = filters.get("to_date")
+
+ if filters.get("patient"):
+ conditions += " AND parent.patient = %(patient)s"
+ values["patient"] = filters.get("patient")
+
+ if not filters.get("show_completed_orders"):
+ conditions += " AND child.is_completed = 0"
+
+ return conditions, values
+
+
+def get_inpatient_details(data, service_unit):
+ service_unit_filtered_data = []
+
+ for entry in data:
+ entry["healthcare_service_unit"] = get_current_healthcare_service_unit(entry.inpatient_record)
+ if entry.is_completed:
+ entry["inpatient_medication_entry"] = get_inpatient_medication_entry(entry.name)
+
+ if service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit:
+ service_unit_filtered_data.append(entry)
+
+ entry.pop("name", None)
+
+ for entry in service_unit_filtered_data:
+ data.remove(entry)
+
+ return data
+
+def get_inpatient_medication_entry(order_entry):
+ return frappe.db.get_value("Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent")
+
+def get_chart_data(data):
+ if not data:
+ return None
+
+ labels = ["Pending", "Completed"]
+ datasets = []
+
+ status_wise_data = {
+ "Pending": 0,
+ "Completed": 0
+ }
+
+ for d in data:
+ if d.is_completed:
+ status_wise_data["Completed"] += 1
+ else:
+ status_wise_data["Pending"] += 1
+
+ datasets.append({
+ "name": "Inpatient Medication Order Status",
+ "values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")]
+ })
+
+ chart = {
+ "data": {
+ "labels": labels,
+ "datasets": datasets
+ },
+ "type": "donut",
+ "height": 300
+ }
+
+ chart["fieldtype"] = "Data"
+
+ return chart
\ No newline at end of file
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
new file mode 100644
index 00000000000..0d3f45f5000
--- /dev/null
+++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import unittest
+import frappe
+import datetime
+from frappe.utils import getdate, now_datetime
+from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
+from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
+from erpnext.healthcare.report.inpatient_medication_orders.inpatient_medication_orders import execute
+
+class TestInpatientMedicationOrders(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ frappe.db.sql("delete from `tabInpatient Medication Order` where company='_Test Company'")
+ frappe.db.sql("delete from `tabInpatient Medication Entry` where company='_Test Company'")
+ self.patient = create_patient()
+ self.ip_record = create_records(self.patient)
+
+ def test_inpatient_medication_orders_report(self):
+ filters = {
+ 'company': '_Test Company',
+ 'from_date': getdate(),
+ 'to_date': getdate(),
+ 'patient': '_Test IPD Patient',
+ 'service_unit': 'Test Service Unit Ip Occupancy - _TC'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'patient': '_Test IPD Patient',
+ 'inpatient_record': self.ip_record.name,
+ 'practitioner': None,
+ 'drug': 'Dextromethorphan',
+ 'drug_name': 'Dextromethorphan',
+ 'dosage': 1.0,
+ 'dosage_form': 'Tablet',
+ 'date': getdate(),
+ 'time': datetime.timedelta(seconds=32400),
+ 'is_completed': 0,
+ 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
+ },
+ {
+ 'patient': '_Test IPD Patient',
+ 'inpatient_record': self.ip_record.name,
+ 'practitioner': None,
+ 'drug': 'Dextromethorphan',
+ 'drug_name': 'Dextromethorphan',
+ 'dosage': 1.0,
+ 'dosage_form': 'Tablet',
+ 'date': getdate(),
+ 'time': datetime.timedelta(seconds=50400),
+ 'is_completed': 0,
+ 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
+ },
+ {
+ 'patient': '_Test IPD Patient',
+ 'inpatient_record': self.ip_record.name,
+ 'practitioner': None,
+ 'drug': 'Dextromethorphan',
+ 'drug_name': 'Dextromethorphan',
+ 'dosage': 1.0,
+ 'dosage_form': 'Tablet',
+ 'date': getdate(),
+ 'time': datetime.timedelta(seconds=75600),
+ 'is_completed': 0,
+ 'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1])
+
+ filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
+ ipme = create_ipme(filters)
+ ipme.submit()
+
+ filters = {
+ 'company': '_Test Company',
+ 'from_date': getdate(),
+ 'to_date': getdate(),
+ 'patient': '_Test IPD Patient',
+ 'service_unit': 'Test Service Unit Ip Occupancy - _TC',
+ 'show_completed_orders': 0
+ }
+
+ report = execute(filters)
+ self.assertEqual(len(report[1]), 0)
+
+ def tearDown(self):
+ if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
+ # cleanup - Discharge
+ schedule_discharge(frappe.as_json({'patient': self.patient}))
+ self.ip_record.reload()
+ mark_invoiced_inpatient_occupancy(self.ip_record)
+
+ self.ip_record.reload()
+ discharge_patient(self.ip_record)
+
+ for entry in frappe.get_all('Inpatient Medication Entry'):
+ doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
+ doc.cancel()
+ doc.delete()
+
+ for entry in frappe.get_all('Inpatient Medication Order'):
+ doc = frappe.get_doc('Inpatient Medication Order', entry.name)
+ doc.cancel()
+ doc.delete()
+
+
+def create_records(patient):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+
+ # Admit
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save()
+ ip_record.reload()
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ ipmo = create_ipmo(patient)
+ ipmo.submit()
+
+ return ip_record
From fbcc3c1b7006069d9bb9739ef57dc3776675a3b4 Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Wed, 25 Nov 2020 04:41:51 +0100
Subject: [PATCH 059/286] fix: Translatable strings (#23783)
* fix: start_pattern
* fix: translatable strings
* fix: add missing semicolon (task)
* fix: add missing semicolon (setup_wizard)
* fix: text should start on the same line
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* fix: move out HTML element as variable
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* fix: pull out message, translate "Undo".
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* fix: typo
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* fix: text should start on the same line
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
* Revert "fix: start_pattern"
This reverts commit decc62e2ab75f45db1df022fe13780c2d0d2560d.
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
---
.../chart_of_accounts_importer.js | 3 +--
.../bank_reconciliation.js | 6 ++----
erpnext/assets/doctype/asset/asset.js | 6 +++---
.../appointment_booking_settings.js | 2 +-
erpnext/projects/doctype/task/task.js | 5 ++++-
erpnext/public/js/hub/pages/Category.vue | 2 +-
erpnext/public/js/hub/pages/FeaturedItems.vue | 12 +++++-------
erpnext/public/js/hub/pages/Item.vue | 8 ++++----
erpnext/public/js/hub/pages/NotFound.vue | 2 +-
erpnext/public/js/hub/pages/Publish.vue | 19 +++++++------------
erpnext/public/js/hub/pages/SavedItems.vue | 11 ++++++++---
erpnext/public/js/hub/pages/Search.vue | 5 ++++-
erpnext/public/js/hub/pages/Seller.vue | 4 ++--
erpnext/public/js/setup_wizard.js | 5 ++++-
14 files changed, 47 insertions(+), 43 deletions(-)
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
index 2235298201f..f795dfa83e6 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
@@ -94,8 +94,7 @@ frappe.ui.form.on('Chart of Accounts Importer', {
callback: function(r) {
if(r.message===false) {
frm.set_value("company", "");
- frappe.throw(__(`Transactions against the company already exist!
- Chart Of accounts can be imported for company with no transactions`));
+ frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."));
} else {
frm.trigger("refresh");
}
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
index 97035278754..6ae81d74021 100644
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
+++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
@@ -156,7 +156,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
setup_transactions_dom() {
const me = this;
- me.parent.$main_section.append(``)
+ me.parent.$main_section.append('');
}
create_datatable() {
@@ -167,9 +167,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
})
}
catch(err) {
- let msg = __(`Your file could not be processed by ERPNext.
- It should be a standard CSV or XLSX file.
- The headers should be in the first row.`)
+ let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
frappe.throw(msg)
}
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 7ad164a8b9b..b2318a2bc62 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -373,8 +373,8 @@ frappe.ui.form.on('Asset', {
doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, '');
frappe.msgprint({
- title: __(`Invalid ${doctype}`),
- message: __(`The selected ${doctype} doesn't contains selected Asset Item.`),
+ title: __('Invalid {0}', [__(doctype)]),
+ message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]),
indicator: 'red'
});
}
@@ -436,7 +436,7 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn];
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
- frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`));
+ frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date."));
book.depreciation_start_date = "";
frm.refresh_field("finance_books");
}
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
index 99b82148d2e..dc3ae8bf41a 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
@@ -4,7 +4,7 @@ function check_times(frm) {
let from_time = Date.parse('01/01/2019 ' + d.from_time);
let to_time = Date.parse('01/01/2019 ' + d.to_time);
if (from_time > to_time) {
- frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`));
+ frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1]));
}
});
}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js
index 8c6a9cf8d7c..002ddb2f409 100644
--- a/erpnext/projects/doctype/task/task.js
+++ b/erpnext/projects/doctype/task/task.js
@@ -49,7 +49,10 @@ frappe.ui.form.on("Task", {
},
callback: function (r) {
if (r.message.length > 0) {
- frappe.msgprint(__(`Cannot convert it to non-group. The following child Tasks exist: ${r.message.join(", ")}.`));
+ let message = __('Cannot convert Task to non-group because the following child Tasks exist: {0}.',
+ [r.message.join(", ")]
+ );
+ frappe.msgprint(message);
frm.reload_doc();
}
}
diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue
index 057fe8bc617..16d06018ff0 100644
--- a/erpnext/public/js/hub/pages/Category.vue
+++ b/erpnext/public/js/hub/pages/Category.vue
@@ -32,7 +32,7 @@ export default {
item_id_fieldname: 'name',
// Constants
- empty_state_message: __(`No items in this category yet.`),
+ empty_state_message: __('No items in this category yet.'),
search_value: '',
diff --git a/erpnext/public/js/hub/pages/FeaturedItems.vue b/erpnext/public/js/hub/pages/FeaturedItems.vue
index ab9990a3230..63ae7e99bbd 100644
--- a/erpnext/public/js/hub/pages/FeaturedItems.vue
+++ b/erpnext/public/js/hub/pages/FeaturedItems.vue
@@ -33,10 +33,8 @@ export default {
// Constants
page_title: __('Your Featured Items'),
- empty_state_message: __(`No featured items yet. Got to your
-
- Published Items
- and feature upto 8 items that you want to highlight to your customers.`)
+ empty_state_message: __('No featured items yet. Got to your {0} and feature up to eight items that you want to highlight to your customers.',
+ [`${__("Published Items")}`])
};
},
created() {
@@ -71,9 +69,9 @@ export default {
const item_name = this.items.filter(item => item.hub_item_name === hub_item_name);
- alert = frappe.show_alert(__(`${item_name} removed.
- Undo`),
- grace_period/1000,
+ alert_message = __('{0} removed. {1}', [item_name,
+ `${__('Undo')}`]);
+ alert = frappe.show_alert(alert_message, grace_period / 1000,
{
'undo-remove': undo_remove.bind(this)
}
diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue
index 51ade42cbae..93002a7b27a 100644
--- a/erpnext/public/js/hub/pages/Item.vue
+++ b/erpnext/public/js/hub/pages/Item.vue
@@ -113,12 +113,12 @@ export default {
let stats = __('No views yet');
if (this.item.view_count) {
- const views_message = __(`${this.item.view_count} Views`);
+ const views_message = __('{0} Views', [this.item.view_count]);
const rating_html = get_rating_html(this.item.average_rating);
const rating_count =
this.item.no_of_ratings > 0
- ? `${this.item.no_of_ratings} reviews`
+ ? __('{0} reviews', [this.item.no_of_ratings])
: __('No reviews yet');
stats = [views_message, rating_html, rating_count];
@@ -310,7 +310,7 @@ export default {
return this.get_item_details();
})
.then(() => {
- frappe.show_alert(__(`${this.item.item_name} Updated`));
+ frappe.show_alert(__('{0} Updated', [this.item.item_name]));
});
},
@@ -337,7 +337,7 @@ export default {
},
unpublish_item() {
- frappe.confirm(__(`Unpublish {0}?`, [this.item.item_name]), () => {
+ frappe.confirm(__('Unpublish {0}?', [this.item.item_name]), () => {
frappe
.call('erpnext.hub_node.api.unpublish_item', {
item_code: this.item.item_code,
diff --git a/erpnext/public/js/hub/pages/NotFound.vue b/erpnext/public/js/hub/pages/NotFound.vue
index 246d31bc681..8901b97802d 100644
--- a/erpnext/public/js/hub/pages/NotFound.vue
+++ b/erpnext/public/js/hub/pages/NotFound.vue
@@ -27,7 +27,7 @@ export default {
},
// Constants
- empty_state_message: __(`Sorry! I could not find what you were looking for.`)
+ empty_state_message: __('Sorry! We could not find what you were looking for.')
};
},
}
diff --git a/erpnext/public/js/hub/pages/Publish.vue b/erpnext/public/js/hub/pages/Publish.vue
index 735f2b92eca..96fa0aae4e5 100644
--- a/erpnext/public/js/hub/pages/Publish.vue
+++ b/erpnext/public/js/hub/pages/Publish.vue
@@ -75,14 +75,11 @@ export default {
// TODO: multiline translations don't work
page_title: __('Publish Items'),
search_placeholder: __('Search Items ...'),
- empty_state_message: __(`No Items selected yet. Browse and click on items below to publish.`),
- valid_items_instruction: __(`Only items with an image and description can be published. Please update them if an item in your inventory does not appear.`),
+ empty_state_message: __('No Items selected yet. Browse and click on items below to publish.'),
+ valid_items_instruction: __('Only items with an image and description can be published. Please update them if an item in your inventory does not appear.'),
last_sync_message: (hub.settings.last_sync_datetime)
- ? __(`Last sync was
-
- ${comment_when(hub.settings.last_sync_datetime)}.
-
- See your Published Items.`)
+ ? __('Last sync was {0}.', [`${comment_when(hub.settings.last_sync_datetime)}`]) +
+ ` ${__('See your Published Items.')}`
: ''
};
},
@@ -147,11 +144,9 @@ export default {
},
add_last_sync_message() {
- this.last_sync_message = __(`Last sync was
-
- ${comment_when(hub.settings.last_sync_datetime)}.
-
- See your Published Items.`);
+ this.last_sync_message = __('Last sync was {0}.',
+ [`${comment_when(hub.settings.last_sync_datetime)}`]
+ ) + `${__('See your Published Items')}.`;
},
clear_last_sync_message() {
diff --git a/erpnext/public/js/hub/pages/SavedItems.vue b/erpnext/public/js/hub/pages/SavedItems.vue
index c29675acd30..7007ddcf8e7 100644
--- a/erpnext/public/js/hub/pages/SavedItems.vue
+++ b/erpnext/public/js/hub/pages/SavedItems.vue
@@ -29,7 +29,7 @@ export default {
// Constants
page_title: __('Saved Items'),
- empty_state_message: __(`You haven't saved any items yet.`)
+ empty_state_message: __('You have not saved any items yet.')
};
},
created() {
@@ -64,8 +64,13 @@ export default {
const item_name = this.items.filter(item => item.hub_item_name === hub_item_name);
- alert = frappe.show_alert(__(`${item_name} removed.
- Undo`),
+ alert = frappe.show_alert(`
+
+ ${__('{0} removed.', [item_name], 'A specific Item has been removed.')}
+
+ ${__('Undo', None, 'Undo removal of item.')}
+
+ `,
grace_period/1000,
{
'undo-remove': undo_remove.bind(this)
diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue
index 103284289bb..c10841e9848 100644
--- a/erpnext/public/js/hub/pages/Search.vue
+++ b/erpnext/public/js/hub/pages/Search.vue
@@ -42,7 +42,10 @@ export default {
computed: {
page_title() {
return this.items.length
- ? __(`Results for "${this.search_value}" ${this.category !== 'All'? `in category ${this.category}` : ''}`)
+ ? __('Results for "{0}" {1}', [
+ this.search_value,
+ this.category !== 'All' ? __('in category {0}', [this.category]) : ''
+ ])
: __('No Items found.');
}
},
diff --git a/erpnext/public/js/hub/pages/Seller.vue b/erpnext/public/js/hub/pages/Seller.vue
index e339eaa3e5b..c0903c64c37 100644
--- a/erpnext/public/js/hub/pages/Seller.vue
+++ b/erpnext/public/js/hub/pages/Seller.vue
@@ -136,7 +136,7 @@ export default {
this.init = false;
this.profile = data.profile;
this.items = data.items;
- this.item_container_heading = data.is_featured_item? "Features Items":"Popular Items";
+ this.item_container_heading = data.is_featured_item ? __('Featured Items') : __('Popular Items');
this.hub_seller = this.items[0].hub_seller;
this.recent_seller_reviews = data.recent_seller_reviews;
this.seller_product_view_stats = data.seller_product_view_stats;
@@ -147,7 +147,7 @@ export default {
this.country = __(profile.country);
this.site_name = __(profile.site_name);
- this.joined_when = __(`Joined ${comment_when(profile.creation)}`);
+ this.joined_when = __('Joined {0}', [comment_when(profile.creation)]);
this.image = profile.logo;
this.sections = [
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 5d21190e378..092f83903ea 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -161,7 +161,10 @@ erpnext.setup.slides_settings = [
if(r.message){
exist = r.message;
me.get_field("bank_account").set_value("");
- frappe.msgprint(__(`Account ${me.values.bank_account} already exists, enter a different name for your bank account`));
+ let message = __('Account {0} already exists. Please enter a different name for your bank account.',
+ [me.values.bank_account]
+ );
+ frappe.msgprint(message);
}
}
});
From 0508e6bdfaaac6771ec65e4e84fc5798ddf4785e Mon Sep 17 00:00:00 2001
From: Nabin Hait
Date: Wed, 25 Nov 2020 09:16:12 +0530
Subject: [PATCH 060/286] fix: Added link of bank reconciliation and clearance
in accounting desk page (#23809)
---
erpnext/accounts/desk_page/accounting/accounting.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json
index b2a3f83e5fd..a18dbffd9ab 100644
--- a/erpnext/accounts/desk_page/accounting/accounting.json
+++ b/erpnext/accounts/desk_page/accounting/accounting.json
@@ -43,7 +43,7 @@
{
"hidden": 0,
"label": "Bank Statement",
- "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
From 6b57cf32854bac4fc95e1c27beb67a2494aba4bb Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 25 Nov 2020 09:17:16 +0530
Subject: [PATCH 061/286] feat: Quality Inspection on Job Card (#23964)
* feat: Quality Inspection on Job Card
* fix(Job Card): quality inspection filter query
* fix: sider issues
---
.../doctype/job_card/job_card.js | 10 +++
.../doctype/job_card/job_card.json | 11 ++-
.../quality_inspection/quality_inspection.js | 24 ++++--
.../quality_inspection.json | 4 +-
.../quality_inspection/quality_inspection.py | 85 +++++++++++++------
5 files changed, 96 insertions(+), 38 deletions(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index b051b3243fd..4e8dd41022b 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -31,6 +31,16 @@ frappe.ui.form.on('Job Card', {
}
}
+ frm.set_query("quality_inspection", function() {
+ return {
+ query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
+ filters: {
+ "item_code": frm.doc.production_item,
+ "reference_name": frm.doc.name
+ }
+ };
+ });
+
frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 575e7190430..5713f697e99 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -20,6 +20,7 @@
"production_item",
"item_name",
"for_quantity",
+ "quality_inspection",
"wip_warehouse",
"column_break_12",
"employee",
@@ -305,11 +306,19 @@
"label": "Sequence Id",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal;",
+ "fieldname": "quality_inspection",
+ "fieldtype": "Link",
+ "label": "Quality Inspection",
+ "no_copy": 1,
+ "options": "Quality Inspection"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-14 12:58:25.327897",
+ "modified": "2020-11-19 18:26:50.531664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 22f29e05b49..376848afaa4 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -31,17 +31,27 @@ frappe.ui.form.on("Quality Inspection", {
// item code based on GRN/DN
cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
- const doctype = (doc.reference_type == "Stock Entry") ?
- "Stock Entry Detail" : doc.reference_type + " Item";
+ let doctype = doc.reference_type;
+
+ if (doc.reference_type !== "Job Card") {
+ doctype = (doc.reference_type == "Stock Entry") ?
+ "Stock Entry Detail" : doc.reference_type + " Item";
+ }
if (doc.reference_type && doc.reference_name) {
+ let filters = {
+ "from": doctype,
+ "inspection_type": doc.inspection_type
+ };
+
+ if (doc.reference_type == doctype)
+ filters["reference_name"] = doc.reference_name;
+ else
+ filters["parent"] = doc.reference_name;
+
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
- filters: {
- "from": doctype,
- "parent": doc.reference_name,
- "inspection_type": doc.inspection_type
- }
+ filters: filters
};
}
},
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index dd95075e284..f6d76194d94 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -73,7 +73,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry",
+ "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
"reqd": 1
},
{
@@ -236,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-21 13:03:11.938072",
+ "modified": "2020-11-19 17:06:05.409963",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 399a63a1860..ae4eb9b9956 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -53,16 +53,28 @@ class QualityInspection(Document):
def update_qc_reference(self):
quality_inspection = self.name if self.docstatus == 1 else ""
- doctype = self.reference_type + ' Item'
- if self.reference_type == 'Stock Entry':
- doctype = 'Stock Entry Detail'
- if self.reference_type and self.reference_name:
- frappe.db.sql("""update `tab{child_doc}` t1, `tab{parent_doc}` t2
- set t1.quality_inspection = %s, t2.modified = %s
- where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name"""
- .format(parent_doc=self.reference_type, child_doc=doctype),
- (quality_inspection, self.modified, self.reference_name, self.item_code))
+ if self.reference_type == 'Job Card':
+ if self.reference_name:
+ frappe.db.sql("""
+ UPDATE `tab{doctype}`
+ SET quality_inspection = %s, modified = %s
+ WHERE name = %s and production_item = %s
+ """.format(doctype=self.reference_type),
+ (quality_inspection, self.modified, self.reference_name, self.item_code))
+
+ else:
+ doctype = self.reference_type + ' Item'
+ if self.reference_type == 'Stock Entry':
+ doctype = 'Stock Entry Detail'
+
+ if self.reference_type and self.reference_name:
+ frappe.db.sql("""
+ UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2
+ SET t1.quality_inspection = %s, t2.modified = %s
+ WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name
+ """.format(parent_doc=self.reference_type, child_doc=doctype),
+ (quality_inspection, self.modified, self.reference_name, self.item_code))
def set_status_based_on_acceptance_formula(self):
for reading in self.readings:
@@ -95,27 +107,44 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
mcond = get_match_cond(filters["from"])
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
- if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\
- and filters.get("inspection_type") != "In Process":
- cond = """and item_code in (select name from `tabItem` where
- inspection_required_before_purchase = 1)"""
- elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\
- and filters.get("inspection_type") != "In Process":
- cond = """and item_code in (select name from `tabItem` where
- inspection_required_before_delivery = 1)"""
- elif filters.get('from') == 'Stock Entry Detail':
- cond = """and s_warehouse is null"""
+ if filters.get("parent"):
+ if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\
+ and filters.get("inspection_type") != "In Process":
+ cond = """and item_code in (select name from `tabItem` where
+ inspection_required_before_purchase = 1)"""
+ elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\
+ and filters.get("inspection_type") != "In Process":
+ cond = """and item_code in (select name from `tabItem` where
+ inspection_required_before_delivery = 1)"""
+ elif filters.get('from') == 'Stock Entry Detail':
+ cond = """and s_warehouse is null"""
- if filters.get('from') in ['Supplier Quotation Item']:
- qi_condition = ""
+ if filters.get('from') in ['Supplier Quotation Item']:
+ qi_condition = ""
- return frappe.db.sql(""" select item_code from `tab{doc}`
- where parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
- {qi_condition} {cond} {mcond}
- order by item_code limit {start}, {page_len}""".format(doc=filters.get('from'),
- parent=filters.get('parent'), cond = cond, mcond = mcond, start = start,
- page_len = page_len, qi_condition = qi_condition),
- {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt})
+ return frappe.db.sql("""
+ SELECT item_code
+ FROM `tab{doc}`
+ WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
+ {qi_condition} {cond} {mcond}
+ ORDER BY item_code limit {start}, {page_len}
+ """.format(doc=filters.get('from'),
+ cond = cond, mcond = mcond, start = start,
+ page_len = page_len, qi_condition = qi_condition),
+ {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt})
+
+ elif filters.get("reference_name"):
+ return frappe.db.sql("""
+ SELECT production_item
+ FROM `tab{doc}`
+ WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
+ {qi_condition} {cond} {mcond}
+ ORDER BY production_item
+ LIMIT {start}, {page_len}
+ """.format(doc=filters.get("from"),
+ cond = cond, mcond = mcond, start = start,
+ page_len = page_len, qi_condition = qi_condition),
+ {'reference_name': filters.get('reference_name'), 'txt': "%%%s%%" % txt})
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
From 7824e812980df8e772c4cf0af90c33ad741585a6 Mon Sep 17 00:00:00 2001
From: Anurag Mishra
Date: Wed, 25 Nov 2020 14:54:50 +0530
Subject: [PATCH 062/286] fix: ignore exception during leave ledger creation
from patch
---
.../doctype/leave_application/leave_application.py | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 3f25f583833..e9bcfb3a8ba 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -376,24 +376,32 @@ class LeaveApplication(Document):
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
+ raise_exception = True
+ if frappe.flags.in_patch:
+ raise_exception=False
+
args = dict(
leaves=self.total_leave_days * -1,
from_date=self.from_date,
to_date=self.to_date,
is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee)
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
''' splits leave application into two ledger entries to consider expiry of allocation '''
+
+ raise_exception = True
+ if frappe.flags.in_patch:
+ raise_exception=False
+
args = dict(
from_date=self.from_date,
to_date=expiry_date,
leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee),
-
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
)
create_leave_ledger_entry(self, args, submit)
From e15b6a91dea531c555f4e60c6929c7740d3a7422 Mon Sep 17 00:00:00 2001
From: Mohammad Hasnain Mohsin Rajan
Date: Wed, 25 Nov 2020 15:36:41 +0530
Subject: [PATCH 063/286] Filters for tax templates (#23998)
* feat: add company filter to tax templates
* fix: remove filer from PO because it is from tran
* fix: linting
* fix: solve translation string issues
* fix: remove doctype name
---
erpnext/accounts/doctype/pos_profile/pos_profile.js | 9 +++++++++
erpnext/public/js/controllers/transaction.js | 11 +++++++++++
erpnext/selling/sales_common.js | 12 +-----------
3 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 558e21c13aa..7f4f7554807 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -35,6 +35,15 @@ frappe.ui.form.on('POS Profile', {
};
});
+ frm.set_query("taxes_and_charges", function() {
+ return {
+ filters: [
+ ['Sales Taxes and Charges Template', 'company', '=', frm.doc.company],
+ ['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
+ ]
+ };
+ });
+
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 1358a4bd088..7f08cd1359f 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -209,6 +209,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
});
}
+ if (this.frm.fields_dict.taxes_and_charges) {
+ this.frm.set_query("taxes_and_charges", function() {
+ return {
+ filters: [
+ ['company', '=', me.frm.doc.company],
+ ['docstatus', '!=', 2]
+ ]
+ };
+ });
+ }
+
},
onload: function() {
var me = this;
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 002cfe41e18..7f00fca8f05 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -42,16 +42,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
- if(this.frm.fields_dict.taxes_and_charges) {
- this.frm.set_query("taxes_and_charges", function() {
- return {
- filters: [
- ['Sales Taxes and Charges Template', 'company', '=', me.frm.doc.company],
- ['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
- ]
- }
- });
- }
if(this.frm.fields_dict.selling_price_list) {
this.frm.set_query("selling_price_list", function() {
@@ -479,7 +469,7 @@ frappe.ui.form.on(cur_frm.doctype,"project", function(frm) {
$.each(frm.doc["items"] || [], function(i, row) {
if(r.message) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
- frappe.msgprint(__("Cost Center For Item with Item Code '"+row.item_name+"' has been Changed to "+ r.message));
+ frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
}
})
}
From e60a62bde5b59c9d067d220d20aa4346795816ff Mon Sep 17 00:00:00 2001
From: Prssanna Desai
Date: Wed, 25 Nov 2020 15:37:02 +0530
Subject: [PATCH 064/286] fix: function imports in account_balance_timeline.py
(#24003)
---
.../account_balance_timeline/account_balance_timeline.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index 39bf4b053a7..85f54f98ba8 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -6,9 +6,8 @@ import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
-from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
-
+from frappe.utils.dashboard import cache_source
+from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()
From 90e33e53fd649f5a52461a9403106aab4b89aeed Mon Sep 17 00:00:00 2001
From: Mohammad Hasnain Mohsin Rajan
Date: Wed, 25 Nov 2020 15:37:54 +0530
Subject: [PATCH 065/286] refactor: Format translation strings (#24004)
* fix: translation strings
* fix: linting
---
erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +--
.../doctype/clinical_procedure/clinical_procedure.js | 6 ++----
erpnext/public/js/controllers/buying.js | 3 +--
erpnext/selling/doctype/sales_order/sales_order.js | 3 +--
erpnext/selling/page/point_of_sale/pos_controller.js | 3 +--
erpnext/setup/doctype/sales_person/sales_person.js | 3 +--
6 files changed, 7 insertions(+), 14 deletions(-)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d8394785c6b..ab4bfb14ecb 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -339,8 +339,7 @@ class JournalEntry(AccountsController):
currency=account_currency)
if flt(voucher_total) < (flt(order.advance_paid) + total):
- frappe.throw(_("Advance paid against {0} {1} cannot be greater \
- than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
+ frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index eb7d4bdebad..1d4411d73de 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (r.message) {
frappe.show_alert({
- message: __('Stock Entry {0} created',
- ['' + r.message + '']),
+ message: __('Stock Entry {0} created', ['' + r.message + '']),
indicator: 'green'
});
}
@@ -105,8 +104,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (!r.exc) {
if (r.message == 'insufficient stock') {
- let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?',
- [frm.doc.warehouse.bold()]);
+ let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]);
frappe.confirm(
msg,
function() {
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 58ac38f0a85..3f5652aa5dd 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -218,8 +218,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
var is_negative_qty = false;
for(var i = 0; i%(name)s', {name:d})
}).join(', ')]),
indicator: 'green'
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 970d8406654..ad1633e71dc 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -644,8 +644,7 @@ erpnext.PointOfSale.Controller = class {
})
} else if (available_qty < qty_needed) {
frappe.show_alert({
- message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.',
- [bold_item_code, bold_warehouse, bold_available_qty]),
+ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange'
});
frappe.utils.play_sound("error");
diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js
index 8f7593d6eef..b71a92f8a98 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.js
+++ b/erpnext/setup/doctype/sales_person/sales_person.js
@@ -5,8 +5,7 @@ frappe.ui.form.on('Sales Person', {
refresh: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
var info = frm.doc.__onload.dashboard_info;
- frm.dashboard.add_indicator(__('Total Contribution Amount: {0}',
- [format_currency(info.allocated_amount, info.currency)]), 'blue');
+ frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
}
},
From f32cff1080f9412ed27a843d1f573021d56d5db5 Mon Sep 17 00:00:00 2001
From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
Date: Wed, 25 Nov 2020 16:00:15 +0530
Subject: [PATCH 066/286] feat : Leave type with partial payment (#23173)
* feat: Partially paid Leaves
* feat: some importatnt validation
* fix: requested changes
* fix: requested changes
* fix: travis, sider, codacy
* fix: changes requested
* test: Partially Paid Leaves
---
erpnext/hr/doctype/employee/employee.json | 4 +-
.../leave_application/leave_application.py | 6 +-
erpnext/hr/doctype/leave_type/leave_type.json | 19 +++++-
erpnext/hr/doctype/leave_type/leave_type.py | 6 ++
.../hr/doctype/leave_type/test_leave_type.py | 5 ++
.../doctype/salary_slip/salary_slip.js | 42 ++++++------
.../doctype/salary_slip/salary_slip.py | 64 +++++++++++++------
.../doctype/salary_slip/test_salary_slip.py | 19 +++++-
8 files changed, 117 insertions(+), 48 deletions(-)
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index da789198e51..4cabe97cc4d 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -672,10 +672,10 @@
"oldfieldtype": "Date"
},
{
- "depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
+ "mandatory_depends_on": "eval:doc.status == \"Left\"",
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@@ -822,7 +822,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2020-10-06 15:58:23.805489",
+ "modified": "2020-10-16 14:41:10.580897",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 3f25f583833..ca79dff1154 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -130,8 +130,7 @@ class LeaveApplication(Document):
if self.status == "Approved":
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
- status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
-
+ status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
@@ -293,7 +292,8 @@ class LeaveApplication(Document):
def set_half_day_date(self):
if self.from_date == self.to_date and self.half_day == 1:
self.half_day_date = self.from_date
- elif self.half_day == 0:
+
+ if self.half_day == 0:
self.half_day_date = None
def notify_employee(self):
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 0af832f903e..4a135e0ffec 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -15,6 +15,8 @@
"column_break_3",
"is_carry_forward",
"is_lwp",
+ "is_ppl",
+ "fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"include_holiday",
@@ -77,6 +79,7 @@
},
{
"default": "0",
+ "depends_on": "eval:doc.is_ppl == 0",
"fieldname": "is_lwp",
"fieldtype": "Check",
"label": "Is Leave Without Pay"
@@ -183,12 +186,26 @@
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.is_lwp == 0",
+ "fieldname": "is_ppl",
+ "fieldtype": "Check",
+ "label": "Is Partially Paid Leave"
+ },
+ {
+ "depends_on": "eval:doc.is_ppl == 1",
+ "fieldname": "fraction_of_daily_salary_per_leave",
+ "fieldtype": "Float",
+ "label": "Fraction of Daily Salary per Leave",
+ "mandatory_depends_on": "eval:doc.is_ppl == 1"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2019-12-12 12:48:37.780254",
+ "modified": "2020-08-26 14:04:54.318687",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py
index c0d12968416..21f180b857d 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.py
+++ b/erpnext/hr/doctype/leave_type/leave_type.py
@@ -21,3 +21,9 @@ class LeaveType(Document):
leave_allocation = [l['name'] for l in leave_allocation]
if leave_allocation:
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
+
+ if self.is_lwp and self.is_ppl:
+ frappe.throw(_("Leave Type can be either without pay or partial pay"))
+
+ if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
+ frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))
diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py
index 0c4f435860a..7fef2975c8a 100644
--- a/erpnext/hr/doctype/leave_type/test_leave_type.py
+++ b/erpnext/hr/doctype/leave_type/test_leave_type.py
@@ -18,9 +18,14 @@ def create_leave_type(**args):
"allow_encashment": args.allow_encashment or 0,
"is_earned_leave": args.is_earned_leave or 0,
"is_lwp": args.is_lwp or 0,
+ "is_ppl":args.is_ppl or 0,
"is_carry_forward": args.is_carry_forward or 0,
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment"
})
+
+ if leave_type.is_ppl:
+ leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
+
return leave_type
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index 7b69dbe8d6d..0671b570d1d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -13,12 +13,12 @@ frappe.ui.form.on("Salary Slip", {
];
});
- frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function(){
+ frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() {
return {
filters: {
employee: frm.doc.employee
}
- }
+ };
};
frm.set_query("salary_component", "earnings", function() {
@@ -26,7 +26,7 @@ frappe.ui.form.on("Salary Slip", {
filters: {
type: "earning"
}
- }
+ };
});
frm.set_query("salary_component", "deductions", function() {
@@ -34,18 +34,18 @@ frappe.ui.form.on("Salary Slip", {
filters: {
type: "deduction"
}
- }
+ };
});
frm.set_query("employee", function() {
- return{
+ return {
query: "erpnext.controllers.queries.employee_query"
- }
+ };
});
},
- start_date: function(frm){
- if(frm.doc.start_date){
+ start_date: function(frm) {
+ if (frm.doc.start_date) {
frm.trigger("set_end_date");
}
},
@@ -54,7 +54,7 @@ frappe.ui.form.on("Salary Slip", {
frm.events.get_emp_and_working_day_details(frm);
},
- set_end_date: function(frm){
+ set_end_date: function(frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: {
@@ -66,22 +66,22 @@ frappe.ui.form.on("Salary Slip", {
frm.set_value('end_date', r.message.end_date);
}
}
- })
+ });
},
company: function(frm) {
var company = locals[':Company'][frm.doc.company];
- if(!frm.doc.letter_head && company.default_letter_head) {
+ if (!frm.doc.letter_head && company.default_letter_head) {
frm.set_value('letter_head', company.default_letter_head);
}
},
refresh: function(frm) {
- frm.trigger("toggle_fields")
+ frm.trigger("toggle_fields");
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
- cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields,false);
- cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields,false);
+ cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
+ cur_frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
},
salary_slip_based_on_timesheet: function(frm) {
@@ -98,12 +98,12 @@ frappe.ui.form.on("Salary Slip", {
frm.events.get_emp_and_working_day_details(frm);
},
- leave_without_pay: function(frm){
+ leave_without_pay: function(frm) {
if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
return frappe.call({
method: 'process_salary_based_on_working_days',
doc: frm.doc,
- callback: function(r, rt) {
+ callback: function() {
frm.refresh();
}
});
@@ -121,10 +121,10 @@ frappe.ui.form.on("Salary Slip", {
return frappe.call({
method: 'get_emp_and_working_day_details',
doc: frm.doc,
- callback: function(r, rt) {
+ callback: function(r) {
frm.refresh();
- if (r.message){
- frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
+ if (r.message[1] !== "Leave" && r.message[0]) {
+ frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as ")+ r.message[0] +__(". You can can change this in ") + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
}
}
});
@@ -141,7 +141,7 @@ frappe.ui.form.on('Salary Slip Timesheet', {
});
// calculate total working hours, earnings based on hourly wages and totals
-var total_work_hours = function(frm, dt, dn) {
+var total_work_hours = function(frm) {
var total_working_hours = 0.0;
$.each(frm.doc["timesheets"] || [], function(i, timesheet) {
total_working_hours += timesheet.working_hours;
@@ -165,4 +165,4 @@ var total_work_hours = function(frm, dt, dn) {
frm.doc.rounded_total = Math.round(frm.doc.net_pay);
refresh_many(['net_pay', 'rounded_total']);
});
-}
+};
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index cecb8cde7c2..7b87ae5e7b7 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -136,8 +136,8 @@ class SalarySlip(TransactionBase):
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct()
- consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
- return consider_unmarked_attendance_as
+ payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
+ return [payroll_based_on, consider_unmarked_attendance_as]
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet:
@@ -210,10 +210,10 @@ class SalarySlip(TransactionBase):
frappe.throw(_("Please set Payroll based on in Payroll settings"))
if payroll_based_on == "Attendance":
- actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays)
+ actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
self.absent_days = absent
else:
- actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days)
+ actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
if not lwp:
lwp = actual_lwp
@@ -300,7 +300,7 @@ class SalarySlip(TransactionBase):
return holidays
- def calculate_lwp_based_on_leave_application(self, holidays, working_days):
+ def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0
holidays = "','".join(holidays)
daily_wages_fraction_for_half_day = \
@@ -311,10 +311,12 @@ class SalarySlip(TransactionBase):
leave = frappe.db.sql("""
SELECT t1.name,
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
- THEN t1.half_day else 0 END
+ THEN t1.half_day else 0 END,
+ t2.is_ppl,
+ t2.fraction_of_daily_salary_per_leave
FROM `tabLeave Application` t1, `tabLeave Type` t2
WHERE t2.name = t1.leave_type
- AND t2.is_lwp = 1
+ AND (t2.is_lwp = 1 or t2.is_ppl = 1)
AND t1.docstatus = 1
AND t1.employee = %(employee)s
AND ifnull(t1.salary_slip, '') = ''
@@ -327,19 +329,35 @@ class SalarySlip(TransactionBase):
""".format(holidays), {"employee": self.employee, "dt": dt})
if leave:
+ equivalent_lwp_count = 0
is_half_day_leave = cint(leave[0][1])
- lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
+ is_partially_paid_leave = cint(leave[0][2])
+ fraction_of_daily_salary_per_leave = flt(leave[0][3])
+
+ equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
+
+ if is_partially_paid_leave:
+ equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+
+ lwp += equivalent_lwp_count
return lwp
- def calculate_lwp_and_absent_days_based_on_attendance(self, holidays):
+ def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
lwp = 0
absent = 0
daily_wages_fraction_for_half_day = \
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
- lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1))
+ leave_types = frappe.get_all("Leave Type",
+ or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]],
+ fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"])
+
+ leave_type_map = {}
+ for leave_type in leave_types:
+ leave_type_map[leave_type.name] = leave_type
+
attendances = frappe.db.sql('''
SELECT attendance_date, status, leave_type
FROM `tabAttendance`
@@ -351,21 +369,30 @@ class SalarySlip(TransactionBase):
''', values=(self.employee, self.start_date, self.end_date), as_dict=1)
for d in attendances:
- if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types:
+ if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys():
continue
if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays:
if d.status == "Absent" or \
- (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]):
+ (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']):
continue
+ if d.leave_type:
+ fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"]
+
if d.status == "Half Day":
- lwp += (1 - daily_wages_fraction_for_half_day)
- elif d.status == "On Leave" and d.leave_type in lwp_leave_types:
- lwp += 1
+ equivalent_lwp = (1 - daily_wages_fraction_for_half_day)
+
+ if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
+ elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys():
+ equivalent_lwp = 1
+ if leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
elif d.status == "Absent":
absent += 1
-
return lwp, absent
def add_earning_for_hourly_wages(self, doc, salary_component, amount):
@@ -949,9 +976,8 @@ class SalarySlip(TransactionBase):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
if payment.total_payment > total_amount:
- frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2}
- against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment),
- frappe.bold(total_amount), frappe.bold(payment.loan)))
+ frappe.throw(_("Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3}").format(
+ payment.idx, frappe.bold(payment.total_payment),frappe.bold(total_amount), frappe.bold(payment.loan)))
self.total_interest_amount += payment.interest_amount
self.total_principal_amount += payment.principal_amount
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 7fe4165362c..e08dc7c9c87 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -13,6 +13,8 @@ from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \
import create_payroll_period, create_exemption_category
@@ -93,14 +95,27 @@ class TestSalarySlip(unittest.TestCase):
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
+ leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1)
+ leave_type_ppl.save()
+
+ alloc = create_leave_allocation(
+ employee = emp_id, from_date = add_days(first_sunday, 4),
+ to_date = add_days(first_sunday, 10), new_leaves_allocated = 3,
+ leave_type = "Test Partially Paid Leave")
+ alloc.save()
+ alloc.submit()
+
+ #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp
+ make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave")
+
ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly")
- self.assertEqual(ss.leave_without_pay, 3)
+ self.assertEqual(ss.leave_without_pay, 4)
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
- self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3)
+ self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
#Gross pay calculation based on attendances
gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay))
From 755b773616cb7e037e2034d2bc0e396f36580a3f Mon Sep 17 00:00:00 2001
From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
Date: Wed, 25 Nov 2020 16:05:17 +0530
Subject: [PATCH 067/286] feat: Leave policy assignment (#23112)
* feat: Leave Policy Assignment
* feat: linking with leave allocation and valiations
* style: removed old code from leave period
* feat: Bulk Leave policy Assignment and grant Leaves
* fix: overlap validation
* feat: earned leaves based on joining date
* feat: automatic grant leave based on leave policy
* patch: create leave policy assignment based on employee current leave policy
* fix: dependent test cases
* test: Leave policy assignment
* fix: some enhancement
* style: break large function into small function
* fix:requested Changes
* fix(patch): Handled old Leave allocatioln
* fix:codacy
* fix: travis and sider,codacy
* fix: codacy
* fix: codacy
* fix: requested changes and sider
Co-authored-by: Nabin Hait
---
erpnext/hooks.py | 4 +-
erpnext/hr/doctype/employee/employee.json | 11 +-
.../employee_grade/employee_grade.json | 130 ++------------
.../hr/doctype/hr_settings/hr_settings.json | 13 +-
.../leave_allocation/leave_allocation.json | 16 +-
.../leave_allocation/leave_allocation.py | 10 ++
.../test_leave_application.py | 37 ++--
.../leave_encashment/test_leave_encashment.py | 18 +-
.../hr/doctype/leave_period/leave_period.js | 78 +--------
.../hr/doctype/leave_period/leave_period.py | 109 +-----------
.../doctype/leave_period/test_leave_period.py | 34 +---
.../leave_policy_assignment/__init__.py | 0
.../leave_policy_assignment.js | 72 ++++++++
.../leave_policy_assignment.json | 160 +++++++++++++++++
.../leave_policy_assignment.py | 163 ++++++++++++++++++
.../leave_policy_assignment_list.js | 138 +++++++++++++++
.../test_leave_policy_assignment.py | 103 +++++++++++
erpnext/hr/doctype/leave_type/leave_type.json | 10 +-
erpnext/hr/utils.py | 129 ++++++++------
erpnext/patches.txt | 1 +
..._based_on_employee_current_leave_policy.py | 77 +++++++++
21 files changed, 899 insertions(+), 414 deletions(-)
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/__init__.py
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
create mode 100644 erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
create mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 741176f33f4..726ab6e22ac 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -347,14 +347,16 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
+ "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
+ "erpnext.hr.utils.allocate_earned_leaves",
+ "erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
- "erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 4cabe97cc4d..4f1c04ff5d0 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -57,7 +57,6 @@
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details",
- "leave_policy",
"attendance_device_id",
"column_break_44",
"holiday_list",
@@ -411,14 +410,6 @@
"oldfieldtype": "Link",
"options": "Branch"
},
- {
- "fetch_from": "grade.default_leave_policy",
- "fetch_if_empty": 1,
- "fieldname": "leave_policy",
- "fieldtype": "Link",
- "label": "Leave Policy",
- "options": "Leave Policy"
- },
{
"description": "Applicable Holiday List",
"fieldname": "holiday_list",
@@ -822,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2020-10-16 14:41:10.580897",
+ "modified": "2020-10-16 15:02:04.283657",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json
index e63ffae0c42..88b061a3c3b 100644
--- a/erpnext/hr/doctype/employee_grade/employee_grade.json
+++ b/erpnext/hr/doctype/employee_grade/employee_grade.json
@@ -1,167 +1,69 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2018-04-13 16:14:24.174138",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2018-04-13 16:14:24.174138",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "default_salary_structure"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "default_leave_policy",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Default Leave Policy",
- "length": 0,
- "no_copy": 0,
- "options": "Leave Policy",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "default_salary_structure",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Default Salary Structure",
- "length": 0,
- "no_copy": 0,
- "options": "Salary Structure",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Salary Structure"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-18 17:17:45.617624",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-08-26 13:12:07.815330",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grade",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 4374d2911aa..f99963504ab 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -21,6 +21,7 @@
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
+ "automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@@ -41,7 +42,7 @@
"description": "Employee records are created using the selected field",
"fieldname": "emp_created_by",
"fieldtype": "Select",
- "label": "Employee Records to Be Created By",
+ "label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@@ -117,7 +118,7 @@
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
- "label": "Restrict Backdated Leave Applications"
+ "label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
@@ -125,13 +126,19 @@
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
+ },
+ {
+ "default": "0",
+ "fieldname": "automatically_allocate_leaves_based_on_leave_policy",
+ "fieldtype": "Check",
+ "label": "Automatically Allocate Leaves Based On Leave Policy"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 11:49:46.168027",
+ "modified": "2020-08-27 14:30:28.995324",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 007497e34a5..4b315014dae 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-02-20 19:10:38",
@@ -24,6 +25,7 @@
"compensatory_request",
"leave_period",
"leave_policy",
+ "leave_policy_assignment",
"carry_forwarded_leaves_count",
"expired",
"amended_from",
@@ -160,9 +162,10 @@
"read_only": 1
},
{
- "fetch_from": "employee.leave_policy",
+ "fetch_from": "leave_policy_assignment.leave_policy",
"fieldname": "leave_policy",
"fieldtype": "Link",
+ "hidden": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
@@ -209,12 +212,21 @@
"fieldtype": "Float",
"label": "Carry Forwarded Leaves",
"read_only": 1
+ },
+ {
+ "fieldname": "leave_policy_assignment",
+ "fieldtype": "Link",
+ "label": "Leave Policy Assignment",
+ "options": "Leave Policy Assignment",
+ "read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-08-08 15:08:42.440909",
+ "links": [],
+ "modified": "2020-08-20 14:25:10.314323",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 03fe3fa035c..a09cd2ea112 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -51,9 +51,19 @@ class LeaveAllocation(Document):
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
+ if self.leave_policy_assignment:
+ self.update_leave_policy_assignments_when_no_allocations_left()
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
+ def update_leave_policy_assignments_when_no_allocations_left(self):
+ allocations = frappe.db.get_list("Leave Allocation", filters = {
+ "docstatus": 1,
+ "leave_policy_assignment": self.leave_policy_assignment
+ })
+ if len(allocations) == 0:
+ frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0)
+
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
frappe.throw(_("To date cannot be before from date"))
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6e909c3f01b..53b7a39e511 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
+from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
test_dependencies = ["Leave Allocation", "Leave Block List"]
@@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leaves_creation(self):
+
+ frappe.db.sql('''delete from `tabLeave Period`''')
+ frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
+ frappe.db.sql('''delete from `tabLeave Allocation`''')
+ frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
+
leave_period = get_leave_period()
employee = get_employee()
leave_type = 'Test Earned Leave Type'
- if not frappe.db.exists('Leave Type', leave_type):
- frappe.get_doc(dict(
- leave_type_name = leave_type,
- doctype = 'Leave Type',
- is_earned_leave = 1,
- earned_leave_frequency = 'Monthly',
- rounding = 0.5,
- max_leaves_allowed = 6
- )).insert()
+ frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1)
+ frappe.get_doc(dict(
+ leave_type_name = leave_type,
+ doctype = 'Leave Type',
+ is_earned_leave = 1,
+ earned_leave_frequency = 'Monthly',
+ rounding = 0.5,
+ max_leaves_allowed = 6
+ )).insert()
+
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
}).insert()
- frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name)
- allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12)
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+ frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves
i = 0
diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
index 99f64634161..bbee18bb0a0 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -9,6 +9,7 @@ from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
+from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
@@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
+ frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
frappe.db.sql('''delete from `tabAdditional Salary`''')
@@ -29,14 +31,22 @@ class TestLeaveEncashment(unittest.TestCase):
# create employee, salary structure and assignment
self.employee = make_employee("test_employee_encashment@example.com")
- frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
+ self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
+
+ data = {
+ "assignment_based_on": "Leave Period",
+ "leave_policy": leave_policy.name,
+ "leave_period": self.leave_period.name
+ }
+
+ leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data))
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
- # create the leave period and assign the leaves
- self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
- self.leave_period.grant_leave_allocation(employee=self.employee)
+ #grant Leaves
+ frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
+
def test_leave_balance_value_and_amount(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')
diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js
index bad2b8766c8..0e88bc16714 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.js
+++ b/erpnext/hr/doctype/leave_period/leave_period.js
@@ -2,14 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Period', {
- refresh: (frm)=>{
- frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0);
- if(!frm.is_new()) {
- frm.add_custom_button(__('Grant Leaves'), function () {
- frm.trigger("grant_leaves");
- });
- }
- },
from_date: (frm)=>{
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
@@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', {
"filters": {
"company": frm.doc.company,
}
- }
- })
- },
- grant_leaves: function(frm) {
- var d = new frappe.ui.Dialog({
- title: __('Grant Leaves'),
- fields: [
- {
- "label": "Filter Employees By (Optional)",
- "fieldname": "sec_break",
- "fieldtype": "Section Break",
- },
- {
- "label": "Employee Grade",
- "fieldname": "grade",
- "fieldtype": "Link",
- "options": "Employee Grade"
- },
- {
- "label": "Department",
- "fieldname": "department",
- "fieldtype": "Link",
- "options": "Department"
- },
- {
- "fieldname": "col_break",
- "fieldtype": "Column Break",
- },
- {
- "label": "Designation",
- "fieldname": "designation",
- "fieldtype": "Link",
- "options": "Designation"
- },
- {
- "label": "Employee",
- "fieldname": "employee",
- "fieldtype": "Link",
- "options": "Employee"
- },
- {
- "fieldname": "sec_break",
- "fieldtype": "Section Break",
- },
- {
- "label": "Add unused leaves from previous allocations",
- "fieldname": "carry_forward",
- "fieldtype": "Check"
- }
- ],
- primary_action: function() {
- var data = d.get_values();
-
- frappe.call({
- doc: frm.doc,
- method: "grant_leave_allocation",
- args: data,
- callback: function(r) {
- if(!r.exc) {
- d.hide();
- frm.reload_doc();
- }
- }
- });
- },
- primary_action_label: __('Grant')
+ };
});
- d.show();
- }
+ },
});
diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py
index 0973ac71985..28a33f6fac8 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.py
+++ b/erpnext/hr/doctype/leave_period/leave_period.py
@@ -7,24 +7,10 @@ import frappe
from frappe import _
from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document
-from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
+from erpnext.hr.utils import validate_overlap
from frappe.utils.background_jobs import enqueue
-from six import iteritems
class LeavePeriod(Document):
- def get_employees(self, args):
- conditions, values = [], []
- for field, value in iteritems(args):
- if value:
- conditions.append("{0}=%s".format(field))
- values.append(value)
-
- condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
-
- employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
- .format(condition=condition_str), tuple(values)))
-
- return employees
def validate(self):
self.validate_dates()
@@ -33,96 +19,3 @@ class LeavePeriod(Document):
def validate_dates(self):
if getdate(self.from_date) >= getdate(self.to_date):
frappe.throw(_("To date can not be equal or less than from date"))
-
-
- def grant_leave_allocation(self, grade=None, department=None, designation=None,
- employee=None, carry_forward=0):
- employee_records = self.get_employees({
- "grade": grade,
- "department": department,
- "designation": designation,
- "name": employee
- })
-
- if employee_records:
- if len(employee_records) > 20:
- frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
- employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
- else:
- grant_leave_alloc_for_employees(employee_records, self, carry_forward)
- else:
- frappe.msgprint(_("No Employee Found"))
-
-def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
- leave_allocations = []
- existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
- leave_type_details = get_leave_type_details()
- count = 0
- for employee in employee_records.keys():
- if employee in existing_allocations_for:
- continue
- count +=1
- leave_policy = get_employee_leave_policy(employee)
- if leave_policy:
- for leave_policy_detail in leave_policy.leave_policy_details:
- if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
- leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
- leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
- leave_allocations.append(leave_allocation)
- frappe.db.commit()
- frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
-
- if leave_allocations:
- frappe.msgprint(_("Leaves has been granted sucessfully"))
-
-def get_existing_allocations(employees, leave_period):
- leave_allocations = frappe.db.sql_list("""
- SELECT DISTINCT
- employee
- FROM `tabLeave Allocation`
- WHERE
- leave_period=%s
- AND employee in (%s)
- AND carry_forward=0
- AND docstatus=1
- """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
- if leave_allocations:
- frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
- .format("\n".join(leave_allocations)))
- return leave_allocations
-
-def get_leave_type_details():
- leave_type_details = frappe._dict()
- leave_types = frappe.get_all("Leave Type",
- fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
- for d in leave_types:
- leave_type_details.setdefault(d.name, d)
- return leave_type_details
-
-def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
- ''' Creates leave allocation for the given employee in the provided leave period '''
- if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
- carry_forward = 0
-
- # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
- if getdate(date_of_joining) > getdate(leave_period.from_date):
- remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
- new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
-
- # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
- if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
- new_leaves_allocated = 0
-
- allocation = frappe.get_doc(dict(
- doctype="Leave Allocation",
- employee=employee,
- leave_type=leave_type,
- from_date=leave_period.from_date,
- to_date=leave_period.to_date,
- new_leaves_allocated=new_leaves_allocated,
- leave_period=leave_period.name,
- carry_forward=carry_forward
- ))
- allocation.save(ignore_permissions = True)
- allocation.submit()
- return allocation.name
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py
index 1762cf917a2..b5857bcd8fe 100644
--- a/erpnext/hr/doctype/leave_period/test_leave_period.py
+++ b/erpnext/hr/doctype/leave_period/test_leave_period.py
@@ -5,43 +5,11 @@ from __future__ import unicode_literals
import frappe, erpnext
import unittest
-from frappe.utils import today, add_months
-from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
test_dependencies = ["Employee", "Leave Type", "Leave Policy"]
class TestLeavePeriod(unittest.TestCase):
- def setUp(self):
- frappe.db.sql("delete from `tabLeave Period`")
-
- def test_leave_grant(self):
- leave_type = "_Test Leave Type"
-
- # create the leave policy
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{
- "leave_type": leave_type,
- "annual_allocation": 20
- }]
- }).insert()
- leave_policy.submit()
-
- # create employee and assign the leave period
- employee = "test_leave_period@employee.com"
- employee_doc_name = make_employee(employee)
- frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name)
-
- # clear the already allocated leave
- frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com")
-
- # create the leave period
- leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
-
- # test leave_allocation
- leave_period.grant_leave_allocation(employee=employee_doc_name)
- self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20)
+ pass
def create_leave_period(from_date, to_date, company=None):
leave_period = frappe.db.get_value('Leave Period',
diff --git a/erpnext/hr/doctype/leave_policy_assignment/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
new file mode 100644
index 00000000000..7c32a0dde09
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Leave Policy Assignment', {
+ onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
+ },
+
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
+ frm.add_custom_button(__("Grant Leave"), function() {
+
+ frappe.call({
+ doc: frm.doc,
+ method: "grant_leave_alloc_for_employee",
+ callback: function(r) {
+ let leave_allocations = r.message;
+ let msg = frm.events.get_success_message(leave_allocations);
+ frappe.msgprint(msg);
+ cur_frm.refresh();
+ }
+ });
+ });
+ }
+ },
+
+ get_success_message: function(leave_allocations) {
+ let msg = __("Leaves has been granted successfully");
+ msg += "
";
+ msg += "
"+__('Leave Type')+"
"+__("Leave Allocation")+"
"+__("Leaves Granted")+"
";
+ for (let key in leave_allocations) {
+ msg += "
\
- If this is undesirable please cancel the corresponding Payment Entry.")
- .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")),
+ frappe.msgprint(
+ _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
+ .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
+ + "
Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n
\n\n
How to get fieldnames
\n\n
The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)
\n\n
Templating
\n\n
Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
"
}
],
"links": [],
- "modified": "2020-11-11 17:49:44.879363",
+ "modified": "2020-12-02 21:36:53.097074",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract Template",
From 8474476316095bc94286dd8dd8f6eb2f4decf651 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 24 Nov 2020 19:04:03 +0530
Subject: [PATCH 095/286] fix: incorrect balance value in stock balance report
---
erpnext/stock/report/stock_balance/stock_balance.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 1339d9b6820..ccd01001bb7 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items):
select
sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate,
sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
- sle.item_code as name, sle.voucher_no
+ sle.item_code as name, sle.voucher_no, sle.stock_value
from
`tabStock Ledger Entry` sle force index (posting_sort_index)
where sle.docstatus < 2 %s %s
@@ -197,7 +197,7 @@ def get_item_warehouse_map(filters, sle):
else:
qty_diff = flt(d.actual_qty)
- value_diff = flt(d.stock_value_difference)
+ value_diff = flt(d.stock_value) - flt(qty_dict.bal_val)
if d.posting_date < from_date:
qty_dict.opening_qty += qty_diff
From 69c5d49b25002dffd8248df3f3916a9a3d1763ab Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Thu, 3 Dec 2020 15:14:54 +0530
Subject: [PATCH 096/286] refactor: to avoid using SQL query to get fiscal year
filters
---
.../accounts/doctype/fiscal_year/fiscal_year.py | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index d80bc7fad10..8faf24d3752 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -116,12 +116,8 @@ def auto_create_fiscal_year():
pass
def get_from_and_to_date(fiscal_year):
- from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date
- from `tabFiscal Year` where name=%s""", (fiscal_year))[0]
-
- from_and_to_date = {
- "from_date": from_and_to_date_tuple[0],
- "to_date": from_and_to_date_tuple[1]
- }
-
- return from_and_to_date
+ fields = [
+ "year_start_date as from_date",
+ "year_end_date as to_date"
+ ]
+ return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
From 26cc662a7879ad1efdcd1d12e2a6c3ea27a93916 Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Thu, 3 Dec 2020 17:45:34 +0530
Subject: [PATCH 097/286] feat: added filter for customer field
---
erpnext/support/doctype/issue/issue.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index fe01d4b983c..33191beddee 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -1,6 +1,13 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
+ frm.set_query('customer', function () {
+ return {
+ filters: {
+ "disabled": 0
+ }
+ }
+ });
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
@@ -145,6 +152,7 @@ frappe.ui.form.on("Issue", {
reset_sla.show();
},
+
timeline_refresh: function(frm) {
// create button for "Help Article"
if(frappe.model.can_create('Help Article')) {
From 25e058833a7dfec25812d4fa5828d94ef61713a5 Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Thu, 3 Dec 2020 18:13:24 +0530
Subject: [PATCH 098/286] fix: sider changes
---
erpnext/support/doctype/issue/issue.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 33191beddee..c20ad5c2fdc 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -6,8 +6,8 @@ frappe.ui.form.on("Issue", {
filters: {
"disabled": 0
}
- }
- });
+ };
+ });
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
From 0139109de2ae31b2283712ee5ea555ed62afae77 Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Fri, 4 Dec 2020 11:28:13 +0530
Subject: [PATCH 099/286] fix: consistency check
---
erpnext/support/doctype/issue/issue.js | 32 +++++++++++++-------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index c20ad5c2fdc..521e671b0d6 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -1,7 +1,7 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
- frm.set_query('customer', function () {
+ frm.set_query("customer", function () {
return {
filters: {
"disabled": 0
@@ -28,14 +28,14 @@ frappe.ui.form.on("Issue", {
},
callback: function (r) {
if (r && r.message) {
- frm.set_query('priority', function() {
+ frm.set_query("priority", function() {
return {
filters: {
"name": ["in", r.message.priority],
}
};
});
- frm.set_query('service_level_agreement', function() {
+ frm.set_query("service_level_agreement", function() {
return {
filters: {
"name": ["in", r.message.service_level_agreements],
@@ -52,9 +52,9 @@ frappe.ui.form.on("Issue", {
if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
if (frm.doc.service_level_agreement) {
frappe.call({
- 'method': 'frappe.client.get',
+ "method": "frappe.client.get",
args: {
- doctype: 'Service Level Agreement',
+ doctype: "Service Level Agreement",
name: frm.doc.service_level_agreement
},
callback: function(data) {
@@ -134,8 +134,8 @@ frappe.ui.form.on("Issue", {
reset_sla.clear();
frappe.show_alert({
- indicator: 'green',
- message: __('Resetting Service Level Agreement.')
+ indicator: "green",
+ message: __("Resetting Service Level Agreement.")
});
frm.call("reset_service_level_agreement", {
@@ -155,33 +155,33 @@ frappe.ui.form.on("Issue", {
timeline_refresh: function(frm) {
// create button for "Help Article"
- if(frappe.model.can_create('Help Article')) {
+ if(frappe.model.can_create("Help Article")) {
// Removing Help Article button if exists to avoid multiple occurance
frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove();
$(''+
__('Help Article') + '')
.appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])'))
- .on('click', function() {
- var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html();
- var doc = frappe.model.get_new_doc('Help Article');
+ .on("click", function() {
+ var content = $(this).parents(".timeline-item:first").find(".timeline-item-content").html();
+ var doc = frappe.model.get_new_doc("Help Article");
doc.title = frm.doc.subject;
doc.content = content;
- frappe.set_route('Form', 'Help Article', doc.name);
+ frappe.set_route("Form", "Help Article", doc.name);
});
}
- if (!frm.timeline.wrapper.find('.btn-split-issue').length) {
+ if (!frm.timeline.wrapper.find(".btn-split-issue").length) {
let split_issue = __("Split Issue")
$(`
${split_issue}
`)
.appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])'))
if (!frm.timeline.wrapper.data("split-issue-event-attached")){
- frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => {
+ frm.timeline.wrapper.on("click", ".btn-split-issue", (e) => {
var dialog = new frappe.ui.Dialog({
title: __("Split Issue"),
fields: [
- {fieldname: 'subject', fieldtype: 'Data', reqd:1, label: __('Subject'), description: __('All communications including and above this shall be moved into the new Issue')}
+ {fieldname: "subject", fieldtype: "Data", reqd:1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")}
],
primary_action_label: __("Split"),
primary_action: function() {
@@ -234,7 +234,7 @@ function set_time_to_resolve_and_response(frm) {
function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
- let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green";
+ let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}
From 28e86cf18312155ed020065b10152f77e8747d8f Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Fri, 4 Dec 2020 00:47:46 +0530
Subject: [PATCH 100/286] fix: pricing rule with transaction not working for
additional product
---
.../doctype/pricing_rule/pricing_rule.json | 4 ++-
.../doctype/pricing_rule/test_pricing_rule.py | 31 +++++++++++++++----
.../accounts/doctype/pricing_rule/utils.py | 11 ++++++-
3 files changed, 38 insertions(+), 8 deletions(-)
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index cc8ed4bc491..d08a854142a 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -406,6 +406,7 @@
"fieldtype": "Column Break"
},
{
+ "default": "0",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
@@ -469,6 +470,7 @@
"options": "UOM"
},
{
+ "description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Rate"
@@ -563,7 +565,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2020-10-28 16:53:14.416172",
+ "modified": "2020-12-04 00:36:24.698219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index ec0a485bfc1..af8d21d9ce4 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -521,6 +521,22 @@ class TestPricingRule(unittest.TestCase):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
+ def test_pricing_rule_for_transaction(self):
+ make_item("Water Flask 1")
+ frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
+ make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
+ apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
+
+ si = create_sales_invoice(qty=5, do_not_submit=True)
+ self.assertEquals(len(si.items), 2)
+ self.assertEquals(si.items[1].rate, 10)
+
+ si1 = create_sales_invoice(qty=2, do_not_submit=True)
+ self.assertEquals(len(si1.items), 1)
+
+ for doc in [si, si1]:
+ doc.delete()
+
def make_pricing_rule(**args):
args = frappe._dict(args)
@@ -539,20 +555,23 @@ def make_pricing_rule(**args):
"rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0,
- "margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
- if args.get("priority"):
- doc.priority = args.get("priority")
+ for field in ["free_item", "free_qty", "free_item_rate", "priority",
+ "margin_type", "price_or_product_discount"]:
+ if args.get(field):
+ doc.set(field, args.get(field))
apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
- doc.append(child_table.get(doc.apply_on), {
- apply_on: args.get(apply_on) or "_Test Item"
- })
+
+ if doc.apply_on != "Transaction":
+ doc.append(child_table.get(doc.apply_on), {
+ apply_on: args.get(apply_on) or "_Test Item"
+ })
doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code":
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index b003328cc47..2c7cd14451d 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -457,6 +457,9 @@ def apply_pricing_rule_on_transaction(doc):
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
doc.total, pricing_rules)
+ if not pricing_rules:
+ remove_free_item(doc)
+
for d in pricing_rules:
if d.price_or_product_discount == 'Price':
if d.apply_discount_on:
@@ -480,6 +483,12 @@ def apply_pricing_rule_on_transaction(doc):
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
+ doc.calculate_taxes_and_totals()
+
+def remove_free_item(doc):
+ for d in doc.items:
+ if d.is_free_item:
+ doc.remove(d)
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
@@ -492,7 +501,7 @@ def get_applied_pricing_rules(pricing_rules):
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
- if pricing_rule.same_item:
+ if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
free_item = item_details.item_code or args.item_code
if not free_item:
From edee530d4cba4d6395e5a6f680fa5c33d962b175 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 4 Dec 2020 12:13:26 +0530
Subject: [PATCH 101/286] fix: paid amount in Sales Invoice POS return resets
to 0
---
erpnext/controllers/taxes_and_totals.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 81d07c1327e..ad58f137ee8 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -641,7 +641,8 @@ class calculate_taxes_and_totals(object):
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
- 'amount': total_amount_to_pay
+ 'amount': total_amount_to_pay,
+ 'default': 1
})
else:
self.doc.is_pos = 0
From 6a2431586ec82290ffad6c24d30cd4e5f777a7a4 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 4 Dec 2020 13:31:36 +0530
Subject: [PATCH 102/286] fix: Make new Customers for account missing test and
set company
---
.../test_opening_invoice_creation_tool.py | 50 +++++++++++++------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 329d84bdb7a..bdfe532b9fb 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -11,15 +11,20 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def make_invoices(self, invoice_type="Sales", company=None):
+ def setUp(self):
+ if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
+ make_company()
+
+ def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
- args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company)
+ args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
+ party_1=party_1, party_2=party_2)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
- invoices = self.make_invoices()
+ invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@@ -45,7 +50,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self):
- invoices = self.make_invoices(invoice_type="Purchase")
+ invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@@ -56,9 +61,11 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.check_expected_values(invoices, expected_value, "Purchase")
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
- company = make_company()
- old_default_receivable_account = frappe.db.get_value("Company", company.name, "default_receivable_account")
- frappe.db.set_value("Company", company.name, "default_receivable_account", "")
+ company = "_Test Opening Invoice Company"
+ party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
+
+ old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
+ frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
@@ -68,18 +75,16 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
cc2.insert()
- frappe.db.set_value("Company", company.name, "cost_center", "Main - _TOIC")
+ frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
- self.make_invoices(company="_Test Opening Invoice Company")
+ self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
self.assertTrue(error_log)
# teardown
- frappe.db.set_value("Company", company.name, "default_receivable_account", old_default_receivable_account)
- company.delete()
- frappe.get_doc("Error Log", error_log).delete()
+ frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
@@ -92,7 +97,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 1.0,
"outstanding_amount": 300,
- "party": "_Test {0}".format(party),
+ "party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@@ -101,7 +106,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 2.0,
"outstanding_amount": 250,
- "party": "_Test {0} 1".format(party),
+ "party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@@ -123,4 +128,19 @@ def make_company():
company.default_currency = "INR"
company.country = "India"
company.insert()
- return company
\ No newline at end of file
+ return company
+
+def make_customer(customer=None):
+ customer_name = customer or "Opening Customer"
+ customer = frappe.get_doc({
+ "doctype": "Customer",
+ "customer_name": customer_name,
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "territory": "All Territories"
+ })
+ if not frappe.db.exists("Customer", customer_name):
+ customer.insert(ignore_permissions=True)
+ return customer.name
+ else:
+ return frappe.db.exists("Customer", customer_name)
\ No newline at end of file
From 931f2e73a7a741a695072fe598f284d4b22b435b Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Fri, 4 Dec 2020 14:57:23 +0530
Subject: [PATCH 103/286] fix: sider changes
---
erpnext/support/doctype/issue/issue.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 521e671b0d6..086755be516 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -155,7 +155,7 @@ frappe.ui.form.on("Issue", {
timeline_refresh: function(frm) {
// create button for "Help Article"
- if(frappe.model.can_create("Help Article")) {
+ if (frappe.model.can_create("Help Article")) {
// Removing Help Article button if exists to avoid multiple occurance
frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove();
$(''+
@@ -181,7 +181,7 @@ frappe.ui.form.on("Issue", {
var dialog = new frappe.ui.Dialog({
title: __("Split Issue"),
fields: [
- {fieldname: "subject", fieldtype: "Data", reqd:1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")}
+ {fieldname: "subject", fieldtype: "Data", reqd: 1, label: __("Subject"), description: __("All communications including and above this shall be moved into the new Issue")}
],
primary_action_label: __("Split"),
primary_action: function() {
From c4eb41c92b399bbb9f4ae005ef98522b30eaa227 Mon Sep 17 00:00:00 2001
From: Kenneth Sequeira
Date: Fri, 4 Dec 2020 15:00:22 +0530
Subject: [PATCH 104/286] fix; styling for sider
---
erpnext/crm/doctype/contract/contract.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js
index 6c0d739c89f..b02d06a6019 100644
--- a/erpnext/crm/doctype/contract/contract.js
+++ b/erpnext/crm/doctype/contract/contract.js
@@ -11,7 +11,7 @@ frappe.ui.form.on("Contract", {
doc: frm.doc
},
callback: function(r) {
- if(r && r.message){
+ if (r && r.message) {
frm.set_value("contract_terms", r.message.contract_terms);
// Populate the fulfilment terms table from a contract template, if any
From a7670852768e640ce0a2cb158faa4d70fb4b14a2 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 4 Dec 2020 18:07:46 +0530
Subject: [PATCH 105/286] fix: Tax template update on supplier
---
erpnext/regional/india/taxes.js | 1 +
erpnext/regional/india/utils.py | 8 ++++++++
2 files changed, 9 insertions(+)
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 3c156479c58..b70b2ec48cc 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -19,6 +19,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
'shipping_address': frm.doc.shipping_address || '',
'shipping_address_name': frm.doc.shipping_address_name || '',
'customer_address': frm.doc.customer_address || '',
+ 'supplier_address': frm.doc.supplier_address,
'customer': frm.doc.customer,
'supplier': frm.doc.supplier,
'supplier_gstin': frm.doc.supplier_gstin,
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 62487ba2aa0..f8520c2d003 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -12,6 +12,7 @@ from erpnext.regional.india import number_state_mapping
from six import string_types
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.utils import get_account_currency
+from frappe.model.utils import get_fetch_values
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
@@ -161,6 +162,8 @@ def get_regional_address_details(party_details, doctype, company):
party_details = json.loads(party_details)
party_details = frappe._dict(party_details)
+ update_party_details(party_details, doctype)
+
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
if is_internal_transfer(party_details, doctype):
@@ -209,6 +212,11 @@ def get_regional_address_details(party_details, doctype, company):
return party_details
+def update_party_details(party_details, doctype):
+ for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']:
+ if party_details.get(address_field):
+ party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
+
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
destination_gstin = party_details.company_gstin
From 7fef622b136c4e7da72651a03c120878e2a6d386 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Fri, 4 Dec 2020 19:18:36 +0530
Subject: [PATCH 106/286] fix: drop ship partial order fixed
---
erpnext/selling/doctype/sales_order/sales_order.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ae227e0110e..3e1c82f9616 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -831,7 +831,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar
for supplier in suppliers:
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
- if len(po) == 0:
+ if len(po) == 0 or any( item.get("delivered_by_supplier") == 1 for item in selected_items):
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
"doctype": "Purchase Order",
From 26b3328af7a96cc6982e54fa3cd41b88a0db63a6 Mon Sep 17 00:00:00 2001
From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
Date: Fri, 4 Dec 2020 19:50:24 +0530
Subject: [PATCH 107/286] fix: leave policy assignment patch (#24061)
---
..._policy_assignment_based_on_employee_current_leave_policy.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
index 80c91376530..90dc0e2e18b 100644
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -52,6 +52,8 @@ def create_assignment(employee, leave_policy, leave_period=None, allocation_exis
if leave_period:
filters["leave_period"] = leave_period
+ frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
+
if not frappe.db.exists("Leave Policy Assignment" , filters):
lpa = frappe.new_doc("Leave Policy Assignment")
lpa.employee = employee
From c866abca56caaa7e2919e3612c4849cf02b72a56 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 4 Dec 2020 18:56:00 +0530
Subject: [PATCH 108/286] fix: (travis) Disable update stock for deferred
accounting item's Sales Invoice
---
.../test_process_deferred_accounting.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
index 31356c6e8b6..e08a0e5cc2b 100644
--- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
+++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
@@ -21,7 +21,7 @@ class TestProcessDeferredAccounting(unittest.TestCase):
item.no_of_months = 12
item.save()
- si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
+ si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
From 2e4faf96f22bcb0180ae6a16b8567a28cd53e873 Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Sat, 5 Dec 2020 13:36:43 +0530
Subject: [PATCH 109/286] fix: Added TDS Payable account number and an error
message
---
.../standard_chart_of_accounts_with_account_number.py | 3 +++
erpnext/accounts/utils.py | 5 ++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
index 6c83e3bd670..acb11e557a5 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
@@ -245,6 +245,9 @@ def get():
"account_number": "2200"
},
_("Duties and Taxes"): {
+ _("TDS Payable"): {
+ "account_number": "2310"
+ },
"account_type": "Tax",
"is_group": 1,
"account_number": "2300"
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 53677cde8aa..267c26d47ba 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -78,7 +78,10 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
- error_msg = _("""{0} {1} not in any active Fiscal Year.""").format(label, formatdate(transaction_date))
+ error_msg = _("""{0} {1} not in any active Fiscal Year""").format(label, formatdate(transaction_date))
+ if company:
+ error_msg = _("""{0} for {1}""").format(error_msg, company)
+
if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)
From b6cf75d2a8b0ee03e00e5efcc4263c273b53ab3d Mon Sep 17 00:00:00 2001
From: Anuja P
Date: Sat, 5 Dec 2020 22:25:17 +0530
Subject: [PATCH 110/286] fix: error messsage indent fix
---
erpnext/accounts/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 267c26d47ba..73e610abb84 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -78,7 +78,7 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
- error_msg = _("""{0} {1} not in any active Fiscal Year""").format(label, formatdate(transaction_date))
+ error_msg = _("""{0} {1} not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company:
error_msg = _("""{0} for {1}""").format(error_msg, company)
From 2d1cfae5967b1969f6fa7351ca9239dac85cb0ab Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 5 Dec 2020 22:44:45 +0530
Subject: [PATCH 111/286] fix: fieldname
---
erpnext/patches/v13_0/update_old_loans.py | 44 ++++++++++++-----------
1 file changed, 23 insertions(+), 21 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index de29d329d11..caec53b3fdf 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -101,32 +101,34 @@ def execute():
process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type,
loan=loan.name)
- accrued_entries = get_accrued_interest_entries(loan.name)
- total_principal, total_interest = frappe.db.get_value('Repayment Schedule', fields=['sum(principal_amount) as total_principal',
- 'sum(interest_amount) as total_interest'], filters={'is_paid': 1, 'parent': loan.name})
- for entry in accrued_entries:
- interest_paid = 0
- principal_paid = 0
+ if frappe.db.has_column('Repayment Schedule', 'paid'):
+ total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name},
+ ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest'])
- if total_interest > entry.interest_amount:
- interest_paid = entry.interest_amount
- else:
- interest_paid = total_interest
+ accrued_entries = get_accrued_interest_entries(loan.name)
+ for entry in accrued_entries:
+ interest_paid = 0
+ principal_paid = 0
- if total_principal > entry.payable_principal_amount:
- principal_paid = entry.payable_principal_amount
- else:
- principal_paid = total_principal
+ if total_interest > entry.interest_amount:
+ interest_paid = entry.interest_amount
+ else:
+ interest_paid = total_interest
- frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
- SET paid_principal_amount = `paid_principal_amount` + %s,
- paid_interest_amount = `paid_interest_amount` + %s
- WHERE name = %s""",
- (principal_paid, interest_paid, entry.name))
+ if total_principal > entry.payable_principal_amount:
+ principal_paid = entry.payable_principal_amount
+ else:
+ principal_paid = total_principal
- total_principal -= principal_paid
- total_interest -= interest_paid
+ frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ SET paid_principal_amount = `paid_principal_amount` + %s,
+ paid_interest_amount = `paid_interest_amount` + %s
+ WHERE name = %s""",
+ (principal_paid, interest_paid, entry.name))
+
+ total_principal -= principal_paid
+ total_interest -= interest_paid
def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc = frappe.new_doc('Loan Type')
From c9067e40cb0a2997d770daa6400230583de922ce Mon Sep 17 00:00:00 2001
From: Michelle Alva <50285544+michellealva@users.noreply.github.com>
Date: Sun, 6 Dec 2020 10:48:32 +0530
Subject: [PATCH 112/286] fix: Change validation message
grammatical fix
---
erpnext/stock/doctype/stock_settings/stock_settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 4c7828b8737..3b9608b8056 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -55,7 +55,7 @@ class StockSettings(Document):
""")
if sle:
- frappe.throw(_("Can't change valuation method, as there are transactions against some items which does not have it's own valuation method"))
+ frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"))
def validate_clean_description_html(self):
if int(self.clean_description_html or 0) \
From cfeca0dc875f918cb2f5e5ab3f907a513ed13b9f Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sun, 6 Dec 2020 18:28:30 +0530
Subject: [PATCH 113/286] fix: Show tax amount in base currencies
---
.../doctype/gstr_3b_report/gstr_3b_report.py | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 787d557e805..68c8a0d4d38 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -192,19 +192,20 @@ class GSTR3BReport(Document):
for d in self.report_dict["itc_elg"]["itc_avl"]:
itc_type = itc_type_map.get(d["ty"])
- gst_category = ["Registered Regular"]
if d["ty"] == 'ISRC':
- reverse_charge = "Y"
+ reverse_charge = ["Y"]
itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas']
else:
- reverse_charge = "N"
+ gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
+ reverse_charge = ["N", "Y"]
for account_head in self.account_heads:
for category in gst_category:
- for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
- d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2)
+ for charge_type in reverse_charge:
+ for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
+ d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
for key in ['iamt', 'camt', 'samt', 'csamt']:
net_itc[key] += flt(d[key], 2)
@@ -264,7 +265,8 @@ class GSTR3BReport(Document):
def get_itc_details(self):
itc_amount = frappe.db.sql("""
- select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge
+ select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
+ t.account_head, s.eligibility_for_itc, s.reverse_charge
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
where s.docstatus = 1 and t.parent = s.name
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
@@ -387,7 +389,7 @@ class GSTR3BReport(Document):
tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql("""
- select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head
+ select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
from `tab{doctype}` s , `tab{template}` t
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
From 063be69b580ca1691767799744b90943906f1a7c Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 7 Dec 2020 10:41:05 +0530
Subject: [PATCH 114/286] fix(patch): set update_modified to False while
setting issue/opportunity metrics (#24073)
---
erpnext/patches/v13_0/rename_issue_doctype_fields.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
index 96a63623c05..fa1dfed6435 100644
--- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py
+++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
@@ -29,7 +29,7 @@ def execute():
'response_by_variance': response_by_variance,
'resolution_by_variance': resolution_by_variance,
'first_response_time': mins_to_first_response
- })
+ }, update_modified=False)
# commit after every 100 updates
count += 1
if count%100 == 0:
@@ -44,7 +44,7 @@ def execute():
count = 0
for entry in opportunities:
mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
- frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response)
+ frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False)
# commit after every 100 updates
count += 1
if count%100 == 0:
From 5f95ba57a8b66e3d4f493c05e4342cf205fdded4 Mon Sep 17 00:00:00 2001
From: Kenneth Sequeira
Date: Mon, 7 Dec 2020 10:45:13 +0530
Subject: [PATCH 115/286] fix: py code clean up and doc help
---
erpnext/crm/doctype/contract_template/contract_template.json | 4 ++--
erpnext/crm/doctype/contract_template/contract_template.py | 2 --
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json
index 9fc24798cd1..7cc5ec13cf7 100644
--- a/erpnext/crm/doctype/contract_template/contract_template.json
+++ b/erpnext/crm/doctype/contract_template/contract_template.json
@@ -52,11 +52,11 @@
"fieldname": "contract_template_help",
"fieldtype": "HTML",
"label": "Contract Template Help",
- "options": "
Contract Template Example
\n\n
Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n
\n\n
How to get fieldnames
\n\n
The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)
\n\n
Templating
\n\n
Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
"
+ "options": "
Contract Template Example
\n\n
Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n
\n\n
How to get fieldnames
\n\n
The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)
\n\n
Templating
\n\n
Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.
{% } %}
{% } %}
From 3eea3c6c954ad9a3e774aca4afd41f219a9bc57a Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Wed, 18 Nov 2020 20:17:52 +0530
Subject: [PATCH 151/286] fix: Table 'tabStock Entry Detail' is specified twice
---
erpnext/controllers/status_updater.py | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 2555edf06b0..8c05134ae41 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -254,22 +254,26 @@ class StatusUpdater(Document):
if not args.get("second_source_extra_cond"):
args["second_source_extra_cond"] = ""
- args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
+ args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
- and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args
+ and (`tab%(second_source_dt)s`.docstatus=1)
+ %(second_source_extra_cond)s), 0) """ % args)[0][0]
if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = ""
- frappe.db.sql("""update `tab%(target_dt)s`
- set %(target_field)s = (
+ args["source_dt_value"] = frappe.db.sql("""
(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
+ """ % args)[0][0] or 0.0
+
+ if args['second_source_condition']:
+ args["source_dt_value"] += flt(args['second_source_condition'])
+
+ frappe.db.sql("""update `tab%(target_dt)s`
+ set %(target_field)s = %(source_dt_value)s %(update_modified)s
where name='%(detail_id)s'""" % args)
def _update_percent_field_in_targets(self, args, update_modified=True):
From f17ea2ccabc5e42b4f3e9b3fe0373670109f75ad Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Fri, 11 Dec 2020 21:30:39 +0530
Subject: [PATCH 152/286] fix: Accounting for internal transfer invoices within
same company (#24021)
* fix: Accounting for internal transfer invoices within same company
* fix: warehouse fetching
* fix: Linting issues
* fix: GL entry fixes and validation for intercompany account
* fix: Account naming changes and other fixes
* fix: Add test for internal transfer
* fix: Test Case
* fix: Add description for fields
* fix: Commonfied code
* fix: Map warehouse and serial no
---
.../purchase_invoice/purchase_invoice.js | 10 ++
.../purchase_invoice/purchase_invoice.json | 32 ++++-
.../purchase_invoice/purchase_invoice.py | 94 ++++++++------
.../purchase_invoice/purchase_invoice_list.js | 16 +--
.../doctype/sales_invoice/sales_invoice.js | 10 ++
.../doctype/sales_invoice/sales_invoice.json | 25 +++-
.../doctype/sales_invoice/sales_invoice.py | 85 +++++++++----
.../sales_invoice/sales_invoice_list.js | 4 +-
.../sales_invoice/test_sales_invoice.py | 115 +++++++++++++++++-
erpnext/buying/doctype/supplier/supplier.py | 6 +
erpnext/controllers/accounts_controller.py | 34 ++++++
erpnext/controllers/buying_controller.py | 21 +++-
erpnext/controllers/stock_controller.py | 14 ++-
erpnext/controllers/taxes_and_totals.py | 14 ++-
.../public/js/controllers/taxes_and_totals.js | 11 +-
erpnext/selling/doctype/customer/customer.py | 8 +-
erpnext/setup/doctype/company/company.js | 3 +-
erpnext/setup/doctype/company/company.json | 29 ++---
18 files changed, 415 insertions(+), 116 deletions(-)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 1d41d0fa2a9..7830cfd3702 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
return (doc.qty<=doc.received_qty) ? "green" : "orange";
});
}
+
+ this.frm.set_query("unrealized_profit_loss_account", function() {
+ return {
+ filters: {
+ company: doc.company,
+ is_group: 0,
+ root_type: "Liability",
+ }
+ };
+ });
},
onload: function() {
this._super();
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 2df77a84c79..c64ffd878c4 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -127,6 +126,7 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
+ "adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@@ -152,9 +152,11 @@
"is_opening",
"against_expense_account",
"column_break_63",
+ "unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
+ "represents_company",
"remarks",
"subscription_section",
"from_date",
@@ -1223,7 +1225,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
- "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled",
+ "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
{
@@ -1330,13 +1332,37 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "default": "0",
+ "description": "Taxes paid while advance payment will be adjusted against this invoice",
+ "fieldname": "adjust_advance_taxes",
+ "fieldtype": "Check",
+ "label": "Adjust Advance Taxes"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_supplier",
+ "description": "Unrealized Profit / Loss account for intra-company transfers",
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_supplier",
+ "description": "Company which internal supplier represents",
+ "fetch_from": "supplier.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:57:18.266978",
+ "modified": "2020-12-11 12:46:12.796378",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 8bd788890a5..d94d261c6bc 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -206,8 +206,8 @@ class PurchaseInvoice(BuyingController):
["Purchase Receipt", "purchase_receipt", "pr_detail"]
])
- def validate_warehouse(self):
- if self.update_stock:
+ def validate_warehouse(self, for_validate=True):
+ if self.update_stock and for_validate:
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
@@ -233,7 +233,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock:
self.validate_item_code()
- self.validate_warehouse()
+ self.validate_warehouse(for_validate)
if auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@@ -449,6 +449,7 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
@@ -457,7 +458,6 @@ class PurchaseInvoice(BuyingController):
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
-
return gl_entries
def check_asset_cwip_enabled(self):
@@ -474,31 +474,30 @@ class PurchaseInvoice(BuyingController):
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- if grand_total:
- # Didnot use base_grand_total to book rounding loss gle
- grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
- self.precision("grand_total"))
- gl_entries.append(
- self.get_gl_dict({
- "account": self.credit_to,
- "party_type": "Supplier",
- "party": self.supplier,
- "due_date": self.due_date,
- "against": self.against_expense_account,
- "credit": grand_total_in_company_currency,
- "credit_in_account_currency": grand_total_in_company_currency \
- if self.party_account_currency==self.company_currency else grand_total,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "project": self.project,
- "cost_center": self.cost_center
- }, self.party_account_currency, item=self)
- )
+ if grand_total and not self.is_internal_transfer():
+ # Didnot use base_grand_total to book rounding loss gle
+ grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
+ self.precision("grand_total"))
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.credit_to,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "due_date": self.due_date,
+ "against": self.against_expense_account,
+ "credit": grand_total_in_company_currency,
+ "credit_in_account_currency": grand_total_in_company_currency \
+ if self.party_account_currency==self.company_currency else grand_total,
+ "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
+ "against_voucher_type": self.doctype,
+ "project": self.project,
+ "cost_center": self.cost_center
+ }, self.party_account_currency, item=self)
+ )
def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@@ -526,7 +525,6 @@ class PurchaseInvoice(BuyingController):
item, voucher_wise_stock_value, account_currency)
if item.from_warehouse:
-
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.warehouse]['account'],
"against": warehouse_account[item.from_warehouse]["account"],
@@ -546,16 +544,18 @@ class PurchaseInvoice(BuyingController):
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
}, warehouse_account[item.from_warehouse]["account_currency"], item=item))
- gl_entries.append(
- self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "cost_center": item.cost_center,
- "project": item.project
- }, account_currency, item=item)
- )
+ # Do not book expense for transfer within same company transfer
+ if not self.is_internal_transfer():
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "against": self.supplier,
+ "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "cost_center": item.cost_center,
+ "project": item.project
+ }, account_currency, item=item)
+ )
else:
gl_entries.append(
@@ -832,7 +832,8 @@ class PurchaseInvoice(BuyingController):
}, account_currency, item=tax)
)
# accumulate valuation tax
- if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
+ if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \
+ and not self.is_internal_transfer():
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.name, 0)
@@ -876,8 +877,19 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock"
- }, item=tax)
- )
+ }, item=tax))
+
+ def make_internal_transfer_gl_entries(self, gl_entries):
+ if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
+ account_currency = get_account_currency(self.unrealized_profit_loss_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.unrealized_profit_loss_account,
+ "against": self.supplier,
+ "credit": flt(self.total_taxes_and_charges),
+ "credit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center
+ }, account_currency, item=self))
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
@@ -1095,7 +1107,9 @@ class PurchaseInvoice(BuyingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
- if outstanding_amount > 0 and due_date < nowdate:
+ if self.is_internal_transfer():
+ self.status = 'Internal Transfer'
+ elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
index 86c2e408c0b..8da7d6fe13d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
@@ -4,23 +4,25 @@
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
- "currency", "is_return", "release_date", "on_hold"],
+ "currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
get_indicator: function(doc) {
- if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
+ if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
- } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
+ } else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"];
- } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
+ } else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
return [__("Temporarily on Hold"), "darkgrey"];
- } else if(frappe.datetime.get_diff(doc.due_date) < 0) {
+ } else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
- } else if(cint(doc.is_return)) {
+ } else if (cint(doc.is_return)) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
- } else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
+ } else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
+ return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
+ } else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
}
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 502e65ed8d0..5efc32e11d9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
+ frm.set_query("unrealized_profit_loss_account", function() {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0,
+ root_type: "Liability",
+ }
+ };
+ });
+
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return',
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 17fbe2def9d..6799fb986aa 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@@ -158,6 +157,7 @@
"more_information",
"inter_company_invoice_reference",
"is_internal_customer",
+ "represents_company",
"customer_group",
"campaign",
"is_discounted",
@@ -171,6 +171,7 @@
"c_form_applicable",
"c_form_no",
"column_break8",
+ "unrealized_profit_loss_account",
"remarks",
"sales_team_section_break",
"sales_partner",
@@ -1655,7 +1656,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
+ "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
"read_only": 1
},
@@ -1950,13 +1951,31 @@
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.is_internal_customer",
+ "description": "Unrealized Profit / Loss account for intra-company transfers",
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_customer",
+ "description": "Company which internal customer represents",
+ "fetch_from": "customer.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:57:45.086303",
+ "modified": "2020-12-11 12:48:31.769958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 81f425f868c..ca6f22cc30b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -758,6 +758,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
@@ -777,7 +778,7 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- if grand_total:
+ if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
@@ -816,6 +817,18 @@ class SalesInvoice(SellingController):
}, account_currency, item=tax)
)
+ def make_internal_transfer_gl_entries(self, gl_entries):
+ if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
+ account_currency = get_account_currency(self.unrealized_profit_loss_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.unrealized_profit_loss_account,
+ "against": self.customer,
+ "debit": flt(self.total_taxes_and_charges),
+ "debit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center
+ }, account_currency, item=self))
+
def make_item_gl_entries(self, gl_entries):
# income account gl entries
for item in self.get("items"):
@@ -838,22 +851,24 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
else:
- income_account = (item.income_account
- if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
+ # Do not book income for transfer within same company
+ if not self.is_internal_transfer():
+ income_account = (item.income_account
+ if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
- account_currency = get_account_currency(income_account)
- gl_entries.append(
- self.get_gl_dict({
- "account": income_account,
- "against": self.customer,
- "credit": flt(item.base_net_amount, item.precision("base_net_amount")),
- "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
- if account_currency==self.company_currency
- else flt(item.net_amount, item.precision("net_amount"))),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
- )
+ account_currency = get_account_currency(income_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": income_account,
+ "against": self.customer,
+ "credit": flt(item.base_net_amount, item.precision("base_net_amount")),
+ "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
+ if account_currency==self.company_currency
+ else flt(item.net_amount, item.precision("net_amount"))),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
# expense account gl entries
if cint(self.update_stock) and \
@@ -1265,7 +1280,9 @@ class SalesInvoice(SellingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
- if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
+ if self.is_internal_transfer():
+ self.status = 'Internal Transfer'
+ elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
@@ -1530,9 +1547,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
+ source_document_warehouse_field = 'target_warehouse'
+ target_document_warehouse_field = 'from_warehouse'
else:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
+ source_document_warehouse_field = 'from_warehouse'
+ target_document_warehouse_field = 'target_warehouse'
validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype)
@@ -1559,6 +1580,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if currency:
target_doc.currency = currency
+ item_field_map = {
+ "doctype": target_doctype + " Item",
+ "field_no_map": [
+ "income_account",
+ "expense_account",
+ "cost_center",
+ "warehouse"
+ ]
+ }
+
+ if source_doc.get('update_stock'):
+ item_field_map.update({
+ 'field_map': {
+ source_document_warehouse_field: target_document_warehouse_field,
+ 'batch_no': 'batch_no',
+ 'serial_no': 'serial_no'
+ }
+ })
+
+
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
@@ -1567,15 +1608,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"taxes_and_charges"
]
},
- doctype +" Item": {
- "doctype": target_doctype + " Item",
- "field_no_map": [
- "income_account",
- "expense_account",
- "cost_center",
- "warehouse"
- ]
- }
+ doctype +" Item": item_field_map
}, target_doc, set_missing_values)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
index 05d49df711a..41140d19388 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
@@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = {
"Credit Note Issued": "darkgrey",
"Unpaid and Discounted": "orange",
"Overdue and Discounted": "red",
- "Overdue": "red"
-
+ "Overdue": "red",
+ "Internal Transfer": "darkgrey"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 46e954d9487..22a4f336547 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1573,7 +1573,7 @@ class TestSalesInvoice(unittest.TestCase):
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
-
+
def test_sales_invoice_with_project_link(self):
from erpnext.projects.doctype.project.test_project import make_project
@@ -1607,9 +1607,9 @@ class TestSalesInvoice(unittest.TestCase):
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", sales_invoice.name, as_dict=1)
-
+
self.assertTrue(gl_entries)
-
+
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["project"], gle.project)
@@ -1781,6 +1781,60 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
+ def test_internal_transfer_gl_entry(self):
+ ## Create internal transfer account
+ account = create_account(account_name="Unrealized Profit",
+ parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
+
+ frappe.db.set_value('Company', '_Test Company with perpetual inventory',
+ 'unrealized_profit_loss_account', account)
+
+ customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory")
+
+ create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory")
+
+ si = create_sales_invoice(
+ company = "_Test Company with perpetual inventory",
+ customer = customer,
+ debit_to = "Debtors - TCP1",
+ warehouse = "Stores - TCP1",
+ income_account = "Sales - TCP1",
+ expense_account = "Cost of Goods Sold - TCP1",
+ cost_center = "Main - TCP1",
+ currency = "INR",
+ do_not_save = 1
+ )
+
+ si.selling_price_list = "_Test Price List Rest of the World"
+ si.update_stock = 1
+ si.items[0].target_warehouse = 'Work In Progress - TCP1'
+ add_taxes(si)
+ si.save()
+ si.submit()
+
+ target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ target_doc.company = '_Test Company with perpetual inventory'
+ target_doc.items[0].warehouse = 'Finished Goods - TCP1'
+ add_taxes(target_doc)
+ target_doc.save()
+ target_doc.submit()
+
+ si_gl_entries = [
+ ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
+ ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
+ ]
+
+ check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
+
+ pi_gl_entries = [
+ ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
+ ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
+ ]
+
+ check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
+
def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
@@ -2039,4 +2093,57 @@ def get_taxes_and_charges():
"parentfield": "taxes",
"rate": 2,
"row_id": 1
- }]
\ No newline at end of file
+ }]
+
+def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
+ if not frappe.db.exists("Customer", customer_name):
+ customer = frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": customer_name,
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory",
+ "is_internal_customer": 1,
+ "represents_company": represents_company
+ })
+
+ customer.append("companies", {
+ "company": allowed_to_interact_with
+ })
+
+ customer.insert()
+ customer_name = customer.name
+ else:
+ customer_name = frappe.db.get_value("Customer", customer_name)
+
+ return customer_name
+
+def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
+ if not frappe.db.exists("Supplier", supplier_name):
+ supplier = frappe.get_doc({
+ "supplier_group": "_Test Supplier Group",
+ "supplier_name": supplier_name,
+ "doctype": "Supplier",
+ "is_internal_supplier": 1,
+ "represents_company": represents_company
+ })
+
+ supplier.append("companies", {
+ "company": allowed_to_interact_with
+ })
+
+ supplier.insert()
+ supplier_name = supplier.name
+ else:
+ supplier_name = frappe.db.exists("Supplier", supplier_name)
+
+ return supplier_name
+
+def add_taxes(doc):
+ doc.append('taxes', {
+ 'account_head': '_Test Account Excise Duty - TCP1',
+ "charge_type": "On Net Total",
+ "cost_center": "Main - TCP1",
+ "description": "Excise Duty",
+ "rate": 12
+ })
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index df143eefa0d..0ee9d180d99 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -49,6 +49,12 @@ class Supplier(TransactionBase):
msgprint(_("Series is mandatory"), raise_exception=1)
validate_party_accounts(self)
+ self.validate_internal_supplier()
+
+ def validate_internal_supplier(self):
+ if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"):
+ frappe.throw(_("Internal Supplier for company {0} already exists").format(
+ frappe.bold(self.represents_company)))
def on_trash(self):
delete_contact_and_address('Supplier', self.name)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 93a79ec934e..32c5d3a3b14 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -107,6 +107,8 @@ class AccountsController(TransactionBase):
else:
self.validate_deferred_start_and_end_date()
+ self.set_inter_company_account()
+
validate_regional(self)
if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self)
@@ -932,6 +934,38 @@ class AccountsController(TransactionBase):
else:
return frappe.db.get_single_value("Global Defaults", "disable_rounded_total")
+ def set_inter_company_account(self):
+ """
+ Set intercompany account for inter warehouse transactions
+ This account will be used in case billing company and internal customer's
+ representation company is same
+ """
+
+ if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
+ unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account')
+
+ if not unrealized_profit_loss_account:
+ msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format(
+ frappe.bold(self.company))
+ frappe.throw(msg)
+
+ self.unrealized_profit_loss_account = unrealized_profit_loss_account
+
+ def is_internal_transfer(self):
+ """
+ It will an internal transfer if its an internal customer and representation
+ company is same as billing company
+ """
+ if self.doctype == 'Sales Invoice':
+ internal_party_field = 'is_internal_customer'
+ else:
+ internal_party_field = 'is_internal_supplier'
+
+ if self.get(internal_party_field) and (self.represents_company == self.company):
+ return True
+
+ return False
+
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 5fabf7017be..286c4f44510 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -42,6 +42,7 @@ class BuyingController(StockController):
self.validate_items()
self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items()
+ self.update_tax_category_for_internal_transfer()
self.validate_warehouse()
self.validate_from_warehouse()
self.set_supplier_address()
@@ -94,13 +95,23 @@ class BuyingController(StockController):
def validate_stock_or_nonstock_items(self):
if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items():
- tax_for_valuation = [d for d in self.get("taxes")
+ msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
+ self.update_tax_category(msg)
+
+ def update_tax_category_for_internal_transfer(self):
+ if self.doctype == 'Purchase Invoice' and self.is_internal_transfer():
+ msg = _('Tax Category has been changed to "Total" as its an internal purchase.')
+ self.update_tax_category(msg)
+
+ def update_tax_category(self, msg):
+ tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]]
- if tax_for_valuation:
- for d in tax_for_valuation:
- d.category = 'Total'
- msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
+ if tax_for_valuation:
+ for d in tax_for_valuation:
+ d.category = 'Total'
+
+ msgprint(msg)
def validate_asset_return(self):
if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2f7b361b394..683d7f77b55 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -77,7 +77,7 @@ class StockController(AccountsController):
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
- # from warehouse account/ target warehouse account
+ # from warehouse account
self.check_expense_account(item_row)
@@ -92,9 +92,16 @@ class StockController(AccountsController):
sle = self.update_stock_ledger_entries(sle)
+ # expense account/ target_warehouse / source_warehouse
+ if item_row.get('target_warehouse'):
+ warehouse = item_row.get('target_warehouse')
+ expense_account = warehouse_account[warehouse]["account"]
+ else:
+ expense_account = item_row.expense_account
+
gl_list.append(self.get_gl_dict({
"account": warehouse_account[sle.warehouse]["account"],
- "against": item_row.expense_account,
+ "against": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
@@ -102,9 +109,8 @@ class StockController(AccountsController):
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
- # expense account
gl_list.append(self.get_gl_dict({
- "account": item_row.expense_account,
+ "account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index ad58f137ee8..8dd2e5bacbd 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -519,6 +519,17 @@ class calculate_taxes_and_totals(object):
if self.doc.docstatus == 0:
self.calculate_outstanding_amount()
+ def is_internal_invoice(self):
+ """
+ Checks if its an internal transfer invoice
+ and decides if to calculate any out standing amount or not
+ """
+
+ if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer():
+ return True
+
+ return False
+
def calculate_outstanding_amount(self):
# NOTE:
# write_off_amount is only for POS Invoice
@@ -526,7 +537,8 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
- if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
+ if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \
+ self.is_internal_invoice(): return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 99f3995a662..22e75780b85 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -609,6 +609,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.calculate_outstanding_amount(update_paid_amount);
},
+ is_internal_invoice: function() {
+ if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
+ if (this.frm.doc.company === this.frm.doc.represents_company) {
+ return true;
+ }
+ }
+ return false;
+ },
+
calculate_outstanding_amount: function(update_paid_amount) {
// NOTE:
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
@@ -617,7 +626,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.calculate_paid_amount();
}
- if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return;
+ if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return;
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 0172d9c128f..29214ee06d9 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -58,6 +58,7 @@ class Customer(TransactionBase):
self.set_loyalty_program()
self.check_customer_group_change()
self.validate_default_bank_account()
+ self.validate_internal_customer()
# set loyalty program tier
if frappe.db.exists('Customer', self.name):
@@ -82,6 +83,11 @@ class Customer(TransactionBase):
if not is_company_account:
frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)))
+ def validate_internal_customer(self):
+ if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"):
+ frappe.throw(_("Internal Customer for company {0} already exists").format(
+ frappe.bold(self.represents_company)))
+
def on_update(self):
self.validate_name_with_customer_group()
self.create_primary_contact()
@@ -398,7 +404,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
# form a list of emails and names to show to the user
credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users_formatted:
- frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
+ frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer))
message = """Please contact any of the following users to extend the credit limits for {0}:
{1}
""".format(customer, '
'.join(credit_controller_users_formatted))
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index cbf67b4cd6f..36033d9daee 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -274,7 +274,8 @@ erpnext.company.setup_queries = function(frm) {
["default_employee_advance_account", {"root_type": "Asset"}],
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
- ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}]
+ ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
+ ["unrealized_profit_loss_account", {"root_type": "Liability"}]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 40938ea0a5e..d49ae7ce8ac 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -46,10 +46,9 @@
"round_off_account",
"round_off_cost_center",
"write_off_account",
- "discount_allowed_account",
- "discount_received_account",
"exchange_gain_loss_account",
"unrealized_exchange_gain_loss_account",
+ "unrealized_profit_loss_account",
"column_break0",
"allow_account_creation_against_child_company",
"default_payable_account",
@@ -261,14 +260,14 @@
{
"fieldname": "create_chart_of_accounts_based_on",
"fieldtype": "Select",
- "label": "Create Chart of Accounts Based on",
+ "label": "Create Chart Of Accounts Based On",
"options": "\nStandard Template\nExisting Company"
},
{
"depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"",
"fieldname": "chart_of_accounts",
"fieldtype": "Select",
- "label": "Chart of Accounts Template",
+ "label": "Chart Of Accounts Template",
"no_copy": 1
},
{
@@ -345,18 +344,6 @@
"label": "Write Off Account",
"options": "Account"
},
- {
- "fieldname": "discount_allowed_account",
- "fieldtype": "Link",
- "label": "Discount Allowed Account",
- "options": "Account"
- },
- {
- "fieldname": "discount_received_account",
- "fieldtype": "Link",
- "label": "Discount Received Account",
- "options": "Account"
- },
{
"fieldname": "exchange_gain_loss_account",
"fieldtype": "Link",
@@ -740,6 +727,12 @@
"fieldtype": "Link",
"label": "Default In Transit Warehouse",
"options": "Warehouse"
+ },
+ {
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -747,7 +740,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2020-08-06 00:38:08.311216",
+ "modified": "2020-12-03 12:27:27.085094",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -808,4 +801,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
From 67dfe5db0d6241292aaad9e0f5a14f85536ea446 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 26 Nov 2020 12:55:27 +0530
Subject: [PATCH 153/286] fix: shipping chanrges not sync in erpnext from
shopify
---
.../connectors/shopify_connection.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py
index efbaa71924e..f0a05ed192f 100644
--- a/erpnext/erpnext_integrations/connectors/shopify_connection.py
+++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py
@@ -260,6 +260,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines:
+ if shipping_charge.get("price"):
+ taxes.append({
+ "charge_type": _("Actual"),
+ "account_head": get_tax_account_head(shipping_charge),
+ "description": shipping_charge["title"],
+ "tax_amount": shipping_charge["price"],
+ "cost_center": shopify_settings.cost_center
+ })
+
for tax in shipping_charge.get("tax_lines"):
taxes.append({
"charge_type": _("Actual"),
From 5bfd6831c4bbe5a451ed0fc54ea0cf74d6fdbf38 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Sun, 6 Dec 2020 17:21:30 +0530
Subject: [PATCH 154/286] fix: delete Receive at Warehouse entry on
cancellation of Send to Warehouse entry
---
erpnext/stock/doctype/stock_entry/stock_entry.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index e3159b95c30..ab4f347d3a8 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -120,6 +120,7 @@ class StockEntry(StockController):
self.update_transferred_qty()
self.update_quality_inspection()
self.delete_auto_created_batches()
+ self.delete_linked_stock_entry()
if self.purpose == 'Material Transfer' and self.add_to_transit:
self.set_material_request_transfer_status('Not Started')
@@ -152,6 +153,12 @@ class StockEntry(StockController):
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card))
+ def delete_linked_stock_entry(self):
+ if self.purpose == "Send to Warehouse":
+ for d in frappe.get_all("Stock Entry", filters={"docstatus": 0,
+ "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}):
+ frappe.delete_doc("Stock Entry", d.name)
+
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
From edb99d4e53ac590bd09760f5b7635b6a2e4cf14b Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 24 Nov 2020 23:23:00 +0530
Subject: [PATCH 155/286] fix: incorrect stock quantity if 'Allow Multiple
Material Consumption' has enabled
---
.../doctype/work_order/test_work_order.py | 33 +++++++++++++++++++
.../doctype/work_order/work_order.js | 3 +-
.../stock/doctype/stock_entry/stock_entry.js | 5 +++
.../stock/doctype/stock_entry/stock_entry.py | 30 ++++++++---------
4 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index e53927918eb..2bf3fbf75e9 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -491,6 +491,39 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
+ def test_partial_material_consumption(self):
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
+ wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
+
+ ste_cancel_list = []
+ ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
+ ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
+
+ ste_cancel_list.extend([ste1, ste2])
+
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
+ s.submit()
+ ste_cancel_list.append(s)
+
+ ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
+ ste1.submit()
+ ste_cancel_list.append(ste1)
+
+ print(wo_order.name)
+ ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
+ self.assertEquals(ste3.fg_completed_qty, 2)
+
+ expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
+ for row in ste3.items:
+ self.assertEquals(row.qty, expected_qty.get(row.item_code))
+
+ for ste_doc in ste_cancel_list:
+ ste_doc.cancel()
+
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
+
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 9ce465ccaf7..a6086fb88da 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -545,7 +545,8 @@ erpnext.work_order = {
var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) {
- if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
+ let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
+ if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1;
}
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 91217582ca4..27fcbb7e2a5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -841,6 +841,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
}
},
+ fg_completed_qty: function() {
+ this.get_items();
+ },
+
get_items: function() {
var me = this;
if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
@@ -850,6 +854,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
// if work order / bom is mentioned, get items
return this.frm.call({
doc: me.frm.doc,
+ freeze: true,
method: "get_items",
callback: function(r) {
if(!r.exc) refresh_field("items");
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index e3159b95c30..415f5243655 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1033,26 +1033,22 @@ class StockEntry(StockController):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order},
- fields=["item_code", "required_qty", "consumed_qty"]
+ fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"]
)
+ work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items:
- qty = item.required_qty
-
item_account_details = get_item_defaults(item.item_code, self.company)
# Take into account consumption if there are any.
- if self.purpose == 'Manufacture':
- req_qty_each = flt(item.required_qty / wo.qty)
- if (flt(item.consumed_qty) != 0):
- remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each)
- exhaust_qty = req_qty_each * wo.produced_qty
- if remaining_qty > exhaust_qty :
- if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
- qty =0
- else:
- qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
- else:
- qty = req_qty_each * flt(self.fg_completed_qty)
+
+ wo_item_qty = item.transferred_qty or item.required_qty
+
+ req_qty_each = (
+ (flt(wo_item_qty) - flt(item.consumed_qty)) /
+ (flt(work_order_qty) - flt(wo.produced_qty))
+ )
+
+ qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0:
self.add_to_stock_entry_detail({
@@ -1134,13 +1130,15 @@ class StockEntry(StockController):
else:
qty = req_qty_each * flt(self.fg_completed_qty)
-
elif backflushed_materials.get(item.item_code):
for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse):
if (qty > req_qty):
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
+ if consumed_qty:
+ qty -= consumed_qty
+
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty)
From ced3b13492b3ce1c95255d4edba54839ab3dbc78 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Sat, 12 Dec 2020 19:31:05 +0530
Subject: [PATCH 156/286] fix: Check for paid field
---
erpnext/patches/v13_0/update_old_loans.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index caec53b3fdf..561e967d6df 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -23,12 +23,14 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'journal_entry_account')
updated_loan_types = []
+ loans_to_close = []
# Update old loan status as closed
- loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule`
- where paid = 0 and docstatus = 1""", as_dict=1)
+ if frappe.db.has_column('Repayment Schedule', 'paid'):
+ loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule`
+ where paid = 0 and docstatus = 1""", as_dict=1)
- loans_to_close = [d.parent for d in loans_list]
+ loans_to_close = [d.parent for d in loans_list]
if loans_to_close:
frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
From c838682188a71d2a71da68f132584414ed8bad83 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 27 Nov 2020 21:55:02 +0530
Subject: [PATCH 157/286] fix: Opening invoices in GSTR-1 report
---
erpnext/regional/report/gstr_1/gstr_1.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 837929709ec..ad3de5f398d 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -151,6 +151,7 @@ class Gstr1Report(object):
{select_columns}
from `tab{doctype}`
where docstatus = 1 {where_conditions}
+ and is_opening = 'No'
order by posting_date desc
""".format(select_columns=self.select_columns, doctype=self.doctype,
where_conditions=conditions), self.filters, as_dict=1)
From f9751f1f95e9f69701da999bcb342c6b620bcf95 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Mon, 14 Dec 2020 16:20:02 +0530
Subject: [PATCH 158/286] feat: project template having dependent tasks
---
erpnext/projects/doctype/project/project.py | 33 +-
.../project_template/project_template.py | 23 +-
erpnext/projects/doctype/task/task.py | 451 +++++++++---------
3 files changed, 282 insertions(+), 225 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index dfb54a2f77f..2d3339773a1 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -55,11 +55,13 @@ class Project(Document):
# create tasks from template
project_tasks = []
+ tmp_task_details = []
for task in template.tasks:
template_task_details = frappe.get_doc("Task", task.task)
+ tmp_task_details.append(template_task_details)
project_tasks.append(self.create_task_from_template(template_task_details))
- #self.dependency_mapping(template.tasks, project_tasks)
+ self.dependency_mapping(tmp_task_details, project_tasks)
def create_task_from_template(self, task_details):
return frappe.get_doc(dict(
@@ -78,16 +80,33 @@ class Project(Document):
duration = task_details.duration
)).insert()
- """ def dependency_mapping(self, template_tasks, project_tasks):
+ def dependency_mapping(self, template_tasks, project_tasks):
for tmp_task in template_tasks:
for prj_task in project_tasks:
if tmp_task.subject == prj_task.subject:
- if tmp_task.depends_on and not prj_task.depends_on:
- for child_task in tmp_task.depends_on:
- child_task_detai
- prj_task.depends_on = tmp_task.depends_on
- """
+ self.check_depends_on_value(tmp_task, prj_task, project_tasks)
+ self.check_for_parent_tasks(tmp_task, prj_task, project_tasks)
+ def check_depends_on_value(self, tmp_task, prj_task, project_tasks):
+ if tmp_task.depends_on and not prj_task.depends_on:
+ for child_task in tmp_task.depends_on:
+ child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+ corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+ if len(corresponding_prj_task):
+ prj_task.append("depends_on",{
+ "task": corresponding_prj_task[0].name
+ })
+ prj_task.save()
+
+ def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks):
+ if tmp_task.parent_task and not prj_task.parent_task:
+ parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject")
+ corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+ if len(corresponding_prj_task):
+ prj_task.parent_task = corresponding_prj_task[0].name
+ print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name)
+ prj_task.save()
+ print(prj_task.name, corresponding_prj_task[0].name)
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py
index ac78135fc42..1beebf7a258 100644
--- a/erpnext/projects/doctype/project_template/project_template.py
+++ b/erpnext/projects/doctype/project_template/project_template.py
@@ -3,8 +3,27 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+import frappe
from frappe.model.document import Document
+from frappe import _
class ProjectTemplate(Document):
- pass
+
+ def validate(self):
+ self.validate_dependencies()
+
+ def validate_dependencies(self):
+ for task in self.tasks:
+ task_details = frappe.get_doc("Task", task.task)
+ if task_details.depends_on:
+ for dependency_task in task_details.depends_on:
+ if not self.check_dependent_task_presence(dependency_task.task):
+ task_details_format = """{0}""".format(task_details.name)
+ dependency_task_format = """{0}""".format(dependency_task.task)
+ frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
+
+ def check_dependent_task_presence(self, task):
+ for task_details in self.tasks:
+ if task_details.task == task:
+ return True
+ return False
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index fb84094ffe6..072a848f263 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,291 +17,310 @@ class CircularReferenceError(frappe.ValidationError): pass
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
- nsm_parent_field = 'parent_task'
+ nsm_parent_field = 'parent_task'
- def get_feed(self):
- return '{0}: {1}'.format(_(self.status), self.subject)
+ def get_feed(self):
+ return '{0}: {1}'.format(_(self.status), self.subject)
- def get_customer_details(self):
- cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
- if cust:
- ret = {'customer_name': cust and cust[0][0] or ''}
- return ret
+ def get_customer_details(self):
+ cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+ if cust:
+ ret = {'customer_name': cust and cust[0][0] or ''}
+ return ret
- def validate(self):
- self.validate_dates()
- self.validate_parent_project_dates()
- self.validate_progress()
- self.validate_status()
- self.update_depends_on()
+ def validate(self):
+ self.validate_dates()
+ self.validate_parent_project_dates()
+ self.validate_progress()
+ self.validate_status()
+ self.update_depends_on()
+ self.validate_dependencies_for_template_task()
- def validate_dates(self):
- if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
- frappe.bold("Expected End Date")))
+ def validate_dates(self):
+ if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
- if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
- frappe.bold("Actual End Date")))
+ if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
- def validate_parent_project_dates(self):
- if not self.project or frappe.flags.in_test:
- return
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
- expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+ expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
- if expected_end_date:
- validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
- validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+ if expected_end_date:
+ validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
- def validate_status(self):
- if self.status!=self.get_db_value("status") and self.status == "Completed":
- for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+ def validate_status(self):
+ if self.status!=self.get_db_value("status") and self.status == "Completed":
+ for d in self.depends_on:
+ if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+ frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
- close_all_assignments(self.doctype, self.name)
+ close_all_assignments(self.doctype, self.name)
- def validate_progress(self):
- if flt(self.progress or 0) > 100:
- frappe.throw(_("Progress % for a task cannot be more than 100."))
+ def validate_progress(self):
+ if flt(self.progress or 0) > 100:
+ frappe.throw(_("Progress % for a task cannot be more than 100."))
- if flt(self.progress) == 100:
- self.status = 'Completed'
+ if flt(self.progress) == 100:
+ self.status = 'Completed'
- if self.status == 'Completed':
- self.progress = 100
+ if self.status == 'Completed':
+ self.progress = 100
- def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
- for d in self.depends_on:
- if d.task and not d.task in depends_on_tasks:
- depends_on_tasks += d.task + ","
- self.depends_on_tasks = depends_on_tasks
+ def validate_dependencies_for_template_task(self):
+ if self.is_template:
+ self.validate_parent_template_task()
+ self.validate_depends_on_tasks()
+
+ def validate_parent_template_task(self):
+ if self.parent_task:
+ if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+ parent_task_format = """{0}""".format(self.parent_task)
+ frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
+
+ def validate_depends_on_tasks(self):
+ if self.depends_on:
+ for task in self.depends_on:
+ if not frappe.db.get_value("Task", task.task, "is_template"):
+ dependent_task_format = """{0}""".format(task.task)
+ frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
- def update_nsm_model(self):
- frappe.utils.nestedset.update_nsm(self)
+ def update_depends_on(self):
+ depends_on_tasks = self.depends_on_tasks or ""
+ for d in self.depends_on:
+ if d.task and not d.task in depends_on_tasks:
+ depends_on_tasks += d.task + ","
+ self.depends_on_tasks = depends_on_tasks
- def on_update(self):
- self.update_nsm_model()
- self.check_recursion()
- self.reschedule_dependent_tasks()
- self.update_project()
- self.unassign_todo()
- self.populate_depends_on()
+ def update_nsm_model(self):
+ frappe.utils.nestedset.update_nsm(self)
- def unassign_todo(self):
- if self.status == "Completed":
- close_all_assignments(self.doctype, self.name)
- if self.status == "Cancelled":
- clear(self.doctype, self.name)
+ def on_update(self):
+ self.update_nsm_model()
+ self.check_recursion()
+ self.reschedule_dependent_tasks()
+ self.update_project()
+ self.unassign_todo()
+ self.populate_depends_on()
- def update_total_expense_claim(self):
- self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
- where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+ def unassign_todo(self):
+ if self.status == "Completed":
+ close_all_assignments(self.doctype, self.name)
+ if self.status == "Cancelled":
+ clear(self.doctype, self.name)
- def update_time_and_costing(self):
- tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
- sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
- sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
- ,self.name, as_dict=1)[0]
- if self.status == "Open":
- self.status = "Working"
- self.total_costing_amount= tl.total_costing_amount
- self.total_billing_amount= tl.total_billing_amount
- self.actual_time= tl.time
- self.act_start_date= tl.start_date
- self.act_end_date= tl.end_date
+ def update_total_expense_claim(self):
+ self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+ where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
- def update_project(self):
- if self.project and not self.flags.from_project:
- frappe.get_cached_doc("Project", self.project).update_project()
+ def update_time_and_costing(self):
+ tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+ sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+ sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+ ,self.name, as_dict=1)[0]
+ if self.status == "Open":
+ self.status = "Working"
+ self.total_costing_amount= tl.total_costing_amount
+ self.total_billing_amount= tl.total_billing_amount
+ self.actual_time= tl.time
+ self.act_start_date= tl.start_date
+ self.act_end_date= tl.end_date
- def check_recursion(self):
- if self.flags.ignore_recursion_check: return
- check_list = [['task', 'parent'], ['parent', 'task']]
- for d in check_list:
- task_list, count = [self.name], 0
- while (len(task_list) > count ):
- tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
- (d[0], d[1], '%s'), cstr(task_list[count]))
- count = count + 1
- for b in tasks:
- if b[0] == self.name:
- frappe.throw(_("Circular Reference Error"), CircularReferenceError)
- if b[0]:
- task_list.append(b[0])
+ def update_project(self):
+ if self.project and not self.flags.from_project:
+ frappe.get_cached_doc("Project", self.project).update_project()
- if count == 15:
- break
+ def check_recursion(self):
+ if self.flags.ignore_recursion_check: return
+ check_list = [['task', 'parent'], ['parent', 'task']]
+ for d in check_list:
+ task_list, count = [self.name], 0
+ while (len(task_list) > count ):
+ tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+ (d[0], d[1], '%s'), cstr(task_list[count]))
+ count = count + 1
+ for b in tasks:
+ if b[0] == self.name:
+ frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+ if b[0]:
+ task_list.append(b[0])
- def reschedule_dependent_tasks(self):
- end_date = self.exp_end_date or self.act_end_date
- if end_date:
- for task_name in frappe.db.sql("""
- select name from `tabTask` as parent
- where parent.project = %(project)s
- and parent.name in (
- select parent from `tabTask Depends On` as child
- where child.task = %(task)s and child.project = %(project)s)
- """, {'project': self.project, 'task':self.name }, as_dict=1):
- task = frappe.get_doc("Task", task_name.name)
- if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
- task_duration = date_diff(task.exp_end_date, task.exp_start_date)
- task.exp_start_date = add_days(end_date, 1)
- task.exp_end_date = add_days(task.exp_start_date, task_duration)
- task.flags.ignore_recursion_check = True
- task.save()
+ if count == 15:
+ break
- def has_webform_permission(self):
- project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
- if project_user:
- return True
+ def reschedule_dependent_tasks(self):
+ end_date = self.exp_end_date or self.act_end_date
+ if end_date:
+ for task_name in frappe.db.sql("""
+ select name from `tabTask` as parent
+ where parent.project = %(project)s
+ and parent.name in (
+ select parent from `tabTask Depends On` as child
+ where child.task = %(task)s and child.project = %(project)s)
+ """, {'project': self.project, 'task':self.name }, as_dict=1):
+ task = frappe.get_doc("Task", task_name.name)
+ if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+ task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+ task.exp_start_date = add_days(end_date, 1)
+ task.exp_end_date = add_days(task.exp_start_date, task_duration)
+ task.flags.ignore_recursion_check = True
+ task.save()
- def populate_depends_on(self):
- if self.parent_task:
- parent = frappe.get_doc('Task', self.parent_task)
- if not self.name in [row.task for row in parent.depends_on]:
- parent.append("depends_on", {
- "doctype": "Task Depends On",
- "task": self.name,
- "subject": self.subject
- })
- parent.save()
+ def has_webform_permission(self):
+ project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+ if project_user:
+ return True
- def on_trash(self):
- if check_if_child_exists(self.name):
- throw(_("Child Task exists for this Task. You can not delete this Task."))
+ def populate_depends_on(self):
+ if self.parent_task:
+ parent = frappe.get_doc('Task', self.parent_task)
+ if not self.name in [row.task for row in parent.depends_on]:
+ parent.append("depends_on", {
+ "doctype": "Task Depends On",
+ "task": self.name,
+ "subject": self.subject
+ })
+ parent.save()
- self.update_nsm_model()
+ def on_trash(self):
+ if check_if_child_exists(self.name):
+ throw(_("Child Task exists for this Task. You can not delete this Task."))
- def after_delete(self):
- self.update_project()
+ self.update_nsm_model()
- def update_status(self):
- if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
- from datetime import datetime
- if self.exp_end_date < datetime.now().date():
- self.db_set('status', 'Overdue', update_modified=False)
- self.update_project()
+ def after_delete(self):
+ self.update_project()
+
+ def update_status(self):
+ if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+ from datetime import datetime
+ if self.exp_end_date < datetime.now().date():
+ self.db_set('status', 'Overdue', update_modified=False)
+ self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
- child_tasks = frappe.get_all("Task", filters={"parent_task": name})
- child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
- return child_tasks
+ child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+ child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+ return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql(""" select name from `tabProject`
- where %(key)s like %(txt)s
- %(mcond)s
- order by name
- limit %(start)s, %(page_len)s""" % {
- 'key': searchfield,
- 'txt': frappe.db.escape('%' + txt + '%'),
- 'mcond':get_match_cond(doctype),
- 'start': start,
- 'page_len': page_len
- })
+ from erpnext.controllers.queries import get_match_cond
+ return frappe.db.sql(""" select name from `tabProject`
+ where %(key)s like %(txt)s
+ %(mcond)s
+ order by name
+ limit %(start)s, %(page_len)s""" % {
+ 'key': searchfield,
+ 'txt': frappe.db.escape('%' + txt + '%'),
+ 'mcond':get_match_cond(doctype),
+ 'start': start,
+ 'page_len': page_len
+ })
@frappe.whitelist()
def set_multiple_status(names, status):
- names = json.loads(names)
- for name in names:
- task = frappe.get_doc("Task", name)
- task.status = status
- task.save()
+ names = json.loads(names)
+ for name in names:
+ task = frappe.get_doc("Task", name)
+ task.status = status
+ task.save()
def set_tasks_as_overdue():
- tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
- for task in tasks:
- if task.status == "Pending Review":
- if getdate(task.review_date) > getdate(today()):
- continue
- frappe.get_doc("Task", task.name).update_status()
+ tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+ for task in tasks:
+ if task.status == "Pending Review":
+ if getdate(task.review_date) > getdate(today()):
+ continue
+ frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
- def set_missing_values(source, target):
- target.append("time_logs", {
- "hours": source.actual_time,
- "completed": source.status == "Completed",
- "project": source.project,
- "task": source.name
- })
+ def set_missing_values(source, target):
+ target.append("time_logs", {
+ "hours": source.actual_time,
+ "completed": source.status == "Completed",
+ "project": source.project,
+ "task": source.name
+ })
- doclist = get_mapped_doc("Task", source_name, {
- "Task": {
- "doctype": "Timesheet"
- }
- }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+ doclist = get_mapped_doc("Task", source_name, {
+ "Task": {
+ "doctype": "Timesheet"
+ }
+ }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
- return doclist
+ return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
- filters = [['docstatus', '<', '2']]
+ filters = [['docstatus', '<', '2']]
- if task:
- filters.append(['parent_task', '=', task])
- elif parent and not is_root:
- # via expand child
- filters.append(['parent_task', '=', parent])
- else:
- filters.append(['ifnull(`parent_task`, "")', '=', ''])
+ if task:
+ filters.append(['parent_task', '=', task])
+ elif parent and not is_root:
+ # via expand child
+ filters.append(['parent_task', '=', parent])
+ else:
+ filters.append(['ifnull(`parent_task`, "")', '=', ''])
- if project:
- filters.append(['project', '=', project])
+ if project:
+ filters.append(['project', '=', project])
- tasks = frappe.get_list(doctype, fields=[
- 'name as value',
- 'subject as title',
- 'is_group as expandable'
- ], filters=filters, order_by='name')
+ tasks = frappe.get_list(doctype, fields=[
+ 'name as value',
+ 'subject as title',
+ 'is_group as expandable'
+ ], filters=filters, order_by='name')
- # return tasks
- return tasks
+ # return tasks
+ return tasks
@frappe.whitelist()
def add_node():
- from frappe.desk.treeview import make_tree_args
- args = frappe.form_dict
- args.update({
- "name_field": "subject"
- })
- args = make_tree_args(**args)
+ from frappe.desk.treeview import make_tree_args
+ args = frappe.form_dict
+ args.update({
+ "name_field": "subject"
+ })
+ args = make_tree_args(**args)
- if args.parent_task == 'All Tasks' or args.parent_task == args.project:
- args.parent_task = None
+ if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+ args.parent_task = None
- frappe.get_doc(args).insert()
+ frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
- data = json.loads(data)
- new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
- new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+ data = json.loads(data)
+ new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+ new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
- for d in data:
- if not d.get("subject"): continue
- new_doc['subject'] = d.get("subject")
- new_task = frappe.get_doc(new_doc)
- new_task.insert()
+ for d in data:
+ if not d.get("subject"): continue
+ new_doc['subject'] = d.get("subject")
+ new_task = frappe.get_doc(new_doc)
+ new_task.insert()
def on_doctype_update():
- frappe.db.add_index("Task", ["lft", "rgt"])
+ frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
- if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
- frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
- if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
- frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
From e64718b2ae7a7a92e8e542e1361437cd030e4015 Mon Sep 17 00:00:00 2001
From: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Date: Tue, 15 Dec 2020 09:16:27 +0530
Subject: [PATCH 159/286] fix: selecting salary component (#24121)
---
.../doctype/additional_salary/additional_salary.js | 8 --------
.../doctype/employee_incentive/employee_incentive.js | 4 ++--
.../payroll/doctype/salary_structure/salary_structure.js | 7 +++----
.../payroll/doctype/salary_structure/salary_structure.py | 2 +-
4 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index 0784de93eb1..7737e6c8869 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,14 +12,6 @@ frappe.ui.form.on('Additional Salary', {
}
};
});
-
- if (!frm.doc.currency) return;
- frm.set_query("salary_component", function() {
- return {
- query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {currency: frm.doc.currency, company: frm.doc.company}
- };
- });
},
employee: function(frm) {
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
index 85d1c54a221..182ce0f83a6 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
@@ -11,11 +11,11 @@ frappe.ui.form.on('Employee Incentive', {
};
});
- if (!frm.doc.currency) return;
+ if (!frm.doc.company) return;
frm.set_query("salary_component", function() {
return {
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
+ filters: {type: "earning", company: frm.doc.company}
};
});
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 7daae49c587..ba824c5d6fa 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -55,17 +55,17 @@ frappe.ui.form.on('Salary Structure', {
},
set_earning_deduction_component: function(frm) {
- if(!frm.doc.currency && !frm.doc.company) return;
+ if(!frm.doc.company) return;
frm.set_query("salary_component", "earnings", function() {
return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
+ filters: {type: "earning", company: frm.doc.company}
};
});
frm.set_query("salary_component", "deductions", function() {
return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company}
+ filters: {type: "deduction", company: frm.doc.company}
};
});
},
@@ -74,7 +74,6 @@ frappe.ui.form.on('Salary Structure', {
currency: function(frm) {
calculate_totals(frm.doc);
frm.trigger("set_dynamic_labels")
- frm.trigger('set_earning_deduction_component');
frm.refresh()
},
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index 877e41d93c5..77914bb5319 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -210,7 +210,7 @@ def get_employees(salary_structure):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
- if len(filters) < 3:
+ if len(filters) < 2:
return {}
return frappe.db.sql("""
From 58e8e06ab7e1965fa4c37d0df9f986d58b095776 Mon Sep 17 00:00:00 2001
From: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Date: Tue, 15 Dec 2020 09:17:17 +0530
Subject: [PATCH 160/286] fix: retention filters (#24123)
* fix: retention filters
* fix: slider
---
erpnext/payroll/doctype/retention_bonus/retention_bonus.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
index 6fe8ccad46b..f8bb40a9cb8 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
@@ -4,9 +4,13 @@
frappe.ui.form.on('Retention Bonus', {
setup: function(frm) {
frm.set_query("employee", function() {
+ if (!frm.doc.company) {
+ frappe.msgprint(__("Please Select Company First"));
+ }
return {
filters: {
- "status": "Active"
+ "status": "Active",
+ "company": frm.doc.company
}
};
});
From 89d14fdf6877f021057a23289a8a6c7e05fa061a Mon Sep 17 00:00:00 2001
From: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Date: Tue, 15 Dec 2020 09:31:30 +0530
Subject: [PATCH 161/286] fix: minor ui changes (#24125)
* fix: minor ui changes
* fix: slider
---
.../employee_advance/employee_advance.js | 39 +++++++++++--------
.../employee_benefit_application.json | 7 +++-
2 files changed, 29 insertions(+), 17 deletions(-)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js
index 7056adf2083..5037ceb489e 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.js
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.js
@@ -18,13 +18,18 @@ frappe.ui.form.on('Employee Advance', {
if (!frm.doc.employee) {
frappe.msgprint(__("Please select employee first"));
}
- var company_currency = erpnext.get_currency(frm.doc.company);
+ let company_currency = erpnext.get_currency(frm.doc.company);
+ let currencies = [company_currency];
+ if (frm.doc.currency && (frm.doc.currency != company_currency)) {
+ currencies.push(frm.doc.currency);
+ }
+
return {
filters: {
"root_type": "Asset",
"is_group": 0,
"company": frm.doc.company,
- "account_currency": ["in", [frm.doc.currency, company_currency]],
+ "account_currency": ["in", currencies],
}
};
});
@@ -181,21 +186,23 @@ frappe.ui.form.on('Employee Advance', {
},
currency: function(frm) {
- var from_currency = frm.doc.currency;
- var company_currency;
- if (!frm.doc.company) {
- company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
- } else {
- company_currency = erpnext.get_currency(frm.doc.company);
+ if (frm.doc.currency) {
+ var from_currency = frm.doc.currency;
+ var company_currency;
+ if (!frm.doc.company) {
+ company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
+ } else {
+ company_currency = erpnext.get_currency(frm.doc.company);
+ }
+ if (from_currency != company_currency) {
+ frm.events.set_exchange_rate(frm, from_currency, company_currency);
+ } else {
+ frm.set_value("exchange_rate", 1.0);
+ frm.set_df_property('exchange_rate', 'hidden', 1);
+ frm.set_df_property("exchange_rate", "description", "" );
+ }
+ frm.refresh_fields();
}
- if (from_currency != company_currency) {
- frm.events.set_exchange_rate(frm, from_currency, company_currency);
- } else {
- frm.set_value("exchange_rate", 1.0);
- frm.set_df_property('exchange_rate', 'hidden', 1);
- frm.set_df_property("exchange_rate", "description", "" );
- }
- frm.refresh_fields();
},
set_exchange_rate: function(frm, from_currency, company_currency) {
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
index 9a5a463152e..4c45580bf01 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
@@ -23,6 +23,7 @@
"employee_benefits",
"totals",
"total_amount",
+ "column_break",
"pro_rata_dispensed_amount"
],
"fields": [
@@ -139,11 +140,15 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break",
+ "fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:49:05.095101",
+ "modified": "2020-12-14 15:52:08.566418",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",
From 85213fa8cbcbadbfa97848433e5a15fda0220dd7 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 15 Dec 2020 09:32:02 +0530
Subject: [PATCH 162/286] fix(Asset): set current asset value before
calculating difference amount (#24119)
---
.../doctype/asset_value_adjustment/asset_value_adjustment.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index c2579ebf708..74ca62ffdad 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -13,8 +13,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class AssetValueAdjustment(Document):
def validate(self):
self.validate_date()
- self.set_difference_amount()
self.set_current_asset_value()
+ self.set_difference_amount()
def on_submit(self):
self.make_depreciation_entry()
From f2206c27e75ad743ec73cc2332bed47917727689 Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Tue, 15 Dec 2020 05:05:16 +0100
Subject: [PATCH 163/286] fix: allow other github links in same PR (#23995)
---
.github/helper/documentation.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
index b603ed5e53d..9cc4663c394 100644
--- a/.github/helper/documentation.py
+++ b/.github/helper/documentation.py
@@ -21,8 +21,8 @@ def docs_link_exists(body):
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
- _, org, repo, _type, ref = parsed_url.path.split('/')
- if org == "frappe" and repo in docs_repos:
+ parts = parsed_url.path.split('/')
+ if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
From 23f0debf8807eeac894cfb5d628fddb80bf0fd72 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 15 Dec 2020 10:00:21 +0530
Subject: [PATCH 164/286] fix: tests
---
erpnext/projects/doctype/project/test_project.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index f9bb1b3ac4d..ea54774d52d 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -79,16 +79,16 @@ class TestProject(unittest.TestCase):
if not task2:
task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
- template = make_project_template("Test Project with Templ - dependent tasks", [task2])
- project = get_project("Test Project with Templ - tasks with parent-child", template)
+ template = make_project_template("Test Project with Templ - dependent tasks", [task1, task2])
+ project = get_project("Test Project with Templ - dependent tasks", template)
tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
- self.assertEqual(tasks[0].subject, 'Test Temp Task with dependency')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]))
- self.assertEqual(tasks[0].depends_on, tasks[1].name)
+ self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]))
+ self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
- self.assertEqual(tasks[1].subject, 'Test Temp Task for dependency')
- self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]) )
+ self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) )
self.assertEqual(len(tasks), 2)
From caf67e608f871adca275e001dddc96c96af4ea77 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 15 Dec 2020 10:00:31 +0530
Subject: [PATCH 165/286] fix: tests
---
erpnext/projects/doctype/project/project.py | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 2d3339773a1..5a9375a0e6c 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -82,31 +82,29 @@ class Project(Document):
def dependency_mapping(self, template_tasks, project_tasks):
for tmp_task in template_tasks:
- for prj_task in project_tasks:
- if tmp_task.subject == prj_task.subject:
- self.check_depends_on_value(tmp_task, prj_task, project_tasks)
- self.check_for_parent_tasks(tmp_task, prj_task, project_tasks)
+ prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0]
+ self.check_depends_on_value(tmp_task, prj_task, project_tasks)
+ self.check_for_parent_tasks(tmp_task, prj_task, project_tasks)
def check_depends_on_value(self, tmp_task, prj_task, project_tasks):
- if tmp_task.depends_on and not prj_task.depends_on:
- for child_task in tmp_task.depends_on:
+ if tmp_task.get("depends_on") and not prj_task.get("depends_on"):
+ for child_task in tmp_task.get("depends_on"):
child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
if len(corresponding_prj_task):
prj_task.append("depends_on",{
"task": corresponding_prj_task[0].name
})
+ print(prj_task.name)
prj_task.save()
def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks):
- if tmp_task.parent_task and not prj_task.parent_task:
- parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject")
+ if tmp_task.get("parent_task") and not prj_task.get("parent_task"):
+ parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject")
corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
if len(corresponding_prj_task):
prj_task.parent_task = corresponding_prj_task[0].name
- print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name)
prj_task.save()
- print(prj_task.name, corresponding_prj_task[0].name)
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
From a6fef7ae6bbdba8a4f922ebdfbf337033c41ac4d Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 15 Dec 2020 11:50:18 +0530
Subject: [PATCH 166/286] feat: parent-child relation tasks
---
erpnext/projects/doctype/project/project.py | 2 +-
erpnext/projects/doctype/project/test_project.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 5a9375a0e6c..13e72fec8a2 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -83,6 +83,7 @@ class Project(Document):
def dependency_mapping(self, template_tasks, project_tasks):
for tmp_task in template_tasks:
prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0]
+ prj_task = frappe.get_doc("Task", prj_task.name)
self.check_depends_on_value(tmp_task, prj_task, project_tasks)
self.check_for_parent_tasks(tmp_task, prj_task, project_tasks)
@@ -95,7 +96,6 @@ class Project(Document):
prj_task.append("depends_on",{
"task": corresponding_prj_task[0].name
})
- print(prj_task.name)
prj_task.save()
def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks):
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index ea54774d52d..c3f56b8e860 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -49,10 +49,10 @@ class TestProject(unittest.TestCase):
if not task3:
task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
- template = make_project_template("Test Project Template - tasks with parent-child", [task1])
+ template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3])
project = get_project("Test Project with Templ - tasks with parent-child", template)
tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
- print(tasks[0].duration)
+
self.assertEqual(tasks[0].subject, 'Test Temp Task parent')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]))
From c553453825d826da24516ffbde05fe2be1c3b938 Mon Sep 17 00:00:00 2001
From: Mohammad Hasnain Mohsin Rajan
Date: Tue, 15 Dec 2020 16:29:10 +0530
Subject: [PATCH 167/286] fix: user is not a field (#24129)
---
erpnext/non_profit/doctype/member/member.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 44b975e9e9d..25d6b538300 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -59,7 +59,7 @@ class Member(Document):
frappe.msgprint(_("A customer is already linked to this Member"))
cust = create_customer(frappe._dict({
'fullname': self.member_name,
- 'email': self.email_id or self.user,
+ 'email': self.email_id or self.email,
'phone': None
}))
@@ -177,4 +177,4 @@ def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, m
mobile=mobile
))
- return member.name
\ No newline at end of file
+ return member.name
From 29778e2fba4b1f073fdfc048f784f755c57a1eeb Mon Sep 17 00:00:00 2001
From: Leela vadlamudi
Date: Tue, 15 Dec 2020 21:23:17 +0530
Subject: [PATCH 168/286] feat: Voice Call Settings doctype added (#24126)
---
erpnext/public/js/telephony.js | 2 +-
.../doctype/voice_call_settings/__init__.py | 0
.../test_voice_call_settings.py | 10 ++
.../voice_call_settings.js | 8 ++
.../voice_call_settings.json | 124 ++++++++++++++++++
.../voice_call_settings.py | 10 ++
6 files changed, 153 insertions(+), 1 deletion(-)
create mode 100644 erpnext/telephony/doctype/voice_call_settings/__init__.py
create mode 100644 erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js
create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
create mode 100644 erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
index bd7f8903066..f9caadeed7f 100644
--- a/erpnext/public/js/telephony.js
+++ b/erpnext/public/js/telephony.js
@@ -20,4 +20,4 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
});
}
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/telephony/doctype/voice_call_settings/__init__.py b/erpnext/telephony/doctype/voice_call_settings/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
new file mode 100644
index 00000000000..85d6adda093
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestVoiceCallSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js
new file mode 100644
index 00000000000..4a61b612d00
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Voice Call Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
new file mode 100644
index 00000000000..25e55a22dce
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
@@ -0,0 +1,124 @@
+{
+ "actions": [],
+ "autoname": "field:user",
+ "creation": "2020-12-08 16:52:40.590146",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "call_receiving_device",
+ "column_break_3",
+ "greeting_message",
+ "agent_busy_message",
+ "agent_unavailable_message"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "permlevel": 1,
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "greeting_message",
+ "fieldtype": "Data",
+ "label": "Greeting Message"
+ },
+ {
+ "fieldname": "agent_busy_message",
+ "fieldtype": "Data",
+ "label": "Agent Busy Message"
+ },
+ {
+ "fieldname": "agent_unavailable_message",
+ "fieldtype": "Data",
+ "label": "Agent Unavailable Message"
+ },
+ {
+ "default": "Computer",
+ "fieldname": "call_receiving_device",
+ "fieldtype": "Select",
+ "label": "Call Receiving Device",
+ "options": "Computer\nPhone"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-14 18:49:34.600194",
+ "modified_by": "Administrator",
+ "module": "Telephony",
+ "name": "Voice Call Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "permlevel": 2,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "permlevel": 2,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
new file mode 100644
index 00000000000..ad3bbf1784d
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class VoiceCallSettings(Document):
+ pass
From 96a5e4effa54bb87c7700b0a060c2a119e02a0ac Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 16 Dec 2020 13:00:55 +0530
Subject: [PATCH 169/286] fix: Tax template update on customer address change
---
erpnext/regional/india/taxes.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index b70b2ec48cc..87baece65d3 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
tax_category: function(frm) {
frm.trigger('get_tax_template');
},
+ customer_address: function(frm) {
+ frm.trigger('get_tax_template');
+ },
get_tax_template: function(frm) {
if (!frm.doc.company) return;
From 87b477a31126e478c2bcc77861975e015474bc6a Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Wed, 16 Dec 2020 13:37:21 +0530
Subject: [PATCH 170/286] feat: patch for project template tasks
---
erpnext/patches.txt | 1 +
.../v13_0/update_project_template_tasks.py | 32 +++++++++++++++++++
2 files changed, 33 insertions(+)
create mode 100644 erpnext/patches/v13_0/update_project_template_tasks.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 86ac613ae5b..435511210bc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -741,3 +741,4 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
+erpnext.patches.v13_0.update_project_template_tasks
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
new file mode 100644
index 00000000000..55f0ff45057
--- /dev/null
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ templates = frappe.get_list("Project Template", fields = ["name"])
+ for template_name in templates:
+ template = frappe.get_doc("Project Template", template_name)
+ replace_tasks = False
+ new_tasks = []
+ for task in template.tasks:
+ if task.subject:
+ replace_tasks = True
+ new_task = frappe.get_doc(dict(
+ doctype = "Task",
+ subject = task.subject,
+ start = task.start,
+ duration = task.duration,
+ task_weight = task.task_weight,
+ description = task.description,
+ is_template = 1
+ )).insert()
+ new_tasks.append(new_task.name)
+ if replace_tasks:
+ template.tasks = []
+ for tsk in new_tasks:
+ template.append("tasks", {
+ "task": tsk
+ })
+ template.save()
\ No newline at end of file
From 9962ba86d0db913d53bf87736e1fdd2194436f09 Mon Sep 17 00:00:00 2001
From: "hasnain2808@gmail.com"
Date: Wed, 16 Dec 2020 14:41:04 +0530
Subject: [PATCH 171/286] fix: charts not displaying when tree_type changed
---
.../purchase_analytics/purchase_analytics.js | 72 +++++++++--------
.../report/sales_analytics/sales_analytics.js | 79 ++++++++++---------
2 files changed, 81 insertions(+), 70 deletions(-)
diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
index e17973c337b..7ee9f2c372a 100644
--- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js
+++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
@@ -75,62 +75,66 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Supplier" || tree_type == "Item") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Supplier" || tree_type == "Item") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
- entry = {
- 'name':row_name,
- 'values':row_values
- }
+ entry = {
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
+ let found = false;
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
+ for (let i = 0; i < new_datasets.length; i++) {
+ if (new_datasets[i].name == row_name) {
found = true;
- new_datasets.splice(i,1);
+ new_datasets.splice(i, 1);
break;
}
}
- if(!found){
+ if (!found) {
new_datasets.push(entry);
}
-
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- },500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
+ },
});
}
}
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 0e565a3fb6f..aad6bfd5ef1 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -74,67 +74,74 @@ frappe.query_reports["Sales Analytics"] = {
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Customer") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Customer") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
} else if (tree_type == "Item") {
- row_values = data.slice(5,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ row_values = data
+ .slice(5, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
entry = {
- 'name':row_name,
- 'values':row_values
- }
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
+ let found = false;
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
+ for (let i = 0; i < new_datasets.length; i++) {
+ if (new_datasets[i].name == row_name) {
found = true;
- new_datasets.splice(i,1);
+ new_datasets.splice(i, 1);
break;
}
}
- if(!found){
+ if (!found) {
new_datasets.push(entry);
}
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- }, 500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
- })
+ },
+ });
},
}
From b184d43e757f2982aab9b900943c789920609f8c Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 14:51:42 +0530
Subject: [PATCH 172/286] refactor: Auto Repeat next schedule date function
params (#23959)
* refactor: Auto Repeat next schedule date function params
* refactor: Auto Repeat next schedule date function params
---
erpnext/selling/doctype/sales_order/sales_order.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 04d85e575cc..accf59ebc45 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -418,8 +418,7 @@ class SalesOrder(SellingController):
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
- delivery_date = get_next_schedule_date(ref_doc_delivery_date,
- auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day))
+ delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
if delivery_date <= transaction_date:
delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date)
From 5a8a52b9c6739f25bbd3b128402cb5a19b40afc0 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 15:54:06 +0530
Subject: [PATCH 173/286] fix: Therapy Type and Therapy Plan field visibility
in Patient Appointment
---
.../patient_appointment/patient_appointment.json | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index ac35acc21ac..35600e48092 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -23,9 +23,9 @@
"procedure_template",
"get_procedure_from_encounter",
"procedure_prescription",
+ "therapy_plan",
"therapy_type",
"get_prescribed_therapies",
- "therapy_plan",
"practitioner",
"practitioner_name",
"department",
@@ -284,7 +284,7 @@
"report_hide": 1
},
{
- "depends_on": "eval:doc.patient;",
+ "depends_on": "eval:doc.patient && doc.therapy_plan;",
"fieldname": "therapy_type",
"fieldtype": "Link",
"label": "Therapy",
@@ -292,17 +292,16 @@
"set_only_once": 1
},
{
- "depends_on": "eval:doc.patient && doc.__islocal;",
+ "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;",
"fieldname": "get_prescribed_therapies",
"fieldtype": "Button",
"label": "Get Prescribed Therapies"
},
{
- "depends_on": "eval: doc.patient && doc.therapy_type",
+ "depends_on": "eval: doc.patient;",
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
"options": "Therapy Plan"
},
{
@@ -348,7 +347,7 @@
}
],
"links": [],
- "modified": "2020-05-21 03:04:21.400893",
+ "modified": "2020-12-16 13:16:58.578503",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
From f2a431d86615a5f3c509605dfb58c62adb24b366 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 16:09:58 +0530
Subject: [PATCH 174/286] fix: filter Therapy Types and Therapy Plan in Patient
Appointment
---
.../patient_appointment.js | 30 +++++++++++++++++++
.../patient_appointment.py | 13 +++++++-
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 2d6b64532b1..79e1775b9db 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', {
filters: {'status': 'Active'}
};
});
+
frm.set_query('practitioner', function() {
return {
filters: {
@@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', {
}
};
});
+
frm.set_query('service_unit', function(){
return {
filters: {
@@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', {
};
});
+ frm.set_query('therapy_plan', function() {
+ return {
+ filters: {
+ 'patient': frm.doc.patient
+ }
+ };
+ });
+
+ frm.trigger('set_therapy_type_filter');
+
if (frm.is_new()) {
frm.page.set_primary_action(__('Check Availability'), function() {
if (!frm.doc.patient) {
@@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', {
}
},
+ therapy_plan: function(frm) {
+ frm.trigger('set_therapy_type_filter');
+ },
+
+ set_therapy_type_filter: function(frm) {
+ if (frm.doc.therapy_plan) {
+ frm.call('get_therapy_types').then(r => {
+ frm.set_query('therapy_type', function() {
+ return {
+ filters: {
+ 'name': ['in', r.message]
+ }
+ };
+ });
+ });
+ }
+ },
+
therapy_type: function(frm) {
if (frm.doc.therapy_type) {
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index e685b20a8c8..dc820cb464e 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -91,6 +91,17 @@ class PatientAppointment(Document):
if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
+ def get_therapy_types(self):
+ if not self.therapy_plan:
+ return
+
+ therapy_types = []
+ doc = frappe.get_doc('Therapy Plan', self.therapy_plan)
+ for entry in doc.therapy_plan_details:
+ therapy_types.append(entry.therapy_type)
+
+ return therapy_types
+
@frappe.whitelist()
def check_payment_fields_reqd(patient):
@@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc):
sales_invoice.flags.ignore_mandatory = True
sales_invoice.save(ignore_permissions=True)
sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True)
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
From d44f45c57be854c1c6c625ffccf86b56203c3dd7 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Wed, 16 Dec 2020 16:28:09 +0530
Subject: [PATCH 175/286] fix: sider issues
---
erpnext/projects/doctype/project/test_project.py | 7 ++++---
erpnext/projects/doctype/task/task.py | 4 ++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index c3f56b8e860..ce56a50b4e2 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -106,17 +106,18 @@ def get_project(name, template):
def make_project(args):
args = frappe._dict(args)
- if args.project_template_name:
- template = make_project_template(args.project_template_name)
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
- project_template = template.name,
expected_start_date = args.start_date
))
+ if args.project_template_name:
+ template = make_project_template(args.project_template_name)
+ project.project_template = template.name
+
if not frappe.db.exists("Project", args.project_name):
project.insert()
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 072a848f263..80b764ba4f0 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -94,7 +94,7 @@ class Task(NestedSet):
def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on:
- if d.task and not d.task in depends_on_tasks:
+ if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
@@ -180,7 +180,7 @@ class Task(NestedSet):
def populate_depends_on(self):
if self.parent_task:
parent = frappe.get_doc('Task', self.parent_task)
- if not self.name in [row.task for row in parent.depends_on]:
+ if self.name not in [row.task for row in parent.depends_on]:
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,
From ff59f18012c0587c5c76ef3bdb74c72ebcff4957 Mon Sep 17 00:00:00 2001
From: prssanna
Date: Wed, 28 Oct 2020 11:02:02 +0530
Subject: [PATCH 176/286] fix: override field_map for job card gantt
---
.../doctype/job_card/job_card_calendar.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
index cf07698ad6a..f4877fdca0b 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
@@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = {
"allDay": "allDay",
"progress": "progress"
},
- gantt: true,
+ gantt: {
+ field_map: {
+ "start": "started_time",
+ "end": "started_time",
+ "id": "name",
+ "title": "subject",
+ "color": "color",
+ "allDay": "allDay",
+ "progress": "progress"
+ }
+ },
filters: [
{
"fieldtype": "Link",
From 23d6afe43a83fc60fecc6d9009ea271e1faf0e6e Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 18:21:08 +0530
Subject: [PATCH 177/286] fix: Auto Repeat Import (#24157)
---
erpnext/selling/doctype/sales_order/sales_order.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index accf59ebc45..9388e0927e1 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -14,7 +14,6 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty
from frappe.desk.notifications import clear_doctype_notifications
from frappe.contacts.doctype.address.address import get_company_address
from erpnext.controllers.selling_controller import SellingController
-from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
From 2528d5ee15a5ce9d5d9634eec016946b1416154d Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Wed, 16 Dec 2020 18:29:49 +0530
Subject: [PATCH 178/286] fix: tests
---
erpnext/projects/doctype/task/test_task.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index d43d132e80e..aded78b8574 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -104,7 +104,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa
task.subject = subject
task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate()
- task.project = project
+ task.project = project or "_Test Project"
task.is_template = is_template
task.start = begin
task.duration = duration
From 21168eab7f5922c7ea653b0883a68b5151acccb5 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 19:39:34 +0530
Subject: [PATCH 179/286] fix: Remove patch for setting next date in
Subscription (#24158)
---
erpnext/patches.txt | 1 -
.../v9_0/fix_subscription_next_date.py | 48 -------------------
2 files changed, 49 deletions(-)
delete mode 100644 erpnext/patches/v9_0/fix_subscription_next_date.py
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 86ac613ae5b..9e33014c38e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -450,7 +450,6 @@ erpnext.patches.v8_9.set_member_party_type
erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile
erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order
erpnext.patches.v9_0.student_admission_childtable_migrate
-erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23
erpnext.patches.v9_0.add_healthcare_domain
erpnext.patches.v9_0.set_variant_item_description
erpnext.patches.v9_0.set_uoms_in_variant_field
diff --git a/erpnext/patches/v9_0/fix_subscription_next_date.py b/erpnext/patches/v9_0/fix_subscription_next_date.py
deleted file mode 100644
index 4595c8dc998..00000000000
--- a/erpnext/patches/v9_0/fix_subscription_next_date.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import getdate
-from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
-
-def execute():
- frappe.reload_doc('accounts', 'doctype', 'subscription')
- fields = ["name", "reference_doctype", "reference_document",
- "start_date", "frequency", "repeat_on_day"]
-
- for d in fields:
- if not frappe.db.has_column('Subscription', d):
- return
-
- doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice')
- for data in frappe.get_all('Subscription',
- fields = fields,
- filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}):
-
- recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id")
- if recurring_id:
- frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s"
- .format(data.reference_doctype), (data.name, recurring_id))
-
- date_field = 'transaction_date'
- if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']:
- date_field = 'posting_date'
-
- start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field)
-
- if start_date and getdate(start_date) != getdate(data.start_date):
- last_ref_date = frappe.db.sql("""
- select {0}
- from `tab{1}`
- where subscription=%s and docstatus < 2
- order by creation desc
- limit 1
- """.format(date_field, data.reference_doctype), data.name)[0][0]
-
- next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day)
-
- frappe.db.set_value("Subscription", data.name, {
- "start_date": start_date,
- "next_schedule_date": next_schedule_date
- }, None)
\ No newline at end of file
From c9f63accddd6a7b24d6c3ff257d8fba3395c8c94 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 17 Dec 2020 00:12:17 +0530
Subject: [PATCH 180/286] fix: do not manufacture same serial no multiple times
---
erpnext/stock/doctype/serial_no/serial_no.py | 24 +++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 295149e2387..25ce2d59695 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -6,7 +6,7 @@ import frappe
import json
from frappe.model.naming import make_autoname
-from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate
+from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form
from erpnext.stock.get_item_details import get_reserved_qty_for_so
from frappe import _, ValidationError
@@ -244,7 +244,7 @@ def validate_serial_no(sle, item_det):
for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order",
- "delivery_document_no", "delivery_document_type", "warehouse",
+ "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type",
"purchase_document_no", "company"], as_dict=1)
if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
@@ -256,9 +256,10 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
sle.item_code), SerialNoItemError)
- if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle):
- frappe.throw(_("Serial No {0} has already been received").format(serial_no),
- SerialNoDuplicateError)
+ if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
+ doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
+ frappe.throw(_("Serial No {0} has already been received in the {1} #{2}")
+ .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type):
@@ -349,7 +350,7 @@ def validate_so_serial_no(sr, sales_order):
frappe.throw(_("""{0} Serial No {1} cannot be delivered""")
.format(msg, sr.name))
-def has_duplicate_serial_no(sn, sle):
+def has_serial_no_exists(sn, sle):
if (sn.warehouse and not sle.skip_serial_no_validaiton
and sle.voucher_type != 'Stock Reconciliation'):
return True
@@ -359,12 +360,13 @@ def has_duplicate_serial_no(sn, sle):
status = False
if sn.purchase_document_no:
- if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \
- sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]:
+ if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and
+ sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]):
status = True
- if status and sle.voucher_type == 'Stock Entry' and \
- frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt':
+ # If status is receipt then system will allow to in-ward the delivered serial no
+ if (status and sle.voucher_type == 'Stock Entry' and
+ frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'):
status = False
return status
@@ -420,7 +422,7 @@ def auto_make_serial_nos(args):
if is_new:
created_numbers.append(sr.name)
- form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers))
+ form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers))
# Setting up tranlated title field for all cases
singular_title = _("Serial Number Created")
From f2bff8e220a26b1ed9a662e010b7e15fe91df73e Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 17 Dec 2020 11:54:59 +0530
Subject: [PATCH 181/286] fix: patch relaod doctype
---
erpnext/patches/v13_0/update_project_template_tasks.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index 55f0ff45057..df1886f616c 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
import frappe
def execute():
+ frappe.reload_doctype("Project Template")
templates = frappe.get_list("Project Template", fields = ["name"])
for template_name in templates:
- template = frappe.get_doc("Project Template", template_name)
+ template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
for task in template.tasks:
From 1872e2c1ac429439de1c8d1f52c79b41e9dd7fdd Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Thu, 17 Dec 2020 14:29:52 +0530
Subject: [PATCH 182/286] fix: wrap assignees in a list
---
erpnext/crm/doctype/appointment/appointment.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 63efeb3cb61..2009ebf7cba 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -126,7 +126,7 @@ class Appointment(Document):
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
- 'assign_to': existing_assignee
+ 'assign_to': [existing_assignee]
})
return
if self._assign:
@@ -139,7 +139,7 @@ class Appointment(Document):
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
- 'assign_to': agent
+ 'assign_to': [agent]
})
break
From 2dbb1d6bc72b28542eec44878c28f1eed069bcca Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 17 Dec 2020 15:49:52 +0530
Subject: [PATCH 183/286] fix: indentation
---
erpnext/patches/v13_0/update_project_template_tasks.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index df1886f616c..8dd0181eceb 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -5,10 +5,9 @@ from __future__ import unicode_literals
import frappe
def execute():
- frappe.reload_doctype("Project Template")
- templates = frappe.get_list("Project Template", fields = ["name"])
- for template_name in templates:
+ for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
+ print(template.tasks)
replace_tasks = False
new_tasks = []
for task in template.tasks:
From 611b42733b82a3ab737fde001f7ca9246f1e6870 Mon Sep 17 00:00:00 2001
From: Anurag Mishra
Date: Thu, 17 Dec 2020 14:49:48 +0530
Subject: [PATCH 184/286] fix: leave policy dashboard fix and roles
---
.../doctype/leave_policy/leave_policy_dashboard.py | 14 +-------------
.../leave_policy_assignment.json | 5 ++++-
2 files changed, 5 insertions(+), 14 deletions(-)
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index ff5dc2ff3e0..e0ec4be2dce 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -4,22 +4,10 @@ from frappe import _
def get_data():
return {
'fieldname': 'leave_policy',
- 'non_standard_fieldnames': {
- 'Employee Grade': 'default_leave_policy'
- },
'transactions': [
- {
- 'label': _('Employees'),
- 'items': ['Employee', 'Employee Grade']
- },
{
'label': _('Leaves'),
'items': ['Leave Allocation']
},
]
- }
-
-
-
-
-
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index ecebb3b7d6c..bbb42227154 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -111,7 +111,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-15 15:18:15.227848",
+ "modified": "2020-12-17 16:27:20.311060",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
@@ -127,6 +127,7 @@
"report": 1,
"role": "HR Manager",
"share": 1,
+ "submit": 1,
"write": 1
},
{
@@ -139,6 +140,7 @@
"report": 1,
"role": "HR User",
"share": 1,
+ "submit": 1,
"write": 1
},
{
@@ -151,6 +153,7 @@
"report": 1,
"role": "System Manager",
"share": 1,
+ "submit": 1,
"write": 1
}
],
From 09f0e9111d6fb79868c58f55ead0f18351c6d216 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 17 Dec 2020 17:20:21 +0530
Subject: [PATCH 185/286] fix: patch
---
erpnext/patches/v13_0/update_project_template_tasks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index 8dd0181eceb..0bcd1d3f3a5 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -5,9 +5,9 @@ from __future__ import unicode_literals
import frappe
def execute():
+ frappe.reload_doc("projects", "doctype", "project_template”)
for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
- print(template.tasks)
replace_tasks = False
new_tasks = []
for task in template.tasks:
From 5a06908bbc247607e61d18f8fd848e1bceaa6e11 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Dec 2020 17:38:53 +0530
Subject: [PATCH 186/286] fix: Add breadccrumbs to item group page
---
erpnext/templates/generators/item_group.html | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 40a064fc768..74b2ae3c515 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,5 +1,9 @@
{% extends "templates/web.html" %}
+{% block breadcrumbs %}
+ {% include "templates/includes/breadcrumbs.html" %}
+{% endblock %}
+
{% block header %}
{{ name }}
{% endblock %}
{% block page_content %}
From 79b71462cbdec8fabbb20f80f7e258bb55a65620 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 17 Dec 2020 18:21:34 +0530
Subject: [PATCH 187/286] fix: patch
---
erpnext/patches/v13_0/update_project_template_tasks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index 0bcd1d3f3a5..1303efd93fb 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
def execute():
- frappe.reload_doc("projects", "doctype", "project_template”)
+ frappe.reload_doc("projects", "doctype", "project_template")
for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
From b074334dcff6d337351809fc991f01489424929e Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Thu, 17 Dec 2020 18:46:59 +0530
Subject: [PATCH 188/286] fix: Typo in tax category doctype query
---
erpnext/regional/india/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index f8520c2d003..f256a66266d 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -53,7 +53,7 @@ def validate_gstin_for_india(doc, method):
.format(doc.gst_state_number))
def validate_tax_category(doc, method):
- if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
+ if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:
From b8e656512e8ab34601149c3f3ca0f9831441545a Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Thu, 17 Dec 2020 20:22:06 +0530
Subject: [PATCH 189/286] fix: test cleanup
---
.../project_template/test_project_template.py | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py
index 6c6b78368ed..95663cdcbbb 100644
--- a/erpnext/projects/doctype/project_template/test_project_template.py
+++ b/erpnext/projects/doctype/project_template/test_project_template.py
@@ -10,19 +10,6 @@ from erpnext.projects.doctype.task.test_task import create_task
class TestProjectTemplate(unittest.TestCase):
pass
-def get_project_template(project_template_name="Test Project Template", project_tasks=[]):
- if not frappe.db.exists('Project Template', project_template_name):
- frappe.get_doc(dict(
- doctype = 'Project Template',
- name = project_template_name,
- tasks = project_tasks or [
- create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
- create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2)
- ]
- )).insert()
-
- return frappe.get_doc('Project Template', project_template_name)
-
def make_project_template(project_template_name, project_tasks=[]):
if not frappe.db.exists('Project Template', project_template_name):
project_tasks = project_tasks or [
From 04f48a011d343cad2dbe667b68408db6d523982e Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 01:31:00 +0530
Subject: [PATCH 190/286] feat: Add year_to_date field
---
erpnext/payroll/doctype/salary_slip/salary_slip.json | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 386618cf083..b64e5a08fe6 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -69,6 +69,7 @@
"net_pay_info",
"net_pay",
"base_net_pay",
+ "year_to_date",
"column_break_53",
"rounded_total",
"base_rounded_total",
@@ -578,13 +579,18 @@
{
"fieldname": "column_break_69",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "year_to_date",
+ "fieldtype": "Currency",
+ "label": "Year To Date"
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-21 23:02:59.400249",
+ "modified": "2020-12-17 21:51:19.612940",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
From 9f1e018e4f868220a985af6784c1940a67d47b82 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 01:35:27 +0530
Subject: [PATCH 191/286] feat: Compute year_to_date
---
.../doctype/salary_slip/salary_slip.py | 25 ++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 20365b191d0..27de46acc30 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -35,6 +35,9 @@ class SalarySlip(TransactionBase):
def autoname(self):
self.name = make_autoname(self.series)
+ def before_save(self):
+ self.compute_year_to_date()
+
def validate(self):
self.status = self.get_status()
self.validate_dates()
@@ -1125,6 +1128,26 @@ class SalarySlip(TransactionBase):
self.gross_pay += self.earnings[i].amount
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
+ def compute_year_to_date(self):
+ year_to_date = 0
+ fiscal_year = frappe.get_list('Fiscal Year',
+ fields = ['year','year_start_date','year_end_date'],
+ filters= {'year_start_date' : ['<=', self.start_date],
+ 'year_end_date' : ['>=', self.end_date]
+ })[0]
+ salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip',
+ fields = ['employee_name', 'start_date', 'end_date', 'net_pay'],
+ filters = {'employee_name' : self.employee_name,
+ 'start_date' : ['>=', fiscal_year.year_start_date],
+ 'end_date' : ['<=', fiscal_year.year_end_date]
+ })
+
+ for salary_slip in salary_slips_from_current_fiscal_year:
+ year_to_date += salary_slip.net_pay
+
+ year_to_date += self.net_pay
+ self.year_to_date = year_to_date
+
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no))
@@ -1135,4 +1158,4 @@ def unlink_ref_doc_from_salary_slip(ref_no):
def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee)
- return policy_template.format(**employee.as_dict())
+ return policy_template.format(**employee.as_dict())
\ No newline at end of file
From 6afa83f2c7ac8a71c05959e21884ccab2733fa14 Mon Sep 17 00:00:00 2001
From: Rushabh Mehta
Date: Fri, 18 Dec 2020 11:05:41 +0530
Subject: [PATCH 192/286] fix(requirements): update to latest pandas
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index c4f9171fcaa..678cf74fef0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ braintree==3.57.1
frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
-pandas==1.0.5
+pandas>=1.0.5
plaid-python==6.0.0
pycountry==19.8.18
PyGithub==1.44.1
From a81519f5571f25d15c47e20eefce257c18c3f1ff Mon Sep 17 00:00:00 2001
From: Afshan
Date: Fri, 18 Dec 2020 11:16:01 +0530
Subject: [PATCH 193/286] fix: error popup for submitted doc
---
.../payroll/doctype/salary_slip/salary_slip.js | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index f7e22c63879..abe873d8393 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -214,14 +214,16 @@ frappe.ui.form.on('Salary Slip Timesheet', {
});
var calculate_totals = function(frm) {
- if (frm.doc.earnings || frm.doc.deductions) {
- frappe.call({
- method: "set_totals",
- doc: frm.doc,
- callback: function() {
- frm.refresh_fields();
- }
- });
+ if (frm.doc.docstatus === 0) {
+ if (frm.doc.earnings || frm.doc.deductions) {
+ frappe.call({
+ method: "set_totals",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh_fields();
+ }
+ });
+ }
}
};
From 89a02d7d3f705742bc95e654e2909a821e883e45 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 14:59:20 +0530
Subject: [PATCH 194/286] feat: Changed Fiscal Year to Payroll Period
---
.../doctype/salary_slip/salary_slip.py | 22 +++++++++----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 27de46acc30..0ea0684a8ff 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -35,9 +35,6 @@ class SalarySlip(TransactionBase):
def autoname(self):
self.name = make_autoname(self.series)
- def before_save(self):
- self.compute_year_to_date()
-
def validate(self):
self.status = self.get_status()
self.validate_dates()
@@ -52,6 +49,7 @@ class SalarySlip(TransactionBase):
self.get_working_days_details(lwp = self.leave_without_pay)
self.calculate_net_pay()
+ self.compute_year_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@@ -1130,19 +1128,21 @@ class SalarySlip(TransactionBase):
def compute_year_to_date(self):
year_to_date = 0
- fiscal_year = frappe.get_list('Fiscal Year',
- fields = ['year','year_start_date','year_end_date'],
- filters= {'year_start_date' : ['<=', self.start_date],
- 'year_end_date' : ['>=', self.end_date]
+ payroll_period = frappe.get_list('Payroll Period',
+ fields = ['start_date','end_date','company'],
+ filters= {'start_date' : ['<=', self.start_date],
+ 'end_date' : ['>=', self.end_date],
+ 'company' : self.company
})[0]
- salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip',
+ salary_slips_from_current_payroll_period = frappe.get_list('Salary Slip',
fields = ['employee_name', 'start_date', 'end_date', 'net_pay'],
filters = {'employee_name' : self.employee_name,
- 'start_date' : ['>=', fiscal_year.year_start_date],
- 'end_date' : ['<=', fiscal_year.year_end_date]
+ 'start_date' : ['>=', payroll_period.start_date],
+ 'end_date' : ['<=', payroll_period.end_date],
+ 'name' : ['!=', self.name]
})
- for salary_slip in salary_slips_from_current_fiscal_year:
+ for salary_slip in salary_slips_from_current_payroll_period:
year_to_date += salary_slip.net_pay
year_to_date += self.net_pay
From d6277cdc7f08f14081b7e425f8a901472c4a73cb Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 18 Dec 2020 21:37:19 +0530
Subject: [PATCH 195/286] feat: Value Based and Numeric Quality Inspection
- Acceptance Formula is optional
- Choose between Value based and Numeric QI
- If numeric, select single or multiple readings
- Added Min, Max and Mean Values for numeric inspection to avoid formula usage
- Deprecated code cleanup in js file
---
.../item_quality_inspection_parameter.json | 54 +++++++++-
.../quality_inspection/quality_inspection.js | 102 +++++++++---------
.../quality_inspection.json | 4 +-
.../quality_inspection/quality_inspection.py | 98 +++++++++++++----
.../quality_inspection_reading.json | 93 ++++++++++++++--
.../quality_inspection_template.py | 4 +-
6 files changed, 268 insertions(+), 87 deletions(-)
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index 888bc2de474..f4501281579 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -8,8 +8,14 @@
"field_order": [
"specification",
"value",
+ "value_based",
+ "single_reading",
"column_break_3",
- "acceptance_formula"
+ "formula_based_criteria",
+ "acceptance_formula",
+ "min_value",
+ "max_value",
+ "mean_value"
],
"fields": [
{
@@ -24,10 +30,11 @@
"width": "200px"
},
{
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
@@ -36,17 +43,56 @@
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings. Example 1: reading_1 > 0.2 and reading_1 < 0.5 \nExample 2: (reading_1 + reading_2) / 2 < 10",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields. Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5 \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Acceptance Criteria Formula"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.value_based",
+ "fieldname": "single_reading",
+ "fieldtype": "Check",
+ "label": "Single Reading"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)",
+ "fieldname": "mean_value",
+ "fieldtype": "Float",
+ "label": "Mean Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "label": "Maximum Value"
+ },
+ {
+ "default": "0",
+ "description": "Non-numeric Inspection.",
+ "fieldname": "value_based",
+ "fieldtype": "Check",
+ "label": "Value Based"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:33:42.421842",
+ "modified": "2020-12-18 21:03:29.828723",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 376848afaa4..f0bf9aed802 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,54 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", {
+ setup: function(frm) {
+ frm.set_query("batch_no", function() {
+ return {
+ filters: {
+ "item": frm.doc.item_code
+ }
+ }
+ });
+
+ // Serial No based on item_code
+ frm.set_query("item_serial_no", function() {
+ var filters = {};
+ if (frm.doc.item_code) {
+ filters = {
+ 'item_code': frm.doc.item_code
+ }
+ }
+ return { filters: filters }
+ });
+
+ // item code based on GRN/DN
+ frm.set_query("item_code", function(doc) {
+ let doctype = doc.reference_type;
+
+ if (doc.reference_type !== "Job Card") {
+ doctype = (doc.reference_type == "Stock Entry") ?
+ "Stock Entry Detail" : doc.reference_type + " Item";
+ }
+
+ if (doc.reference_type && doc.reference_name) {
+ let filters = {
+ "from": doctype,
+ "inspection_type": doc.inspection_type
+ };
+
+ if (doc.reference_type == doctype)
+ filters["reference_name"] = doc.reference_name;
+ else
+ filters["parent"] = doc.reference_name;
+
+ return {
+ query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
+ filters: filters
+ };
+ }
+ });
+ },
+
item_code: function(frm) {
if (frm.doc.item_code) {
return frm.call({
@@ -26,55 +74,5 @@ frappe.ui.form.on("Quality Inspection", {
}
});
}
- }
-})
-
-// item code based on GRN/DN
-cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
- let doctype = doc.reference_type;
-
- if (doc.reference_type !== "Job Card") {
- doctype = (doc.reference_type == "Stock Entry") ?
- "Stock Entry Detail" : doc.reference_type + " Item";
- }
-
- if (doc.reference_type && doc.reference_name) {
- let filters = {
- "from": doctype,
- "inspection_type": doc.inspection_type
- };
-
- if (doc.reference_type == doctype)
- filters["reference_name"] = doc.reference_name;
- else
- filters["parent"] = doc.reference_name;
-
- return {
- query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
- filters: filters
- };
- }
-},
-
-// Serial No based on item_code
-cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
- var filters = {};
- if (doc.item_code) {
- filters = {
- 'item_code': doc.item_code
- }
- }
- return { filters: filters }
-}
-
-cur_frm.set_query("batch_no", function(doc) {
- return {
- filters: {
- "item": doc.item_code
- }
- }
-})
-
-cur_frm.add_fetch('item_code', 'item_name', 'item_name');
-cur_frm.add_fetch('item_code', 'description', 'description');
-
+ },
+})
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index f6d76194d94..edfe7e98b2e 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -136,6 +136,7 @@
"width": "50%"
},
{
+ "fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -143,6 +144,7 @@
"read_only": 1
},
{
+ "fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
@@ -236,7 +238,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-19 17:06:05.409963",
+ "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index ae4eb9b9956..a7a023bcbf3 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
@@ -16,7 +16,7 @@ class QualityInspection(Document):
self.get_item_specification_details()
if self.readings:
- self.set_status_based_on_acceptance_formula()
+ self.inspect_and_set_status()
def get_item_specification_details(self):
if not self.quality_inspection_template:
@@ -29,9 +29,7 @@ class QualityInspection(Document):
parameters = get_template_details(self.quality_inspection_template)
for d in parameters:
child = self.append('readings', {})
- child.specification = d.specification
- child.value = d.value
- child.acceptance_formula = d.acceptance_formula
+ child.update(d)
child.status = "Accepted"
def get_quality_inspection_template(self):
@@ -76,28 +74,84 @@ class QualityInspection(Document):
""".format(parent_doc=self.reference_type, child_doc=doctype),
(quality_inspection, self.modified, self.reference_name, self.item_code))
- def set_status_based_on_acceptance_formula(self):
+ def inspect_and_set_status(self):
for reading in self.readings:
- if not reading.acceptance_formula: continue
+ if reading.formula_based_criteria:
+ self.set_status_based_on_acceptance_formula(reading)
+ else:
+ self.set_status_based_on_acceptance_values(reading)
+
+ def set_status_based_on_acceptance_values(self, reading):
+ if cint(reading.value_based):
+ result = reading.get("reading_value") == reading.get("value")
+ else:
+ # numeric readings
+ if cint(reading.single_reading):
+ reading_1 = flt(reading.get("reading_1"))
+ result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value"))
+ else:
+ result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading)
+
+ reading.status = "Accepted" if result else "Rejected"
+
+ def min_max_criteria_passed(self, reading):
+ """Determine whether all readings fall in the acceptable range."""
+ for i in range(1, 11):
+ reading_field = reading.get("reading_" + str(i))
+ if reading_field is not None:
+ result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value"))
+ if not result: return False
+ return True
+
+ def mean_criteria_passed(self, reading):
+ """Determine whether mean of all readings is acceptable."""
+ if reading.get("mean_value"):
+ from statistics import mean
+ readings_list = []
- condition = reading.acceptance_formula
- data = {}
for i in range(1, 11):
- field = "reading_" + str(i)
- data[field] = flt(reading.get(field)) or 0
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None:
+ readings_list.append(flt(reading_value))
- try:
- result = frappe.safe_eval(condition, None, data)
- reading.status = "Accepted" if result else "Rejected"
- except SyntaxError:
- frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
- title=_("Invalid Formula"))
- except NameError as e:
- field = frappe.bold(e.args[0].split()[1])
- frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
- .format(reading.idx, field),
- title=_("Invalid Formula"))
+ actual_mean = mean(readings_list) if readings_list else 0
+ return True if actual_mean == reading.get("mean_value") else False
+ return True # no mean value, nothing to check
+
+ def set_status_based_on_acceptance_formula(self, reading):
+ if not reading.acceptance_formula:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
+ title=_("Missing Formula"))
+
+ condition = reading.acceptance_formula
+ data = self.get_formula_evaluation_data(reading)
+
+ try:
+ result = frappe.safe_eval(condition, None, data)
+ reading.status = "Accepted" if result else "Rejected"
+ except NameError as e:
+ field = frappe.bold(e.args[0].split()[1])
+ frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
+ .format(reading.idx, field),
+ title=_("Invalid Formula"))
+ except Exception:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
+ title=_("Invalid Formula"))
+
+ def get_formula_evaluation_data(self, reading):
+ data = {}
+ if cint(reading.value_based):
+ data = {"reading_value": reading.get("reading_value")}
+ else:
+ # numeric readings
+ data = {"reading_1": flt(reading.get("reading_1"))}
+ if not cint(reading.single_reading):
+ # if multiple numeric readings add all readings to data
+ for i in range(2, 11):
+ field = "reading_" + str(i)
+ data[field] = flt(reading.get(field))
+ return data
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index c1976dd1fb5..db95fabee0b 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,21 +7,30 @@
"engine": "InnoDB",
"field_order": [
"specification",
- "value",
"status",
+ "value",
+ "value_based",
"column_break_4",
+ "formula_based_criteria",
"acceptance_formula",
+ "min_value",
+ "max_value",
+ "mean_value",
"section_break_3",
+ "reading_value",
+ "section_break_14",
+ "single_reading",
+ "section_break_12",
"reading_1",
"reading_2",
"reading_3",
- "column_break_10",
"reading_4",
+ "column_break_10",
"reading_5",
"reading_6",
- "column_break_14",
"reading_7",
"reading_8",
+ "column_break_14",
"reading_9",
"reading_10"
],
@@ -38,10 +47,11 @@
},
{
"columns": 2,
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
@@ -56,6 +66,7 @@
},
{
"columns": 1,
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_2",
"fieldtype": "Data",
"in_list_view": 1,
@@ -65,6 +76,7 @@
},
{
"columns": 1,
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_3",
"fieldtype": "Data",
"in_list_view": 1,
@@ -73,6 +85,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_4",
"fieldtype": "Data",
"label": "Reading 4",
@@ -80,6 +93,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_5",
"fieldtype": "Data",
"label": "Reading 5",
@@ -87,6 +101,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_6",
"fieldtype": "Data",
"label": "Reading 6",
@@ -94,6 +109,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_7",
"fieldtype": "Data",
"label": "Reading 7",
@@ -101,6 +117,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_8",
"fieldtype": "Data",
"label": "Reading 8",
@@ -108,6 +125,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_9",
"fieldtype": "Data",
"label": "Reading 9",
@@ -115,6 +133,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_10",
"fieldtype": "Data",
"label": "Reading 10",
@@ -133,15 +152,18 @@
"options": "Accepted\nRejected"
},
{
+ "depends_on": "value_based",
"fieldname": "section_break_3",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Value Based Inspection"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings. Example 1: reading_1 > 0.2 and reading_1 < 0.5 \nExample 2: (reading_1 + reading_2) / 2 < 10",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields. Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5 \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
"label": "Acceptance Criteria Formula"
@@ -153,12 +175,69 @@
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)",
+ "fieldname": "mean_value",
+ "fieldtype": "Float",
+ "label": "Mean Value"
+ },
+ {
+ "default": "0",
+ "fieldname": "single_reading",
+ "fieldtype": "Check",
+ "label": "Single Reading"
+ },
+ {
+ "depends_on": "eval:!doc.value_based",
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "description": "Applied on each reading.",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "description": "Applied on each reading.",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "label": "Maximum Value"
+ },
+ {
+ "default": "0",
+ "description": "Non-numeric Inspection.",
+ "fieldname": "value_based",
+ "fieldtype": "Check",
+ "label": "Value Based"
+ },
+ {
+ "depends_on": "value_based",
+ "fieldname": "reading_value",
+ "fieldtype": "Data",
+ "label": "Reading Value"
+ },
+ {
+ "depends_on": "eval:!doc.value_based",
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break",
+ "label": "Numeric Inspection"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2020-12-18 21:02:04.865777",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index e2848469b88..7dd0febc203 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -13,6 +13,8 @@ def get_template_details(template):
if not template: return []
return frappe.get_all('Item Quality Inspection Parameter',
- fields=["specification", "value", "acceptance_formula"],
+ fields=["specification", "value", "acceptance_formula",
+ "value_based", "formula_based_criteria", "single_reading",
+ "min_value", "max_value", "mean_value"],
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx")
\ No newline at end of file
From 1b1df6b6bcabedcf27ebc5c1cc4f2c0fac1e73b8 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 23:51:05 +0530
Subject: [PATCH 196/286] feat: Add month_to_date field
---
erpnext/payroll/doctype/salary_slip/salary_slip.json | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index b64e5a08fe6..5141868adb7 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -73,6 +73,7 @@
"column_break_53",
"rounded_total",
"base_rounded_total",
+ "month_to_date",
"section_break_55",
"total_in_words",
"column_break_69",
@@ -583,14 +584,21 @@
{
"fieldname": "year_to_date",
"fieldtype": "Currency",
- "label": "Year To Date"
+ "label": "Year To Date(Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "month_to_date",
+ "fieldtype": "Currency",
+ "label": "Month To Date"
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-17 21:51:19.612940",
+ "modified": "2020-12-18 23:23:10.484574",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
From 59fbf702dad1c4231d19fe8cfb4970862e4aaad0 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 23:52:11 +0530
Subject: [PATCH 197/286] feat: Compute month_to_date
---
.../doctype/salary_slip/salary_slip.py | 20 +++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 0ea0684a8ff..e86a7fc3158 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -50,6 +50,7 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay()
self.compute_year_to_date()
+ self.compute_month_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@@ -1138,8 +1139,7 @@ class SalarySlip(TransactionBase):
fields = ['employee_name', 'start_date', 'end_date', 'net_pay'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', payroll_period.start_date],
- 'end_date' : ['<=', payroll_period.end_date],
- 'name' : ['!=', self.name]
+ 'end_date' : ['<', self.start_date]
})
for salary_slip in salary_slips_from_current_payroll_period:
@@ -1148,6 +1148,22 @@ class SalarySlip(TransactionBase):
year_to_date += self.net_pay
self.year_to_date = year_to_date
+ def compute_month_to_date(self):
+ month_to_date = 0
+ date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d")
+ first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year)
+ salary_slips_from_this_month = frappe.get_list('Salary Slip',
+ fields = ['employee_name', 'start_date', 'net_pay'],
+ filters = {'employee_name' : self.employee_name,
+ 'start_date' : ['>=', first_day_of_the_month],
+ 'end_date' : ['<', self.start_date]
+ })
+ for salary_slip in salary_slips_from_this_month:
+ month_to_date += salary_slip.net_pay
+
+ month_to_date += self.net_pay
+ self.month_to_date = month_to_date
+
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no))
From ddd9fe49fca4f05f31bc97e462a478aa52477e2d Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Fri, 18 Dec 2020 23:58:05 +0530
Subject: [PATCH 198/286] feat: Add month_to_date field
---
erpnext/payroll/doctype/salary_slip/salary_slip.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 5141868adb7..d981a39953d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -591,14 +591,16 @@
{
"fieldname": "month_to_date",
"fieldtype": "Currency",
- "label": "Month To Date"
+ "label": "Month To Date(Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-18 23:23:10.484574",
+ "modified": "2020-12-18 23:57:41.042954",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
From 0c4f97368d05f2277ccca54b583be0a104acf7a9 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 21 Dec 2020 11:44:48 +0530
Subject: [PATCH 199/286] chore: UX improvement
- Removed 'single reading' checkbox, unnecessary
- Removed 'Mean' field and added computed mean to formula data
- Changed 'Value Based' to 'Non-Numeric'
- Re-arranged fields
---
.../item_quality_inspection_parameter.json | 43 +++++-------
.../quality_inspection/quality_inspection.py | 57 +++++++---------
.../quality_inspection_reading.json | 65 +++++--------------
.../quality_inspection_template.py | 3 +-
4 files changed, 58 insertions(+), 110 deletions(-)
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index f4501281579..9b980a1e013 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -8,14 +8,12 @@
"field_order": [
"specification",
"value",
- "value_based",
- "single_reading",
+ "non_numeric",
"column_break_3",
- "formula_based_criteria",
- "acceptance_formula",
"min_value",
"max_value",
- "mean_value"
+ "formula_based_criteria",
+ "acceptance_formula"
],
"fields": [
{
@@ -27,10 +25,10 @@
"oldfieldtype": "Data",
"print_width": "200px",
"reqd": 1,
- "width": "200px"
+ "width": "100px"
},
{
- "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
@@ -44,10 +42,9 @@
},
{
"depends_on": "formula_based_criteria",
- "description": "Simple Python formula applied on Reading fields. Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5 \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
+ "description": "Simple Python formula applied on Reading fields. Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5 \nNumeric eg. 2: mean > 3.5 (mean of populated fields) \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
- "in_list_view": 1,
"label": "Acceptance Criteria Formula"
},
{
@@ -57,42 +54,32 @@
"label": "Formula Based Criteria"
},
{
- "default": "0",
- "depends_on": "eval:!doc.value_based",
- "fieldname": "single_reading",
- "fieldtype": "Check",
- "label": "Single Reading"
- },
- {
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)",
- "fieldname": "mean_value",
- "fieldtype": "Float",
- "label": "Mean Value"
- },
- {
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"fieldname": "min_value",
"fieldtype": "Float",
+ "in_list_view": 1,
"label": "Minimum Value"
},
{
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"fieldname": "max_value",
"fieldtype": "Float",
+ "in_list_view": 1,
"label": "Maximum Value"
},
{
"default": "0",
- "description": "Non-numeric Inspection.",
- "fieldname": "value_based",
+ "fieldname": "non_numeric",
"fieldtype": "Check",
- "label": "Value Based"
+ "in_list_view": 1,
+ "label": "Non-Numeric",
+ "width": "80px"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-18 21:03:29.828723",
+ "modified": "2020-12-21 11:37:55.387677",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index a7a023bcbf3..f582658d871 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -79,46 +79,27 @@ class QualityInspection(Document):
if reading.formula_based_criteria:
self.set_status_based_on_acceptance_formula(reading)
else:
+ # if not formula based check acceptance values set
self.set_status_based_on_acceptance_values(reading)
def set_status_based_on_acceptance_values(self, reading):
- if cint(reading.value_based):
+ if cint(reading.non_numeric):
result = reading.get("reading_value") == reading.get("value")
else:
# numeric readings
- if cint(reading.single_reading):
- reading_1 = flt(reading.get("reading_1"))
- result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value"))
- else:
- result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading)
+ result = self.min_max_criteria_passed(reading)
reading.status = "Accepted" if result else "Rejected"
def min_max_criteria_passed(self, reading):
"""Determine whether all readings fall in the acceptable range."""
for i in range(1, 11):
- reading_field = reading.get("reading_" + str(i))
- if reading_field is not None:
- result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value"))
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
if not result: return False
return True
- def mean_criteria_passed(self, reading):
- """Determine whether mean of all readings is acceptable."""
- if reading.get("mean_value"):
- from statistics import mean
- readings_list = []
-
- for i in range(1, 11):
- reading_value = reading.get("reading_" + str(i))
- if reading_value is not None:
- readings_list.append(flt(reading_value))
-
- actual_mean = mean(readings_list) if readings_list else 0
- return True if actual_mean == reading.get("mean_value") else False
-
- return True # no mean value, nothing to check
-
def set_status_based_on_acceptance_formula(self, reading):
if not reading.acceptance_formula:
frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
@@ -141,18 +122,30 @@ class QualityInspection(Document):
def get_formula_evaluation_data(self, reading):
data = {}
- if cint(reading.value_based):
+ if cint(reading.non_numeric):
data = {"reading_value": reading.get("reading_value")}
else:
# numeric readings
- data = {"reading_1": flt(reading.get("reading_1"))}
- if not cint(reading.single_reading):
- # if multiple numeric readings add all readings to data
- for i in range(2, 11):
- field = "reading_" + str(i)
- data[field] = flt(reading.get(field))
+ for i in range(1, 11):
+ field = "reading_" + str(i)
+ data[field] = flt(reading.get(field))
+ data["mean"] = self.calculate_mean(reading)
+
return data
+ def calculate_mean(self, reading):
+ """Calculate mean of all non-empty readings."""
+ from statistics import mean
+ readings_list = []
+
+ for i in range(1, 11):
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ readings_list.append(flt(reading_value))
+
+ actual_mean = mean(readings_list) if readings_list else 0
+ return actual_mean
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index db95fabee0b..0792f26d2ab 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -9,18 +9,15 @@
"specification",
"status",
"value",
- "value_based",
+ "non_numeric",
"column_break_4",
- "formula_based_criteria",
- "acceptance_formula",
"min_value",
"max_value",
- "mean_value",
+ "formula_based_criteria",
+ "acceptance_formula",
"section_break_3",
"reading_value",
"section_break_14",
- "single_reading",
- "section_break_12",
"reading_1",
"reading_2",
"reading_3",
@@ -47,7 +44,7 @@
},
{
"columns": 2,
- "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
@@ -66,7 +63,6 @@
},
{
"columns": 1,
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_2",
"fieldtype": "Data",
"in_list_view": 1,
@@ -76,7 +72,6 @@
},
{
"columns": 1,
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_3",
"fieldtype": "Data",
"in_list_view": 1,
@@ -85,7 +80,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_4",
"fieldtype": "Data",
"label": "Reading 4",
@@ -93,7 +87,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_5",
"fieldtype": "Data",
"label": "Reading 5",
@@ -101,7 +94,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_6",
"fieldtype": "Data",
"label": "Reading 6",
@@ -109,7 +101,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_7",
"fieldtype": "Data",
"label": "Reading 7",
@@ -117,7 +108,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_8",
"fieldtype": "Data",
"label": "Reading 8",
@@ -125,7 +115,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_9",
"fieldtype": "Data",
"label": "Reading 9",
@@ -133,7 +122,6 @@
"oldfieldtype": "Data"
},
{
- "depends_on": "eval:!doc.single_reading",
"fieldname": "reading_10",
"fieldtype": "Data",
"label": "Reading 10",
@@ -152,7 +140,7 @@
"options": "Accepted\nRejected"
},
{
- "depends_on": "value_based",
+ "depends_on": "non_numeric",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Value Based Inspection"
@@ -163,7 +151,7 @@
},
{
"depends_on": "formula_based_criteria",
- "description": "Simple Python formula applied on Reading fields. Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5 \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
+ "description": "Simple Python formula applied on Reading fields. Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5 \nNumeric eg. 2: mean > 3.5 (mean of populated fields) \nValue based eg.: reading_value in (\"A\", \"B\", \"C)",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
"label": "Acceptance Criteria Formula"
@@ -183,61 +171,42 @@
"label": "Formula Based Criteria"
},
{
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)",
- "fieldname": "mean_value",
- "fieldtype": "Float",
- "label": "Mean Value"
- },
- {
- "default": "0",
- "fieldname": "single_reading",
- "fieldtype": "Check",
- "label": "Single Reading"
- },
- {
- "depends_on": "eval:!doc.value_based",
- "fieldname": "section_break_12",
- "fieldtype": "Section Break",
- "hide_border": 1
- },
- {
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"description": "Applied on each reading.",
"fieldname": "min_value",
"fieldtype": "Float",
"label": "Minimum Value"
},
{
- "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
"description": "Applied on each reading.",
"fieldname": "max_value",
"fieldtype": "Float",
"label": "Maximum Value"
},
{
- "default": "0",
- "description": "Non-numeric Inspection.",
- "fieldname": "value_based",
- "fieldtype": "Check",
- "label": "Value Based"
- },
- {
- "depends_on": "value_based",
+ "depends_on": "non_numeric",
"fieldname": "reading_value",
"fieldtype": "Data",
"label": "Reading Value"
},
{
- "depends_on": "eval:!doc.value_based",
+ "depends_on": "eval:!doc.non_numeric",
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"label": "Numeric Inspection"
+ },
+ {
+ "default": "0",
+ "fieldname": "non_numeric",
+ "fieldtype": "Check",
+ "label": "Non-Numeric"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-18 21:02:04.865777",
+ "modified": "2020-12-21 11:36:24.885019",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index 7dd0febc203..c5a7974a732 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -14,7 +14,6 @@ def get_template_details(template):
return frappe.get_all('Item Quality Inspection Parameter',
fields=["specification", "value", "acceptance_formula",
- "value_based", "formula_based_criteria", "single_reading",
- "min_value", "max_value", "mean_value"],
+ "non_numeric", "formula_based_criteria", "min_value", "max_value"],
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx")
\ No newline at end of file
From 68f91c96400226254875016d0e0b95bdc3816580 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 21 Dec 2020 12:24:45 +0530
Subject: [PATCH 200/286] chore: Added tests for new ux
- Test for value based inspection
- tweaks in test for formula based inspection
- tweaks in create_quality_inspection as status in child row is auto set now
---
.../test_quality_inspection.py | 59 ++++++++++++++++---
1 file changed, 50 insertions(+), 9 deletions(-)
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 2c40009426e..d0bfb466e05 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
qa.delete()
dn.delete()
+ def test_value_based_qi_readings(self):
+ # Test QI based on acceptance values (Non formula)
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [{
+ "specification": "Iron Content", # numeric reading
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ },
+ {
+ "specification": "Particle Inspection Needed", # non-numeric reading
+ "non_numeric": 1,
+ "value": "Yes",
+ "reading_value": "Yes"
+ }]
+
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
+ readings=readings, do_not_save=True)
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Accepted")
+
+ qa.delete()
+ dn.delete()
+
def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{
- "specification": "Iron Content",
+ "specification": "Iron Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
- "reading_1": 0.4
+ "reading_1": "0.4"
},
{
- "specification": "Calcium Content",
+ "specification": "Calcium Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
- "reading_1": 0.7
+ "reading_1": "0.7"
},
{
- "specification": "Mg Content",
- "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
- "reading_1": 0.5,
- "reading_2": 0.7,
+ "specification": "Mg Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "mean < 0.9",
+ "reading_1": "0.5",
+ "reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues
+ },
+ {
+ "specification": "Calcium Content", # non-numeric reading
+ "formula_based_criteria": 1,
+ "non_numeric": 1,
+ "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
+ "reading_value": "Grade B"
}]
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase):
self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted")
+ self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete()
dn.delete()
@@ -86,8 +124,11 @@ def create_quality_inspection(**args):
qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1
qa.inspected_by = frappe.session.user
+ qa.status = args.status or "Accepted"
- readings = args.readings or {"specification": "Size", "status": args.status}
+ readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10}
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list):
for entry in readings:
From 0e222173ea431b46344f7b73866390c15e80106e Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Mon, 21 Dec 2020 13:44:03 +0530
Subject: [PATCH 201/286] fix: don't set primary action if workflow is set
---
erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index cb48abbc363..31abaf40bf2 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -39,7 +39,7 @@ frappe.ui.form.on('Payroll Entry', {
}
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
}
- if ((frm.doc.employees || []).length) {
+ if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit').then(()=>{
From eae31f02cc1a5254292b7c621513e70b91d10b22 Mon Sep 17 00:00:00 2001
From: marination
Date: Mon, 21 Dec 2020 13:58:44 +0530
Subject: [PATCH 202/286] fix: Sider (missing semi-colons)
---
.../doctype/quality_inspection/quality_inspection.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index f0bf9aed802..2ec8a070052 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -10,18 +10,18 @@ frappe.ui.form.on("Quality Inspection", {
filters: {
"item": frm.doc.item_code
}
- }
+ };
});
// Serial No based on item_code
frm.set_query("item_serial_no", function() {
- var filters = {};
+ let filters = {};
if (frm.doc.item_code) {
filters = {
'item_code': frm.doc.item_code
- }
+ };
}
- return { filters: filters }
+ return { filters: filters };
});
// item code based on GRN/DN
@@ -75,4 +75,4 @@ frappe.ui.form.on("Quality Inspection", {
});
}
},
-})
\ No newline at end of file
+});
\ No newline at end of file
From a77b8c9fcc1ed7a2a44d2cdf3a3b50c5d8a4366f Mon Sep 17 00:00:00 2001
From: Nabin Hait
Date: Mon, 21 Dec 2020 14:45:50 +0530
Subject: [PATCH 203/286] Repost item valuation (#24031)
* feat: Reposting logic for future finished/transferred item
* feat: added fields to identify needs to recalculate rate while reposting
* refactor: Set rate for outgoing and finished items
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Get outgoing rate for purchase return
* refactor: Get incoming rate for sales return
* test: Added tests for reposting valuation of transferred/finished/returned items
* feat: added incoming rate field in DN, SI and Packed Item table
* feat: get incoming rate for returned item
* fix: no error while getting valuation rate in stock entry
* fix: update stock ledger for DN and SI
* feat: update item valuation rate in PR and PI based on supplied items cost
* feat: SLE reposting logic for sales return and subcontracted item with test cases
* feat: update qty in future sle
* feat: repost future sle and gle via Repost Item Valuation
* fix: Skip unwanted function calling while reposting
* fix: repost sle for specific item and warehouse
* test: Modified tests for backdated stock reco
* fix: ignore cancelled sle in few methods
* feat: role allowed to do backdated entry
* feat: Show reposting status on stock valuation related reports
* fix: minor fixes
* fix: fixed sider issues
* fix: serial no fix related to immutable ledger
* fix: Test cases fixes related to perpetual inventory
* fix: Test cases fixed
* fix: Fixed reposting on cancel and test cases
* feat: Restart reposting item valuation
* refactor: Code cleanup using small functions and test case fixes
* fix: minor fixes
* fix: Raise on error while reposting item valuation
* fix: minor fix
* fix: Tests fixed
* fix: skip some validation ig gle made from reposting
* fix: test fixes
* fix: debugging stock and account validation
* fix: debugging stock and account validation
* fix: debugging travis for stock and account sync validation
* fix: debugging travis
* fix: debugging travis
* fix: debugging travis
---
.../accounts/doctype/account/test_account.py | 2 +-
.../doctype/coupon_code/test_coupon_code.py | 50 +-
erpnext/accounts/doctype/gl_entry/gl_entry.py | 24 +-
.../journal_entry/test_journal_entry.py | 68 +-
.../loyalty_program/test_loyalty_program.py | 2 -
.../purchase_invoice/purchase_invoice.py | 19 +-
.../purchase_invoice/test_purchase_invoice.py | 72 +--
.../doctype/sales_invoice/sales_invoice.py | 17 +-
.../doctype/sales_invoice/test_records.json | 3 +-
.../sales_invoice/test_sales_invoice.py | 128 ++--
.../sales_invoice_item.json | 30 +-
erpnext/accounts/general_ledger.py | 38 +-
erpnext/accounts/utils.py | 24 +-
.../purchase_order_item.json | 2 +-
erpnext/controllers/buying_controller.py | 129 ++--
.../controllers/sales_and_purchase_return.py | 42 ++
erpnext/controllers/selling_controller.py | 116 ++--
erpnext/controllers/stock_controller.py | 101 +--
.../doctype/work_order/test_work_order.py | 2 -
.../sales_order_item/sales_order_item.json | 2 +-
.../setup/doctype/company/test_records.json | 18 +-
erpnext/stock/doctype/batch/test_batch.py | 5 -
erpnext/stock/doctype/bin/bin.py | 14 +-
.../doctype/delivery_note/delivery_note.py | 4 +-
.../delivery_note/test_delivery_note.py | 7 +-
.../delivery_note_item.json | 11 +-
erpnext/stock/doctype/item/test_records.json | 10 +
.../item_alternative/test_item_alternative.py | 2 -
.../landed_cost_taxes_and_charges.json | 8 +-
.../landed_cost_voucher.py | 18 +-
.../test_landed_cost_voucher.py | 12 +-
.../material_request/test_material_request.py | 3 -
.../doctype/packed_item/packed_item.json | 18 +-
.../purchase_receipt/purchase_receipt.py | 4 +-
.../purchase_receipt/test_purchase_receipt.py | 188 +++---
.../purchase_receipt_item.json | 2 +-
.../doctype/repost_item_valuation/__init__.py | 0
.../repost_item_valuation.js | 52 ++
.../repost_item_valuation.json | 215 +++++++
.../repost_item_valuation.py | 89 +++
.../test_repost_item_valuation.py | 10 +
erpnext/stock/doctype/serial_no/serial_no.py | 33 +-
.../stock/doctype/serial_no/test_serial_no.py | 3 -
.../stock/doctype/stock_entry/stock_entry.js | 25 +-
.../doctype/stock_entry/stock_entry.json | 3 +-
.../stock/doctype/stock_entry/stock_entry.py | 303 +++++----
.../doctype/stock_entry/test_stock_entry.py | 83 +--
.../stock_entry_detail.json | 74 ++-
.../stock_ledger_entry.json | 62 +-
.../stock_ledger_entry/stock_ledger_entry.py | 45 +-
.../test_stock_ledger_entry.py | 395 +++++++++++-
.../stock_reconciliation.py | 4 +-
.../test_stock_reconciliation.py | 47 +-
.../stock_settings/stock_settings.json | 31 +-
.../stock/doctype/warehouse/test_warehouse.py | 61 +-
erpnext/stock/doctype/warehouse/warehouse.py | 1 -
.../report/stock_analytics/stock_analytics.py | 2 +
.../report/stock_balance/stock_balance.py | 3 +-
.../stock/report/stock_ledger/stock_ledger.py | 3 +-
.../stock_projected_qty.py | 3 +-
...rehouse_wise_item_balance_age_and_value.py | 2 +
erpnext/stock/stock_balance.py | 18 +-
erpnext/stock/stock_ledger.py | 597 +++++++++++++-----
erpnext/stock/utils.py | 11 +-
64 files changed, 2336 insertions(+), 1034 deletions(-)
create mode 100644 erpnext/stock/doctype/repost_item_valuation/__init__.py
create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
create mode 100644 erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
create mode 100644 erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 0605d89a7e2..113bea00645 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -172,7 +172,7 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", doc)
-def _make_test_records(verbose):
+def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
accounts = [
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index 340b9dd58ad..622bd33e20a 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -28,22 +28,22 @@ def test_create_test_data():
"item_group": "_Test Item Group",
"item_name": "_Test Tesla Car",
"apply_warehouse_wise_reorder_level": 0,
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"gst_hsn_code": "999800",
"valuation_rate": 5000,
"standard_rate":5000,
"item_defaults": [{
- "company": "_Test Company with perpetual inventory",
- "default_warehouse": "Stores - TCP1",
+ "company": "_Test Company",
+ "default_warehouse": "Stores - _TC",
"default_price_list":"_Test Price List",
- "expense_account": "Cost of Goods Sold - TCP1",
- "buying_cost_center": "Main - TCP1",
- "selling_cost_center": "Main - TCP1",
- "income_account": "Sales - TCP1"
+ "expense_account": "Cost of Goods Sold - _TC",
+ "buying_cost_center": "Main - _TC",
+ "selling_cost_center": "Main - _TC",
+ "income_account": "Sales - _TC"
}],
"show_in_website": 1,
"route":"-test-tesla-car",
- "website_warehouse": "Stores - TCP1"
+ "website_warehouse": "Stores - _TC"
})
item.insert()
# create test item price
@@ -65,12 +65,12 @@ def test_create_test_data():
"items": [{
"item_code": "_Test Tesla Car"
}],
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"coupon_code_based":1,
"selling": 1,
"rate_or_discount": "Discount Percentage",
"discount_percentage": 30,
- "company": "_Test Company with perpetual inventory",
+ "company": "_Test Company",
"currency":"INR",
"for_price_list":"_Test Price List"
})
@@ -85,7 +85,7 @@ def test_create_test_data():
})
sales_partner.insert()
# create test item coupon code
- if not frappe.db.exists("Coupon Code","SAVE30"):
+ if not frappe.db.exists("Coupon Code", "SAVE30"):
coupon_code = frappe.get_doc({
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
@@ -102,35 +102,27 @@ class TestCouponCode(unittest.TestCase):
test_create_test_data()
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.set_user("Administrator")
- def test_1_check_coupon_code_used_before_so(self):
- coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # reset used coupon code count
- coupon_code.used=0
- coupon_code.save()
- # check no coupon code is used before sales order is made
- self.assertEqual(coupon_code.get("used"),0)
+ def test_sales_order_with_coupon_code(self):
+ frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
- def test_2_sales_order_with_coupon_code(self):
- so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
- customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
+ so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
+ customer="_Test Customer", selling_price_list="_Test Price List",
+ item_code="_Test Tesla Car", rate=5000, qty=1,
do_not_submit=True)
- so = frappe.get_doc('Sales Order', so.name)
- # check item price before coupon code is applied
self.assertEqual(so.items[0].rate, 5000)
+
so.coupon_code='SAVE30'
so.sales_partner='_Test Coupon Partner'
so.save()
+
# check item price after coupon code is applied
self.assertEqual(so.items[0].rate, 3500)
+
so.submit()
-
- def test_3_check_coupon_code_used_after_so(self):
- doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # check no coupon code is used before sales order is made
- self.assertEqual(doc.get("used"),1)
+ self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index def9ed6803e..c4412749080 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -30,20 +30,22 @@ class GLEntry(Document):
self.pl_must_have_cost_center()
self.validate_cost_center()
- self.check_pl_account()
- self.validate_party()
- self.validate_currency()
+ if not self.flags.from_repost:
+ self.check_pl_account()
+ self.validate_party()
+ self.validate_currency()
- def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'):
- self.validate_account_details(adv_adj)
- self.validate_dimensions_for_pl_and_bs()
+ def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
+ if not from_repost:
+ self.validate_account_details(adv_adj)
+ self.validate_dimensions_for_pl_and_bs()
validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj)
# Update outstanding amt on against voucher
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
- and self.against_voucher and update_outstanding == 'Yes':
+ and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
@@ -106,8 +108,8 @@ class GLEntry(Document):
from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1:
- frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
- transactions''').format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
+ .format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive")
@@ -136,8 +138,8 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group():
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
- be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
+ .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 53c07583d8e..1d2eacdb80c 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -75,54 +75,40 @@ class TestJournalEntry(unittest.TestCase):
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self):
- from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
- set_perpetual_inventory()
+ company = "_Test Company with perpetual inventory"
+ stock_account = get_inventory_account(company)
- jv = frappe.copy_doc({
- "cheque_date": nowdate(),
- "cheque_no": "33",
- "company": "_Test Company with perpetual inventory",
- "doctype": "Journal Entry",
- "accounts": [
- {
- "account": "Debtors - TCP1",
- "party_type": "Customer",
- "party": "_Test Customer",
- "credit_in_account_currency": 400.0,
- "debit_in_account_currency": 0.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- },
- {
- "account": "_Test Bank - TCP1",
- "credit_in_account_currency": 0.0,
- "debit_in_account_currency": 400.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- }
- ],
- "naming_series": "_T-Journal Entry-",
- "posting_date": nowdate(),
- "user_remark": "test",
- "voucher_type": "Bank Entry"
- })
-
- jv.get("accounts")[0].update({
- "account": get_inventory_account('_Test Company with perpetual inventory'),
- "company": "_Test Company with perpetual inventory",
- "party_type": None,
- "party": None
+ jv = frappe.new_doc("Journal Entry")
+ jv.company = company
+ jv.posting_date = nowdate()
+ jv.append("accounts", {
+ "account": stock_account,
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": 100
})
+
+ jv.append("accounts", {
+ "account": "Stock Adjustment - TCP1",
+ "credit_in_account_currency": 100,
+ "cost_center": "Main - TCP1",
+ })
+ jv.insert()
- self.assertRaises(StockAccountInvalidTransaction, jv.submit)
- jv.cancel()
- set_perpetual_inventory(0)
+ from erpnext.accounts.utils import get_stock_and_account_balance
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
+
+ if account_bal == stock_bal:
+ self.assertRaises(StockAccountInvalidTransaction, jv.submit)
+ frappe.db.rollback()
+ else:
+ jv.submit()
+ jv.cancel()
def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC",
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 5278d8b2412..31994885aa6 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -8,12 +8,10 @@ import unittest
from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase):
@classmethod
def setUpClass(self):
- set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc
create_records()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index d94d261c6bc..b52678e8d3b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -410,10 +410,13 @@ class PurchaseInvoice(BuyingController):
# this sequence because outstanding may get -negative
self.make_gl_entries()
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
@@ -421,7 +424,7 @@ class PurchaseInvoice(BuyingController):
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -436,9 +439,11 @@ class PurchaseInvoice(BuyingController):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
+ self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else:
self.stock_received_but_not_billed = None
- self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+ self.expenses_included_in_valuation = None
+
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@@ -452,7 +457,7 @@ class PurchaseInvoice(BuyingController):
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
-
+
gl_entries = merge_similar_entries(gl_entries)
self.make_payment_gl_entries(gl_entries)
@@ -994,11 +999,15 @@ class PurchaseInvoice(BuyingController):
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_project(self):
project_list = []
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f2499d24b5b..c0506ba97f6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -9,8 +9,7 @@ import frappe.model
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
- test_records as pr_test_records, make_purchase_receipt, get_taxes
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@@ -33,13 +32,10 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
- wrapper = frappe.copy_doc(test_records[0])
- set_perpetual_inventory(0, wrapper.company)
- self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
- wrapper.insert()
- wrapper.submit()
- wrapper.load_from_db()
- dl = wrapper
+ pi = frappe.copy_doc(test_records[0])
+ self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
+ pi.insert()
+ pi.submit()
expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0],
@@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase):
"Round Off - _TC": [0, 0.3]
}
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1)
+ where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self):
- pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
+ pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
+ warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ get_taxes_and_charges=True, qty=10)
+
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
self.check_gle_for_pi(pi.name)
@@ -198,8 +198,6 @@ class TestPurchaseInvoice(unittest.TestCase):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
-
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items:
@@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
- def test_gl_entries_with_aia_for_non_stock_items(self):
- pi = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(1, pi.company)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
- pi.get("items")[0].item_code = "_Test Non Stock Item"
- pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
- pi.get("taxes").pop(0)
- pi.get("taxes").pop(1)
- pi.insert()
- pi.submit()
- pi.load_from_db()
+ def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+
self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -265,17 +257,15 @@ class TestPurchaseInvoice(unittest.TestCase):
order by account asc""", pi.name, as_dict=1)
self.assertTrue(gl_entries)
- expected_values = sorted([
- ["_Test Payable - _TC", 0, 620],
- ["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
- ["_Test Account VAT - _TC", 120.0, 0],
- ])
+ expected_values = [
+ ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
+ ["Creditors - TCP1", 0, 250]
+ ]
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
- set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0])
@@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
- def test_return_purchase_invoice(self):
- set_perpetual_inventory()
+ def test_return_purchase_invoice_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
- pi = make_purchase_invoice()
-
- return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2)
+ return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return
@@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = {
- "Creditors - _TC": [100.0, 0.0],
- "Stock Received But Not Billed - _TC": [0.0, 100.0],
+ "Creditors - TCP1": [100.0, 0.0],
+ "Stock Received But Not Billed - TCP1": [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
- set_perpetual_inventory(0)
-
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
-
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
@@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
- self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+ self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
- set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index ca6f22cc30b..50734c865cd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -179,6 +179,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@@ -258,6 +261,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@@ -279,7 +286,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
if cint(self.update_stock):
@@ -722,22 +729,20 @@ class SalesInvoice(SellingController):
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
- def make_gl_entries(self, gl_entries=None):
- from erpnext.accounts.general_ledger import make_reverse_gl_entries
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
+ from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
- from erpnext.accounts.general_ledger import make_gl_entries
-
# if POS and amount is written off, updating outstanding amt after posting all gl entries
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index 11ebe6a573a..ee6419db20a 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -17,7 +17,8 @@
"description": "138-CMS Shoe",
"doctype": "Sales Invoice Item",
"income_account": "Sales - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "item_code": "138-CMS Shoe",
"item_name": "138-CMS Shoe",
"parentfield": "items",
"qty": 1.0,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 22a4f336547..ceb79079893 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -10,7 +10,6 @@ from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -659,7 +658,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
si = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(0, si.company)
si.insert()
si.submit()
@@ -815,7 +813,6 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_si_without_payment(self):
- set_perpetual_inventory()
make_pos_profile()
pos = copy.deepcopy(test_records[1])
@@ -829,9 +826,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
- set_perpetual_inventory()
-
- si = frappe.get_doc(test_records[1])
+ si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
+ income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
si.get("items")[0].item_code = None
si.insert()
si.submit()
@@ -842,24 +838,16 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ ["Debtors - TCP1", 100.0, 0.0],
+ ["Sales - TCP1", 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
- set_perpetual_inventory(0)
-
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
- set_perpetual_inventory()
- si = frappe.get_doc(test_records[1])
- si.get("items")[0].item_code = "_Test Non Stock Item"
- si.insert()
- si.submit()
+ si = create_sales_invoice(item="_Test Non Stock Item")
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
@@ -867,17 +855,14 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ [si.debit_to, 100.0, 0.0],
+ [test_records[1]["items"][0]["income_account"], 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
- set_perpetual_inventory(0)
def _insert_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
@@ -1106,7 +1091,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.grand_total, 859.43)
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
@@ -1776,64 +1760,69 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ target_doc.items[0].update({
+ "expense_account": "Cost of Goods Sold - _TC1",
+ "cost_center": "Main - _TC1",
+ "warehouse": "Stores - _TC1"
+ })
target_doc.submit()
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
- def test_internal_transfer_gl_entry(self):
- ## Create internal transfer account
- account = create_account(account_name="Unrealized Profit",
- parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
+ # def test_internal_transfer_gl_entry(self):
+ # ## Create internal transfer account
+ # account = create_account(account_name="Unrealized Profit",
+ # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
- frappe.db.set_value('Company', '_Test Company with perpetual inventory',
- 'unrealized_profit_loss_account', account)
+ # frappe.db.set_value('Company', '_Test Company with perpetual inventory',
+ # 'unrealized_profit_loss_account', account)
- customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
+ # "_Test Company with perpetual inventory")
- create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
+ # "_Test Company with perpetual inventory")
- si = create_sales_invoice(
- company = "_Test Company with perpetual inventory",
- customer = customer,
- debit_to = "Debtors - TCP1",
- warehouse = "Stores - TCP1",
- income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1",
- currency = "INR",
- do_not_save = 1
- )
+ # si = create_sales_invoice(
+ # company = "_Test Company with perpetual inventory",
+ # customer = customer,
+ # debit_to = "Debtors - TCP1",
+ # warehouse = "Stores - TCP1",
+ # income_account = "Sales - TCP1",
+ # expense_account = "Cost of Goods Sold - TCP1",
+ # cost_center = "Main - TCP1",
+ # currency = "INR",
+ # do_not_save = 1
+ # )
- si.selling_price_list = "_Test Price List Rest of the World"
- si.update_stock = 1
- si.items[0].target_warehouse = 'Work In Progress - TCP1'
- add_taxes(si)
- si.save()
- si.submit()
+ # si.selling_price_list = "_Test Price List Rest of the World"
+ # si.update_stock = 1
+ # si.items[0].target_warehouse = 'Work In Progress - TCP1'
+ # add_taxes(si)
+ # si.save()
+ # si.submit()
- target_doc = make_inter_company_transaction("Sales Invoice", si.name)
- target_doc.company = '_Test Company with perpetual inventory'
- target_doc.items[0].warehouse = 'Finished Goods - TCP1'
- add_taxes(target_doc)
- target_doc.save()
- target_doc.submit()
+ # target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ # target_doc.company = '_Test Company with perpetual inventory'
+ # target_doc.items[0].warehouse = 'Finished Goods - TCP1'
+ # add_taxes(target_doc)
+ # target_doc.save()
+ # target_doc.submit()
- si_gl_entries = [
- ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
- ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
- ]
+ # si_gl_entries = [
+ # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
+ # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
+ # ]
- check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
+ # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
- pi_gl_entries = [
- ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
- ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
- ]
+ # pi_gl_entries = [
+ # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
+ # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
+ # ]
- check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
+ # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -1991,14 +1980,19 @@ def create_sales_invoice(**args):
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
+ "item_name": args.item_name or "_Test Item",
+ "description": args.description or "_Test Item",
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1,
+ "uom": args.uom or "Nos",
+ "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no
+ "serial_no": args.serial_no,
+ "conversion_factor": 1
})
if not args.do_not_save:
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index fb3dd6a92a1..36950757989 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-06-04 11:02:19",
"doctype": "DocType",
@@ -51,6 +52,7 @@
"column_break_24",
"base_net_rate",
"base_net_amount",
+ "incoming_rate",
"drop_ship",
"delivered_by_supplier",
"accounting",
@@ -792,20 +794,28 @@
"options": "Project"
},
{
- "depends_on": "eval:parent.update_stock == 1",
- "fieldname": "sales_invoice_item",
- "fieldtype": "Data",
- "ignore_user_permissions": 1,
- "label": "Sales Invoice Item",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- }
+ "depends_on": "eval:parent.update_stock == 1",
+ "fieldname": "sales_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "Sales Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-20 11:24:41.749986",
+ "modified": "2020-09-23 19:59:04.879322",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 9a091bf57bc..c7f0c8781c0 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -15,13 +15,13 @@ class ClosedAccountingPeriod(frappe.ValidationError): pass
class StockAccountInvalidTransaction(frappe.ValidationError): pass
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
-def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'):
+def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
- save_entries(gl_map, adv_adj, update_outstanding)
+ save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
@@ -119,8 +119,9 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head:
return e
-def save_entries(gl_map, adv_adj, update_outstanding):
- validate_cwip_accounts(gl_map)
+def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
+ if not from_repost:
+ validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map)
@@ -128,24 +129,24 @@ def save_entries(gl_map, adv_adj, update_outstanding):
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
for entry in gl_map:
- make_entry(entry, adv_adj, update_outstanding)
+ make_entry(entry, adv_adj, update_outstanding, from_repost)
- # check against budget
- validate_expense_against_budget(entry)
-
- validate_account_for_perpetual_inventory(gl_map)
+ if not from_repost:
+ validate_account_for_perpetual_inventory(gl_map)
-def make_entry(args, adv_adj, update_outstanding):
+def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry")
gle.update(args)
gle.flags.ignore_permissions = 1
+ gle.flags.from_repost = from_repost
gle.insert()
- gle.run_method("on_update_with_args", adv_adj, update_outstanding)
+ gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.submit()
# check against budget
- validate_expense_against_budget(args)
+ if not from_repost:
+ validate_expense_against_budget(args)
def validate_account_for_perpetual_inventory(gl_map):
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
@@ -161,7 +162,7 @@ def validate_account_for_perpetual_inventory(gl_map):
# Always use current date to get stock and account balance as there can future entries for
# other items
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
- getdate(), gl_map[0].company)
+ gl_map[0].posting_date, gl_map[0].company)
if gl_map[0].voucher_type=="Journal Entry":
# In case of Journal Entry, there are no corresponding SL entries,
@@ -176,8 +177,8 @@ def validate_account_for_perpetual_inventory(gl_map):
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
diff = flt(stock_bal - account_bal, precision)
- error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
- stock_bal, account_bal, frappe.bold(account))
+ error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format(
+ stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date)
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
@@ -185,9 +186,10 @@ def validate_account_for_perpetual_inventory(gl_map):
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = {
- 'accounts':[
- {'account': account, db_or_cr_warehouse_account : abs(diff)},
- {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
+ 'accounts':[
+ {'account': account, db_or_cr_warehouse_account : abs(diff)},
+ {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)}
+ ]
}
frappe.msgprint(msg="""{0} {1} """.format(error_reason, error_resolution),
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 550aaef4040..540ac841823 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -928,7 +928,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
_delete_gl_entries(voucher_type, voucher_no)
- voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True)
+ voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
@@ -947,7 +947,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
- where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
+ where
+ timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
+ and is_cancelled = 0
+ {condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
@@ -964,3 +967,20 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
+
+def compare_existing_and_expected_gle(existing_gle, expected_gle):
+ matched = True
+ for entry in expected_gle:
+ account_existed = False
+ for e in existing_gle:
+ if entry.account == e.account:
+ account_existed = True
+ if entry.account == e.account and entry.against_account == e.against_account \
+ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
+ and (entry.debit != e.debit or entry.credit != e.credit):
+ matched = False
+ break
+ if not account_existed:
+ matched = False
+ break
+ return matched
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 10db240a446..c691e9f9f85 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -732,7 +732,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-30 11:59:47.670951",
+ "modified": "2020-12-07 11:59:47.670951",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 286c4f44510..dc61870df30 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -16,6 +16,8 @@ from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
+from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController):
def __setup__(self):
@@ -63,7 +65,7 @@ class BuyingController(StockController):
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- self.update_valuation_rate("items")
+ self.update_valuation_rate()
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -177,7 +179,7 @@ class BuyingController(StockController):
self.in_words = money_in_words(amount, self.currency)
# update valuation rate
- def update_valuation_rate(self, parentfield):
+ def update_valuation_rate(self, reset_outgoing_rate=True):
"""
item_tax_amount is the total tax amount applied on that item
stored for valuation
@@ -188,7 +190,7 @@ class BuyingController(StockController):
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
- for d in self.get(parentfield):
+ for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
@@ -198,7 +200,7 @@ class BuyingController(StockController):
if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount
- for i, item in enumerate(self.get(parentfield)):
+ for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_and_asset_items_qty
@@ -216,16 +218,34 @@ class BuyingController(StockController):
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
- rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
- + landed_cost_voucher_amount) / qty_in_stock_uom)
+ item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
+ item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
+ + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
else:
item.valuation_rate = 0.0
+ def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
+ supplied_items_cost = 0.0
+ for d in self.get("supplied_items"):
+ if d.reference_name == item_row_id:
+ if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ rate = get_incoming_rate({
+ "item_code": d.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * d.consumed_qty,
+ "serial_no": d.serial_no
+ })
+
+ if rate > 0:
+ d.rate = rate
+
+ d.amount = flt(d.consumed_qty) * flt(d.rate)
+ supplied_items_cost += flt(d.amount)
+
+ return supplied_items_cost
+
def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@@ -352,35 +372,17 @@ class BuyingController(StockController):
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
+ def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
- rm.main_item_code = fg_item_doc.item_code
+ rm.main_item_code = fg_item_row.item_code
- rm.reference_name = fg_item_doc.name
+ rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
- if not raw_material_data.get('non_stock_item'):
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": raw_material_data.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * qty,
- "serial_no": rm.serial_no
- })
-
- if not rm.rate:
- rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company=self.company)
-
- rm.amount = qty * flt(rm.rate)
- fg_item_doc.rm_supp_cost += rm.amount
-
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
@@ -389,7 +391,7 @@ class BuyingController(StockController):
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
- if self.doctype == 'Purchase Receipt' and item.purchase_order:
+ if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
@@ -406,7 +408,7 @@ class BuyingController(StockController):
reserve_warehouse = None
conversion_factor = item.conversion_factor
- if (self.doctype == 'Purchase Receipt' and item.purchase_order and
+ if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
@@ -434,9 +436,7 @@ class BuyingController(StockController):
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
- if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
+ rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -444,29 +444,8 @@ class BuyingController(StockController):
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
-
- # get raw materials rate
- if self.doctype == "Purchase Receipt":
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": bom_item.item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * required_qty,
- "serial_no": rm.serial_no
- })
- if not rm.rate:
- rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company = self.company)
- else:
- rm.rate = bom_item.rate
-
- rm.amount = required_qty * flt(rm.rate)
- raw_materials_cost += flt(rm.amount)
-
- if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- item.rm_supp_cost = raw_materials_cost
+ elif not rm.reserve_warehouse:
+ rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
@@ -579,7 +558,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "dependant_sle_voucher_detail_no": d.name
})
sl_entries.append(from_warehouse_sle)
@@ -589,28 +569,20 @@ class BuyingController(StockController):
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
- filters = {
- "voucher_type": self.doctype,
- "voucher_no": self.return_against,
- "item_code": d.item_code
- }
-
- if (self.doctype == "Purchase Invoice" and self.update_stock
- and d.get("purchase_invoice_item")):
- filters["voucher_detail_no"] = d.purchase_invoice_item
- elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
- filters["voucher_detail_no"] = d.purchase_receipt_item
-
- original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
+ outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
sle.update({
- "outgoing_rate": original_incoming_rate
+ "outgoing_rate": outgoing_rate,
+ "recalculate_rate": 1
})
+ if d.from_warehouse:
+ sle.dependant_sle_voucher_detail_no = d.name
else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update({
- "incoming_rate": incoming_rate
+ "incoming_rate": incoming_rate,
+ "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
})
sl_entries.append(sle)
@@ -618,7 +590,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "recalculate_rate": 1
})
sl_entries.append(from_warehouse_sle)
@@ -666,6 +639,7 @@ class BuyingController(StockController):
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
+ "dependant_sle_voucher_detail_no": d.reference_name
}))
def on_submit(self):
@@ -857,6 +831,7 @@ class BuyingController(StockController):
else:
validate_item_type(self, "is_purchase_item", "purchase")
+
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 5299b25601d..8f65c31f3d1 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -365,3 +365,45 @@ def make_return_doc(doctype, source_name, target_doc=None):
}, target_doc, set_missing_values)
return doclist
+
+def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
+ if not return_against:
+ return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
+
+ return_against_item_field = get_return_against_item_fields(voucher_type)
+
+ filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
+ return_against, item_code, return_against_item_field, item_row)
+
+ if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ select_field = "incoming_rate"
+ else:
+ select_field = "abs(stock_value_difference / actual_qty)"
+
+ return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
+
+def get_return_against_item_fields(voucher_type):
+ return_against_item_fields = {
+ "Purchase Receipt": "purchase_receipt_item",
+ "Purchase Invoice": "purchase_invoice_item",
+ "Delivery Note": "dn_detail",
+ "Sales Invoice": "sales_invoice_item"
+ }
+ return return_against_item_fields[voucher_type]
+
+def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row):
+ filters = {
+ "voucher_type": voucher_type,
+ "voucher_no": return_against,
+ "item_code": item_code
+ }
+
+ if item_row:
+ reference_voucher_detail_no = item_row.get(return_against_item_field)
+ else:
+ reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field)
+
+ if reference_voucher_detail_no:
+ filters["voucher_detail_no"] = reference_voucher_detail_no
+
+ return filters
\ No newline at end of file
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 4dbd7bfa186..85cfb951fcc 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -13,6 +13,7 @@ from frappe.contacts.doctype.address.address import get_address_display
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
def __setup__(self):
@@ -48,6 +49,7 @@ class SellingController(StockController):
self.set_customer_address()
self.validate_for_duplicate_items()
self.validate_target_warehouse()
+ self.set_incoming_rate()
def set_missing_values(self, for_validate=False):
@@ -230,7 +232,8 @@ class SellingController(StockController):
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': p.incoming_rate
}))
else:
il.append(frappe._dict({
@@ -248,7 +251,8 @@ class SellingController(StockController):
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': d.incoming_rate
}))
return il
@@ -307,69 +311,89 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows)
+ def set_incoming_rate(self):
+ if self.doctype not in ("Delivery Note", "Sales Invoice"):
+ return
+
+ items = self.get("items") + (self.get("packed_items") or [])
+ for d in items:
+ if not cint(self.get("is_return")):
+ # Get incoming rate based on original item cost based on valuation method
+ d.incoming_rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1*flt(d.qty),
+ "serial_no": d.serial_no,
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
+ elif self.get("return_against"):
+ # Get incoming rate of return entry from reference document
+ # based on original item cost as per valuation method
+ d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
+
def update_stock_ledger(self):
self.update_reserved_qty()
sl_entries = []
+ # Loop over items and packed items table
for d in self.get_item_list():
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
if flt(d.conversion_factor)==0.0:
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
- return_rate = 0
- if cint(self.is_return) and self.return_against and self.docstatus==1:
- against_document_no = (d.get("sales_invoice_item")
- if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
- return_rate = self.get_incoming_rate_for_return(d.item_code,
- self.return_against, against_document_no)
-
- # On cancellation or if return entry submission, make stock ledger entry for
+ # On cancellation or return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
if d.target_warehouse:
- target_warehouse_sle = self.get_sl_entries(d, {
- "actual_qty": flt(d.qty),
- "warehouse": d.target_warehouse
- })
-
- if self.docstatus == 1:
- if not cint(self.is_return):
- args = frappe._dict({
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1*flt(d.qty),
- "serial_no": d.serial_no,
- "company": d.company,
- "voucher_type": d.voucher_type,
- "voucher_no": d.name,
- "allow_zero_valuation": d.allow_zero_valuation
- })
- target_warehouse_sle.update({
- "incoming_rate": get_incoming_rate(args)
- })
- else:
- target_warehouse_sle.update({
- "outgoing_rate": return_rate
- })
- sl_entries.append(target_warehouse_sle)
+ sl_entries.append(self.get_sle_for_target_warehouse(d))
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
+
self.make_sl_entries(sl_entries)
+ def get_sle_for_source_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": -1*flt(item_row.qty),
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": cint(self.is_return)
+ })
+ if item_row.target_warehouse and not cint(self.is_return):
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
+ def get_sle_for_target_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": flt(item_row.qty),
+ "warehouse": item_row.target_warehouse
+ })
+
+ if self.docstatus == 1:
+ if not cint(self.is_return):
+ sle.update({
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": 1
+ })
+ else:
+ sle.update({
+ "outgoing_rate": item_row.incoming_rate
+ })
+ if item_row.warehouse:
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
def set_po_nos(self, for_validate=False):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
if for_validate and self.po_no:
@@ -463,4 +487,4 @@ def set_default_income_account_for_item(obj):
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
- set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
+ set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
\ No newline at end of file
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 683d7f77b55..51c063c2c0b 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -24,7 +24,7 @@ class StockController(AccountsController):
self.validate_serialized_batch()
self.validate_customer_provided_item()
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -34,12 +34,12 @@ class StockController(AccountsController):
if self.docstatus==1:
if not gl_entries:
gl_entries = self.get_gl_entries(warehouse_account)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
gl_entries = []
gl_entries = self.get_asset_gl_entry(gl_entries)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -70,7 +70,6 @@ class StockController(AccountsController):
gl_list = []
warehouse_with_no_account = []
-
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
@@ -125,7 +124,7 @@ class StockController(AccountsController):
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):
- frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
+ frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
return process_gl_map(gl_list)
@@ -309,23 +308,6 @@ class StockController(AccountsController):
return serialized_items
- def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
- incoming_rate = 0.0
- cond = ''
- if against_document and item_code:
- if against_document_no:
- cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
-
- incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
- from `tabStock Ledger Entry`
- where voucher_type = %s and voucher_no = %s
- and item_code = %s {0} limit 1""".format(cond),
- (self.doctype, against_document, item_code))
-
- incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
-
- return incoming_rate
-
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
@@ -409,19 +391,64 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
-def compare_existing_and_expected_gle(existing_gle, expected_gle):
- matched = True
- for entry in expected_gle:
- account_existed = False
- for e in existing_gle:
- if entry.account == e.account:
- account_existed = True
- if entry.account == e.account and entry.against_account == e.against_account \
- and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
- and (entry.debit != e.debit or entry.credit != e.credit):
- matched = False
- break
- if not account_existed:
- matched = False
+ def repost_future_sle_and_gle(self):
+ args = frappe._dict({
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "company": self.company
+ })
+
+ if check_if_future_sle_exists(args):
+ create_repost_item_valuation_entry(args)
+
+def check_if_future_sle_exists(args):
+ sl_entries = frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
+ fields=["item_code", "warehouse"],
+ order_by="creation asc")
+
+ distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
+
+ sle_exists = False
+ for item_code, warehouse in distinct_item_warehouses:
+ args.update({
+ "item_code": item_code,
+ "warehouse": warehouse
+ })
+ if get_sle(args):
+ sle_exists = True
break
- return matched
+ return sle_exists
+
+def get_sle(args):
+ return frappe.db.sql("""
+ select name
+ from `tabStock Ledger Entry`
+ where
+ item_code=%(item_code)s
+ and warehouse=%(warehouse)s
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ limit 1
+ """, args)
+
+def create_repost_item_valuation_entry(args):
+ args = frappe._dict(args)
+ repost_entry = frappe.new_doc("Repost Item Valuation")
+ repost_entry.based_on = args.based_on
+ if not args.based_on:
+ repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse"
+ repost_entry.voucher_type = args.voucher_type
+ repost_entry.voucher_no = args.voucher_no
+ repost_entry.item_code = args.item_code
+ repost_entry.warehouse = args.warehouse
+ repost_entry.posting_date = args.posting_date
+ repost_entry.posting_time = args.posting_time
+ repost_entry.company = args.company
+ repost_entry.allow_zero_rate = args.allow_zero_rate
+ repost_entry.flags.ignore_links = True
+ repost_entry.save()
+ repost_entry.submit()
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 2bf3fbf75e9..ce9699e1b3c 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -6,7 +6,6 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
@@ -18,7 +17,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestWorkOrder(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index eff17f8bc78..159655b74bb 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -785,7 +785,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-29 20:54:32.309460",
+ "modified": "2020-012-07 20:54:32.309460",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json
index 21302417d2b..9e55702ddc9 100644
--- a/erpnext/setup/doctype/company/test_records.json
+++ b/erpnext/setup/doctype/company/test_records.json
@@ -7,7 +7,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC1",
@@ -17,7 +18,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC2",
@@ -27,7 +29,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC3",
@@ -38,7 +41,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC4",
@@ -50,7 +54,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC5",
@@ -61,7 +66,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "TCP1",
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index c2a3d3c151f..e41f1a8aaaf 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,13 +8,8 @@ import unittest
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint, flt
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestBatch(unittest.TestCase):
-
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 7acdec728b6..ab19b77ad8e 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -16,22 +16,30 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
-
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.sle_id
+ "sle_id": args.name
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+ # Update qty_after_transaction in future SLEs of this item and warehouse
+ update_qty_in_future_sle(args)
+
def update_qty(self, args):
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3f3407e3501..1a6a5550927 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -217,6 +217,7 @@ class DeliveryNote(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def on_cancel(self):
super(DeliveryNote, self).on_cancel()
@@ -234,7 +235,8 @@ class DeliveryNote(SellingController):
self.cancel_packing_slips()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 6b4663a688a..559f8be0dea 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -10,8 +10,7 @@ import frappe.defaults
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import get_gl_entries, set_perpetual_inventory
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip
from erpnext.stock.doctype.stock_entry.test_stock_entry \
import make_stock_entry, make_serialized_item, get_qty_after_transaction
@@ -24,9 +23,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.doctype.item.test_item import create_item
class TestDeliveryNote(unittest.TestCase):
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -43,7 +39,6 @@ class TestDeliveryNote(unittest.TestCase):
def test_delivery_note_no_gl_entry(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
stock_queue = json.loads(get_previous_sle({
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 7b471874af7..4bbf3de5940 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -56,6 +56,7 @@
"base_net_rate",
"base_net_amount",
"billed_amt",
+ "incoming_rate",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -732,16 +733,22 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
- "label": "Returned Qty in Stock UOM",
+ "label": "Returned Qty in Stock UOM"
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-07-31 20:12:43.054342",
+ "modified": "2020-12-07 19:59:27.119856",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 9ca887c77e3..8f437b13f0d 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -458,5 +458,15 @@
"item_tax_template": "_Test Item Tax Template 1"
}
]
+ },
+ {
+ "description": "_Test",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_code": "138-CMS Shoe",
+ "item_group": "_Test Item Group",
+ "item_name": "138-CMS Shoe",
+ "stock_uom": "_Test UOM",
+ "gst_hsn_code": "999800"
}
]
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index f045e4f9114..d5700fe5147 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -12,11 +12,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestItemAlternative(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
make_items()
def test_alternative_item_for_subcontract_rm(self):
diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
index 0cc243d4cb5..64331c7d578 100644
--- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
+++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2014-07-11 11:51:00.453717",
"doctype": "DocType",
"editable_grid": 1,
@@ -31,16 +32,19 @@
"reqd": 1
},
{
+ "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "expense_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Expense Account",
+ "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"options": "Account",
- "reqd": 1
+ "print_hide": 1
}
],
"istable": 1,
- "modified": "2019-09-30 18:28:32.070655",
+ "links": [],
+ "modified": "2020-12-04 00:22:14.373312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index bc3d3266add..9ec6b8946cc 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -77,9 +77,9 @@ class LandedCostVoucher(Document):
company_currency = erpnext.get_company_currency(self.company)
for account in self.taxes:
if get_account_currency(account.expense_account) != company_currency:
- frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency.
- Please select expense account with account currency as {1}""")
- .format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account Currency"))
+ frappe.throw(_("Row {}: Expense account currency should be same as company's default currency.").format(account.idx)
+ + _("Please select expense account with account currency as {}.").format(frappe.bold(company_currency)),
+ title=_("Invalid Account Currency"))
def set_total_taxes_and_charges(self):
self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")])
@@ -121,7 +121,7 @@ class LandedCostVoucher(Document):
doc.set_landed_cost_voucher_amount()
# set valuation amount in pr item
- doc.update_valuation_rate("items")
+ doc.update_valuation_rate(reset_outgoing_rate=False)
# db_update will update and save landed_cost_voucher_amount and voucher_amount in PR
for item in doc.get("items"):
@@ -143,6 +143,7 @@ class LandedCostVoucher(Document):
doc.docstatus = 1
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
doc.make_gl_entries()
+ doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
for item in self.get('items'):
@@ -152,14 +153,13 @@ class LandedCostVoucher(Document):
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty:
- frappe.throw(_('There are not enough asset created or linked to {0}. \
- Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty))
+ frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document)
+ + _('Please create or link {0} Assets with respective document.').format(item.qty))
if docs:
for d in docs:
if d.docstatus == 1:
- frappe.throw(_('{2} {0} has submitted Assets.\
- Remove Item {1} from table to continue.').format(
- item.receipt_document, item.item_code, item.receipt_document_type))
+ frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.')
+ .format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code)))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 3f2c5daf669..b97213e4fba 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.utils import flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_purchase_receipt
+ import get_gl_entries, test_records as pr_test_records, make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -27,7 +27,7 @@ class TestLandedCostVoucher(unittest.TestCase):
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount")
self.assertEqual(pr_lc_value, 25.0)
@@ -89,7 +89,7 @@ class TestLandedCostVoucher(unittest.TestCase):
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
+ create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name},
"landed_cost_voucher_amount")
@@ -137,7 +137,7 @@ class TestLandedCostVoucher(unittest.TestCase):
serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate")
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
serial_no = frappe.db.get_value("Serial No", "SN001",
["warehouse", "purchase_rate"], as_dict=1)
@@ -160,7 +160,7 @@ class TestLandedCostVoucher(unittest.TestCase):
})
pr.submit()
- lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
self.assertEqual(lcv.items[0].applicable_charges, 41.07)
self.assertEqual(lcv.items[2].applicable_charges, 41.08)
@@ -236,7 +236,7 @@ def make_landed_cost_voucher(** args):
return lcv
-def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
+def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
lcv = frappe.new_doc("Landed Cost Voucher")
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 19924b16363..0a29fa05e1a 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -12,9 +12,6 @@ from erpnext.stock.doctype.material_request.material_request \
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
- def setUp(self):
- erpnext.set_perpetual_inventory(0)
-
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 2ac5c426c03..f1d7f8c8c9e 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:00",
"doctype": "DocType",
"editable_grid": 1,
@@ -14,6 +15,7 @@
"target_warehouse",
"column_break_9",
"qty",
+ "uom",
"section_break_9",
"serial_no",
"column_break_11",
@@ -23,7 +25,7 @@
"actual_qty",
"projected_qty",
"column_break_16",
- "uom",
+ "incoming_rate",
"page_break",
"prevdoc_doctype",
"parent_detail_docname"
@@ -199,11 +201,21 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-11-26 20:09:59.400960",
+ "links": [],
+ "modified": "2020-09-24 09:25:13.050151",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
@@ -212,4 +224,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 97e0fa738cd..226064bae78 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -181,6 +181,7 @@ class PurchaseReceipt(BuyingController):
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name
@@ -209,7 +210,8 @@ class PurchaseReceipt(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
def get_current_stock(self):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 9b8eeed1a12..83012d355ff 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -9,14 +9,15 @@ import frappe.defaults
from frappe.utils import cint, flt, cstr, today, random_string, add_days
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import make_item
from six import iteritems
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+
class TestPurchaseReceipt(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
@@ -112,6 +113,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
+ pr.cancel()
+
def test_batched_serial_no_purchase(self):
item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
if not item:
@@ -183,22 +186,30 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+
+ pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+
+ se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
+ se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+ qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
- company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
+ company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0)
+ pr.cancel()
+ se1.cancel()
+ se2.cancel()
def test_subcontracting_over_receipt(self):
"""
@@ -216,13 +227,13 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code)
- po = create_purchase_order(item_code=item_code, qty=1,
+ po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 1", qty=1, basic_rate=100)
- make_stock_entry(target="_Test Warehouse - _TC",
+ se1 = make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 1", qty=10, basic_rate=100)
+ se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
rm_items = [
{
@@ -254,6 +265,13 @@ class TestPurchaseReceipt(unittest.TestCase):
pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit)
+ pr1.cancel()
+ se.cancel()
+ se1.cancel()
+ se2.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@@ -284,6 +302,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse)
+ pr.cancel()
+
def test_purchase_return_partial(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
@@ -371,6 +391,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr.per_returned, 100)
self.assertEqual(pr.status, 'Return Issued')
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
@@ -388,6 +411,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(actual_qty, -2)
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
@@ -415,6 +441,10 @@ class TestPurchaseReceipt(unittest.TestCase):
"delivery_document_no": return_pr.name
})
+ return_pr.cancel()
+ pr.reload()
+ pr.cancel()
+
def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM"
if not frappe.db.exists('Item', item_code):
@@ -431,6 +461,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
+ return_pr.cancel()
+ pr.cancel()
+
def test_closed_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status
@@ -440,6 +473,9 @@ class TestPurchaseReceipt(unittest.TestCase):
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
+ pr.reload()
+ pr.cancel()
+
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
@@ -482,6 +518,16 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill")
+ pr2.cancel()
+ pi2.reload()
+ pi2.cancel()
+ pi1.reload()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_against_purchase_receipt(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -509,6 +555,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(serial_no, frappe.db.get_value("Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
+ new_pr_doc.cancel()
+
def test_not_accept_duplicate_serial_no(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -519,16 +567,19 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = item.name
serial_no = random_string(5)
- make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
- create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
+ pr1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
+ dn = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
- pr = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
- self.assertRaises(SerialNoDuplicateError, pr.submit)
+ pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
+ self.assertRaises(SerialNoDuplicateError, pr2.submit)
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, se.submit)
+ dn.cancel()
+ pr1.cancel()
+
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
@@ -549,7 +600,7 @@ class TestPurchaseReceipt(unittest.TestCase):
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
- 'depreciation_expense_account': '_Test Depreciation - _TC'
+ 'depreciation_expense_account': '_Test Depreciations - _TC'
}]
}).insert()
@@ -568,6 +619,8 @@ class TestPurchaseReceipt(unittest.TestCase):
location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location")
+ pr.cancel()
+
def test_purchase_return_with_submitted_asset(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
@@ -594,6 +647,9 @@ class TestPurchaseReceipt(unittest.TestCase):
pr_return.submit()
+ pr_return.cancel()
+ pr.cancel()
+
def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - TCP1"
@@ -605,7 +661,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -623,6 +680,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@@ -648,6 +707,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_make_purchase_invoice_from_pr_for_returned_qty(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
@@ -663,6 +724,12 @@ class TestPurchaseReceipt(unittest.TestCase):
pi = make_purchase_invoice(pr.name)
self.assertEquals(pi.items[0].qty, 3)
+ pr1.cancel()
+ pr.reload()
+ pr.cancel()
+ po.reload()
+ po.cancel()
+
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append("items", {
@@ -689,8 +756,14 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1)
+ pr2.cancel()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+
def test_stock_transfer_from_purchase_receipt(self):
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory")
+ pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
@@ -713,18 +786,20 @@ class TestPurchaseReceipt(unittest.TestCase):
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
- def test_stock_transfer_from_purchase_receipt_with_valuation(self):
- warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1')
- warehouse.account = '_Test Account Stock In Hand - TCP1'
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ def test_stock_transfer_from_purchase_receipt_with_valuation(self):
+ create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
+ properties={"account": '_Test Account Stock In Hand - TCP1'})
+
+ pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
- pr.items[0].from_warehouse = 'Work In Progress - TCP1'
+ pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = ''
@@ -749,7 +824,7 @@ class TestPurchaseReceipt(unittest.TestCase):
]
expected_sle = {
- 'Work In Progress - TCP1': -5,
+ '_Test Warehouse for Valuation - TCP1': -5,
'Stores - TCP1': 5
}
@@ -761,60 +836,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2])
- warehouse.account = ''
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- def test_backdated_purchase_receipt(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 5", warehouse="Stores - _TC5",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
-
- def test_backdated_purchase_receipt_for_same_company_different_warehouse(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Finished Goods - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -877,6 +901,12 @@ class TestPurchaseReceipt(unittest.TestCase):
update_backflush_based_on("BOM")
+ pr.delete()
+ se.cancel()
+ ste2.cancel()
+ ste1.cancel()
+ po.cancel()
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
@@ -972,6 +1002,8 @@ def make_purchase_receipt(**args):
pr.posting_date = args.posting_date or today()
if args.posting_time:
pr.posting_time = args.posting_time
+ if args.posting_date or args.posting_time:
+ pr.set_posting_time = 1
pr.company = args.company or "_Test Company"
pr.supplier = args.supplier or "_Test Supplier"
pr.is_subcontracted = args.is_subcontracted or "No"
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 84c64aa8f85..871b255b06a 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -866,7 +866,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-02 10:00:38.204294",
+ "modified": "2020-12-07 10:00:38.204294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/repost_item_valuation/__init__.py b/erpnext/stock/doctype/repost_item_valuation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
new file mode 100644
index 00000000000..e429cd5e304
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Repost Item Valuation', {
+ setup: function(frm) {
+ frm.set_query("warehouse", () => {
+ let filters = {
+ 'is_group': 0
+ };
+ if (frm.doc.company) filters['company'] = frm.doc.company;
+ return {filters: filters};
+ });
+
+ frm.set_query("voucher_type", () => {
+ return {
+ filters: {
+ name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note',
+ 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']]
+ }
+ };
+ });
+
+ if (frm.doc.company) {
+ frm.set_query("voucher_no", () => {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
+ }
+ },
+ refresh: function(frm) {
+ if (frm.doc.status == "Failed") {
+ frm.add_custom_button(__('Restart'), function () {
+ frm.trigger("restart_reposting");
+ }).addClass("btn-primary");
+ }
+ },
+
+ restart_reposting: function(frm) {
+ frappe.call({
+ method: "restart_reposting",
+ doc: frm.doc,
+ callback: function(r) {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
new file mode 100644
index 00000000000..071fc86d9b3
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -0,0 +1,215 @@
+{
+ "actions": [],
+ "autoname": "REPOST-ITEM-VAL-.######",
+ "creation": "2020-10-22 22:27:07.742161",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "based_on",
+ "voucher_type",
+ "voucher_no",
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "column_break_5",
+ "status",
+ "company",
+ "allow_negative_stock",
+ "via_landed_cost_voucher",
+ "allow_zero_rate",
+ "amended_from",
+ "error_section",
+ "error_log"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Item"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Warehouse"
+ },
+ {
+ "fetch_from": "voucher_no.posting_date",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "voucher_no.posting_time",
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time"
+ },
+ {
+ "default": "Queued",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Queued\nIn Progress\nCompleted\nFailed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Repost Item Valuation",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.status=='Failed'",
+ "fieldname": "error_section",
+ "fieldtype": "Section Break",
+ "label": "Error"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Long Text",
+ "label": "Error Log",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "warehouse.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher Type",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "DocType"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher No",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "voucher_type"
+ },
+ {
+ "default": "Transaction",
+ "fieldname": "based_on",
+ "fieldtype": "Select",
+ "label": "Based On",
+ "options": "Transaction\nItem and Warehouse",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_negative_stock",
+ "fieldtype": "Check",
+ "label": "Allow Negative Stock"
+ },
+ {
+ "default": "0",
+ "fieldname": "via_landed_cost_voucher",
+ "fieldtype": "Check",
+ "label": "Via Landed Cost Voucher"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Rate"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-12-10 07:52:12.476589",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Repost Item Valuation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
new file mode 100644
index 00000000000..a942f2edda7
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+from frappe.model.document import Document
+from frappe.utils import cint
+from erpnext.stock.stock_ledger import repost_future_sle
+from erpnext.accounts.utils import update_gl_entries_after
+
+
+class RepostItemValuation(Document):
+ def validate(self):
+ self.set_status()
+ self.reset_field_values()
+ self.set_company()
+
+ def reset_field_values(self):
+ if self.based_on == 'Transaction':
+ self.item_code = None
+ self.warehouse = None
+ else:
+ self.voucher_type = None
+ self.voucher_no = None
+
+ def set_company(self):
+ if self.voucher_type and self.voucher_no:
+ self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
+ elif self.warehouse:
+ self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
+
+ def set_status(self, status=None):
+ if not status:
+ status = 'Queued'
+ self.db_set('status', status)
+
+ def on_submit(self):
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+
+ def restart_reposting(self):
+ self.set_status('Queued')
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=True, doc=self)
+
+def repost(doc):
+ try:
+ doc.set_status('In Progress')
+ frappe.db.commit()
+
+ repost_sl_entries(doc)
+ repost_gl_entries(doc)
+ doc.set_status('Completed')
+ except Exception:
+ frappe.db.rollback()
+ traceback = frappe.get_traceback()
+ frappe.log_error(traceback)
+ frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback)
+ doc.set_status('Failed')
+ raise
+ finally:
+ frappe.db.commit()
+
+def repost_sl_entries(doc):
+ if doc.based_on == 'Transaction':
+ repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
+ allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+ else:
+ repost_future_sle(args=[frappe._dict({
+ "item_code": doc.item_code,
+ "warehouse": doc.warehouse,
+ "posting_date": doc.posting_date,
+ "posting_time": doc.posting_time
+ })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+
+def repost_gl_entries(doc):
+ if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
+ return
+
+ if doc.based_on == 'Transaction':
+ ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
+ items, warehouses = ref_doc.get_items_and_warehouses()
+ else:
+ items = [doc.item_code]
+ warehouses = [doc.warehouse]
+
+ update_gl_entries_after(doc.posting_date, doc.posting_time,
+ warehouses, items, company=doc.company)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
new file mode 100644
index 00000000000..13ceb68669c
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestRepostItemValuation(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 295149e2387..39ccf49c81c 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -134,17 +134,13 @@ class SerialNo(StockController):
sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict:
if sle_dict.get("incoming", []):
- sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0]
- if sle_list:
- entries["purchase_sle"] = sle_list[0]
+ entries["purchase_sle"] = sle_dict["incoming"][0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0]
else:
entries["last_sle"] = sle_dict["outgoing"][0]
- sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0]
- if sle_list:
- entries["delivery_sle"] = sle_list[0]
+ entries["delivery_sle"] = sle_dict["outgoing"][0]
return entries
@@ -155,11 +151,12 @@ class SerialNo(StockController):
for sle in frappe.db.sql("""
SELECT voucher_type, voucher_no,
- posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled
+ posting_date, posting_time, incoming_rate, actual_qty, serial_no
FROM
`tabStock Ledger Entry`
WHERE
item_code=%s AND company = %s
+ AND is_cancelled = 0
AND (serial_no = %s
OR serial_no like %s
OR serial_no like %s
@@ -179,7 +176,7 @@ class SerialNo(StockController):
def on_trash(self):
sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry`
- where serial_no like %s and item_code=%s""",
+ where serial_no like %s and item_code=%s and is_cancelled=0""",
("%%%s%%" % self.name, self.item_code), as_dict=True)
# Find the exact match
@@ -229,7 +226,7 @@ def validate_serial_no(sle, item_det):
if serial_nos:
frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
SerialNoNotRequiredError)
- else:
+ elif not sle.is_cancelled:
if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty))
@@ -247,10 +244,6 @@ def validate_serial_no(sle, item_det):
"delivery_document_no", "delivery_document_type", "warehouse",
"purchase_document_no", "company"], as_dict=1)
- if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
- frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
- .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError)
-
if sr.item_code!=sle.item_code:
if not allow_serial_nos_with_different_item(serial_no, sle):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
@@ -277,7 +270,7 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no,
sle.batch_no), SerialNoBatchError)
- if not sr.warehouse:
+ if not sle.is_cancelled and not sr.warehouse:
frappe.throw(_("Serial No {0} does not belong to any Warehouse")
.format(serial_no), SerialNoWarehouseError)
@@ -327,6 +320,12 @@ def validate_serial_no(sle, item_det):
elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
SerialNoRequiredError)
+ elif serial_nos:
+ for serial_no in serial_nos:
+ sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1)
+ if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
+ frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
+ .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse))
def validate_material_transfer_entry(sle_doc):
sle_doc.update({
@@ -334,7 +333,7 @@ def validate_material_transfer_entry(sle_doc):
"skip_serial_no_validaiton": False
})
- if (sle_doc.voucher_type == "Stock Entry" and
+ if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and
frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"):
if sle_doc.actual_qty < 0:
sle_doc.skip_update_serial_no = True
@@ -379,7 +378,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
if stock_entry.purpose in ("Repack", "Manufacture"):
for d in stock_entry.get("items"):
- if d.serial_no and (d.s_warehouse or d.t_warehouse):
+ if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
serial_nos = get_serial_nos(d.serial_no)
if sle_serial_no in serial_nos:
allow_serial_nos = True
@@ -388,7 +387,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
def update_serial_nos(sle, item_det):
if sle.skip_update_serial_no: return
- if not sle.serial_no and cint(sle.actual_qty) > 0 \
+ if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
frappe.db.set(sle, "serial_no", serial_nos)
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index ab061076e52..ed70790b2ca 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -12,7 +12,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
@@ -38,8 +37,6 @@ class TestSerialNo(unittest.TestCase):
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
def test_inter_company_transfer(self):
- set_perpetual_inventory(0, "_Test Company 1")
- set_perpetual_inventory(0)
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 27fcbb7e2a5..98116ec1832 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -510,22 +510,31 @@ frappe.ui.form.on('Stock Entry', {
calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
-
- const total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; })
- );
-
+ let total_basic_amount = 0;
+ if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.is_finished_item ? flt(i.basic_amount) : 0;
+ })
+ );
+ } else {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.t_warehouse ? flt(i.basic_amount) : 0;
+ })
+ );
+ }
+
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
- if (item.t_warehouse && total_basic_amount) {
+ if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
} else {
item.additional_cost = 0;
}
- item.amount = flt(item.basic_amount + flt(item.additional_cost),
- precision("amount", item));
+ item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 61e0df67238..5aed08102c7 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -644,9 +644,10 @@
],
"icon": "fa fa-file-text",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-11 19:10:07.954981",
+ "modified": "2020-09-09 12:59:02.508943",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 32d7e6eb34c..afdb54ceaa2 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -18,7 +18,7 @@ from erpnext.stock.utils import get_bin
from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
-
+from erpnext.accounts.general_ledger import process_gl_map
import json
from six import string_types, itervalues, iteritems
@@ -58,6 +58,7 @@ class StockEntry(StockController):
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
+ self.mark_finished_and_scrap_items()
self.validate_finished_goods()
self.validate_with_material_request()
self.validate_batch()
@@ -75,13 +76,11 @@ class StockEntry(StockController):
else:
set_batch_nos(self, 's_warehouse')
- self.set_incoming_rate()
self.validate_serialized_batch()
self.set_actual_qty()
- self.calculate_rate_and_amount(update_finished_item_rate=False)
+ self.calculate_rate_and_amount()
def on_submit(self):
-
self.update_stock_ledger()
update_serial_nos_after_submit(self, "items")
@@ -89,11 +88,15 @@ class StockEntry(StockController):
self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items()
+
self.make_gl_entries()
+
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.validate_reserved_serial_no_consumption()
self.update_transferred_qty()
self.update_quality_inspection()
+
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
@@ -113,9 +116,10 @@ class StockEntry(StockController):
self.update_work_order()
self.update_stock_ledger()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.update_transferred_qty()
self.update_quality_inspection()
@@ -256,11 +260,10 @@ class StockEntry(StockController):
def validate_fg_completed_qty(self):
if self.purpose == "Manufacture" and self.work_order:
- production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
- for item in self.items:
- if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty:
+ for d in self.items:
+ if d.is_finished_item and d.qty != self.fg_completed_qty:
frappe.throw(_("Finished product quantity {0} and For Quantity {1} cannot be different")
- .format(item.qty, self.fg_completed_qty))
+ .format(d.qty, self.fg_completed_qty))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -382,21 +385,6 @@ class StockEntry(StockController):
frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
- def set_incoming_rate(self):
- if self.purpose == "Repack":
- self.set_basic_rate_for_finished_goods()
-
- for d in self.items:
- if d.s_warehouse:
- args = self.get_args_for_incoming_rate(d)
- d.basic_rate = get_incoming_rate(args)
- elif d.allow_zero_valuation_rate and not d.s_warehouse:
- d.basic_rate = 0.0
- elif d.t_warehouse and not d.basic_rate:
- d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
- self.doctype, self.name, d.allow_zero_valuation_rate,
- currency=erpnext.get_company_currency(self.company), company=self.company)
-
def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@@ -432,57 +420,64 @@ class StockEntry(StockController):
d.serial_no = transferred_serial_no
def get_stock_and_rate(self):
+ """
+ Updates rate and availability of all the items.
+ Called from Update Rate and Availability button.
+ """
self.set_work_order_details()
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
- def calculate_rate_and_amount(self, force=False,
- update_finished_item_rate=True, raise_error_if_no_rate=True):
- self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate)
+ def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
self.distribute_additional_costs()
self.update_valuation_rate()
self.set_total_incoming_outgoing_value()
self.set_total_amount()
- def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True):
- """get stock and incoming rate on posting date"""
- raw_material_cost = 0.0
- scrap_material_cost = 0.0
- fg_basic_rate = 0.0
+ def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ """
+ Set rate for outgoing, scrapped and finished items
+ """
+ # Set rate for outgoing items
+ outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
+ # Set basic rate for incoming items
for d in self.get('items'):
- if d.t_warehouse: fg_basic_rate = flt(d.basic_rate)
- args = self.get_args_for_incoming_rate(d)
+ if d.s_warehouse or d.set_basic_rate_manually: continue
- # get basic rate
- if not d.bom_no:
- if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
+ if d.allow_zero_valuation_rate:
+ d.basic_rate = 0.0
+ elif d.is_finished_item:
+ if self.purpose == "Manufacture":
+ d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost)
+ elif self.purpose == "Repack":
+ d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
+
+ if not d.basic_rate and not d.allow_zero_valuation_rate:
+ d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
+ self.doctype, self.name, d.allow_zero_valuation_rate,
+ currency=erpnext.get_company_currency(self.company), company=self.company,
+ raise_error_if_no_rate=raise_error_if_no_rate)
+
+ d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
+ d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
+
+ def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
+ outgoing_items_cost = 0.0
+ for d in self.get('items'):
+ if d.s_warehouse:
+ if reset_outgoing_rate:
+ args = self.get_args_for_incoming_rate(d)
+ rate = get_incoming_rate(args)
+ if rate > 0:
+ d.basic_rate = rate
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
- raw_material_cost += flt(d.basic_amount)
-
- # get scrap items basic rate
- if d.bom_no:
- if not flt(d.basic_rate) and not d.allow_zero_valuation_rate and \
- getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate),
- self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
- d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
-
- if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
-
- scrap_material_cost += flt(d.basic_amount)
-
- number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse])
- if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate:
- self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost)
+ outgoing_items_cost += flt(d.basic_amount)
+ return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
return frappe._dict({
@@ -498,44 +493,44 @@ class StockEntry(StockController):
"allow_zero_valuation": item.allow_zero_valuation_rate,
})
- def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0):
- total_fg_qty = 0
- if not raw_material_cost and self.get("items"):
- raw_material_cost = sum([flt(row.basic_amount) for row in self.items
- if row.s_warehouse and not row.t_warehouse])
+ def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
+ finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
+ if len(finished_items) == 1:
+ return flt(outgoing_items_cost / finished_item_qty)
+ else:
+ unique_finished_items = set(finished_items)
+ if len(unique_finished_items) == 1:
+ total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
+ return flt(outgoing_items_cost / total_fg_qty)
- total_fg_qty = sum([flt(row.qty) for row in self.items
- if row.t_warehouse and not row.s_warehouse])
+ def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
+ scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
- if self.purpose in ["Manufacture", "Repack"]:
- for d in self.get("items"):
- if (d.transfer_qty and (d.bom_no or d.t_warehouse)
- and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)):
+ # Get raw materials cost from BOM if multiple material consumption entries
+ if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
+ bom_items = self.get_bom_raw_materials(finished_item_qty)
+ outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
- if (self.work_order and self.purpose == "Manufacture"
- and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")):
- bom_items = self.get_bom_raw_materials(d.transfer_qty)
- raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
-
- if raw_material_cost and self.purpose == "Manufacture":
- d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate"))
- d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
- elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
- d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
- d.basic_amount = d.basic_rate * flt(d.qty)
+ return flt(outgoing_items_cost - scrap_items_cost)
def distribute_additional_costs(self):
- if self.purpose == "Material Issue":
+ # If no incoming items, set additional costs blank
+ if not any([d.item_code for d in self.items if d.t_warehouse]):
self.additional_costs = []
self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")])
- total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
- for d in self.get("items"):
- if d.t_warehouse and total_basic_amount:
- d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs
- else:
- d.additional_cost = 0
+ if self.purpose in ("Repack", "Manufacture"):
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
+ else:
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+
+ if incoming_items_cost:
+ for d in self.get("items"):
+ if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
+ d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
+ else:
+ d.additional_cost = 0
def update_valuation_rate(self):
for d in self.get("items"):
@@ -638,71 +633,115 @@ class StockEntry(StockController):
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
+ def mark_finished_and_scrap_items(self):
+ if self.purpose in ("Repack", "Manufacture"):
+ if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+ return
+
+ finished_item = self.get_finished_item()
+
+ for d in self.items:
+ if d.t_warehouse and not d.s_warehouse:
+ if self.purpose=="Repack" or d.item_code == finished_item:
+ d.is_finished_item = 1
+ else:
+ d.is_scrap_item = 1
+ else:
+ d.is_finished_item = 0
+ d.is_scrap_item = 0
+
+ def get_finished_item(self):
+ finished_item = None
+ if self.work_order:
+ finished_item = frappe.db.get_value("Work Order", self.work_order, "production_item")
+ elif self.bom_no:
+ finished_item = frappe.db.get_value("BOM", self.bom_no, "item")
+
+ return finished_item
+
def validate_finished_goods(self):
"""validation: finished good quantity should be same as manufacturing quantity"""
if not self.work_order: return
- items_with_target_warehouse = []
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
-
production_item, wo_qty = frappe.db.get_value("Work Order",
self.work_order, ["production_item", "qty"])
+ number_of_finished_items = 0
for d in self.get('items'):
- if (self.purpose != "Send to Subcontractor" and d.bom_no
- and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
- format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ if d.is_finished_item:
+ if d.item_code != production_item:
+ frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
+ .format(d.item_code, self.work_order))
+ elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
+ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
+ format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ number_of_finished_items += 1
- if self.work_order and self.purpose == "Manufacture" and d.t_warehouse:
- items_with_target_warehouse.append(d.item_code)
+ if number_of_finished_items > 1:
+ frappe.throw(_("Multiple items cannot be marked as finished item"))
+
+ if self.purpose == "Manufacture":
+ allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
+ "overproduction_percentage_for_work_order"))
- if self.work_order and self.purpose == "Manufacture":
allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
if self.fg_completed_qty > allowed_qty:
frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
.format(flt(self.fg_completed_qty), wo_qty))
- if production_item not in items_with_target_warehouse:
- frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
- .format(production_item))
-
def update_stock_ledger(self):
sl_entries = []
+ finished_item_row = self.get_finished_item_row()
- # make sl entries for source warehouse first, then do for target warehouse
- for d in self.get('items'):
- if cstr(d.s_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.s_warehouse),
- "actual_qty": -flt(d.transfer_qty),
- "incoming_rate": 0
- }))
-
- for d in self.get('items'):
- if cstr(d.t_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.t_warehouse),
- "actual_qty": flt(d.transfer_qty),
- "incoming_rate": flt(d.valuation_rate)
- }))
-
- # On cancellation, make stock ledger entry for
- # target warehouse first, to update serial no values properly
-
- # if cstr(d.s_warehouse) and self.docstatus == 2:
- # sl_entries.append(self.get_sl_entries(d, {
- # "warehouse": cstr(d.s_warehouse),
- # "actual_qty": -flt(d.transfer_qty),
- # "incoming_rate": 0
- # }))
+ # make sl entries for source warehouse first
+ self.get_sle_for_source_warehouse(sl_entries, finished_item_row)
+ # SLE for target warehouse
+ self.get_sle_for_target_warehouse(sl_entries, finished_item_row)
+
+ # reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
self.make_sl_entries(sl_entries)
+ def get_finished_item_row(self):
+ finished_item_row = None
+ if self.purpose in ("Manufacture", "Repack"):
+ for d in self.get('items'):
+ if d.is_finished_item:
+ finished_item_row = d
+
+ return finished_item_row
+
+ def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.s_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.s_warehouse),
+ "actual_qty": -flt(d.transfer_qty),
+ "incoming_rate": 0
+ })
+ if cstr(d.t_warehouse):
+ sle.dependant_sle_voucher_detail_no = d.name
+ elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse):
+ sle.dependant_sle_voucher_detail_no = finished_item_row.name
+
+ sl_entries.append(sle)
+
+ def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.t_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.t_warehouse),
+ "actual_qty": flt(d.transfer_qty),
+ "incoming_rate": flt(d.valuation_rate)
+ })
+ if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
+ sle.recalculate_rate = 1
+
+ sl_entries.append(sle)
+
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
@@ -747,7 +786,7 @@ class StockEntry(StockController):
"credit": -1 * amount # put it as negative credit instead of debit purposefully
}, item=d))
- return gl_entries
+ return process_gl_map(gl_entries)
def update_work_order(self):
def _validate_work_order(pro_doc):
@@ -996,6 +1035,7 @@ class StockEntry(StockController):
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
+ "is_finished_item": 1
}
}, bom_no = self.bom_no)
@@ -1034,6 +1074,7 @@ class StockEntry(StockController):
for item in itervalues(item_dict):
item.from_warehouse = ""
+ item.is_scrap_item = 1
return item_dict
def get_unconsumed_raw_materials(self):
@@ -1246,6 +1287,8 @@ class StockEntry(StockController):
se_child.subcontracted_item = item_dict[d].get("main_item_code")
se_child.cost_center = (item_dict[d].get("cost_center") or
get_default_cost_center(item_dict[d], company = self.company))
+ se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
+ se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 9b6744ca3c0..1a641855aa2 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -6,7 +6,6 @@ import frappe, unittest
import frappe.defaults
from frappe.utils import flt, nowdate, nowtime
from erpnext.stock.doctype.serial_no.serial_no import *
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle
from frappe.permissions import add_user_permission, remove_user_permission
@@ -32,7 +31,6 @@ def get_sle(**args):
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
- set_perpetual_inventory(0)
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -213,7 +211,6 @@ class TestStockEntry(unittest.TestCase):
def test_repack_no_change_in_valuation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
@@ -235,8 +232,6 @@ class TestStockEntry(unittest.TestCase):
order by account desc""", repack.name, as_dict=1)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0, repack.company)
-
def test_repack_with_additional_costs(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@@ -474,7 +469,6 @@ class TestStockEntry(unittest.TestCase):
def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
- set_perpetual_inventory(0, company)
frappe.get_doc("User", "test2@example.com")\
.add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
frappe.set_user("test2@example.com")
@@ -500,7 +494,7 @@ class TestStockEntry(unittest.TestCase):
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
- set_perpetual_inventory(0, st1.company)
+
frappe.set_user("test@example.com")
st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert)
@@ -698,47 +692,54 @@ class TestStockEntry(unittest.TestCase):
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
- def test_material_consumption(self):
- from erpnext.manufacturing.doctype.work_order.work_order \
- import make_stock_entry as _make_stock_entry
- bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
- "is_default": 1, "docstatus": 1})
+ # def test_material_consumption(self):
+ # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
+ # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
- work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test FG Item 2",
- "bom_no": bom_no,
- "qty": 4.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC",
- "additional_operating_cost": 1000
- })
- work_order.insert()
- work_order.submit()
+ # from erpnext.manufacturing.doctype.work_order.work_order \
+ # import make_stock_entry as _make_stock_entry
+ # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
+ # "is_default": 1, "docstatus": 1})
- make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
- make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
+ # work_order = frappe.new_doc("Work Order")
+ # work_order.update({
+ # "company": "_Test Company",
+ # "fg_warehouse": "_Test Warehouse 1 - _TC",
+ # "production_item": "_Test FG Item 2",
+ # "bom_no": bom_no,
+ # "qty": 4.0,
+ # "stock_uom": "_Test UOM",
+ # "wip_warehouse": "_Test Warehouse - _TC",
+ # "additional_operating_cost": 1000,
+ # "use_multi_level_bom": 1
+ # })
+ # work_order.insert()
+ # work_order.submit()
- item_quantity = {
- '_Test Item': 10.0,
- '_Test Item 2': 12.0,
- '_Test Serialized Item With Series': 6.0
- }
+ # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
+ # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
- stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
- for d in stock_entry.get('items'):
- self.assertEqual(item_quantity.get(d.item_code), d.qty)
+ # item_quantity = {
+ # '_Test Item': 2.0,
+ # '_Test Item 2': 12.0,
+ # '_Test Serialized Item With Series': 6.0
+ # }
+
+ # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
+ # for d in stock_entry.get('items'):
+ # self.assertEqual(item_quantity.get(d.item_code), d.qty)
def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
+ se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt',
+ qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def test_gle_for_opening_stock_entry(self):
- mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
+ mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
+ company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
+ expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
self.assertRaises(OpeningEntryAccountError, mr.save)
@@ -759,8 +760,8 @@ class TestStockEntry(unittest.TestCase):
"company":"_Test Company with perpetual inventory",
"items":[
{
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
+ "item_code":"_Test Item",
+ "description":"_Test Item",
"qty": 1,
"basic_rate": 0,
"uom":"Nos",
@@ -769,8 +770,8 @@ class TestStockEntry(unittest.TestCase):
"cost_center": "Main - TCP1"
},
{
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
+ "item_code":"_Test Item",
+ "description":"_Test Item",
"qty": 2,
"basic_rate": 0,
"uom":"Nos",
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 79e8f9af8fc..6fe60298eeb 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -13,8 +13,10 @@
"t_warehouse",
"sec_break1",
"item_code",
- "col_break2",
"item_name",
+ "col_break2",
+ "is_finished_item",
+ "is_scrap_item",
"subcontracted_item",
"section_break_8",
"description",
@@ -22,35 +24,37 @@
"item_group",
"image",
"image_view",
- "quantity_and_rate",
- "set_basic_rate_manually",
+ "quantity_section",
"qty",
- "basic_rate",
- "basic_amount",
- "additional_cost",
- "amount",
- "valuation_rate",
- "col_break3",
- "uom",
- "conversion_factor",
- "stock_uom",
"transfer_qty",
"retain_sample",
+ "column_break_20",
+ "uom",
+ "stock_uom",
+ "conversion_factor",
"sample_quantity",
+ "rates_section",
+ "basic_rate",
+ "additional_cost",
+ "valuation_rate",
+ "allow_zero_valuation_rate",
+ "col_break3",
+ "set_basic_rate_manually",
+ "basic_amount",
+ "amount",
"serial_no_batch",
"serial_no",
"col_break4",
"batch_no",
- "quality_inspection",
"accounting",
"expense_account",
- "col_break5",
"accounting_dimensions_section",
"cost_center",
+ "project",
"dimension_col_break",
"more_info",
- "allow_zero_valuation_rate",
"actual_qty",
+ "transferred_qty",
"bom_no",
"allow_alternative_item",
"col_break6",
@@ -62,9 +66,8 @@
"ste_detail",
"po_detail",
"column_break_51",
- "transferred_qty",
"reference_purchase_receipt",
- "project"
+ "quality_inspection"
],
"fields": [
{
@@ -159,11 +162,6 @@
"options": "image",
"print_hide": 1
},
- {
- "fieldname": "quantity_and_rate",
- "fieldtype": "Section Break",
- "label": "Quantity and Rate"
- },
{
"bold": 1,
"fieldname": "qty",
@@ -321,10 +319,6 @@
"options": "Account",
"print_hide": 1
},
- {
- "fieldname": "col_break5",
- "fieldtype": "Column Break"
- },
{
"default": ":Company",
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
@@ -335,6 +329,7 @@
"print_hide": 1
},
{
+ "collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information"
@@ -456,6 +451,7 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
@@ -498,6 +494,32 @@
"fieldname": "set_basic_rate_manually",
"fieldtype": "Check",
"label": "Set Basic Rate Manually"
+ },
+ {
+ "fieldname": "quantity_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rates_section",
+ "fieldtype": "Section Break",
+ "label": "Rates"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_scrap_item",
+ "fieldtype": "Check",
+ "label": "Is Scrap Item"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_finished_item",
+ "fieldtype": "Check",
+ "label": "Is Finished Item"
}
],
"idx": 1,
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index fda17e08ab3..2463a21ed61 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -8,26 +8,33 @@
"engine": "InnoDB",
"field_order": [
"item_code",
- "serial_no",
- "batch_no",
"warehouse",
"posting_date",
"posting_time",
+ "column_break_6",
"voucher_type",
"voucher_no",
"voucher_detail_no",
+ "dependant_sle_voucher_detail_no",
+ "recalculate_rate",
+ "section_break_11",
"actual_qty",
+ "qty_after_transaction",
"incoming_rate",
"outgoing_rate",
- "stock_uom",
- "qty_after_transaction",
+ "column_break_17",
"valuation_rate",
"stock_value",
"stock_value_difference",
"stock_queue",
- "project",
+ "section_break_21",
"company",
+ "stock_uom",
+ "project",
+ "batch_no",
+ "column_break_26",
"fiscal_year",
+ "serial_no",
"is_cancelled",
"to_rename"
],
@@ -50,7 +57,6 @@
{
"fieldname": "serial_no",
"fieldtype": "Long Text",
- "in_list_view": 1,
"label": "Serial No",
"print_width": "100px",
"read_only": 1,
@@ -59,7 +65,6 @@
{
"fieldname": "batch_no",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Data",
@@ -119,6 +124,7 @@
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_filter": 1,
+ "in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"oldfieldname": "voucher_no",
@@ -142,6 +148,7 @@
"fieldname": "actual_qty",
"fieldtype": "Float",
"in_filter": 1,
+ "in_list_view": 1,
"label": "Actual Quantity",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
@@ -152,6 +159,7 @@
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
+ "in_list_view": 1,
"label": "Incoming Rate",
"oldfieldname": "incoming_rate",
"oldfieldtype": "Currency",
@@ -217,13 +225,11 @@
{
"fieldname": "stock_queue",
"fieldtype": "Text",
- "hidden": 1,
"label": "Stock Queue (FIFO)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
"print_hide": 1,
- "read_only": 1,
- "report_hide": 1
+ "read_only": 1
},
{
"fieldname": "project",
@@ -269,14 +275,48 @@
"hidden": 1,
"label": "To Rename",
"search_index": 1
+ },
+ {
+ "fieldname": "dependant_sle_voucher_detail_no",
+ "fieldtype": "Data",
+ "label": "Dependant SLE Voucher Detail No"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "recalculate_rate",
+ "fieldtype": "Check",
+ "label": "Recalculate Incoming/Outgoing Rate",
+ "no_copy": 1,
+ "read_only": 1
}
],
"hide_toolbar": 1,
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-04-23 05:57:03.985520",
+ "modified": "2020-09-07 11:10:35.318872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index bb356f694a4..a5c303ccb4d 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -10,8 +10,10 @@ from frappe.model.document import Document
from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
from erpnext.accounts.utils import get_fiscal_year
+from frappe.core.doctype.role.role import get_users
class StockFreezeError(frappe.ValidationError): pass
+class BackDatedStockTransaction(frappe.ValidationError): pass
exclude_from_linked_with = True
@@ -34,7 +36,6 @@ class StockLedgerEntry(Document):
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
- self.validate_future_posting()
def on_submit(self):
self.check_stock_frozen_date()
@@ -48,7 +49,7 @@ class StockLedgerEntry(Document):
def calculate_batch_qty(self):
if self.batch_no:
batch_qty = frappe.db.get_value("Stock Ledger Entry",
- {"docstatus": 1, "batch_no": self.batch_no},
+ {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
@@ -88,14 +89,14 @@ class StockLedgerEntry(Document):
# check if batch number is required
if self.voucher_type != 'Stock Reconciliation':
- if item_det.has_batch_no ==1:
+ if item_det.has_batch_no == 1:
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
if not self.batch_no:
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
- elif item_det.has_batch_no ==0 and self.batch_no:
+ elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
if item_det.has_variants:
@@ -142,28 +143,28 @@ class StockLedgerEntry(Document):
is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self):
- last_transaction_time = frappe.db.sql("""
- select MAX(timestamp(posting_date, posting_time)) as posting_time
- from `tabStock Ledger Entry`
- where docstatus = 1 and item_code = %s
- and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
+ authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
+ if authorized_role:
+ authorized_users = get_users(authorized_role)
+ if authorized_users and frappe.session.user not in authorized_users:
+ last_transaction_time = frappe.db.sql("""
+ select MAX(timestamp(posting_date, posting_time)) as posting_time
+ from `tabStock Ledger Entry`
+ where docstatus = 1 and item_code = %s
+ and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
- cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
+ cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
- if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
- msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
- frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
+ if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
+ msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
+ frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
- msg += "
" + _("Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.").format(
- frappe.bold(self.item_code), frappe.bold(self.warehouse))
+ msg += "
" + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format(
+ frappe.bold(self.item_code), frappe.bold(self.warehouse))
- msg += "
" + _("Please remove this item and try to submit again or update the posting time.")
- frappe.throw(msg, title=_("Backdated Stock Entry"))
-
- def validate_future_posting(self):
- if date_diff(self.posting_date, getdate()) > 0:
- msg = _("Posting future stock transactions are not allowed due to Immutable Ledger")
- frappe.throw(msg, title=_("Future Posting Not Allowed"))
+ msg += "
" + _("Please contact any of the following users to {} this transaction.")
+ msg += " " + " ".join(authorized_users)
+ frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
def on_doctype_update():
if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 04dae83447b..59f1f3961b6 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -5,8 +5,397 @@ from __future__ import unicode_literals
import frappe
import unittest
-
-# test_records = frappe.get_test_records('Stock Ledger Entry')
+from frappe.utils import today, add_days
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
+ import create_stock_reconciliation
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
class TestStockLedgerEntry(unittest.TestCase):
- pass
+ def setUp(self):
+ items = create_items()
+
+ # delete SLE and BINs for all items
+ frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+ frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+
+ def test_item_cost_reposting(self):
+ company = "_Test Company"
+
+ # _Test Item for Reposting at Stores warehouse on 10-04-2020: Qty = 50, Rate = 100
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=100,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-10',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Finished Goods - _TC",
+ qty=10,
+ rate=200,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-20',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
+ make_stock_entry(
+ item_code="_Test Item for Reposting",
+ source="Stores - _TC",
+ target="Finished Goods - _TC",
+ company=company,
+ qty=10,
+ expense_account="Stock Adjustment - _TC",
+ posting_date='2020-04-30',
+ posting_time='14:00'
+ )
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
+
+ # Repack entry on 5-5-2020
+ repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
+
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
+
+ # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=150,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-12',
+ posting_time='14:00'
+ )
+
+
+ # Check valuation rate of finished goods warehouse after back-dated entry at Stores
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(target_wh_sle.get("incoming_rate"), 150)
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
+
+ # Check valuation rate of repacked item after back-dated entry at Stores
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
+
+ # Check updated rate in Repack entry
+ repack.reload()
+ self.assertEqual(repack.items[0].get("basic_rate"), 150)
+ self.assertEqual(repack.items[1].get("basic_rate"), 750)
+
+ def test_purchase_return_valuation_reposting(self):
+ pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
+
+ return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
+
+ # check sle
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 100)
+ self.assertEqual(stock_value_difference, -200)
+
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 110)
+ self.assertEqual(stock_value_difference, -220)
+
+ def test_sales_return_valuation_reposting(self):
+ company = "_Test Company"
+ item_code="_Test Item for Reposting"
+
+ # Purchase Return: Qty = 5, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+
+ self.assertEqual(dn.items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 200)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+ self.assertEqual(outgoing_rate, 110)
+
+ dn.reload()
+ self.assertEqual(dn.items[0].incoming_rate, 110)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 110)
+ self.assertEqual(stock_value_difference, 220)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.items[0].incoming_rate, 110)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_reposting_of_sales_return_for_packed_item(self):
+ company = "_Test Company"
+ packed_item_code="_Test Item for Reposting"
+ bundled_item = "_Test Bundled Item for Reposting"
+ create_product_bundle_item(bundled_item, [[packed_item_code, 4]])
+
+ # Purchase Return: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+
+ self.assertEqual(dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 800)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+ self.assertEqual(outgoing_rate, 101)
+
+ dn.reload()
+ self.assertEqual(dn.packed_items[0].incoming_rate, 101)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 101)
+ self.assertEqual(stock_value_difference, 808)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 101)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_sub_contracted_item_costing(self):
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ company = "_Test Company"
+ rm_item_code="_Test Item for Reposting"
+ subcontracted_item = "_Test Subcontracted Item for Reposting"
+
+ frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
+ make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
+
+ # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
+
+ # Purchase Receipt for subcontracted item
+ pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20',
+ warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC",
+ item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes")
+
+ self.assertEqual(pr1.items[0].valuation_rate, 120)
+
+ # Update raw material's valuation via LCV, Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ pr1.reload()
+ self.assertEqual(pr1.items[0].valuation_rate, 125)
+
+ # check outgoing_rate for DN after reposting
+ incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate")
+ self.assertEqual(incoming_rate, 125)
+
+ # cleanup data
+ pr1.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_back_dated_entry_not_allowed(self):
+ # Back dated stock transactions are only allowed to stock managers
+ frappe.db.set_value("Stock Settings", None,
+ "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
+
+ # Set User with Stock User role but not Stock Manager
+ frappe.set_user("test@example.com")
+ user = frappe.get_doc("User", "test@example.com")
+ user.add_roles("Stock User")
+ user.remove_roles("Stock Manager")
+
+ stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1), do_not_submit=True)
+
+ # Block back-dated entry
+ self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+
+ user.add_roles("Stock Manager")
+
+ # Back dated entry allowed to Stock Manager
+ back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1))
+
+ back_dated_se_2.cancel()
+ stock_entry_on_today.cancel()
+
+ frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.set_user("Administrator")
+
+
+def create_repack_entry(**args):
+ args = frappe._dict(args)
+ repack = frappe.new_doc("Stock Entry")
+ repack.stock_entry_type = "Repack"
+ repack.company = args.company or "_Test Company"
+ repack.posting_date = args.posting_date
+ repack.set_posting_time = 1
+ repack.append("items", {
+ "item_code": "_Test Item for Reposting",
+ "s_warehouse": "Stores - _TC",
+ "qty": 5,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("items", {
+ "item_code": "_Test Finished Item for Reposting",
+ "t_warehouse": "Finished Goods - _TC",
+ "qty": 1,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("additional_costs", {
+ "expense_account": "Freight and Forwarding Charges - _TC",
+ "description": "transport cost",
+ "amount": 40
+ })
+
+ repack.save()
+ repack.submit()
+
+ return repack
+
+def create_product_bundle_item(new_item_code, packed_items):
+ if not frappe.db.exists("Product Bundle", new_item_code):
+ item = frappe.new_doc("Product Bundle")
+ item.new_item_code = new_item_code
+
+ for d in packed_items:
+ item.append("items", {
+ "item_code": d[0],
+ "qty": d[1]
+ })
+
+ item.save()
+
+def create_items():
+ items = ["_Test Item for Reposting", "_Test Finished Item for Reposting",
+ "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"]
+ for d in items:
+ properties = {"valuation_method": "FIFO"}
+ if d == "_Test Bundled Item for Reposting":
+ properties.update({"is_stock_item": 0})
+ elif d == "_Test Subcontracted Item for Reposting":
+ properties.update({"is_sub_contracted_item": 1})
+
+ make_item(d, properties=properties)
+
+ return items
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 00b8f69c083..5b40292ea8f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -37,14 +37,16 @@ class StockReconciliation(StockController):
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 23d48d4ac76..088456f8651 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -8,12 +8,11 @@ from __future__ import unicode_literals
import frappe, unittest
from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on
+from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase):
@@ -29,16 +28,17 @@ class TestStockReconciliation(unittest.TestCase):
self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method):
- insert_existing_sle(warehouse='Stores - TCP1')
+ se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1')
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
# [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
+
input_data = [
- [50, 1000],
- [25, 900],
- ["", 1000],
- [20, ""],
- [0, ""]
+ [50, 1000, "2012-12-26", "12:00"],
+ [25, 900, "2012-12-26", "12:00"],
+ ["", 1000, "2012-12-20", "12:05"],
+ [20, "", "2012-12-26", "12:05"],
+ [0, "", "2012-12-31", "12:10"]
]
for d in input_data:
@@ -47,13 +47,13 @@ class TestStockReconciliation(unittest.TestCase):
last_sle = get_previous_sle({
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
- "posting_date": nowdate(),
- "posting_time": nowtime()
+ "posting_date": d[2],
+ "posting_time": d[3]
})
# submit stock reconciliation
stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1],
- posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1",
+ posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1",
company=company, expense_account = "Stock Adjustment - TCP1")
# check stock value
@@ -81,10 +81,15 @@ class TestStockReconciliation(unittest.TestCase):
stock_reco.cancel()
+ se3.cancel()
+ se2.cancel()
+ se1.cancel()
+
def test_get_items(self):
- create_warehouse("_Test Warehouse Group 1", {"is_group": 1})
+ create_warehouse("_Test Warehouse Group 1",
+ {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
create_warehouse("_Test Warehouse Ledger 1",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"})
+ {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
@@ -95,8 +100,6 @@ class TestStockReconciliation(unittest.TestCase):
[items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
def test_stock_reco_for_serialized_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -148,8 +151,6 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc.cancel()
def test_stock_reco_for_batch_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -196,15 +197,17 @@ class TestStockReconciliation(unittest.TestCase):
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item",
target=warehouse, qty=10, basic_rate=700)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item",
source=warehouse, qty=15)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target=warehouse, qty=15, basic_rate=1200)
+ return se1, se2, se3
+
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
@@ -256,6 +259,10 @@ def create_stock_reconciliation(**args):
return sr
def set_valuation_method(item_code, valuation_method):
+ existing_valuation_method = get_valuation_method(item_code)
+ if valuation_method == existing_valuation_method:
+ return
+
frappe.db.set_value("Item", item_code, "valuation_method", valuation_method)
for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]):
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index a1666579d12..859aea2eb60 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -28,7 +28,9 @@
"inter_warehouse_transfer_settings_section",
"allow_from_dn",
"allow_from_pr",
- "freeze_stock_entries",
+ "control_historical_stock_transactions_section",
+ "role_allowed_to_create_edit_back_dated_transactions",
+ "column_break_26",
"stock_frozen_upto",
"stock_frozen_upto_days",
"stock_auth_role",
@@ -156,21 +158,20 @@
"label": "Notify by Email on Creation of Automatic Material Request"
},
{
- "fieldname": "freeze_stock_entries",
- "fieldtype": "Section Break",
- "label": "Freeze Stock Entries"
- },
- {
+ "description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto",
"fieldtype": "Date",
"label": "Stock Frozen Upto"
},
{
+ "description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days",
"fieldtype": "Int",
"label": "Freeze Stocks Older Than (Days)"
},
{
+ "depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)",
+ "description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",
"fieldname": "stock_auth_role",
"fieldtype": "Link",
"label": "Role Allowed to Edit Frozen Stock",
@@ -210,6 +211,22 @@
"fieldname": "allow_from_pr",
"fieldtype": "Check",
"label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice"
+ },
+ {
+ "description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",
+ "fieldname": "role_allowed_to_create_edit_back_dated_transactions",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Create/Edit Back-dated Transactions",
+ "options": "User"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "control_historical_stock_transactions_section",
+ "fieldtype": "Section Break",
+ "label": "Control Historical Stock Transactions"
}
],
"icon": "icon-cog",
@@ -217,7 +234,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-11-23 15:26:54.225608",
+ "modified": "2020-11-23 22:26:54.225608",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 3101e8af4c7..95478f61f0a 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -10,13 +10,10 @@ from frappe.test_runner import make_test_records
import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext import set_perpetual_inventory
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
-
test_records = frappe.get_test_records('Warehouse')
-
class TestWarehouse(unittest.TestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
@@ -37,63 +34,63 @@ class TestWarehouse(unittest.TestCase):
self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self):
- set_perpetual_inventory(1)
- create_warehouse("Test Warehouse for Renaming 1")
- account = get_inventory_account("_Test Company", "Test Warehouse for Renaming 1 - _TC")
+ create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
+ account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company")
+ if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
+ frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self):
- set_perpetual_inventory(1)
+ company = "_Test Company with perpetual inventory"
+ create_warehouse("Test Warehouse for Merging 1", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
+ create_warehouse("Test Warehouse for Merging 2", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 1")
- create_warehouse("Test Warehouse for Merging 2")
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - _TC",
- qty=1, rate=100)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - _TC",
- qty=1, rate=100)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
+ qty=1, rate=100, company=company)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
+ qty=1, rate=100, company=company)
existing_bin_qty = (
cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
)
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC",
- "Test Warehouse for Merging 2 - _TC", merge=True)
+ frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
+ "Test Warehouse for Merging 2 - TCP1", merge=True)
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC"))
+ self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - _TC"}))
+ filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def create_warehouse(warehouse_name, properties=None, company=None):
if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index cd86be31150..6c84f168fd4 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -29,7 +29,6 @@ class Warehouse(NestedSet):
self.set_onload('account', account)
load_address_and_contact(self)
-
def on_update(self):
self.update_nsm_model()
diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py
index 54eefdfaaa4..0cc8ca48aac 100644
--- a/erpnext/stock/report/stock_analytics/stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/stock_analytics.py
@@ -7,9 +7,11 @@ from frappe import _, scrub
from frappe.utils import getdate, flt
from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details)
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
columns = get_columns(filters)
data = get_data(filters)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ccd01001bb7..e5d4d626c47 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -7,12 +7,13 @@ from frappe import _
from frappe.utils import flt, cint, getdate, now, date_diff
from erpnext.stock.utils import add_additional_uom_columns
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
-
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 86af5e0c868..7b5701a9932 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -5,11 +5,12 @@ from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
columns = get_columns()
items = get_items(filters)
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index c8efb1637f9..1183e41d041 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, today
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
include_uom = filters.get("include_uom")
columns = get_columns()
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index ebcb106b02a..04f7d347ba8 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -11,9 +11,11 @@ from frappe.utils import flt, cint, getdate
from erpnext.stock.report.stock_balance.stock_balance import (get_item_details,
get_item_reorder_details, get_item_warehouse_map, get_items, get_stock_ledger_entries)
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index b5ae1b78eb4..8ba1f1ca5c7 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -6,6 +6,7 @@ import frappe
from frappe.utils import flt, cstr, nowdate, nowtime
from erpnext.stock.utils import update_bin
from erpnext.stock.stock_ledger import update_entries_after
+from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False):
"""
@@ -56,12 +57,18 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False,
update_bin_qty(item_code, warehouse, qty_dict)
def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False):
- update_entries_after({ "item_code": item_code, "warehouse": warehouse },
- allow_zero_rate=allow_zero_rate, allow_negative_stock=allow_negative_stock)
+ create_repost_item_valuation_entry({
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": "1900-01-01",
+ "posting_time": "00:01",
+ "allow_negative_stock": allow_negative_stock,
+ "allow_zero_rate": allow_zero_rate
+ })
def get_balance_qty_from_sle(item_code, warehouse):
balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry`
- where item_code=%s and warehouse=%s
+ where item_code=%s and warehouse=%s and is_cancelled=0
order by posting_date desc, posting_time desc, creation desc
limit 1""", (item_code, warehouse))
@@ -191,7 +198,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
print(d[0], d[1], d[2], serial_nos[0][0])
sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry`
- where item_code = %s and warehouse = %s
+ where item_code = %s and warehouse = %s and is_cancelled = 0
order by posting_date desc limit 1""", (d[0], d[1]))
sle_dict = {
@@ -223,7 +230,8 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
})
update_bin(args)
- update_entries_after({
+
+ create_repost_item_valuation_entry({
"item_code": d[0],
"warehouse": d[1],
"posting_date": posting_date,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index f4490f1b01e..5b9ada0ee56 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import cint, flt, cstr, now, now_datetime
+from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel
+from erpnext.stock.utils import get_bin
import json
-
from six import iteritems
# future reposting
@@ -25,32 +26,23 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
- sle_id = None
- if via_landed_cost_voucher or cancel:
- sle['posting_date'] = now_datetime().strftime('%Y-%m-%d')
- sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f')
+ if cancel:
+ sle['actual_qty'] = -flt(sle.get('actual_qty'))
- if cancel:
- sle['actual_qty'] = -flt(sle.get('actual_qty'))
-
- if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
- sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['incoming_rate'] = 0.0
-
- if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
- sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['outgoing_rate'] = 0.0
+ if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
+ sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['incoming_rate'] = 0.0
+ if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
+ sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['outgoing_rate'] = 0.0
if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation":
- sle_id = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
-
- args = sle.copy()
- args.update({
- "sle_id": sle_id
- })
+ sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
+
+ args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
@@ -68,8 +60,36 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.insert()
sle.submit()
- return sle.name
+ return sle
+def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False):
+ if not args and voucher_type and voucher_no:
+ args = get_args_for_voucher(voucher_type, voucher_no)
+
+ distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args]
+
+ i = 0
+ while i < len(args):
+ obj = update_entries_after({
+ "item_code": args[i].item_code,
+ "warehouse": args[i].warehouse,
+ "posting_date": args[i].posting_date,
+ "posting_time": args[i].posting_time
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ for item_wh, new_sle in iteritems(obj.new_items):
+ if item_wh not in distinct_item_warehouses:
+ args.append(new_sle)
+
+ i += 1
+
+def get_args_for_voucher(voucher_type, voucher_no):
+ return frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
+ fields=["item_code", "warehouse", "posting_date", "posting_time"],
+ order_by="creation asc",
+ group_by="item_code, warehouse"
+ )
class update_entries_after(object):
"""
@@ -86,141 +106,299 @@ class update_entries_after(object):
}
"""
def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
- from frappe.model.meta import get_field_precision
-
- self.exceptions = []
+ self.exceptions = {}
self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
- self.allow_negative_stock = allow_negative_stock
self.via_landed_cost_voucher = via_landed_cost_voucher
- if not self.allow_negative_stock:
- self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings",
- "allow_negative_stock"))
+ self.allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- self.args = args
- for key, value in iteritems(args):
- setattr(self, key, value)
+ self.args = frappe._dict(args)
+ self.item_code = args.get("item_code")
+ if self.args.sle_id:
+ self.args['name'] = self.args.sle_id
- self.previous_sle = self.get_sle_before_datetime()
- self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict()
+ self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
+ self.get_precision()
+ self.valuation_method = get_valuation_method(self.item_code)
+ self.new_items = {}
+
+ self.data = frappe._dict()
+ self.initialize_previous_data(self.args)
+
+ self.build()
+
+ def get_precision(self):
+ company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
+ currency=company_base_currency)
+
+ def initialize_previous_data(self, args):
+ """
+ Get previous sl entries for current item for each related warehouse
+ and assigns into self.data dict
+
+ :Data Structure:
+
+ self.data = {
+ warehouse1: {
+ 'previus_sle': {},
+ 'qty_after_transaction': 10,
+ 'valuation_rate': 100,
+ 'stock_value': 1000,
+ 'prev_stock_value': 1000,
+ 'stock_queue': '[[10, 100]]',
+ 'stock_value_difference': 1000
+ }
+ }
+
+ """
+ self.data.setdefault(args.warehouse, frappe._dict())
+ warehouse_dict = self.data[args.warehouse]
+ previous_sle = self.get_sle_before_datetime(args)
+ warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
- setattr(self, key, flt(self.previous_sle.get(key)))
+ setattr(warehouse_dict, key, flt(previous_sle.get(key)))
- self.company = frappe.db.get_value("Warehouse", self.warehouse, "company")
- self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
- currency=frappe.get_cached_value('Company', self.company, "default_currency"))
+ warehouse_dict.update({
+ "prev_stock_value": previous_sle.stock_value or 0.0,
+ "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
+ "stock_value_difference": 0.0
+ })
- self.prev_stock_value = self.previous_sle.stock_value or 0.0
- self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]")
- self.valuation_method = get_valuation_method(self.item_code)
- self.stock_value_difference = 0.0
- self.build(args.get('sle_id'))
-
- def build(self, sle_id):
- if sle_id:
- sle = get_sle_by_id(sle_id)
- self.process_sle(sle)
+ def build(self):
+ if self.args.get("sle_id"):
+ self.process_sle_against_current_voucher()
else:
- # includes current entry!
- entries_to_fix = self.get_sle_after_datetime()
- for sle in entries_to_fix:
+ entries_to_fix = self.get_future_entries_to_fix()
+
+ i = 0
+ while i < len(entries_to_fix):
+ sle = entries_to_fix[i]
+ i += 1
+
self.process_sle(sle)
+ if sle.dependant_sle_voucher_detail_no:
+ self.get_dependent_entries_to_fix(entries_to_fix, sle)
+
if self.exceptions:
self.raise_exceptions()
self.update_bin()
- def update_bin(self):
- # update bin
- bin_name = frappe.db.get_value("Bin", {
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
+ def process_sle_against_current_voucher(self):
+ sl_entries = self.get_sle_against_current_voucher()
+ for sle in sl_entries:
+ self.process_sle(sle)
- if not bin_name:
- bin_doc = frappe.get_doc({
- "doctype": "Bin",
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
- bin_doc.insert(ignore_permissions=True)
- else:
- bin_doc = frappe.get_doc("Bin", bin_name)
+ def get_sle_against_current_voucher(self):
+ return frappe.db.sql("""
+ select
+ *, timestamp(posting_date, posting_time) as "timestamp"
+ from
+ `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_type = %(voucher_type)s
+ and voucher_no = %(voucher_no)s
+ order by
+ creation ASC
+ for update
+ """, self.args, as_dict=1)
- bin_doc.update({
- "valuation_rate": self.valuation_rate,
- "actual_qty": self.qty_after_transaction,
- "stock_value": self.stock_value
- })
- bin_doc.flags.via_stock_ledger_entry = True
+ def get_future_entries_to_fix(self):
+ # includes current entry!
+ args = self.data[self.args.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
+
+ return list(self.get_sle_after_datetime(args))
- bin_doc.save(ignore_permissions=True)
+ def get_dependent_entries_to_fix(self, entries_to_fix, sle):
+ dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
+ excluded_sle=sle.name)
+
+ if not dependant_sle:
+ return
+ elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
+ return
+ elif dependant_sle.item_code != self.item_code \
+ and (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items:
+ self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle
+ return
+
+ self.initialize_previous_data(dependant_sle)
+
+ args = self.data[dependant_sle.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
+ future_sle_for_dependant = list(self.get_sle_after_datetime(args))
+
+ entries_to_fix.extend(future_sle_for_dependant)
+ entries_to_fix = sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle):
+ # previous sle data for this warehouse
+ self.wh_data = self.data[sle.warehouse]
+
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
if not self.validate_negative_stock(sle):
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
return
+ # Get dynamic incoming/outgoing rate
+ self.get_dynamic_incoming_outgoing_rate(sle)
+
if sle.serial_no:
self.get_serialized_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
- self.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert
- self.valuation_rate = sle.valuation_rate
- self.qty_after_transaction = sle.qty_after_transaction
- self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.valuation_rate = sle.valuation_rate
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
self.get_fifo_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
# rounding as per precision
- self.stock_value = flt(self.stock_value, self.precision)
-
- stock_value_difference = self.stock_value - self.prev_stock_value
-
- self.prev_stock_value = self.stock_value
+ self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+ stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
+ self.wh_data.prev_stock_value = self.wh_data.stock_value
# update current sle
- sle.qty_after_transaction = self.qty_after_transaction
- sle.valuation_rate = self.valuation_rate
- sle.stock_value = self.stock_value
- sle.stock_queue = json.dumps(self.stock_queue)
+ sle.qty_after_transaction = self.wh_data.qty_after_transaction
+ sle.valuation_rate = self.wh_data.valuation_rate
+ sle.stock_value = self.wh_data.stock_value
+ sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
+ self.update_outgoing_rate_on_transaction(sle)
+
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
- diff = self.qty_after_transaction + flt(sle.actual_qty)
+ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
exc = sle.copy().update({"diff": diff})
- self.exceptions.append(exc)
+ self.exceptions.setdefault(sle.warehouse, []).append(exc)
return False
else:
return True
+ def get_dynamic_incoming_outgoing_rate(self, sle):
+ # Get updated incoming/outgoing rate from transaction
+ if sle.recalculate_rate:
+ rate = self.get_incoming_outgoing_rate_from_transaction(sle)
+
+ if flt(sle.actual_qty) >= 0:
+ sle.incoming_rate = rate
+ else:
+ sle.outgoing_rate = rate
+
+ def get_incoming_outgoing_rate_from_transaction(self, sle):
+ rate = 0
+ # Material Transfer, Repack, Manufacturing
+ if sle.voucher_type == "Stock Entry":
+ rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
+ # Sales and Purchase Return
+ elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
+ if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
+ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return # don't move this import to top
+ rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no)
+ else:
+ if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ rate_field = "valuation_rate"
+ else:
+ rate_field = "incoming_rate"
+
+ # check in item table
+ item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
+ sle.voucher_detail_no, ["item_code", rate_field])
+
+ if item_code == sle.item_code:
+ rate = incoming_rate
+ else:
+ if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ ref_doctype = "Packed Item"
+ else:
+ ref_doctype = "Purchase Receipt Item Supplied"
+
+ rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
+ "item_code": sle.item_code}, rate_field)
+
+ return rate
+
+ def update_outgoing_rate_on_transaction(self, sle):
+ """
+ Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
+ In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
+ """
+ if sle.actual_qty and sle.voucher_detail_no:
+ outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
+
+ if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
+ self.update_rate_on_stock_entry(sle, outgoing_rate)
+ elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
+ elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ self.update_rate_on_purchase_receipt(sle, outgoing_rate)
+
+ def update_rate_on_stock_entry(self, sle, outgoing_rate):
+ frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
+
+ # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
+ stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no)
+ stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
+ stock_entry.db_update()
+ for d in stock_entry.items:
+ d.db_update()
+
+ def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
+ # Update item's incoming rate on transaction
+ item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
+ if item_code == sle.item_code:
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
+ else:
+ # packed item
+ frappe.db.set_value("Packed Item",
+ {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
+ "incoming_rate", outgoing_rate)
+
+ def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
+ if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
+ else:
+ frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
+
+ # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
+ if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
+ doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no)
+ doc.update_valuation_rate(reset_outgoing_rate=False)
+ for d in (doc.items + doc.supplied_items):
+ d.db_update()
+
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
@@ -228,7 +406,7 @@ class update_entries_after(object):
if incoming_rate < 0:
# wrong incoming rate
- incoming_rate = self.valuation_rate
+ incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
if incoming_rate:
@@ -236,22 +414,25 @@ class update_entries_after(object):
elif actual_qty < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
- outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
- stock_value_change = -1 * outgoing_value
+ if not sle.is_cancelled:
+ outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
+ stock_value_change = -1 * outgoing_value
+ else:
+ stock_value_change = actual_qty * sle.outgoing_rate
- new_stock_qty = self.qty_after_transaction + actual_qty
+ new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
if new_stock_qty > 0:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + stock_value_change
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
if new_stock_value >= 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -287,39 +468,39 @@ class update_entries_after(object):
def get_moving_average_values(self, sle):
actual_qty = flt(sle.actual_qty)
- new_stock_qty = flt(self.qty_after_transaction) + actual_qty
+ new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
if new_stock_qty >= 0:
if actual_qty > 0:
- if flt(self.qty_after_transaction) <= 0:
- self.valuation_rate = sle.incoming_rate
+ if flt(self.wh_data.qty_after_transaction) <= 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
else:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.incoming_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
elif sle.outgoing_rate:
if new_stock_qty:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.outgoing_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
else:
- self.valuation_rate = sle.outgoing_rate
+ self.wh_data.valuation_rate = sle.outgoing_rate
else:
- if flt(self.qty_after_transaction) >= 0 and sle.outgoing_rate:
- self.valuation_rate = sle.outgoing_rate
+ if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
+ self.wh_data.valuation_rate = sle.outgoing_rate
- if not self.valuation_rate and actual_qty > 0:
- self.valuation_rate = sle.incoming_rate
+ if not self.wh_data.valuation_rate and actual_qty > 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
# Get valuation rate from previous SLE or Item master, if item does not have the
# allow zero valuration rate flag set
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -329,22 +510,22 @@ class update_entries_after(object):
outgoing_rate = flt(sle.outgoing_rate)
if actual_qty > 0:
- if not self.stock_queue:
- self.stock_queue.append([0, 0])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, 0])
# last row has the same rate, just updated the qty
- if self.stock_queue[-1][1]==incoming_rate:
- self.stock_queue[-1][0] += actual_qty
+ if self.wh_data.stock_queue[-1][1]==incoming_rate:
+ self.wh_data.stock_queue[-1][0] += actual_qty
else:
- if self.stock_queue[-1][0] > 0:
- self.stock_queue.append([actual_qty, incoming_rate])
+ if self.wh_data.stock_queue[-1][0] > 0:
+ self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
- qty = self.stock_queue[-1][0] + actual_qty
- self.stock_queue[-1] = [qty, incoming_rate]
+ qty = self.wh_data.stock_queue[-1][0] + actual_qty
+ self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:
- if not self.stock_queue:
+ if not self.wh_data.stock_queue:
# Get valuation rate from last sle if exists or from valuation rate field in item master
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
@@ -354,35 +535,35 @@ class update_entries_after(object):
else:
_rate = 0
- self.stock_queue.append([0, _rate])
+ self.wh_data.stock_queue.append([0, _rate])
index = None
if outgoing_rate > 0:
# Find the entry where rate matched with outgoing rate
- for i, v in enumerate(self.stock_queue):
+ for i, v in enumerate(self.wh_data.stock_queue):
if v[1] == outgoing_rate:
index = i
break
# If no entry found with outgoing rate, collapse stack
if index == None:
- new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate
- new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop
- self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
+ new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
+ new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
+ self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
break
else:
index = 0
# select first batch or the batch with same rate
- batch = self.stock_queue[index]
+ batch = self.wh_data.stock_queue[index]
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
- self.stock_queue.pop(index)
- if not self.stock_queue and qty_to_pop:
+ self.wh_data.stock_queue.pop(index)
+ if not self.wh_data.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
- self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
+ self.wh_data.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
break
else:
@@ -391,14 +572,14 @@ class update_entries_after(object):
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
- stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
- stock_qty = sum((flt(batch[0]) for batch in self.stock_queue))
+ stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
+ stock_qty = sum((flt(batch[0]) for batch in self.wh_data.stock_queue))
if stock_qty:
- self.valuation_rate = stock_value / flt(stock_qty)
+ self.wh_data.valuation_rate = stock_value / flt(stock_qty)
- if not self.stock_queue:
- self.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.valuation_rate])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -413,39 +594,56 @@ class update_entries_after(object):
else:
return 0
- def get_sle_before_datetime(self):
+ def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
- if self.args.get('sle_id'):
- self.args['name'] = self.args.get('sle_id')
+ sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
+ sle = sle[0] if sle else frappe._dict()
+ return sle
- return get_stock_ledger_entries(self.args, "<=", "desc", "limit 1", for_update=False)
-
- def get_sle_after_datetime(self):
+ def get_sle_after_datetime(self, args):
"""get Stock Ledger Entries after a particular datetime, for reposting"""
- return get_stock_ledger_entries(self.previous_sle or frappe._dict({
- "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }),
- ">", "asc", for_update=True, check_serial_no=False)
+ return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
def raise_exceptions(self):
- deficiency = min(e["diff"] for e in self.exceptions)
+ msg_list = []
+ for warehouse, exceptions in iteritems(self.exceptions):
+ deficiency = min(e["diff"] for e in exceptions)
- if ((self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]) in
- frappe.local.flags.currently_saving):
+ if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
+ frappe.local.flags.currently_saving):
- msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse))
- else:
- msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse),
- self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"],
- frappe.get_desk_link(self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]))
+ msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', self.item_code),
+ frappe.get_desk_link('Warehouse', warehouse))
+ else:
+ msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', self.item_code),
+ frappe.get_desk_link('Warehouse', warehouse),
+ exceptions[0]["posting_date"], exceptions[0]["posting_time"],
+ frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
- if self.verbose:
- frappe.throw(msg, NegativeStockError, title='Insufficient Stock')
- else:
- raise NegativeStockError(msg)
+ if msg:
+ msg_list.append(msg)
+
+ if msg_list:
+ message = "\n\n".join(msg_list)
+ if self.verbose:
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ else:
+ raise NegativeStockError(message)
+
+ def update_bin(self):
+ # update bin for each warehouse
+ for warehouse, data in iteritems(self.data):
+ bin_doc = get_bin(self.item_code, warehouse)
+
+ bin_doc.update({
+ "valuation_rate": data.valuation_rate,
+ "actual_qty": data.qty_after_transaction,
+ "stock_value": data.stock_value
+ })
+ bin_doc.flags.via_stock_ledger_entry = True
+ bin_doc.save(ignore_permissions=True)
def get_previous_sle(args, for_update=False):
"""
@@ -489,6 +687,7 @@ def get_stock_ledger_entries(previous_sle, operator=None,
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
+ and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
%(limit)s %(for_update)s""" % {
@@ -498,10 +697,11 @@ def get_stock_ledger_entries(previous_sle, operator=None,
"order": order
}, previous_sle, as_dict=1, debug=debug)
-def get_sle_by_id(sle_id):
- return frappe.db.get_all('Stock Ledger Entry',
- fields=['*', 'timestamp(posting_date, posting_time) as timestamp'],
- filters={'name': sle_id})[0]
+def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
+ return frappe.db.get_value('Stock Ledger Entry',
+ {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
+ ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
+ as_dict=1)
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
@@ -529,7 +729,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
if last_valuation_rate:
- return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate
+ return flt(last_valuation_rate[0][0])
# If negative stock allowed, and item delivered without any incoming entry,
# system does not found any SLE, then take valuation rate from Item
@@ -561,3 +761,54 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
return valuation_rate
+
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
+def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
+ allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+
+ if args.actual_qty < 0 and not allow_negative_stock:
+ sle = get_future_sle_with_negative_qty(args)
+ if sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(sle[0]["qty_after_transaction"]),
+ frappe.get_desk_link('Item', args.item_code),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ sle[0]["posting_date"], sle[0]["posting_time"],
+ frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+def get_future_sle_with_negative_qty(args):
+ return frappe.db.sql("""
+ select
+ qty_after_transaction, posting_date, posting_time,
+ voucher_type, voucher_no
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ and is_cancelled = 0
+ and qty_after_transaction < 0
+ limit 1
+ """, args, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f9ac25443ea..4ea7e4fcd6e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -63,6 +63,7 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
SELECT item_code, stock_value, name, warehouse
FROM `tabStock Ledger Entry` sle
WHERE posting_date <= %s {0}
+ and is_cancelled = 0
ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
""".format(condition), values, as_dict=1)
@@ -211,7 +212,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
raise_error_if_no_rate=raise_error_if_no_rate)
- return in_rate
+ return flt(in_rate)
def get_avg_purchase_rate(serial_nos):
"""get average value of serial numbers"""
@@ -375,4 +376,10 @@ def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, v
outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0
- return outgoing_rate
\ No newline at end of file
+ return outgoing_rate
+
+def is_reposting_item_valuation_in_progress():
+ reposting_in_progress = frappe.db.exists("Repost Item Valuation",
+ {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+ if reposting_in_progress:
+ frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
\ No newline at end of file
From 9466e42e7095f7f4ff32230ab7dace6642455ba9 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Mon, 21 Dec 2020 20:52:20 +0530
Subject: [PATCH 204/286] fix: change request modifications
---
.../v13_0/update_project_template_tasks.py | 8 +-
erpnext/projects/doctype/project/project.py | 51 +++++------
.../projects/doctype/project/test_project.py | 87 ++++++++++---------
.../project_template/project_template.py | 5 +-
erpnext/projects/doctype/task/task.json | 4 +-
erpnext/projects/doctype/task/test_task.py | 2 +-
6 files changed, 82 insertions(+), 75 deletions(-)
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
index 1303efd93fb..26c42592816 100644
--- a/erpnext/patches/v13_0/update_project_template_tasks.py
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -6,7 +6,13 @@ import frappe
def execute():
frappe.reload_doc("projects", "doctype", "project_template")
- for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1):
+ for template_name in frappe.db.sql("""
+ select
+ name
+ from
+ `tabProject Template` """,
+ as_dict=1):
+
template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 13e72fec8a2..2cdfb7af444 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -59,8 +59,8 @@ class Project(Document):
for task in template.tasks:
template_task_details = frappe.get_doc("Task", task.task)
tmp_task_details.append(template_task_details)
- project_tasks.append(self.create_task_from_template(template_task_details))
-
+ task = self.create_task_from_template(template_task_details)
+ project_tasks.append(task)
self.dependency_mapping(tmp_task_details, project_tasks)
def create_task_from_template(self, task_details):
@@ -75,36 +75,33 @@ class Project(Document):
task_weight = task_details.task_weight,
type = task_details.type,
issue = task_details.issue,
- is_group = task_details.is_group,
- start = task_details.start,
- duration = task_details.duration
+ is_group = task_details.is_group
)).insert()
def dependency_mapping(self, template_tasks, project_tasks):
- for tmp_task in template_tasks:
- prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0]
- prj_task = frappe.get_doc("Task", prj_task.name)
- self.check_depends_on_value(tmp_task, prj_task, project_tasks)
- self.check_for_parent_tasks(tmp_task, prj_task, project_tasks)
+ for template_task in template_tasks:
+ project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
+ if template_task.get("depends_on") and not project_task.get("depends_on"):
+ self.check_depends_on_value(template_task, project_task, project_tasks)
+ if template_task.get("parent_task") and not project_task.get("parent_task"):
+ self.check_for_parent_tasks(template_task, project_task, project_tasks)
- def check_depends_on_value(self, tmp_task, prj_task, project_tasks):
- if tmp_task.get("depends_on") and not prj_task.get("depends_on"):
- for child_task in tmp_task.get("depends_on"):
- child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
- corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
- if len(corresponding_prj_task):
- prj_task.append("depends_on",{
- "task": corresponding_prj_task[0].name
- })
- prj_task.save()
+ def check_depends_on_value(self, template_task, project_task, project_tasks):
+ for child_task in template_task.get("depends_on"):
+ child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.append("depends_on",{
+ "task": corresponding_project_task[0].name
+ })
+ project_task.save()
- def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks):
- if tmp_task.get("parent_task") and not prj_task.get("parent_task"):
- parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject")
- corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
- if len(corresponding_prj_task):
- prj_task.parent_task = corresponding_prj_task[0].name
- prj_task.save()
+ def check_for_parent_tasks(self, template_task, project_task, project_tasks):
+ parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.parent_task = corresponding_project_task[0].name
+ project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index ce56a50b4e2..1d2980ce461 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -17,78 +17,79 @@ class TestProject(unittest.TestCase):
"""
Test Action: Basic Test of a Project created from template. The template has a single task.
"""
- frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"')
- frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks')
+ project_name = "Test Project with Template - No Parent and Dependend Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
- task1 = task_exists("Test Temp Task with no parent and dependency")
+ task1 = task_exists("Test Template Task with No Parent and Dependency")
if not task1:
- task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=5, duration=3)
+ task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
- template = make_project_template("Test Project Template - no parent and dependend tasks", [task1])
- project = get_project("Test Project with Templ - no parent and dependend tasks", template)
- tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+ template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
- self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]))
+ self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
self.assertEqual(len(tasks), 1)
def test_project_template_having_parent_child_tasks(self):
+ project_name = "Test Project with Template - Tasks with Parent-Child Relation"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
- frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"')
- frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child')
-
- task1 = task_exists("Test Temp Task parent")
+ task1 = task_exists("Test Template Task Parent")
if not task1:
- task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1)
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
- task2 = task_exists("Test Temp Task child 1")
+ task2 = task_exists("Test Template Task Child 1")
if not task2:
- task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
+ task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
- task3 = task_exists("Test Temp Task child 2")
+ task3 = task_exists("Test Template Task Child 2")
if not task3:
- task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
+ task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
- template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3])
- project = get_project("Test Project with Templ - tasks with parent-child", template)
- tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+ template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
- self.assertEqual(tasks[0].subject, 'Test Temp Task parent')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]))
+ self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
- self.assertEqual(tasks[1].subject, 'Test Temp Task child 1')
- self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]))
+ self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
self.assertEqual(tasks[1].parent_task, tasks[0].name)
- self.assertEqual(tasks[2].subject, 'Test Temp Task child 2')
- self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, tasks[2]))
+ self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
+ self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
self.assertEqual(tasks[2].parent_task, tasks[0].name)
self.assertEqual(len(tasks), 3)
def test_project_template_having_dependent_tasks(self):
+ project_name = "Test Project with Template - Dependent Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
- frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"')
- frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks')
-
- task1 = task_exists("Test Temp Task for dependency")
+ task1 = task_exists("Test Template Task for Dependency")
if not task1:
- task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1)
+ task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
- task2 = task_exists("Test Temp Task with dependency")
+ task2 = task_exists("Test Template Task with Dependency")
if not task2:
- task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
+ task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
- template = make_project_template("Test Project with Templ - dependent tasks", [task1, task2])
- project = get_project("Test Project with Templ - dependent tasks", template)
- tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+ template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
- self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency')
- self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]))
+ self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
- self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) )
+ self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
self.assertEqual(len(tasks), 2)
@@ -129,5 +130,5 @@ def task_exists(subject):
return False
return frappe.get_doc("Task", result[0].name)
-def calculate_end_date(project, task):
- return getdate(add_days(project.expected_start_date, task.start + task.duration))
\ No newline at end of file
+def calculate_end_date(project, start, duration):
+ return getdate(add_days(project.expected_start_date, start + duration))
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py
index 1beebf7a258..aace40240c4 100644
--- a/erpnext/projects/doctype/project_template/project_template.py
+++ b/erpnext/projects/doctype/project_template/project_template.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
+from frappe.utils import get_link_to_form
class ProjectTemplate(Document):
@@ -18,8 +19,8 @@ class ProjectTemplate(Document):
if task_details.depends_on:
for dependency_task in task_details.depends_on:
if not self.check_dependent_task_presence(dependency_task.task):
- task_details_format = """{0}""".format(task_details.name)
- dependency_task_format = """{0}""".format(dependency_task.task)
+ task_details_format = get_link_to_form("Task",task_details.name)
+ dependency_task_format = get_link_to_form("Task", dependency_task.task)
frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
def check_dependent_task_presence(self, task):
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index a9e3d9bc0fe..bb55256f7d9 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -371,11 +371,13 @@
"label": "Is Template"
},
{
+ "depends_on": "is_template",
"fieldname": "start",
"fieldtype": "Int",
"label": "Begin On (Days)"
},
{
+ "depends_on": "is_template",
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration (Days)"
@@ -386,7 +388,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
- "modified": "2020-12-07 13:26:53.614689",
+ "modified": "2020-12-21 11:59:24.196834",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index aded78b8574..25714f8cde3 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -104,7 +104,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa
task.subject = subject
task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate()
- task.project = project or "_Test Project"
+ task.project = project or None if is_template else "_Test Project"
task.is_template = is_template
task.start = begin
task.duration = duration
From c36cab81f229376cbdde96cf7cfe4ccbd33b6f36 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Mon, 21 Dec 2020 23:46:02 +0530
Subject: [PATCH 205/286] fix: Update year_to_date and month_to_date field
labels to show company currency
---
.../doctype/salary_slip/salary_slip.js | 6 +++---
.../doctype/salary_slip/salary_slip.json | 20 +++++++++++++++++--
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index f7e22c63879..56948717628 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", {
change_form_labels: function(frm, company_currency) {
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
- "base_net_pay", "base_rounded_total", "base_total_in_words"],
+ "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
company_currency);
- frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"],
+ frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"],
frm.doc.currency);
// toggle fields
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
- "base_net_pay", "base_rounded_total", "base_total_in_words"],
+ "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
frm.doc.currency != company_currency);
},
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index d981a39953d..43deee43aac 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -70,10 +70,12 @@
"net_pay",
"base_net_pay",
"year_to_date",
+ "base_year_to_date",
"column_break_53",
"rounded_total",
"base_rounded_total",
"month_to_date",
+ "base_month_to_date",
"section_break_55",
"total_in_words",
"column_break_69",
@@ -584,12 +586,26 @@
{
"fieldname": "year_to_date",
"fieldtype": "Currency",
+ "label": "Year To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "month_to_date",
+ "fieldtype": "Currency",
+ "label": "Month To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_year_to_date",
+ "fieldtype": "Currency",
"label": "Year To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
- "fieldname": "month_to_date",
+ "fieldname": "base_month_to_date",
"fieldtype": "Currency",
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
@@ -600,7 +616,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-18 23:57:41.042954",
+ "modified": "2020-12-21 23:43:44.959840",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
From 090783804bdc59f22b8a7afee43cb3ddabcd37b2 Mon Sep 17 00:00:00 2001
From: Ganga Manoj
Date: Mon, 21 Dec 2020 23:52:05 +0530
Subject: [PATCH 206/286] fix: Improve month_to_date computation
---
erpnext/payroll/doctype/salary_slip/salary_slip.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index e86a7fc3158..02e5f2d1d12 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext
import datetime, math
-from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate
+from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname
from frappe import msgprint, _
@@ -1150,8 +1150,7 @@ class SalarySlip(TransactionBase):
def compute_month_to_date(self):
month_to_date = 0
- date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d")
- first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year)
+ first_day_of_the_month = get_first_day(self.start_date)
salary_slips_from_this_month = frappe.get_list('Salary Slip',
fields = ['employee_name', 'start_date', 'net_pay'],
filters = {'employee_name' : self.employee_name,
From 3a26f26671e19df3acd7ef690300810e1d5026d3 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 22 Dec 2020 11:56:59 +0530
Subject: [PATCH 207/286] fix: get_doc to avoid modified error
---
erpnext/projects/doctype/project/project.py | 35 ++++++++++---------
.../projects/doctype/project/test_project.py | 2 +-
2 files changed, 19 insertions(+), 18 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 2cdfb7af444..97134602f86 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -81,27 +81,28 @@ class Project(Document):
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
- if template_task.get("depends_on") and not project_task.get("depends_on"):
- self.check_depends_on_value(template_task, project_task, project_tasks)
- if template_task.get("parent_task") and not project_task.get("parent_task"):
- self.check_for_parent_tasks(template_task, project_task, project_tasks)
+ project_task = frappe.get_doc("Task", project_task.name)
+ self.check_depends_on_value(template_task, project_task, project_tasks)
+ self.check_for_parent_tasks(template_task, project_task, project_tasks)
def check_depends_on_value(self, template_task, project_task, project_tasks):
- for child_task in template_task.get("depends_on"):
- child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
- corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
- if len(corresponding_project_task):
- project_task.append("depends_on",{
- "task": corresponding_project_task[0].name
- })
- project_task.save()
+ if template_task.get("depends_on") and not project_task.get("depends_on"):
+ for child_task in template_task.get("depends_on"):
+ child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.append("depends_on",{
+ "task": corresponding_project_task[0].name
+ })
+ project_task.save()
def check_for_parent_tasks(self, template_task, project_task, project_tasks):
- parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
- corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
- if len(corresponding_project_task):
- project_task.parent_task = corresponding_project_task[0].name
- project_task.save()
+ if template_task.get("parent_task") and not project_task.get("parent_task"):
+ parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.parent_task = corresponding_project_task[0].name
+ project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 1d2980ce461..d77b14ce33c 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -52,7 +52,7 @@ class TestProject(unittest.TestCase):
template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
project = get_project(project_name, template)
- tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
From 468f67a4de5e267d7518105541a4dddbdfdcf610 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Tue, 22 Dec 2020 12:44:09 +0530
Subject: [PATCH 208/286] fix: Add parent for all-products page
---
erpnext/www/all-products/index.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index 0394e4b2cc5..7d7793ac49b 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -15,6 +15,9 @@ def get_context(context):
context.items = get_products_for_website(field_filters, attribute_filters, search)
+ # Add homepage as parent
+ context.parents = [{"name": frappe._("Home"), "route":"/"}]
+
product_settings = get_product_settings()
context.field_filters = get_field_filter_data() \
if product_settings.enable_field_filters else []
From 6900a79421b141e9d86d7e111ba9eac06e7cf75d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 11:37:13 +0100
Subject: [PATCH 209/286] fix: fail silently
---
erpnext/regional/germany/accounts_controller.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
index 5b2b31f2043..0ab027b4d6e 100644
--- a/erpnext/regional/germany/accounts_controller.py
+++ b/erpnext/regional/germany/accounts_controller.py
@@ -37,7 +37,14 @@ def validate_regional(doc):
for field in required_fields:
condition = field.get("condition")
- if condition and not frappe.safe_eval(condition, doc.as_dict()):
+ condition_true = True
+ try:
+ condition_true = frappe.safe_eval(condition, doc.as_dict())
+ except:
+ # invalid condition should not result in an error
+ pass
+
+ if condition and not condition_true:
continue
field_name = field.get("field_name")
From 5adbe49ca65b9230531341e0d2d906670e39002e Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 11:37:43 +0100
Subject: [PATCH 210/286] refactor: translation syntax
---
erpnext/regional/germany/accounts_controller.py | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
index 0ab027b4d6e..63da96bddab 100644
--- a/erpnext/regional/germany/accounts_controller.py
+++ b/erpnext/regional/germany/accounts_controller.py
@@ -55,9 +55,6 @@ def validate_regional(doc):
def missing(field_label, regulation):
"""Notify the user that a required field is missing."""
- context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.'
- msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format(
- field_label=frappe.bold(_(field_label)),
- regulation=regulation
- )
- )
+ translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.')
+ formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
+ msgprint(formatted_msg)
From a69021018aea2b2e51f4cccb999dad97bcdc5752 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 11:38:09 +0100
Subject: [PATCH 211/286] test: add test for accounts controller
---
erpnext/regional/germany/test_accounts_controller.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 erpnext/regional/germany/test_accounts_controller.py
diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py
new file mode 100644
index 00000000000..63bb843d307
--- /dev/null
+++ b/erpnext/regional/germany/test_accounts_controller.py
@@ -0,0 +1,12 @@
+import frappe
+import unittest
+from erpnext.regional.germany.accounts_controller import validate_regional
+
+
+class TestAccountsController(unittest.TestCase):
+
+ def setUp(self):
+ self.sales_invoice = frappe.get_last_doc('Sales Invoice')
+
+ def test_validate_regional(self):
+ validate_regional(self.sales_invoice)
From 511be6466df429acde392aa458c9215cbde48238 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 11:43:33 +0100
Subject: [PATCH 212/286] Revert "fix: fail silently"
This reverts commit 6900a79421b141e9d86d7e111ba9eac06e7cf75d.
---
erpnext/regional/germany/accounts_controller.py | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
index 63da96bddab..b789960ca01 100644
--- a/erpnext/regional/germany/accounts_controller.py
+++ b/erpnext/regional/germany/accounts_controller.py
@@ -37,14 +37,7 @@ def validate_regional(doc):
for field in required_fields:
condition = field.get("condition")
- condition_true = True
- try:
- condition_true = frappe.safe_eval(condition, doc.as_dict())
- except:
- # invalid condition should not result in an error
- pass
-
- if condition and not condition_true:
+ if condition and not frappe.safe_eval(condition, doc.as_dict()):
continue
field_name = field.get("field_name")
From 4ebee5014eebcf49669ccabda45c971f3822c814 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 22 Dec 2020 18:14:46 +0530
Subject: [PATCH 213/286] feat: aholiday check before setting start and end
date in task
---
erpnext/projects/doctype/project/project.py | 20 +++++++++++++++++--
.../projects/doctype/project/test_project.py | 3 ---
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 97134602f86..f6bb6e9e745 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -13,6 +13,7 @@ from frappe.desk.reportview import get_match_cond
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
class Project(Document):
def get_feed(self):
@@ -69,8 +70,8 @@ class Project(Document):
subject = task_details.subject,
project = self.name,
status = 'Open',
- exp_start_date = add_days(self.expected_start_date, task_details.start),
- exp_end_date = add_days(self.expected_start_date, task_details.start + task_details.duration),
+ exp_start_date = self.calculate_start_date(task_details),
+ exp_end_date = self.calculate_end_date(task_details),
description = task_details.description,
task_weight = task_details.task_weight,
type = task_details.type,
@@ -78,6 +79,21 @@ class Project(Document):
is_group = task_details.is_group
)).insert()
+ def calculate_start_date(self, task_details):
+ self.start_date = add_days(self.expected_start_date, task_details.start)
+ self.start_date = self.update_if_holiday(self.start_date)
+ return self.start_date
+
+ def calculate_end_date(self, task_details):
+ self.end_date = add_days(self.start_date, task_details.duration)
+ return self.update_if_holiday(self.end_date)
+
+ def update_if_holiday(self, date):
+ holiday_list = self.holiday_list or get_holiday_list()
+ while is_holiday(holiday_list, date):
+ date = add_days(date, 1)
+ return date
+
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index d77b14ce33c..0faf97670d8 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -14,9 +14,6 @@ from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase):
def test_project_with_template_having_no_parent_and_depend_tasks(self):
- """
- Test Action: Basic Test of a Project created from template. The template has a single task.
- """
project_name = "Test Project with Template - No Parent and Dependend Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
From 6cf018c762ee4d67bfc83b9f6fc3814b51462734 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 22 Dec 2020 19:40:41 +0530
Subject: [PATCH 214/286] fix: holiday update in tests
---
erpnext/projects/doctype/project/project.py | 16 ++++++++--------
erpnext/projects/doctype/project/test_project.py | 8 ++++++--
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index f6bb6e9e745..60f85b0e7a6 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -81,18 +81,12 @@ class Project(Document):
def calculate_start_date(self, task_details):
self.start_date = add_days(self.expected_start_date, task_details.start)
- self.start_date = self.update_if_holiday(self.start_date)
+ self.start_date = update_if_holiday(self.holiday_list, self.start_date)
return self.start_date
def calculate_end_date(self, task_details):
self.end_date = add_days(self.start_date, task_details.duration)
- return self.update_if_holiday(self.end_date)
-
- def update_if_holiday(self, date):
- holiday_list = self.holiday_list or get_holiday_list()
- while is_holiday(holiday_list, date):
- date = add_days(date, 1)
- return date
+ return update_if_holiday(self.holiday_list, self.end_date)
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
@@ -547,3 +541,9 @@ def set_project_status(project, status):
project.status = status
project.save()
+
+def update_if_holiday(holiday_list, date):
+ holiday_list = holiday_list or get_holiday_list()
+ while is_holiday(holiday_list, date):
+ date = add_days(date, 1)
+ return date
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 0faf97670d8..af978e85fd5 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
-from erpnext.projects.doctype.project.project import set_project_status
+from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday
from erpnext.projects.doctype.task.test_task import create_task
from frappe.utils import getdate, nowdate, add_days
@@ -128,4 +128,8 @@ def task_exists(subject):
return frappe.get_doc("Task", result[0].name)
def calculate_end_date(project, start, duration):
- return getdate(add_days(project.expected_start_date, start + duration))
\ No newline at end of file
+ start = add_days(project.expected_start_date, start)
+ start = update_if_holiday(project.holiday_list, start)
+ end = add_days(start, duration)
+ end = update_if_holiday(project.holiday_list, end)
+ return getdate(end)
\ No newline at end of file
From 8dec1c142f96bb171c0e23c92ef9d3b1100cf6b6 Mon Sep 17 00:00:00 2001
From: pateljannat
Date: Tue, 22 Dec 2020 19:55:31 +0530
Subject: [PATCH 215/286] fix: removed unused imports
---
erpnext/projects/doctype/project/test_project.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index af978e85fd5..97b67b38eb3 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
-from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday
+from erpnext.projects.doctype.project.project import update_if_holiday
from erpnext.projects.doctype.task.test_task import create_task
from frappe.utils import getdate, nowdate, add_days
From 2acd8cbc02aca5904e35ece8dfb4b1608e23891e Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 17:34:22 +0100
Subject: [PATCH 216/286] fix: sider
---
erpnext/regional/germany/accounts_controller.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
index b789960ca01..7f76493608e 100644
--- a/erpnext/regional/germany/accounts_controller.py
+++ b/erpnext/regional/germany/accounts_controller.py
@@ -48,6 +48,6 @@ def validate_regional(doc):
def missing(field_label, regulation):
"""Notify the user that a required field is missing."""
- translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.')
+ translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
msgprint(formatted_msg)
From df8ea194064d5d7abcdf1a696324af55d679baa3 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 22 Dec 2020 17:34:31 +0100
Subject: [PATCH 217/286] fix: whitespace
---
erpnext/regional/germany/test_accounts_controller.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py
index 63bb843d307..8bd378c971f 100644
--- a/erpnext/regional/germany/test_accounts_controller.py
+++ b/erpnext/regional/germany/test_accounts_controller.py
@@ -7,6 +7,6 @@ class TestAccountsController(unittest.TestCase):
def setUp(self):
self.sales_invoice = frappe.get_last_doc('Sales Invoice')
-
+
def test_validate_regional(self):
validate_regional(self.sales_invoice)
From 1fb412e3f6b0099082601b6539b0ce62f0345438 Mon Sep 17 00:00:00 2001
From: Tim Gates
Date: Wed, 23 Dec 2020 11:39:37 +1100
Subject: [PATCH 218/286] docs: fix simple typo, udpate -> update
There is a small typo in erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py.
Should read `update` rather than `udpate`.
---
erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
index ad043dd99d3..97e217aa054 100644
--- a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
+++ b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
@@ -5,11 +5,11 @@ from __future__ import unicode_literals
import frappe
def execute():
- # udpate sales cycle
+ # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
- # udpate purchase cycle
+ # update purchase cycle
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)
From 09c6842199f90a11702b40a72d6bf7ccec22c08b Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Wed, 23 Dec 2020 11:29:26 +0530
Subject: [PATCH 219/286] fix: accounting entries of asset when submitting
purchase receipt
---
erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 97e0fa738cd..878dd588779 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -323,7 +323,7 @@ class PurchaseReceipt(BuyingController):
elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse)
- elif d.item_code not in stock_items and flt(d.qty) and auto_accounting_for_non_stock_items:
+ elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account)
From 0411a43c6e4c8b5940e7068be0893bea8f0b872b Mon Sep 17 00:00:00 2001
From: Nabin Hait
Date: Wed, 23 Dec 2020 12:14:41 +0530
Subject: [PATCH 220/286] fix: set finished good item rate based on qty
---
erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index afdb54ceaa2..579b8c5fe1d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -511,7 +511,7 @@ class StockEntry(StockController):
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
- return flt(outgoing_items_cost - scrap_items_cost)
+ return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
def distribute_additional_costs(self):
# If no incoming items, set additional costs blank
From d556847fca7ac82e7f11fc5d30de30ebb45ad63e Mon Sep 17 00:00:00 2001
From: Anurag Mishra
Date: Thu, 17 Dec 2020 15:17:25 +0530
Subject: [PATCH 221/286] fix: allow addition and removal of employee in
payroll Entry
---
.../payroll_employee_detail.json | 5 +--
.../doctype/payroll_entry/payroll_entry.js | 38 ++++++++++---------
.../doctype/payroll_entry/payroll_entry.json | 5 +--
3 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
index 8a55224dca7..09c7eb9a456 100644
--- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
+++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
@@ -17,8 +17,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
- "options": "Employee",
- "read_only": 1
+ "options": "Employee"
},
{
"fetch_from": "employee.employee_name",
@@ -52,7 +51,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-09-30 12:40:07.999878",
+ "modified": "2020-12-17 15:43:29.542977",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Employee Detail",
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index cb48abbc363..28236c04994 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -31,7 +31,7 @@ frappe.ui.form.on('Payroll Entry', {
refresh: function(frm) {
if (frm.doc.docstatus == 0) {
- if(!frm.is_new()) {
+ if (!frm.is_new()) {
frm.page.clear_primary_action();
frm.add_custom_button(__("Get Employees"),
function() {
@@ -61,33 +61,33 @@ frappe.ui.form.on('Payroll Entry', {
doc: frm.doc,
method: 'fill_employee_details',
}).then(r => {
- if (r.docs && r.docs[0].employees){
+ if (r.docs && r.docs[0].employees) {
frm.employees = r.docs[0].employees;
frm.dirty();
frm.save();
frm.refresh();
- if(r.docs[0].validate_attendance){
+ if (r.docs[0].validate_attendance) {
render_employee_attendance(frm, r.message);
}
}
- })
+ });
},
create_salary_slips: function(frm) {
frm.call({
doc: frm.doc,
method: "create_salary_slips",
- callback: function(r) {
+ callback: function() {
frm.refresh();
frm.toolbar.refresh();
}
- })
+ });
},
add_context_buttons: function(frm) {
- if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
+ if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm);
- } else if(frm.doc.salary_slips_created) {
+ } else if (frm.doc.salary_slips_created) {
frm.add_custom_button(__("Submit Salary Slip"), function() {
submit_salary_slip(frm);
}).addClass("btn-primary");
@@ -192,9 +192,9 @@ frappe.ui.form.on('Payroll Entry', {
},
start_date: function (frm) {
- if(!in_progress && frm.doc.start_date){
+ if (!in_progress && frm.doc.start_date) {
frm.trigger("set_end_date");
- }else{
+ } else {
// reset flag
in_progress = false;
}
@@ -228,7 +228,7 @@ frappe.ui.form.on('Payroll Entry', {
}
},
- set_end_date: function(frm){
+ set_end_date: function(frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: {
@@ -243,9 +243,9 @@ frappe.ui.form.on('Payroll Entry', {
});
},
- validate_attendance: function(frm){
- if(frm.doc.validate_attendance && frm.doc.employees){
- frappe.call({
+ validate_attendance: function(frm) {
+ if (frm.doc.validate_attendance && frm.doc.employees) {
+ frappe.call ({
method: 'validate_employee_attendance',
args: {},
callback: function(r) {
@@ -255,7 +255,7 @@ frappe.ui.form.on('Payroll Entry', {
freeze: true,
freeze_message: __('Validating Employee Attendance...')
});
- }else{
+ } else {
frm.fields_dict.attendance_detail_html.html("");
}
},
@@ -274,14 +274,16 @@ const submit_salary_slip = function (frm) {
frappe.call({
method: 'submit_salary_slips',
args: {},
- callback: function() {frm.events.refresh(frm);},
+ callback: function() {
+ frm.events.refresh(frm);
+ },
doc: frm.doc,
freeze: true,
freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
});
},
function() {
- if(frappe.dom.freeze_count) {
+ if (frappe.dom.freeze_count) {
frappe.dom.unfreeze();
frm.events.refresh(frm);
}
@@ -316,4 +318,4 @@ let render_employee_attendance = function(frm, data) {
data: data
})
);
-}
+};
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
index 7a48dd14758..0444134aa4d 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
@@ -129,8 +129,7 @@
"fieldname": "employees",
"fieldtype": "Table",
"label": "Employee Details",
- "options": "Payroll Employee Detail",
- "read_only": 1
+ "options": "Payroll Employee Detail"
},
{
"fieldname": "section_break_13",
@@ -290,7 +289,7 @@
"icon": "fa fa-cog",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-23 13:00:33.753228",
+ "modified": "2020-12-17 15:13:17.766210",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Entry",
From 1ab4f09ee9b597c7775d5d2a0fb2ad6732660686 Mon Sep 17 00:00:00 2001
From: Shivam Mishra
Date: Wed, 23 Dec 2020 15:18:41 +0530
Subject: [PATCH 222/286] fix: use file_url to save file and not file name
---
erpnext/hr/doctype/employee/employee.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index dfc600ca3c5..0fde3a12ac8 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -135,7 +135,7 @@ class Employee(NestedSet):
try:
frappe.get_doc({
"doctype": "File",
- "file_name": self.image,
+ "file_url": self.image,
"attached_to_doctype": "User",
"attached_to_name": self.user_id
}).insert()
From d2f91e8c1189185c49113e2db72ab0352bcac5d0 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 24 Dec 2020 11:03:36 +0530
Subject: [PATCH 223/286] fix: Do not cancel reference document on Quality
Inspection cancellation (#24197)
---
.../stock/doctype/quality_inspection/quality_inspection.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 376848afaa4..03e3de115b7 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,11 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", {
+ refresh: function(frm) {
+ // Ignore cancellation of reference doctype on cancel all.
+ frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
+ },
+
item_code: function(frm) {
if (frm.doc.item_code) {
return frm.call({
From 527a156512f773c092b13465f567e06258e708cf Mon Sep 17 00:00:00 2001
From: Anurag Mishra
Date: Wed, 23 Dec 2020 17:22:51 +0530
Subject: [PATCH 224/286] feat: validated employees whose salary has been
already precessed
---
.../doctype/payroll_entry/payroll_entry.js | 104 +++++++++++++-----
.../doctype/payroll_entry/payroll_entry.py | 25 ++++-
2 files changed, 97 insertions(+), 32 deletions(-)
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 28236c04994..2288a277917 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', {
}
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
- frm.set_query("department", function() {
+ frm.events.department_filters(frm);
+ frm.events.payroll_payable_account_filters(frm);
+ },
+
+ department_filters: function (frm) {
+ frm.set_query("department", function () {
return {
"filters": {
"company": frm.doc.company,
}
};
});
+ },
- frm.set_query("payroll_payable_account", function() {
+ payroll_payable_account_filters: function (frm) {
+ frm.set_query("payroll_payable_account", function () {
return {
filters: {
"company": frm.doc.company,
@@ -29,12 +36,12 @@ frappe.ui.form.on('Payroll Entry', {
});
},
- refresh: function(frm) {
+ refresh: function (frm) {
if (frm.doc.docstatus == 0) {
if (!frm.is_new()) {
frm.page.clear_primary_action();
frm.add_custom_button(__("Get Employees"),
- function() {
+ function () {
frm.events.get_employee_details(frm);
}
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
@@ -42,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', {
if ((frm.doc.employees || []).length) {
frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => {
- frm.save('Submit').then(()=>{
+ frm.save('Submit').then(() => {
frm.page.clear_primary_action();
frm.refresh();
frm.events.refresh(frm);
@@ -73,36 +80,36 @@ frappe.ui.form.on('Payroll Entry', {
});
},
- create_salary_slips: function(frm) {
+ create_salary_slips: function (frm) {
frm.call({
doc: frm.doc,
method: "create_salary_slips",
- callback: function() {
+ callback: function () {
frm.refresh();
frm.toolbar.refresh();
}
});
},
- add_context_buttons: function(frm) {
+ add_context_buttons: function (frm) {
if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm);
} else if (frm.doc.salary_slips_created) {
- frm.add_custom_button(__("Submit Salary Slip"), function() {
+ frm.add_custom_button(__("Submit Salary Slip"), function () {
submit_salary_slip(frm);
}).addClass("btn-primary");
}
},
- add_bank_entry_button: function(frm) {
+ add_bank_entry_button: function (frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries',
args: {
'name': frm.doc.name
},
- callback: function(r) {
+ callback: function (r) {
if (r.message && !r.message.submitted) {
- frm.add_custom_button("Make Bank Entry", function() {
+ frm.add_custom_button("Make Bank Entry", function () {
make_bank_entry(frm);
}).addClass("btn-primary");
}
@@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', {
},
payroll_frequency: function (frm) {
- frm.trigger("set_start_end_dates");
- frm.events.clear_employee_table(frm);
+ frm.trigger("set_start_end_dates").then( ()=> {
+ frm.events.clear_employee_table(frm);
+ frm.events.get_employee_with_salary_slip_and_set_query(frm);
+ });
+ },
+
+ employee_filters: function (frm, emp_list) {
+ frm.set_query('employee', 'employees', () => {
+ return {
+ filters: {
+ name: ["not in", emp_list]
+ }
+ };
+ });
+ },
+
+ get_employee_with_salary_slip_and_set_query: function (frm) {
+ frappe.db.get_list('Salary Slip', {
+ filters: {
+ start_date: frm.doc.start_date,
+ end_date: frm.doc.end_date,
+ docstatus: 1,
+ },
+ fields: ['employee']
+ }).then((emp) => {
+ var emp_list = [];
+ emp.forEach((employee_data) => {
+ emp_list.push(Object.values(employee_data)[0]);
+ });
+ frm.events.employee_filters(frm, emp_list);
+ });
},
company: function (frm) {
@@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', {
from_currency: frm.doc.currency,
to_currency: company_currency,
},
- callback: function(r) {
+ callback: function (r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
- frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
- + " = [?] " + company_currency);
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
+ " = [?] " + company_currency);
}
});
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
- frm.set_df_property("exchange_rate", "description", "" );
+ frm.set_df_property("exchange_rate", "description", "");
}
}
},
@@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', {
}
},
- set_end_date: function(frm) {
+ set_end_date: function (frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: {
@@ -243,12 +279,12 @@ frappe.ui.form.on('Payroll Entry', {
});
},
- validate_attendance: function(frm) {
+ validate_attendance: function (frm) {
if (frm.doc.validate_attendance && frm.doc.employees) {
- frappe.call ({
+ frappe.call({
method: 'validate_employee_attendance',
args: {},
- callback: function(r) {
+ callback: function (r) {
render_employee_attendance(frm, r.message);
},
doc: frm.doc,
@@ -270,11 +306,11 @@ frappe.ui.form.on('Payroll Entry', {
const submit_salary_slip = function (frm) {
frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'),
- function() {
+ function () {
frappe.call({
method: 'submit_salary_slips',
args: {},
- callback: function() {
+ callback: function () {
frm.events.refresh(frm);
},
doc: frm.doc,
@@ -282,7 +318,7 @@ const submit_salary_slip = function (frm) {
freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
});
},
- function() {
+ function () {
if (frappe.dom.freeze_count) {
frappe.dom.unfreeze();
frm.events.refresh(frm);
@@ -297,9 +333,11 @@ let make_bank_entry = function (frm) {
return frappe.call({
doc: cur_frm.doc,
method: "make_payment_entry",
- callback: function() {
+ callback: function () {
frappe.set_route(
- 'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name}
+ 'List', 'Journal Entry', {
+ "Journal Entry Account.reference_name": frm.doc.name
+ }
);
},
freeze: true,
@@ -311,11 +349,19 @@ let make_bank_entry = function (frm) {
}
};
-
-let render_employee_attendance = function(frm, data) {
+let render_employee_attendance = function (frm, data) {
frm.fields_dict.attendance_detail_html.html(
frappe.render_template('employees_to_mark_attendance', {
data: data
})
);
};
+
+frappe.ui.form.on('Payroll Employee Detail', {
+ employee: function(frm) {
+ frm.events.clear_employee_table(frm);
+ if (!frm.doc.payroll_frequency) {
+ frappe.throw(__("Please set a Payroll Frequency"));
+ }
+ }
+});
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 8c2d9740ece..a25a6e7a32c 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext
from frappe.model.document import Document
from dateutil.relativedelta import relativedelta
-from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff
+from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and
from frappe import _
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@@ -19,16 +19,26 @@ class PayrollEntry(Document):
# check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees):
- self.set_onload("submitted_ss", True)
+ self.set_onload("submitted_ss", True)
def on_submit(self):
self.create_salary_slips()
def before_submit(self):
+ self.validate_employee_details()
if self.validate_attendance:
if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
+ def validate_employee_details(self):
+ emp_with_sal_slip = []
+ for employee_details in self.employees:
+ if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
+ emp_with_sal_slip.append(employee_details.employee)
+
+ if len(emp_with_sal_slip):
+ frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
+
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name)))
@@ -71,8 +81,17 @@ class PayrollEntry(Document):
and t2.docstatus = 1
%s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
+
+ emp_list = self.remove_payrolled_employees(emp_list)
return emp_list
+ def remove_payrolled_employees(self, emp_list):
+ for employee_details in emp_list:
+ if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
+ emp_list.remove(employee_details)
+
+ return emp_list
+
def fill_employee_details(self):
self.set('employees', [])
employees = self.get_emp_list()
@@ -542,7 +561,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True):
title = _("Creating Salary Slips..."))
else:
salary_slip_name = frappe.db.sql(
- '''SELECT
+ '''SELECT
name
FROM `tabSalary Slip`
WHERE company=%s
From ed208194323a85b49bead2f593076c44dc0117f4 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 24 Dec 2020 16:24:10 +0530
Subject: [PATCH 225/286] fix: travis
---
.../stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 +-
erpnext/stock/doctype/serial_no/serial_no.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 9b8eeed1a12..5921651feca 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -527,7 +527,7 @@ class TestPurchaseReceipt(unittest.TestCase):
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True)
- self.assertRaises(SerialNoDuplicateError, se.submit)
+ se.submit()
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 25ce2d59695..86f3c1f5616 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -365,8 +365,8 @@ def has_serial_no_exists(sn, sle):
status = True
# If status is receipt then system will allow to in-ward the delivered serial no
- if (status and sle.voucher_type == 'Stock Entry' and
- frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'):
+ if (status and sle.voucher_type == "Stock Entry" and frappe.db.get_value("Stock Entry",
+ sle.voucher_no, "purpose") in ("Material Receipt", "Material Transfer")):
status = False
return status
From 304100db3b01992a8c87c6e32838456dae383f54 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Mon, 30 Nov 2020 13:05:28 +0530
Subject: [PATCH 226/286] fix: don't cancel job card if manufacturing entry has
made
---
.../doctype/job_card/job_card.py | 55 ++++++++++++-------
.../doctype/work_order/test_work_order.py | 50 +++++++++++++----
2 files changed, 75 insertions(+), 30 deletions(-)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index d15d81ed93d..ec28eb7795c 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -17,6 +17,7 @@ class OverlapError(frappe.ValidationError): pass
class OperationMismatchError(frappe.ValidationError): pass
class OperationSequenceError(frappe.ValidationError): pass
+class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def validate(self):
@@ -217,33 +218,49 @@ class JobCard(Document):
field = "operation_id"
data = self.get_current_operation_data()
if data and len(data) > 0:
- for_quantity = data[0].completed_qty
- time_in_mins = data[0].time_in_mins
+ for_quantity = flt(data[0].completed_qty)
+ time_in_mins = flt(data[0].time_in_mins)
- if self.get(field):
- time_data = frappe.db.sql("""
+ wo = frappe.get_doc('Work Order', self.work_order)
+ if self.operation_id:
+ self.validate_produced_quantity(for_quantity, wo)
+ self.update_work_order_data(for_quantity, time_in_mins, wo)
+
+ def validate_produced_quantity(self, for_quantity, wo):
+ if self.docstatus < 2: return
+
+ if wo.produced_qty > for_quantity:
+ first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
+ .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
+
+ second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
+ .format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
+
+ frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
+ JobCardCancelError, title = _("Error"))
+
+ def update_work_order_data(self, for_quantity, time_in_mins, wo):
+ time_data = frappe.db.sql("""
SELECT
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s
- and jc.{0} = %s and jc.docstatus = 1
- """.format(field), (self.work_order, self.get(field)), as_dict=1)
+ and jc.operation_id = %s and jc.docstatus = 1
+ """, (self.work_order, self.operation_id), as_dict=1)
- wo = frappe.get_doc('Work Order', self.work_order)
+ for data in wo.operations:
+ if data.get("name") == self.operation_id:
+ data.completed_qty = for_quantity
+ data.actual_operation_time = time_in_mins
+ data.actual_start_time = time_data[0].start_time if time_data else None
+ data.actual_end_time = time_data[0].end_time if time_data else None
- for data in wo.operations:
- if data.get("name") == self.get(field):
- data.completed_qty = for_quantity
- data.actual_operation_time = time_in_mins
- data.actual_start_time = time_data[0].start_time if time_data else None
- data.actual_end_time = time_data[0].end_time if time_data else None
-
- wo.flags.ignore_validate_update_after_submit = True
- wo.update_operation_status()
- wo.calculate_operating_cost()
- wo.set_actual_dates()
- wo.save()
+ wo.flags.ignore_validate_update_after_submit = True
+ wo.update_operation_status()
+ wo.calculate_operating_cost()
+ wo.set_actual_dates()
+ wo.save()
def get_current_operation_data(self):
return frappe.get_all('Job Card',
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index ce9699e1b3c..a77bd159afe 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import unittest
import frappe
-from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
+from frappe.utils import flt, now, add_months, cint, today, add_to_date
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
@@ -14,6 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
class TestWorkOrder(unittest.TestCase):
def setUp(self):
@@ -369,21 +370,49 @@ class TestWorkOrder(unittest.TestCase):
self.assertEqual(ste.total_additional_costs, 1000)
def test_job_card(self):
+ stock_entries = []
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
- if data:
- frappe.db.set_value("Manufacturing Settings",
- None, "disable_capacity_planning", 0)
+ bom, bom_item = data
- bom, bom_item = data
+ bom_doc = frappe.get_doc('BOM', bom)
+ work_order = make_wo_order_test_record(item=bom_item, qty=1,
+ bom_no=bom, source_warehouse="_Test Warehouse - _TC")
- bom_doc = frappe.get_doc('BOM', bom)
- work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
- self.assertTrue(work_order.planned_end_date)
+ for row in work_order.required_items:
+ stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
+ target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100)
+ stock_entries.append(stock_entry_doc)
- job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
- self.assertEqual(len(job_cards), len(bom_doc.operations))
+ ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+ ste.submit()
+ stock_entries.append(ste)
+
+ job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+ self.assertEqual(len(job_cards), len(bom_doc.operations))
+
+ for i, job_card in enumerate(job_cards):
+ doc = frappe.get_doc("Job Card", job_card)
+ doc.append("time_logs", {
+ "from_time": now(),
+ "hours": i,
+ "to_time": add_to_date(now(), i),
+ "completed_qty": doc.for_quantity
+ })
+ doc.submit()
+
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
+ ste1.submit()
+ stock_entries.append(ste1)
+
+ for job_card in job_cards:
+ doc = frappe.get_doc("Job Card", job_card)
+ self.assertRaises(JobCardCancelError, doc.cancel)
+
+ stock_entries.reverse()
+ for stock_entry in stock_entries:
+ stock_entry.cancel()
def test_capcity_planning(self):
frappe.db.set_value("Manufacturing Settings", None, {
@@ -509,7 +538,6 @@ class TestWorkOrder(unittest.TestCase):
ste1.submit()
ste_cancel_list.append(ste1)
- print(wo_order.name)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
self.assertEquals(ste3.fg_completed_qty, 2)
From aea62da544b21025023c22317be39ec38e77b772 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 24 Dec 2020 22:44:31 +0530
Subject: [PATCH 227/286] fix: multiple pricing rule with margin type not
working
---
erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 6 +++++-
erpnext/accounts/doctype/pricing_rule/utils.py | 10 +++++++++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 55a5b0e5139..05652642eb0 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -345,9 +345,13 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
or (pricing_rule.margin_type == 'Percentage')):
item_details.margin_type = pricing_rule.margin_type
- item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
item_details.has_margin = True
+ if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None:
+ item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount
+ else:
+ item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
+
if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 2c7cd14451d..fb1fbe484ed 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -164,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s`
- where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+ where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+
+ if parenttype in ["Customer Group", "Item Group", "Territory"]:
+ parent_field = "parent_{0}".format(frappe.scrub(parenttype))
+ root_name = frappe.db.get_list(parenttype,
+ {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
+
+ if root_name and root_name[0][0]:
+ parent_groups.append(root_name[0][0])
if parent_groups:
if allow_blank: parent_groups.append('')
From 6d74f5b59bd08ccaea79e714b3db657ef78352ed Mon Sep 17 00:00:00 2001
From: Saqib
Date: Fri, 25 Dec 2020 10:26:43 +0530
Subject: [PATCH 228/286] feat: GST E Invoicing (#23455)
* feat: init e-invoice settings
* feat: read public key file
* feat: rsa encryption with public key
* feat: save token and sek from auth request
* chore: handle error response
* feat: AES decryption of SEK with appkey
* feat: decrypt json data with SEK
* feat: make e invoice from erpnext sales invoice
* feat: generate IRN
* feat: decode signed json and QR code
* chore: validations
* feat: cancel IRN
* feat: complete e-invoice schema
* chore: move e-invoice settings to regional
* chore: split einvoice settings and operations
* chore: rename schema to template & js cleanup
* feat: make IRN field on regional setup
* feat: Generate & Cancel IRN from Sales Invoice
* chore: minor fixes
* fix: item discount
* chore: show irn cancelled check after cancellation
* fix: hide cancel irn dialog on error
* fix: public key is required on validate
* fix: cannot find attached key file
* fix: validation if e invoicing is disabled
* fix: do not show generate irn for invalid supply type
* fix: update irn_cancelled after cancelling irn
* chore: show irn field for proper gst_category
* feat: e-way bill details in e-invoice
* fix: save e-way bill no on irn generation
* chore: no copy on e invoice custom fields
* feat: cancel e-way bill before cancelling IRN
* feat: manual download / upload json
* chore: group e-invoicing actions
* fix: fn name
* chore: save signed invoice and qrcode after uplaoding irn
* fix: fetch token if not valid
* chore: move einvoicing stuff to seperate folder
* feat: QRCode Image and E-Invoice Print Format
* fix: bug
* fix: invalid syntax
* chore: code cleanup
* chore: clean up e invoice actions
* fix: download & upload e-invoice
* fix: print format
* fix: validations
* fix: add permissions on regional setup
* feat: add patch
* fix: validate document name
* fix: return date
* fix: credit note einvoice
* fix: validations
* fix: error logging
* fix: e_invoice module not found
* fix: add missing package
* fix: rename e_invoice_utils.py
* fix: einvoice field validation
* fix: patch
* fix: invoice totals calculation
* fix: other charges calculation
* chore: improve document name validation message
* fix: qr code image string
* feat: initialize GSP connector
* chore: remove unwanted fields
* fix: qr code generation
* feat: fetch and cache GSTIN details
* feat: generate & cancel IRN
* feat: cancel eway bill
* chore: remove unwanted fuctions
* chore: clean up einvoice actions
* fix: attach qrcode on irn generation
* fix: generate & cancel IRN
* fix: show/hide eway bill fields
* fix: valiations
* feat: generate eway bill from IRN
* chore: remove unwanted imports
* chore: error logging
* feat: header & footer in GST E Invoice
* chore: remove test pincode
* fix: invalid syntax
* feat: cess non advolem on einvoice item
* chore: remove fetch token from e invocie settings
* fix: imports
* fix: error handling
* feat: update timeline on einvoice actions
* fix: qrcode image size
* fix: exclude intra company transactions
* fix: eway bill test
* fix: ewaybill mandatory conditions
* chore: add tests
* fix: returning condition
* feat: log e-invocing requests
* chore: add ack date and ack no field for print formats
* fix: sider issues
* feat: show e-invoice preview before IRN generation
* fix: use as_list for error message
* fix: minor ux issues
* fix: dialog is undefined
* fix: error handling
* feat: add docs link to e invoice settings
* feat: multiple gstins for e invoicing
* fix: uncomment test condition
* fix: remove test pincode
* fix: cannot cancel irn without submitting sales invoice
* chore: code cleanup
* fix: sider issues
* fix: e invoice request log permissions
Co-authored-by: Nabin Hait
---
.../doctype/sales_invoice/regional/india.js | 2 +
.../doctype/sales_invoice/sales_invoice.py | 2 +-
.../sales_invoice/test_sales_invoice.py | 269 +++--
.../print_format/gst_e_invoice/__init__.py | 0
.../gst_e_invoice/gst_e_invoice.html | 162 +++
.../gst_e_invoice/gst_e_invoice.json | 24 +
erpnext/controllers/accounts_controller.py | 10 +
erpnext/hooks.py | 3 +-
erpnext/patches.txt | 1 +
.../patches/v12_0/setup_einvoice_fields.py | 55 +
.../doctype/e_invoice_request_log/__init__.py | 0
.../e_invoice_request_log.js | 8 +
.../e_invoice_request_log.json | 103 ++
.../e_invoice_request_log.py | 10 +
.../test_e_invoice_request_log.py | 10 +
.../doctype/e_invoice_settings/__init__.py | 0
.../e_invoice_settings/e_invoice_settings.js | 11 +
.../e_invoice_settings.json | 58 ++
.../e_invoice_settings/e_invoice_settings.py | 14 +
.../test_e_invoice_settings.py | 10 +
.../doctype/e_invoice_user/__init__.py | 0
.../e_invoice_user/e_invoice_user.json | 48 +
.../doctype/e_invoice_user/e_invoice_user.py | 10 +
erpnext/regional/india/e_invoice/__init__.py | 0
.../india/e_invoice/einv_item_template.json | 31 +
.../india/e_invoice/einv_template.json | 110 ++
.../india/e_invoice/einv_validation.json | 956 ++++++++++++++++++
erpnext/regional/india/e_invoice/einvoice.js | 305 ++++++
erpnext/regional/india/e_invoice/utils.py | 772 ++++++++++++++
erpnext/regional/india/setup.py | 31 +-
requirements.txt | 1 +
31 files changed, 2922 insertions(+), 94 deletions(-)
create mode 100644 erpnext/accounts/print_format/gst_e_invoice/__init__.py
create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json
create mode 100644 erpnext/patches/v12_0/setup_einvoice_fields.py
create mode 100644 erpnext/regional/doctype/e_invoice_request_log/__init__.py
create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
create mode 100644 erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
create mode 100644 erpnext/regional/doctype/e_invoice_settings/__init__.py
create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
create mode 100644 erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
create mode 100644 erpnext/regional/doctype/e_invoice_user/__init__.py
create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
create mode 100644 erpnext/regional/india/e_invoice/__init__.py
create mode 100644 erpnext/regional/india/e_invoice/einv_item_template.json
create mode 100644 erpnext/regional/india/e_invoice/einv_template.json
create mode 100644 erpnext/regional/india/e_invoice/einv_validation.json
create mode 100644 erpnext/regional/india/e_invoice/einvoice.js
create mode 100644 erpnext/regional/india/e_invoice/utils.py
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js
index 6336db16ebc..f54bce8aac7 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js
@@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %}
+{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice');
+erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", {
setup: function(frm) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 50734c865cd..40009ac69d0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -232,9 +232,9 @@ class SalesInvoice(SellingController):
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def before_cancel(self):
+ super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
-
def on_cancel(self):
super(SalesInvoice, self).on_cancel()
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ceb79079893..3c681eeecf2 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1825,93 +1825,7 @@ class TestSalesInvoice(unittest.TestCase):
# check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self):
- if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Address for Eway bill",
- "address_type": "Billing",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+91 0000000000",
- "gstin": "27AAECE4835E1ZR",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "401108"
- }).insert()
-
- address.append("links", {
- "link_doctype": "Company",
- "link_name": "_Test Company"
- })
-
- address.save()
-
- if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Customer-Address for Eway bill",
- "address_type": "Shipping",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+91 0000000000",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "410038"
- }).insert()
-
- address.append("links", {
- "link_doctype": "Customer",
- "link_name": "_Test Customer"
- })
-
- address.save()
-
- gst_settings = frappe.get_doc("GST Settings")
-
- gst_account = frappe.get_all(
- "GST Account",
- fields=["cgst_account", "sgst_account", "igst_account"],
- filters = {"company": "_Test Company"})
-
- if not gst_account:
- gst_settings.append("gst_accounts", {
- "company": "_Test Company",
- "cgst_account": "CGST - _TC",
- "sgst_account": "SGST - _TC",
- "igst_account": "IGST - _TC",
- })
-
- gst_settings.save()
-
- si = create_sales_invoice(do_not_save =1, rate = '60000')
-
- si.distance = 2000
- si.company_address = "_Test Address for Eway bill-Billing"
- si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
- si.vehicle_no = "KA12KA1234"
- si.gst_category = "Registered Regular"
-
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "CGST - _TC",
- "cost_center": "Main - _TC",
- "description": "CGST @ 9.0",
- "rate": 9
- })
-
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "SGST - _TC",
- "cost_center": "Main - _TC",
- "description": "SGST @ 9.0",
- "rate": 9
- })
+ si = make_sales_invoice_for_ewaybill()
si.submit()
@@ -1927,6 +1841,187 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
+
+ def test_einvoice_submission_without_irn(self):
+ # init
+ frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
+ country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = make_sales_invoice_for_ewaybill()
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ si.irn = 'test_irn'
+ si.submit()
+
+ # reset
+ frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
+ frappe.flags.country = country
+
+ def test_einvoice_json(self):
+ from erpnext.regional.india.e_invoice.utils import make_einvoice
+
+ customer_gstin = '27AACCM7806M1Z3'
+ customer_gstin_dtls = {
+ 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
+ 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
+ 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
+ }
+ company_gstin = '27AAECE4835E1ZR'
+ company_gstin_dtls = {
+ 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
+ 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
+ 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
+ }
+ # set cache gstin details to avoid fetching details which will require connection to GSP servers
+ frappe.local.gstin_cache = {}
+ frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
+ frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
+
+ si = make_sales_invoice_for_ewaybill()
+ si.naming_series = 'INV-2020-.#####'
+ si.items = []
+ si.append("items", {
+ "item_code": "_Test Item",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 2,
+ "rate": 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+ si.append("items", {
+ "item_code": "_Test Item 2",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 4,
+ "rate": 150,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+ si.save()
+
+ einvoice = make_einvoice(si)
+
+ total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']])
+ total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']])
+ total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']])
+ total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']])
+ total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']])
+
+ self.assertEqual(einvoice['Version'], '1.1')
+ self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value)
+ self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value)
+ self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value)
+ self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value)
+ self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
+ self.assertTrue(einvoice['EwbDtls'])
+
+def make_sales_invoice_for_ewaybill():
+ if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Address for Eway bill",
+ "address_type": "Billing",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AAECE4835E1ZR",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "401108"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Company",
+ "link_name": "_Test Company"
+ })
+
+ address.save()
+
+ if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Customer-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AACCM7806M1Z3",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "410038"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Customer",
+ "link_name": "_Test Customer"
+ })
+
+ address.save()
+
+ if not frappe.db.exists('Supplier', '_Test Transporter'):
+ frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": "_Test Transporter",
+ "country": "India",
+ "supplier_group": "_Test Supplier Group",
+ "supplier_type": "Company",
+ "is_transporter": 1
+ }).insert()
+
+ gst_settings = frappe.get_doc("GST Settings")
+
+ gst_account = frappe.get_all(
+ "GST Account",
+ fields=["cgst_account", "sgst_account", "igst_account"],
+ filters = {"company": "_Test Company"})
+
+ if not gst_account:
+ gst_settings.append("gst_accounts", {
+ "company": "_Test Company",
+ "cgst_account": "CGST - _TC",
+ "sgst_account": "SGST - _TC",
+ "igst_account": "IGST - _TC",
+ })
+
+ gst_settings.save()
+
+ si = create_sales_invoice(do_not_save =1, rate = '60000')
+
+ si.distance = 2000
+ si.company_address = "_Test Address for Eway bill-Billing"
+ si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
+ si.vehicle_no = "KA12KA1234"
+ si.gst_category = "Registered Regular"
+ si.mode_of_transport = 'Road'
+ si.transporter = '_Test Transporter'
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "CGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "CGST @ 9.0",
+ "rate": 9
+ })
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "SGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "SGST @ 9.0",
+ "rate": 9
+ })
+
+ return si
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
diff --git a/erpnext/accounts/print_format/gst_e_invoice/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
new file mode 100644
index 00000000000..9827e00b71b
--- /dev/null
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -0,0 +1,162 @@
+{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
+{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+
+