-[](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 068/361] 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 069/361] 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 070/361] 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 071/361] 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 072/361] 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.')}}
\
- 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 118/361] 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 119/361] 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 120/361] 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 121/361] 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 122/361] 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 7b2d518059f1a131a0ece9e6872c5ed8c4a93d04 Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 4 Dec 2020 11:28:26 +0530
Subject: [PATCH 123/361] fix: Dimension filters in accounting reports
---
erpnext/public/js/utils.js | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 891bbe5b598..2635d47f886 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -194,15 +194,21 @@ $.extend(erpnext.utils, {
add_dimensions: function(report_name, index) {
let filters = frappe.query_reports[report_name].filters;
- erpnext.dimension_filters.forEach((dimension) => {
- let found = filters.some(el => el.fieldname === dimension['fieldname']);
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ callback: function(r) {
+ let accounting_dimensions = r.message[0];
+ accounting_dimensions.forEach((dimension) => {
+ let found = filters.some(el => el.fieldname === dimension['fieldname']);
- if (!found) {
- filters.splice(index, 0 ,{
- "fieldname": dimension["fieldname"],
- "label": __(dimension["label"]),
- "fieldtype": "Link",
- "options": dimension["document_type"]
+ if (!found) {
+ filters.splice(index, 0 ,{
+ "fieldname": dimension["fieldname"],
+ "label": __(dimension["label"]),
+ "fieldtype": "Link",
+ "options": dimension["document_type"]
+ });
+ }
});
}
});
From 28e86cf18312155ed020065b10152f77e8747d8f Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Fri, 4 Dec 2020 00:47:46 +0530
Subject: [PATCH 124/361] 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 125/361] 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 126/361] 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 127/361] 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 128/361] 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 129/361] 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 130/361] 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 131/361] 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 132/361] 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 133/361] 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 134/361] 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 135/361] 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 136/361] 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 137/361] 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 138/361] 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 139/361] 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 175/361] 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 176/361] 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 177/361] 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 178/361] 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 179/361] 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 180/361] 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 181/361] 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 182/361] 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 183/361] 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 184/361] 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 185/361] 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 186/361] 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 187/361] 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 188/361] 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 189/361] 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 190/361] 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 191/361] 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 192/361] 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 193/361] 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 194/361] 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 195/361] 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 196/361] 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 924f99bead32bb4eba656019a15931f797eb04ae Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Wed, 16 Dec 2020 15:42:50 +0530
Subject: [PATCH 197/361] fix: Help message
---
.../accounting_dimension_filter.js | 12 ++++++++++++
.../accounting_dimension_filter.json | 19 +++++++++++++++++--
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
index a2526e92c36..74b7b516763 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -6,6 +6,18 @@ frappe.ui.form.on('Accounting Dimension Filter', {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
+
+ let help_content =
+ `
+
+
+
+ {{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
+
+
+
`;
+
+ frm.set_df_property('dimension_filter_help', 'options', help_content);
},
onload: function(frm) {
frm.set_query('applicable_on_account', 'accounts', function() {
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
index 7736b2dffb2..c0327ad0ad8 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
@@ -14,7 +14,9 @@
"section_break_4",
"accounts",
"column_break_6",
- "dimensions"
+ "dimensions",
+ "section_break_10",
+ "dimension_filter_help"
],
"fields": [
{
@@ -89,11 +91,24 @@
"reqd": 1,
"show_days": 1,
"show_seconds": 1
+ },
+ {
+ "fieldname": "dimension_filter_help",
+ "fieldtype": "HTML",
+ "label": "Dimension Filter Help",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-24 12:34:42.458713",
+ "modified": "2020-12-16 15:27:23.659285",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
From 5a8a52b9c6739f25bbd3b128402cb5a19b40afc0 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Wed, 16 Dec 2020 15:54:06 +0530
Subject: [PATCH 198/361] 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 199/361] 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 200/361] 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 201/361] 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 202/361] 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 203/361] 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 204/361] 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 205/361] 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 206/361] 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 207/361] 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 208/361] 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 209/361] 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 210/361] 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 211/361] 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 212/361] 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 213/361] 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 214/361] 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 215/361] 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 216/361] 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 217/361] 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 218/361] 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 219/361] 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 220/361] 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 221/361] 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 222/361] 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 223/361] 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 224/361] 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 225/361] 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 226/361] 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 227/361] 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 228/361] 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 229/361] 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 230/361] 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 231/361] 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 232/361] 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 233/361] 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 234/361] 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 235/361] 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 236/361] 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 237/361] 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 238/361] 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 239/361] 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 240/361] 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 241/361] 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 242/361] 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 243/361] 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 244/361] 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 245/361] 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 246/361] 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 247/361] 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 248/361] 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 249/361] 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 250/361] 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 251/361] 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 252/361] 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 253/361] 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) -%}
+
+