Merge branch 'develop' into asset-capitalization
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 511b682..1cf9a5b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -23,7 +23,7 @@
1. **Steps to Reproduce:** The bug report must have a list of steps needed to reproduce a bug. If we cannot reproduce it, then we cannot solve it.
1. **Version Number:** Please add the version number in your report. Often a bug is fixed in the latest version
1. **Clear Title:** Add a clear subject to your bug report like "Unable to submit Purchase Order without Basic Rate" instead of just "Cannot Submit"
-1. **Screenshots:** Screenshots are a great way of communicating the issues. Try adding annotations or using LiceCAP to take a screencast in `gif`.
+1. **Screenshots:** Screenshots are a great way of communicating issues. Try adding annotations or using LiceCAP to take a screencast in `gif`.
### Feature Request Guidelines
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index f8abb6c..97bccf5 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -93,7 +93,7 @@
for version in $(seq 12 13)
do
echo "Updating to v$version"
- branch_name="version-$version"
+ branch_name="version-$version-hotfix"
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
diff --git a/README.md b/README.md
index 1105a97..9609353 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[](https://www.codetriage.com/frappe/erpnext)
[](https://codecov.io/gh/frappe/erpnext)
+[](https://hub.docker.com/r/frappe/erpnext-worker)
[https://erpnext.com](https://erpnext.com)
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 9d99ebb..a5de50f 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -4,7 +4,7 @@
from erpnext.hooks import regional_overrides
-__version__ = '13.9.0'
+__version__ = '14.0.0-dev'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index a4b6e0b..a3ef384 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -78,6 +78,7 @@
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
+ node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
@@ -175,7 +176,7 @@
&& node.expandable && !node.hide_add;
},
click: function() {
- var me = frappe.treeview_settings['Account'].treeview;
+ var me = frappe.views.trees['Account'];
me.new_node();
},
btnClass: "hidden-xs"
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 9dd882a..750e129 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -8,7 +8,7 @@
}
let help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index aa132a0..7451917 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -19,6 +19,9 @@
frappe.db.set_default("add_taxes_from_item_tax_template",
self.get("add_taxes_from_item_tax_template", 0))
+ frappe.db.set_default("enable_common_party_accounting",
+ self.get("enable_common_party_accounting", 0))
+
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
diff --git a/erpnext/accounts/doctype/advance_tax/__init__.py b/erpnext/accounts/doctype/advance_tax/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_tax/__init__.py
diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.json b/erpnext/accounts/doctype/advance_tax/advance_tax.json
new file mode 100644
index 0000000..68706ab
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_tax/advance_tax.json
@@ -0,0 +1,56 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-11-25 10:24:39.836195",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "reference_detail",
+ "account_head",
+ "allocated_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "label": "Reference Name",
+ "options": "reference_type"
+ },
+ {
+ "fieldname": "reference_detail",
+ "fieldtype": "Data",
+ "label": "Reference Detail"
+ },
+ {
+ "fieldname": "account_head",
+ "fieldtype": "Link",
+ "label": "Account Head",
+ "options": "Account"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "label": "Allocated Amount",
+ "options": "party_account_currency"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-25 10:27:51.712286",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Advance Tax",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/advance_tax/advance_tax.py b/erpnext/accounts/doctype/advance_tax/advance_tax.py
new file mode 100644
index 0000000..2e784ef
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_tax/advance_tax.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class AdvanceTax(Document):
+ pass
diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
index 4d63499..05b284a 100644
--- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
@@ -25,8 +25,7 @@
"allocated_amount",
"column_break_13",
"base_tax_amount",
- "base_total",
- "base_allocated_amount"
+ "base_total"
],
"fields": [
{
@@ -169,12 +168,6 @@
"options": "currency"
},
{
- "fieldname": "base_allocated_amount",
- "fieldtype": "Currency",
- "label": "Allocated Amount (Company Currency)",
- "options": "Company:company:default_currency"
- },
- {
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
@@ -186,7 +179,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-09 11:46:58.373170",
+ "modified": "2021-11-25 11:10:10.945027",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Taxes and Charges",
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 002e312..e7371fb 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -342,7 +342,15 @@
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
- cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+
+ company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
+ root_type = frappe.get_value("Account", company_account, "root_type")
+
+ if root_type == "Liability":
+ cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
+ else:
+ cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+
return f"""
SELECT
@@ -426,7 +434,7 @@
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
- mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
+ mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
filters={"default_account": bank_account}, fields=["parent"])]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
company_currency = get_company_currency(company)
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
index f90f867..6951b2a 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
@@ -6,7 +6,7 @@
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
index daf667c..e9f813c 100644
--- a/erpnext/accounts/doctype/party_link/party_link.py
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -25,3 +25,17 @@
if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}')
.format(self.primary_role, self.primary_party, existing_party_link[0]))
+
+
+@frappe.whitelist()
+def create_party_link(primary_role, primary_party, secondary_party):
+ party_link = frappe.new_doc('Party Link')
+ party_link.primary_role = primary_role
+ party_link.primary_party = primary_party
+ party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
+ party_link.secondary_party = secondary_party
+
+ party_link.save(ignore_permissions=True)
+
+ return party_link
+
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index ee2e319..c8d1db9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -61,7 +61,6 @@
"taxes_and_charges_section",
"purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template",
- "advance_tax_account",
"column_break_55",
"apply_tax_withholding_amount",
"tax_withholding_category",
@@ -686,15 +685,6 @@
"hide_border": 1
},
{
- "depends_on": "eval:doc.apply_tax_withholding_amount",
- "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
- "fieldname": "advance_tax_account",
- "fieldtype": "Link",
- "label": "Advance Tax Account",
- "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
- "options": "Account"
- },
- {
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
@@ -730,7 +720,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-22 17:50:24.632806",
+ "modified": "2021-11-24 18:58:24.919764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 26fd16a..c1b056b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -20,7 +20,7 @@
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
-from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
from erpnext.controllers.accounts_controller import (
@@ -339,7 +339,7 @@
for k, v in no_oustanding_refs.items():
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"))
+ .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@@ -433,23 +433,12 @@
if not self.apply_tax_withholding_amount:
return
- if not self.advance_tax_account:
- frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
-
net_total = self.paid_amount
- for reference in self.get("references"):
- net_total_for_tds = 0
- if reference.reference_doctype == 'Purchase Order':
- net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
-
- if net_total_for_tds:
- net_total = net_total_for_tds
-
# Adding args as purchase invoice to get TDS amount
args = frappe._dict({
'company': self.company,
- 'doctype': 'Purchase Invoice',
+ 'doctype': 'Payment Entry',
'supplier': self.party,
'posting_date': self.posting_date,
'net_total': net_total
@@ -461,7 +450,6 @@
return
tax_withholding_details.update({
- 'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
})
@@ -623,7 +611,7 @@
if not total_negative_outstanding:
frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice")
- .format(self.payment_type, ("to" if self.party_type=="Customer" else "from"),
+ .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")),
self.party_type), InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
@@ -689,6 +677,7 @@
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
+ gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
def add_party_gl_entries(self, gl_entries):
@@ -752,7 +741,8 @@
"against": self.party if self.payment_type=="Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount,
- "cost_center": self.cost_center
+ "cost_center": self.cost_center,
+ "post_net_value": True
}, item=self)
)
if self.payment_type in ("Receive", "Internal Transfer"):
@@ -782,14 +772,10 @@
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to
- payment_or_advance_account = self.get_party_account_for_taxes()
+ payment_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount
- if self.advance_tax_account:
- tax_amount = -1 * tax_amount
- base_tax_amount = -1 * base_tax_amount
-
gl_entries.append(
self.get_gl_dict({
"account": d.account_head,
@@ -798,19 +784,21 @@
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
- "cost_center": d.cost_center
+ "cost_center": d.cost_center,
+ "post_net_value": True,
}, account_currency, item=d))
- if not d.included_in_paid_amount or self.advance_tax_account:
+ if not d.included_in_paid_amount:
gl_entries.append(
self.get_gl_dict({
- "account": payment_or_advance_account,
+ "account": payment_account,
"against": against,
rev_dr_or_cr: tax_amount,
rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
"cost_center": self.cost_center,
+ "post_net_value": True,
}, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries):
@@ -832,9 +820,7 @@
)
def get_party_account_for_taxes(self):
- if self.advance_tax_account:
- return self.advance_tax_account
- elif self.payment_type == 'Receive':
+ if self.payment_type == 'Receive':
return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from
@@ -1106,7 +1092,7 @@
if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
- .format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
+ .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
return data
@@ -1599,13 +1585,6 @@
})
pe.set_difference_amount()
- if doc.doctype == 'Purchase Order' and doc.apply_tds:
- pe.apply_tax_withholding_amount = 1
- pe.tax_withholding_category = doc.tax_withholding_category
-
- if not pe.advance_tax_account:
- pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
-
return pe
def get_bank_cash_account(doc, bank_account):
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 03bb72b..6a84a65 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -548,10 +548,14 @@
return doclist
-def validate_payment(doc, method=""):
- if not frappe.db.has_column(doc.reference_doctype, 'status'):
+def validate_payment(doc, method=None):
+ if doc.reference_doctype != "Payment Request" or (
+ frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
+ != "Paid"
+ ):
return
- status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
- if status == 'Paid':
- frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname))
\ No newline at end of file
+ frappe.throw(
+ _("The Payment Request {0} is already paid, cannot process payment twice")
+ .format(doc.reference_docname)
+ )
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 34572fd..d0e555e 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -88,9 +88,10 @@
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
+ cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
- "cost_center": acc.cost_center or company_cost_center,
+ "cost_center": cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index 0e29755..030b4ca 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -66,8 +66,8 @@
company = create_company()
surplus_account = create_account()
- cost_center1 = create_cost_center("Test Cost Center 1")
- cost_center2 = create_cost_center("Test Cost Center 2")
+ cost_center1 = create_cost_center("Main")
+ cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company=company,
@@ -86,7 +86,10 @@
debit_to="Debtors - TPC"
)
- pcv = self.make_period_closing_voucher()
+ pcv = self.make_period_closing_voucher(submit=False)
+ pcv.cost_center_wise_pnl = 1
+ pcv.save()
+ pcv.submit()
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -149,7 +152,7 @@
self.assertEqual(pcv_gle, expected_gle)
- def make_period_closing_voucher(self):
+ def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc({
@@ -163,7 +166,8 @@
"remarks": "test"
})
pcv.insert()
- pcv.submit()
+ if submit:
+ pcv.submit()
return pcv
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index bff8587..0c6e7ed 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -171,6 +171,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -1561,16 +1562,23 @@
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-27 20:12:57.306772",
+ "modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 8b71eb0..3f85668 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -46,6 +46,7 @@
"base_amount",
"pricing_rules",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -800,14 +801,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-01-04 17:34:49.924531",
+ "modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 8434970..0720d9b 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -110,9 +110,15 @@
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
+
loyalty_amount_sum, loyalty_points_sum = 0, 0
+
rounding_adjustment, base_rounding_adjustment = 0, 0
rounded_total, base_rounded_total = 0, 0
+
+ loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
+
+
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
@@ -146,6 +152,8 @@
found = True
if not found:
tax.charge_type = 'Actual'
+ tax.idx = idx
+ idx += 1
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
@@ -163,8 +171,8 @@
payments.append(payment)
rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total
- base_rounding_adjustment += doc.rounding_adjustment
- base_rounded_total += doc.rounded_total
+ base_rounding_adjustment += doc.base_rounding_adjustment
+ base_rounded_total += doc.base_rounded_total
if loyalty_points_sum:
@@ -176,9 +184,9 @@
invoice.set('payments', payments)
invoice.set('taxes', taxes)
invoice.set('rounding_adjustment',rounding_adjustment)
- invoice.set('rounding_adjustment',base_rounding_adjustment)
- invoice.set('base_rounded_total',base_rounded_total)
+ invoice.set('base_rounding_adjustment',base_rounding_adjustment)
invoice.set('rounded_total',rounded_total)
+ invoice.set('base_rounded_total',base_rounded_total)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
index d79ad5f..8267582 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
@@ -38,7 +38,7 @@
refresh: function(frm) {
var help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index e05e4a1..d8b8606 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -543,6 +543,75 @@
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
+ def test_pricing_rule_for_different_currency(self):
+ make_item("Test Sanitizer Item")
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Sanitizer Rule",
+ "apply_on": "Item Code",
+ "items": [{
+ "item_code": "Test Sanitizer Item",
+ }],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 0,
+ "priority": 2,
+ "margin_type": "Percentage",
+ "margin_rate_or_amount": 0.0,
+ "company": "_Test Company"
+ }
+
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.rate_or_discount = 'Rate'
+ rule.rate = 100.0
+ rule.insert()
+
+ rule1 = frappe.get_doc(pricing_rule_record)
+ rule1.currency = 'USD'
+ rule1.rate_or_discount = 'Rate'
+ rule1.rate = 2.0
+ rule1.priority = 1
+ rule1.insert()
+
+ args = frappe._dict({
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "USD",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate()
+ })
+
+ details = get_item_details(args)
+ self.assertEqual(details.price_list_rate, 2.0)
+
+
+ args = frappe._dict({
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "INR",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate()
+ })
+
+ details = get_item_details(args)
+ self.assertEqual(details.price_list_rate, 100.0)
+
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 9655ac4..02bfc9d 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -264,6 +264,11 @@
else:
p.variant_of = None
+ if len(pricing_rules) > 1:
+ filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
+ if filtered_rules:
+ pricing_rules = filtered_rules
+
# find pricing rule with highest priority
if pricing_rules:
max_priority = max(cint(p.priority) for p in pricing_rules)
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
index e3fac07..5fbe93e 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
@@ -20,6 +20,9 @@
product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
+class TransactionExists(frappe.ValidationError):
+ pass
+
class PromotionalScheme(Document):
def validate(self):
if not self.selling and not self.buying:
@@ -28,6 +31,40 @@
or self.product_discount_slabs):
frappe.throw(_("Price or product discount slabs are required"))
+ self.validate_applicable_for()
+ self.validate_pricing_rules()
+
+ def validate_applicable_for(self):
+ if self.applicable_for:
+ applicable_for = frappe.scrub(self.applicable_for)
+
+ if not self.get(applicable_for):
+ msg = (f'The field {frappe.bold(self.applicable_for)} is required')
+ frappe.throw(_(msg))
+
+ def validate_pricing_rules(self):
+ if self.is_new():
+ return
+
+ transaction_exists = False
+ docnames = []
+
+ # If user has changed applicable for
+ if self._doc_before_save.applicable_for == self.applicable_for:
+ return
+
+ docnames = frappe.get_all('Pricing Rule',
+ filters= {'promotional_scheme': self.name})
+
+ for docname in docnames:
+ if frappe.db.exists('Pricing Rule Detail',
+ {'pricing_rule': docname.name, 'docstatus': ('<', 2)}):
+ raise_for_transaction_exists(self.name)
+
+ if docnames and not transaction_exists:
+ for docname in docnames:
+ frappe.delete_doc('Pricing Rule', docname.name)
+
def on_update(self):
pricing_rules = frappe.get_all(
'Pricing Rule',
@@ -67,6 +104,13 @@
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', rule.name)
+def raise_for_transaction_exists(name):
+ msg = (f"""You can't change the {frappe.bold(_('Applicable For'))}
+ because transactions are present against the Promotional Scheme {frappe.bold(name)}. """)
+ msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.'
+
+ frappe.throw(_(msg), TransactionExists)
+
def get_pricing_rules(doc, rules=None):
if rules is None:
rules = {}
@@ -84,45 +128,59 @@
new_doc = []
args = get_args_for_pricing_rule(doc)
applicable_for = frappe.scrub(doc.get('applicable_for'))
+
for idx, d in enumerate(doc.get(child_doc)):
if d.name in rules:
- for applicable_for_value in args.get(applicable_for):
- temp_args = args.copy()
- docname = frappe.get_all(
- 'Pricing Rule',
- fields = ["promotional_scheme_id", "name", applicable_for],
- filters = {
- 'promotional_scheme_id': d.name,
- applicable_for: applicable_for_value
- }
- )
-
- if docname:
- pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
- else:
- pr = frappe.new_doc("Pricing Rule")
- pr.title = doc.name
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
-
+ if not args.get(applicable_for):
+ docname = get_pricing_rule_docname(d)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname)
new_doc.append(pr)
+ else:
+ for applicable_for_value in args.get(applicable_for):
+ docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
+ d, docname, applicable_for, applicable_for_value)
+ new_doc.append(pr)
- else:
+ elif args.get(applicable_for):
applicable_for_values = args.get(applicable_for) or []
for applicable_for_value in applicable_for_values:
- pr = frappe.new_doc("Pricing Rule")
- pr.title = doc.name
- temp_args = args.copy()
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
+ d, applicable_for=applicable_for, value= applicable_for_value)
+
new_doc.append(pr)
+ else:
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d)
+ new_doc.append(pr)
return new_doc
+def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str:
+ fields = ['promotional_scheme_id', 'name']
+ filters = {
+ 'promotional_scheme_id': row.name
+ }
+ if applicable_for:
+ fields.append(applicable_for)
+ filters[applicable_for] = applicable_for_value
+ docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters)
+ return docname[0].name if docname else ''
+
+def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None):
+ if docname:
+ pr = frappe.get_doc("Pricing Rule", docname)
+ else:
+ pr = frappe.new_doc("Pricing Rule")
+
+ pr.title = doc.name
+ temp_args = args.copy()
+
+ if value:
+ temp_args[applicable_for] = value
+
+ return set_args(temp_args, pr, doc, child_doc, discount_fields, d)
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
pr.update(args)
@@ -145,6 +203,7 @@
apply_on: d.get(apply_on),
'uom': d.uom
})
+
return pr
def get_args_for_pricing_rule(doc):
diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
index e1852ae..49192a4 100644
--- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
@@ -5,10 +5,17 @@
import frappe
+from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+
class TestPromotionalScheme(unittest.TestCase):
+ def setUp(self):
+ if frappe.db.exists('Promotional Scheme', '_Test Scheme'):
+ frappe.delete_doc('Promotional Scheme', '_Test Scheme')
+
def test_promotional_scheme(self):
- ps = make_promotional_scheme()
+ ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer')
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules),1)
@@ -39,22 +46,62 @@
filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
-def make_promotional_scheme():
+ def test_promotional_scheme_without_applicable_for(self):
+ ps = make_promotional_scheme()
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+
+ self.assertTrue(len(price_rules), 1)
+ frappe.delete_doc('Promotional Scheme', ps.name)
+
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertEqual(price_rules, [])
+
+ def test_change_applicable_for_in_promotional_scheme(self):
+ ps = make_promotional_scheme()
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertTrue(len(price_rules), 1)
+
+ so = make_sales_order(qty=5, currency='USD', do_not_save=True)
+ so.set_missing_values()
+ so.save()
+ self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule)
+
+ ps.applicable_for = 'Customer'
+ ps.append('customer', {
+ 'customer': '_Test Customer'
+ })
+
+ self.assertRaises(TransactionExists, ps.save)
+
+ frappe.delete_doc('Sales Order', so.name)
+ frappe.delete_doc('Promotional Scheme', ps.name)
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertEqual(price_rules, [])
+
+def make_promotional_scheme(**args):
+ args = frappe._dict(args)
+
ps = frappe.new_doc('Promotional Scheme')
ps.name = '_Test Scheme'
ps.append('items',{
'item_code': '_Test Item'
})
+
ps.selling = 1
ps.append('price_discount_slabs',{
'min_qty': 4,
+ 'validate_applied_rule': 0,
'discount_percentage': 20,
'rule_description': 'Test'
})
- ps.applicable_for = 'Customer'
- ps.append('customer',{
- 'customer': "_Test Customer"
- })
+
+ ps.company = '_Test Company'
+ if args.applicable_for:
+ ps.applicable_for = args.applicable_for
+ ps.append(frappe.scrub(args.applicable_for), {
+ frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))
+ })
+
ps.save()
return ps
diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
index a70d5c9..aa3696d 100644
--- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
+++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
@@ -136,7 +136,7 @@
"label": "Threshold for Suggestion"
},
{
- "default": "1",
+ "default": "0",
"fieldname": "validate_applied_rule",
"fieldtype": "Check",
"label": "Validate Applied Rule"
@@ -169,7 +169,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-19 15:49:29.598727",
+ "modified": "2021-11-16 00:25:33.843996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Price Discount",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 3526e48..1a398ab 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -592,8 +592,17 @@
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
if (frm.doc.company) {
- frappe.db.get_value('Company', frm.doc.company, 'default_payable_account', (r) => {
- frm.set_value('credit_to', r.default_payable_account);
+ frappe.call({
+ method:
+ "erpnext.accounts.party.get_party_account",
+ args: {
+ party_type: 'Supplier',
+ party: frm.doc.supplier,
+ company: frm.doc.company
+ },
+ callback: (response) => {
+ if (response) frm.set_value("credit_to", response.message);
+ },
});
}
},
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 03cbc4a..bd01164 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -130,6 +130,7 @@
"allocate_advances_automatically",
"get_advances",
"advances",
+ "advance_tax",
"payment_schedule_section",
"payment_terms_template",
"ignore_default_payment_terms_template",
@@ -1408,13 +1409,21 @@
{
"fieldname": "column_break_147",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "advance_tax",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Advance Tax",
+ "options": "Advance Tax",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-12 20:55:16.145651",
+ "modified": "2021-11-25 13:31:02.716727",
"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 62e3dc8..48b5cb9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -427,6 +427,7 @@
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
+ self.update_advance_tax_references()
self.process_common_party_accounting()
@@ -472,8 +473,6 @@
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
- self.allocate_advance_taxes(gl_entries)
-
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@@ -729,7 +728,7 @@
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
- "remarks": self.remarks or "Accounting Entry for Stock",
+ "remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
"project": item.project or self.project
}, item=item)
@@ -937,7 +936,7 @@
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": valuation_tax[tax.name],
- "remarks": self.remarks or "Accounting Entry for Stock"
+ "remarks": self.remarks or _("Accounting Entry for Stock")
}, item=tax))
@property
@@ -1074,6 +1073,7 @@
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.update_advance_tax_references(cancel=1)
def update_project(self):
project_list = []
@@ -1150,7 +1150,10 @@
if not self.tax_withholding_category:
return
- tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
+ tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category)
+
+ # Adjust TDS paid on advances
+ self.allocate_advance_tds(tax_withholding_details, advance_taxes)
if not tax_withholding_details:
return
@@ -1174,6 +1177,39 @@
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
+ def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
+ self.set('advance_tax', [])
+ for tax in advance_taxes:
+ allocated_amount = 0
+ pending_amount = flt(tax.tax_amount - tax.allocated_amount)
+ if flt(tax_withholding_details.get('tax_amount')) >= pending_amount:
+ tax_withholding_details['tax_amount'] -= pending_amount
+ allocated_amount = pending_amount
+ elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount:
+ allocated_amount = tax_withholding_details['tax_amount']
+ tax_withholding_details['tax_amount'] = 0
+
+ self.append('advance_tax', {
+ 'reference_type': 'Payment Entry',
+ 'reference_name': tax.parent,
+ 'reference_detail': tax.name,
+ 'account_head': tax.account_head,
+ 'allocated_amount': allocated_amount
+ })
+
+ def update_advance_tax_references(self, cancel=0):
+ for tax in self.get('advance_tax'):
+ at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
+
+ if cancel:
+ frappe.qb.update(at).set(
+ at.allocated_amount, at.allocated_amount - tax.allocated_amount
+ ).where(at.name == tax.reference_detail).run()
+ else:
+ frappe.qb.update(at).set(
+ at.allocated_amount, at.allocated_amount + tax.allocated_amount
+ ).where(at.name == tax.reference_detail).run()
+
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 6e81c6d..aa2408e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -13,6 +13,7 @@
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
+from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -35,6 +36,27 @@
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
+ def test_purchase_invoice_received_qty(self):
+ """
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
+ """
+ pi = make_purchase_invoice(
+ qty=1,
+ rejected_qty=1,
+ received_qty=3,
+ item_code="_Test Item Home Desktop 200",
+ rejected_warehouse = "_Test Rejected Warehouse - _TC",
+ update_stock=True, do_not_save=True)
+ self.assertRaises(QtyMismatchError, pi.save)
+
+ pi.items[0].received_qty = 0
+ pi.save()
+ self.assertEqual(pi.items[0].received_qty, 2)
+
+ # teardown
+ pi.delete()
+
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
pi = frappe.copy_doc(test_records[0])
@@ -811,29 +833,12 @@
pi.shipping_rule = shipping_rule.name
pi.insert()
-
- shipping_amount = 0.0
- for condition in shipping_rule.get("conditions"):
- if not condition.to_value or (flt(condition.from_value) <= pi.net_total <= flt(condition.to_value)):
- shipping_amount = condition.shipping_amount
-
- shipping_charge = {
- "doctype": "Purchase Taxes and Charges",
- "category": "Valuation and Total",
- "charge_type": "Actual",
- "account_head": shipping_rule.account,
- "cost_center": shipping_rule.cost_center,
- "tax_amount": shipping_amount,
- "description": shipping_rule.name,
- "add_deduct_tax": "Add"
- }
- pi.append("taxes", shipping_charge)
pi.save()
self.assertEqual(pi.net_total, 1250)
- self.assertEqual(pi.total_taxes_and_charges, 462.3)
- self.assertEqual(pi.grand_total, 1712.3)
+ self.assertEqual(pi.total_taxes_and_charges, 354.1)
+ self.assertEqual(pi.grand_total, 1604.1)
def test_make_pi_without_terms(self):
pi = make_purchase_invoice(do_not_save=1)
@@ -1155,25 +1160,21 @@
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15')
- po.apply_tds = 1
- po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
po.submit()
- # Update Unrealized Profit / Loss Account which is used as default advance tax account
- frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
-
# Create Payment Entry Against the order
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
payment_entry.paid_from = 'Cash - _TC'
+ payment_entry.apply_tax_withholding_amount = 1
+ payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
payment_entry.save()
payment_entry.submit()
# Check GLE for Payment Entry
expected_gle = [
- ['_Test Account Excise Duty - _TC', 3000, 0],
['Cash - _TC', 0, 27000],
- ['Creditors - _TC', 27000, 0],
+ ['Creditors - _TC', 30000, 0],
['TDS Payable - _TC', 0, 3000],
]
@@ -1199,9 +1200,7 @@
# Zero net effect on final TDS Payable on invoice
expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000],
- ['_Test Account Excise Duty - _TC', -3000],
- ['Creditors - _TC', -27000],
- ['TDS Payable - _TC', 0]
+ ['Creditors - _TC', -30000]
]
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
@@ -1214,6 +1213,14 @@
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount)
+ payment_entry.load_from_db()
+ self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
+
+ purchase_invoice.cancel()
+
+ payment_entry.load_from_db()
+ self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index d39a9fc..f9b2efd 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -22,10 +22,10 @@
"received_qty",
"qty",
"rejected_qty",
- "stock_uom",
"col_break2",
"uom",
"conversion_factor",
+ "stock_uom",
"stock_qty",
"sec_break1",
"price_list_rate",
@@ -175,7 +175,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Qty"
+ "label": "Received Qty",
+ "read_only": 1
},
{
"bold": 1,
@@ -223,7 +224,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
- "label": "Stock Qty",
+ "label": "Accepted Qty in Stock UOM",
"print_hide": 1,
"read_only": 1,
"reqd": 1
@@ -870,10 +871,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 16:04:03.538643",
+ "modified": "2021-11-15 17:04:07.191013",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index bee153b..39dfd8d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -15,8 +15,17 @@
let me = this;
if (this.frm.doc.company) {
- frappe.db.get_value('Company', this.frm.doc.company, 'default_receivable_account', (r) => {
- me.frm.set_value('debit_to', r.default_receivable_account);
+ frappe.call({
+ method:
+ "erpnext.accounts.party.get_party_account",
+ args: {
+ party_type: 'Customer',
+ party: this.frm.doc.customer,
+ company: this.frm.doc.company
+ },
+ callback: (response) => {
+ if (response) me.frm.set_value("debit_to", response.message);
+ },
});
}
}
@@ -507,15 +516,6 @@
}
}
-// project name
-//--------------------------
-cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
- return{
- query: "erpnext.controllers.queries.get_project_name",
- filters: {'customer': doc.customer}
- }
-}
-
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
@@ -969,7 +969,7 @@
}
if (frm.doc.is_debit_note) {
- frm.set_df_property('return_against', 'label', 'Adjustment Against');
+ frm.set_df_property('return_against', 'label', __('Adjustment Against'));
}
if (frappe.boot.active_domains.includes("Healthcare")) {
@@ -979,10 +979,10 @@
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
- },"Get Items From");
+ },__("Get Items From"));
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
- },"Get Items From");
+ },__("Get Items From"));
}
}
else {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 93e32f1..545abf7 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -182,6 +182,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -2019,6 +2020,12 @@
"label": "Total Billing Hours",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-10-11 20:19:38.667508",
+ "modified": "2021-10-21 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2086,4 +2093,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 5297cc9..7a1f591 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -831,8 +831,6 @@
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
- self.allocate_advance_taxes(gl_entries)
-
self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries)
@@ -1935,7 +1933,7 @@
mpa.parent = mp.name and
mpa.company = %s and
mp.enabled = 1 and
- mp.name in (%s)
+ mp.name in %s
group by
mp.name
""", (company, mode_of_payments), as_dict=1)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 969756a..6a488ea 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1603,28 +1603,12 @@
si.shipping_rule = shipping_rule.name
si.insert()
-
- shipping_amount = 0.0
- for condition in shipping_rule.get("conditions"):
- if not condition.to_value or (flt(condition.from_value) <= si.net_total <= flt(condition.to_value)):
- shipping_amount = condition.shipping_amount
-
- shipping_charge = {
- "doctype": "Sales Taxes and Charges",
- "category": "Valuation and Total",
- "charge_type": "Actual",
- "account_head": shipping_rule.account,
- "cost_center": shipping_rule.cost_center,
- "tax_amount": shipping_amount,
- "description": shipping_rule.name
- }
- si.append("taxes", shipping_charge)
si.save()
self.assertEqual(si.net_total, 1250)
- self.assertEqual(si.total_taxes_and_charges, 577.05)
- self.assertEqual(si.grand_total, 1827.05)
+ self.assertEqual(si.total_taxes_and_charges, 468.85)
+ self.assertEqual(si.grand_total, 1718.85)
@@ -2316,6 +2300,7 @@
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
# create a customer
@@ -2324,13 +2309,7 @@
supplier = create_supplier(supplier_name="_Test Common Supplier").name
# create a party link between customer & supplier
- # set primary role as supplier
- party_link = frappe.new_doc("Party Link")
- party_link.primary_role = "Supplier"
- party_link.primary_party = supplier
- party_link.secondary_role = "Customer"
- party_link.secondary_party = customer
- party_link.save()
+ party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
@@ -2406,6 +2385,29 @@
si.reload()
self.assertEqual(si.status, "Paid")
+ def test_sales_commission(self):
+ si = frappe.copy_doc(test_records[0])
+ item = copy.deepcopy(si.get('items')[0])
+ item.update({
+ "qty": 1,
+ "rate": 500,
+ "grant_commission": 1
+ })
+ si.append("items", item)
+
+ # Test valid values
+ for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
+ si.commission_rate = commission_rate
+ si.save()
+ self.assertEqual(si.amount_eligible_for_commission, 500)
+ self.assertEqual(si.total_commission, total_commission)
+
+ # Test invalid values
+ for commission_rate in (101, -1):
+ si.reload()
+ si.commission_rate = commission_rate
+ self.assertRaises(frappe.ValidationError, si.save)
+
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
@@ -2418,6 +2420,32 @@
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ def test_over_billing_case_against_delivery_note(self):
+ '''
+ Test a case where duplicating the item with qty = 1 in the invoice
+ allows overbilling even if it is disabled
+ '''
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')
+ frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0)
+
+ dn = create_delivery_note()
+ dn.submit()
+
+ si = make_sales_invoice(dn.name)
+ # make a copy of first item and add it to invoice
+ item_copy = frappe.copy_doc(si.items[0])
+ si.append('items', item_copy)
+ si.save()
+
+ with self.assertRaises(frappe.ValidationError) as err:
+ si.submit()
+
+ self.assertTrue("cannot overbill" in str(err.exception).lower())
+
+ frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
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 b90f3f0..ae9ac35 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -47,6 +47,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -828,15 +829,23 @@
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-19 13:41:53.435827",
+ "modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 63b714e..1dae87f 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -519,8 +519,6 @@
2. Change the `Subscription` status to 'Past Due Date'
3. Change the `Subscription` status to 'Cancelled'
"""
- if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
- self.update_subscription_period(add_days(self.current_invoice_end, 1))
if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
@@ -528,6 +526,9 @@
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate)
+ if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
+ self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
self.cancel_subscription_at_period_end()
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index dc1818a..5bb9b93 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -95,7 +95,7 @@
frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
.format(tax_withholding_category, inv.company, party))
- tax_amount, tax_deducted = get_tax_amount(
+ tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
party_type, parties,
inv, tax_details,
posting_date, pan_no
@@ -106,7 +106,10 @@
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
- return tax_row
+ if inv.doctype == 'Purchase Invoice':
+ return tax_row, tax_deducted_on_advances
+ else:
+ return tax_row
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
@@ -194,6 +197,10 @@
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
to_date=tax_details.to_date, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
+ tax_deducted_on_advances = 0
+
+ if inv.doctype == 'Purchase Invoice':
+ tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details)
tax_deducted = 0
if taxable_vouchers:
@@ -223,7 +230,7 @@
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
- return tax_amount, tax_deducted
+ return tax_amount, tax_deducted, tax_deducted_on_advances
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
@@ -281,6 +288,29 @@
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
+def get_taxes_deducted_on_advances_allocated(inv, tax_details):
+ advances = [d.reference_name for d in inv.get('advances')]
+ tax_info = []
+
+ if advances:
+ pe = frappe.qb.DocType("Payment Entry").as_("pe")
+ at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
+
+ tax_info = frappe.qb.from_(at).inner_join(pe).on(
+ pe.name == at.parent
+ ).select(
+ at.parent, at.name, at.tax_amount, at.allocated_amount
+ ).where(
+ pe.tax_withholding_category == tax_details.get('tax_withholding_category')
+ ).where(
+ at.parent.isin(advances)
+ ).where(
+ at.account_head == tax_details.account_head
+ ).run(as_dict=True)
+
+ return tax_info
+
+
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 8ef7d7e..1836db6 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -73,8 +73,28 @@
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
+ update_net_values(entry)
+
return gl_map
+def update_net_values(entry):
+ # In some scenarios net value needs to be shown in the ledger
+ # This method updates net values as debit or credit
+ if entry.post_net_value and entry.debit and entry.credit:
+ if entry.debit > entry.credit:
+ entry.debit = entry.debit - entry.credit
+ entry.debit_in_account_currency = entry.debit_in_account_currency \
+ - entry.credit_in_account_currency
+ entry.credit = 0
+ entry.credit_in_account_currency = 0
+ else:
+ entry.credit = entry.credit - entry.debit
+ entry.credit_in_account_currency = entry.credit_in_account_currency \
+ - entry.debit_in_account_currency
+
+ entry.debit = 0
+ entry.debit_in_account_currency = 0
+
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 2108bc1..e904768 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -83,7 +83,8 @@
if party_type=="Customer":
party_details["sales_team"] = [{
"sales_person": d.sales_person,
- "allocated_percentage": d.allocated_percentage or None
+ "allocated_percentage": d.allocated_percentage or None,
+ "commission_rate": d.commission_rate
} for d in party.get("sales_team")]
# supplier tax withholding category
@@ -218,7 +219,7 @@
return out
@frappe.whitelist()
-def get_party_account(party_type, party, company=None):
+def get_party_account(party_type, party=None, company=None):
"""Returns the account for the given `party`.
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
@@ -226,8 +227,11 @@
if not company:
frappe.throw(_("Please select a Company"))
- if not party:
- return
+ if not party and party_type in ['Customer', 'Supplier']:
+ default_account_name = "default_receivable_account" \
+ if party_type=="Customer" else "default_payable_account"
+
+ return frappe.get_cached_value('Company', company, default_account_name)
account = frappe.db.get_value("Party Account",
{"parenttype": party_type, "parent": party, "company": company}, "account")
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index e24a5f9..d3e836a 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -92,6 +92,11 @@
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
+ },
+ {
+ "fieldname": "show_zero_values",
+ "label": __("Show zero values"),
+ "fieldtype": "Check"
}
],
"formatter": function(value, row, column, data, default_formatter) {
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index c71bc17..01799d5 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -22,7 +22,11 @@
get_cash_flow_accounts,
)
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
-from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts
+from erpnext.accounts.report.financial_statements import (
+ filter_out_zero_value_rows,
+ get_fiscal_year_data,
+ sort_accounts,
+)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data,
)
@@ -265,7 +269,7 @@
return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
- accounts, accounts_by_name = get_account_heads(root_type,
+ accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters)
if not accounts: return []
@@ -294,6 +298,8 @@
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
+ out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
+
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -370,7 +376,7 @@
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
- return accounts, accounts_by_name
+ return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 4bb44b3..1e89b65 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -420,8 +420,7 @@
{additional_conditions}
and posting_date <= %(to_date)s
and is_cancelled = 0
- {distributed_cost_center_query}
- order by account, posting_date""".format(
+ {distributed_cost_center_query}""".format(
additional_conditions=additional_conditions,
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 050403d..385c8b2 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -6,7 +6,7 @@
import frappe
from frappe import _, _dict
-from frappe.utils import cstr, flt, getdate
+from frappe.utils import cstr, getdate
from erpnext import get_company_currency, get_default_company
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -17,6 +17,8 @@
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_account_currency
+# to cache translations
+TRANSLATIONS = frappe._dict()
def execute(filters=None):
if not filters:
@@ -42,10 +44,20 @@
columns = get_columns(filters)
+ update_translations()
+
res = get_result(filters, account_details)
return columns, res
+def update_translations():
+ TRANSLATIONS.update(
+ dict(
+ OPENING = _('Opening'),
+ TOTAL = _('Total'),
+ CLOSING_TOTAL = _('Closing (Opening + Total)')
+ )
+ )
def validate_filters(filters, account_details):
if not filters.get("company"):
@@ -351,9 +363,9 @@
credit_in_account_currency=0.0
)
return _dict(
- opening = _get_debit_credit_dict(_('Opening')),
- total = _get_debit_credit_dict(_('Total')),
- closing = _get_debit_credit_dict(_('Closing (Opening + Total)'))
+ opening = _get_debit_credit_dict(TRANSLATIONS.OPENING),
+ total = _get_debit_credit_dict(TRANSLATIONS.TOTAL),
+ closing = _get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL)
)
def group_by_field(group_by):
@@ -378,22 +390,23 @@
entries = []
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get('group_by'))
+ group_by_voucher_consolidated = filters.get("group_by") == 'Group by Voucher (Consolidated)'
if filters.get('show_net_values_in_party_account'):
account_type_map = get_account_type_map(filters.get('company'))
def update_value_in_dict(data, key, gle):
- data[key].debit += flt(gle.debit)
- data[key].credit += flt(gle.credit)
+ data[key].debit += gle.debit
+ data[key].credit += gle.credit
- data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
- data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
+ data[key].debit_in_account_currency += gle.debit_in_account_currency
+ data[key].credit_in_account_currency += gle.credit_in_account_currency
if filters.get('show_net_values_in_party_account') and \
account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
- net_value = flt(data[key].debit) - flt(data[key].credit)
- net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
- - flt(data[key].credit_in_account_currency)
+ net_value = data[key].debit - data[key].credit
+ net_value_in_account_currency = data[key].debit_in_account_currency \
+ - data[key].credit_in_account_currency
if net_value < 0:
dr_or_cr = 'credit'
@@ -411,19 +424,29 @@
data[key].against_voucher += ', ' + gle.against_voucher
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
- for gle in gl_entries:
- if (gle.posting_date < from_date or
- (cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries"))):
- update_value_in_dict(gle_map[gle.get(group_by)].totals, 'opening', gle)
- update_value_in_dict(totals, 'opening', gle)
+ show_opening_entries = filters.get("show_opening_entries")
- update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
+ for gle in gl_entries:
+ group_by_value = gle.get(group_by)
+
+ if (gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries)):
+ if not group_by_voucher_consolidated:
+ update_value_in_dict(gle_map[group_by_value].totals, 'opening', gle)
+ update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle)
+
+ update_value_in_dict(totals, 'opening', gle)
update_value_in_dict(totals, 'closing', gle)
elif gle.posting_date <= to_date:
- if filters.get("group_by") != 'Group by Voucher (Consolidated)':
- gle_map[gle.get(group_by)].entries.append(gle)
- elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
+ if not group_by_voucher_consolidated:
+ update_value_in_dict(gle_map[group_by_value].totals, 'total', gle)
+ update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle)
+ update_value_in_dict(totals, 'total', gle)
+ update_value_in_dict(totals, 'closing', gle)
+
+ gle_map[group_by_value].entries.append(gle)
+
+ elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
@@ -435,9 +458,7 @@
update_value_in_dict(consolidated_gle, key, gle)
for key, value in consolidated_gle.items():
- update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
update_value_in_dict(totals, 'total', value)
- update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
update_value_in_dict(totals, 'closing', value)
entries.append(value)
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 856b97d..685f2d6 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -44,7 +44,7 @@
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (data && data.indent == 0.0) {
+ if (data && (data.indent == 0.0 || row[1].content == "Total")) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index 5fff3fd..76c560a 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "modified": "2021-08-19 18:57:07.468202",
+ "modified": "2021-11-13 19:14:23.730198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 9d5a242..84effc0 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -19,7 +19,7 @@
data = []
group_wise_columns = frappe._dict({
- "invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \
+ "invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description",
"warehouse", "qty", "base_rate", "buying_rate", "base_amount",
"buying_amount", "gross_profit", "gross_profit_percent", "project"],
"item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate",
@@ -77,13 +77,15 @@
row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
- row[0] = frappe.bold("Total")
+ row[0] = "Total"
+
data.append(row)
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
+ "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
"posting_date": _("Posting Date") + ":Date:100",
"posting_time": _("Posting Time") + ":Data:100",
"item_code": _("Item Code") + ":Link/Item:100",
@@ -122,7 +124,7 @@
def get_column_names():
return frappe._dict({
- 'parent': 'sales_invoice',
+ 'invoice_or_item': 'sales_invoice',
'customer': 'customer',
'customer_group': 'customer_group',
'posting_date': 'posting_date',
@@ -245,19 +247,28 @@
self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
- if row.parent in self.returned_invoices \
- and row.item_code in self.returned_invoices[row.parent]:
- returned_item_rows = self.returned_invoices[row.parent][row.item_code]
- for returned_item_row in returned_item_rows:
- row.qty += flt(returned_item_row.qty)
- row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
- row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
- if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row):
- row = self.set_average_rate(row)
- self.grouped_data.append(row)
- self.add_to_totals(row)
+ if row.indent == 1.0:
+ if row.parent in self.returned_invoices \
+ and row.item_code in self.returned_invoices[row.parent]:
+ returned_item_rows = self.returned_invoices[row.parent][row.item_code]
+ for returned_item_row in returned_item_rows:
+ row.qty += flt(returned_item_row.qty)
+ row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
+ row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
+ if (flt(row.qty) or row.base_amount):
+ row = self.set_average_rate(row)
+ self.grouped_data.append(row)
+ self.add_to_totals(row)
+
self.set_average_gross_profit(self.totals)
- self.grouped_data.append(self.totals)
+
+ if self.filters.get("group_by") == "Invoice":
+ self.totals.indent = 0.0
+ self.totals.parent_invoice = ""
+ self.totals.invoice_or_item = "Total"
+ self.si_list.append(self.totals)
+ else:
+ self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -446,7 +457,7 @@
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
- row.parent = row.item_code
+ row.invoice_or_item = row.item_code
if frappe.db.exists('Product Bundle', row.item_code):
self.add_bundle_items(row, index)
@@ -455,7 +466,8 @@
return frappe._dict({
'parent_invoice': "",
'indent': 0.0,
- 'parent': row.parent,
+ 'invoice_or_item': row.parent,
+ 'parent': None,
'posting_date': row.posting_date,
'posting_time': row.posting_time,
'project': row.project,
@@ -499,7 +511,8 @@
return frappe._dict({
'parent_invoice': product_bundle.item_code,
'indent': product_bundle.indent + 1,
- 'parent': item.item_code,
+ 'parent': None,
+ 'invoice_or_item': item.item_code,
'posting_date': product_bundle.posting_date,
'posting_time': product_bundle.posting_time,
'project': product_bundle.project,
diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
index a740de3..9dd4e43 100644
--- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
+++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
@@ -1,27 +1,30 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2013-05-06 12:28:23",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 3,
- "is_standard": "Yes",
- "modified": "2017-03-06 05:52:57.645281",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Partners Commission",
- "owner": "Administrator",
- "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
- "ref_doctype": "Sales Invoice",
- "report_name": "Sales Partners Commission",
- "report_type": "Query Report",
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2013-05-06 12:28:23",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 3,
+ "is_standard": "Yes",
+ "modified": "2021-10-06 06:26:07.881340",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Partners Commission",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
+ "ref_doctype": "Sales Invoice",
+ "report_name": "Sales Partners Commission",
+ "report_type": "Query Report",
"roles": [
{
"role": "Accounts Manager"
- },
+ },
{
"role": "Accounts User"
}
]
-}
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 18a56d3..d554d52 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -60,6 +60,10 @@
if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime());
}
+ },
+
+ stock_items_on_form_rendered() {
+ erpnext.setup_serial_or_batch_no();
}
});
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index d780c18..36848e9 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -118,9 +118,10 @@
for stock_item in self.get('stock_items'):
stock_entry.append('items', {
"s_warehouse": self.warehouse,
- "item_code": stock_item.item,
+ "item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity,
- "basic_rate": stock_item.valuation_rate
+ "basic_rate": stock_item.valuation_rate,
+ "serial_no": stock_item.serial_no
})
stock_entry.insert()
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 81b4f6c..7c0d057 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -11,12 +11,15 @@
create_asset_data,
set_depreciation_settings_in_company,
)
+from erpnext.stock.doctype.item.test_item import create_item
class TestAssetRepair(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
set_depreciation_settings_in_company()
create_asset_data()
+ create_item("_Test Stock Item")
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
@@ -70,9 +73,28 @@
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
- self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
+ self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+ def test_serialized_item_consumption(self):
+ from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ stock_entry = make_serialized_item()
+ serial_nos = stock_entry.get("items")[0].serial_no
+ serial_no = serial_nos.split("\n")[0]
+
+ # should not raise any error
+ create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code,
+ warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1)
+
+ # should raise error
+ asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC",
+ item_code = stock_entry.get("items")[0].item_code)
+
+ asset_repair.repair_status = "Completed"
+ self.assertRaises(SerialNoRequiredError, asset_repair.submit)
+
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
@@ -137,11 +159,12 @@
if args.stock_consumption:
asset_repair.stock_consumption = 1
- asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", {
- "item": args.item or args.item_code or "_Test Item",
+ "item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
- "consumed_quantity": args.qty or 1
+ "consumed_quantity": args.qty or 1,
+ "serial_no": args.serial_no
})
asset_repair.insert(ignore_if_duplicate=True)
@@ -158,7 +181,7 @@
})
stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse,
- "item_code": asset_repair.stock_items[0].item,
+ "item_code": asset_repair.stock_items[0].item_code,
"qty": asset_repair.stock_items[0].consumed_quantity
})
stock_entry.submit()
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
index 528f0ec..f63add1 100644
--- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -5,20 +5,14 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "item",
+ "item_code",
"valuation_rate",
"consumed_quantity",
- "total_value"
+ "total_value",
+ "serial_no"
],
"fields": [
{
- "fieldname": "item",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Item",
- "options": "Item"
- },
- {
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
@@ -38,12 +32,24 @@
"in_list_view": 1,
"label": "Total Value",
"read_only": 1
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-12 03:19:55.006300",
+ "modified": "2021-11-11 18:23:00.492483",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index 79c8861..36f510b 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -14,6 +14,14 @@
}
}
});
+ frm.set_query('asset', function() {
+ return {
+ filters: {
+ calculate_depreciation: 1,
+ docstatus: 1
+ }
+ };
+ });
},
onload: function(frm) {
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 b93f474..0b646ed 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -10,7 +10,11 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
+from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
+from erpnext.regional.india.utils import (
+ get_depreciation_amount as get_depreciation_amount_for_india,
+)
class AssetValueAdjustment(Document):
@@ -90,6 +94,7 @@
def reschedule_depreciations(self, asset_value):
asset = frappe.get_doc('Asset', self.asset)
+ country = frappe.get_value('Company', self.company, 'country')
for d in asset.finance_books:
d.value_after_depreciation = asset_value
@@ -111,8 +116,10 @@
depreciation_amount = days * rate_per_day
from_date = data.schedule_date
else:
- depreciation_amount = asset.get_depreciation_amount(value_after_depreciation,
- no_of_depreciations, d)
+ if country == "India":
+ depreciation_amount = get_depreciation_amount_for_india(asset, value_after_depreciation, d)
+ else:
+ depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount)
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 7ee9196..f0899b0 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -83,6 +83,12 @@
frm.trigger("get_supplier_group_details");
}, __('Actions'));
+ if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ frm.add_custom_button(__('Link with Customer'), function () {
+ frm.trigger('show_party_link_dialog');
+ }, __('Actions'));
+ }
+
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
@@ -128,5 +134,42 @@
else {
frm.toggle_reqd("represents_company", false);
}
+ },
+ show_party_link_dialog: function(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __('Select a Customer'),
+ fields: [{
+ fieldtype: 'Link', label: __('Customer'),
+ options: 'Customer', fieldname: 'customer', reqd: 1
+ }],
+ primary_action: function({ customer }) {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link',
+ args: {
+ primary_role: 'Supplier',
+ primary_party: frm.doc.name,
+ secondary_party: customer
+ },
+ freeze: true,
+ callback: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Successfully linked to Customer'),
+ alert: true
+ });
+ },
+ error: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Linking to Customer Failed. Please try again.'),
+ title: __('Linking Failed'),
+ indicator: 'red'
+ });
+ }
+ });
+ },
+ primary_action_label: __('Create Link')
+ });
+ dialog.show();
}
});
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 32659d6..14d2ccd 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -11,7 +11,11 @@
)
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
-from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
+from erpnext.accounts.party import ( # noqa
+ get_dashboard_info,
+ get_timeline_data,
+ validate_party_accounts,
+)
from erpnext.utilities.transaction_base import TransactionBase
diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json
index 2623585..3668b25 100644
--- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json
+++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.json
@@ -1,184 +1,70 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:criteria_name",
- "beta": 0,
- "creation": "2017-05-29 01:32:43.064891",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:criteria_name",
+ "creation": "2017-05-29 01:32:43.064891",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "criteria_name",
+ "max_score",
+ "formula",
+ "weight"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "criteria_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Criteria Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
+ "fieldname": "criteria_name",
+ "fieldtype": "Data",
+ "label": "Criteria Name",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "100",
- "fieldname": "max_score",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Max Score",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "100",
+ "fieldname": "max_score",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Max Score",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "formula",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 1,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Criteria Formula",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "formula",
+ "fieldtype": "Small Text",
+ "ignore_xss_filter": 1,
+ "in_list_view": 1,
+ "label": "Criteria Formula",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "weight",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Criteria Weight",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "weight",
+ "fieldtype": "Percent",
+ "label": "Criteria Weight"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-22 10:47:00.000822",
- "modified_by": "Administrator",
- "module": "Buying",
- "name": "Supplier Scorecard Criteria",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2021-11-11 18:34:58.477648",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Supplier Scorecard Criteria",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 9fa22ed..63e7904 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -146,11 +146,6 @@
self.validate_party()
self.validate_currency()
- if self.doctype == 'Purchase Invoice':
- self.calculate_paid_amount()
- # apply tax withholding only if checked and applicable
- self.set_tax_withholding()
-
if self.doctype in ['Purchase Invoice', 'Sales Invoice']:
pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
@@ -165,6 +160,11 @@
self.set_inter_company_account()
+ if self.doctype == 'Purchase Invoice':
+ self.calculate_paid_amount()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
+
validate_regional(self)
if self.doctype != 'Material Request':
@@ -251,7 +251,12 @@
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
calculate_taxes_and_totals(self)
- if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
+ if self.doctype in (
+ 'Sales Order',
+ 'Delivery Note',
+ 'Sales Invoice',
+ 'POS Invoice',
+ ):
self.calculate_commission()
self.calculate_contribution()
@@ -525,7 +530,8 @@
'is_opening': self.get("is_opening") or "No",
'party_type': None,
'party': None,
- 'project': self.get("project")
+ 'project': self.get("project"),
+ 'post_net_value': args.get('post_net_value')
})
accounting_dimensions = get_accounting_dimensions()
@@ -806,7 +812,6 @@
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
- self.update_allocated_advance_taxes_on_cancel()
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
unlink_ref_doc_from_payment_entries(self)
@@ -854,29 +859,6 @@
return tax_map
- def update_allocated_advance_taxes_on_cancel(self):
- if self.get('advances'):
- tax_accounts = [d.account_head for d in self.get('taxes')]
- allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'],
- filters={'voucher_no': self.name, 'account': ('in', tax_accounts)},
- group_by='account', as_list=1))
-
- tax_map = self.get_tax_map()
-
- for pe in self.get('advances'):
- if pe.reference_type == 'Payment Entry':
- pe = frappe.get_doc('Payment Entry', pe.reference_name)
- for tax in pe.get('taxes'):
- allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head)
- if allocated_amount > tax.tax_amount:
- allocated_amount = tax.tax_amount
-
- if allocated_amount:
- frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount',
- tax.allocated_amount - allocated_amount)
- tax_map[tax.account_head] -= allocated_amount
- allocated_tax_map[tax.account_head] -= allocated_amount
-
def get_amount_and_base_amount(self, item, enable_discount_accounting):
amount = item.net_amount
base_amount = item.base_net_amount
@@ -960,58 +942,10 @@
}, item=self)
)
- def allocate_advance_taxes(self, gl_entries):
- tax_map = self.get_tax_map()
- for pe in self.get("advances"):
- if pe.reference_type == "Payment Entry" and \
- frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'):
- pe = frappe.get_doc("Payment Entry", pe.reference_name)
- for tax in pe.get("taxes"):
- account_currency = get_account_currency(tax.account_head)
-
- if self.doctype == "Purchase Invoice":
- dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
- rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
- else:
- dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
- rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
-
- party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
- unallocated_amount = tax.tax_amount - tax.allocated_amount
- if tax_map.get(tax.account_head):
- amount = tax_map.get(tax.account_head)
- if amount < unallocated_amount:
- unallocated_amount = amount
-
- gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "against": party,
- dr_or_cr: unallocated_amount,
- dr_or_cr + "_in_account_currency": unallocated_amount
- if account_currency==self.company_currency
- else unallocated_amount,
- "cost_center": tax.cost_center
- }, account_currency, item=tax))
-
- gl_entries.append(
- self.get_gl_dict({
- "account": pe.advance_tax_account,
- "against": party,
- rev_dr_cr: unallocated_amount,
- rev_dr_cr + "_in_account_currency": unallocated_amount
- if account_currency==self.company_currency
- else unallocated_amount,
- "cost_center": tax.cost_center
- }, account_currency, item=tax))
-
- frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount",
- tax.allocated_amount + unallocated_amount)
-
- tax_map[tax.account_head] -= unallocated_amount
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
from erpnext.controllers.status_updater import get_allowance_for
+
item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None
@@ -1032,12 +966,7 @@
.format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
continue
- already_billed = frappe.db.sql("""
- select sum(%s)
- from `tab%s`
- where %s=%s and docstatus=1 and parent != %s
- """ % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
- (item.get(item_ref_dn), self.name))[0][0]
+ already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
self.precision(based_on, item))
@@ -1065,6 +994,43 @@
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
.format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
+ def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
+ '''
+ Returns Sum of Amount of
+ Sales/Purchase Invoice Items
+ that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
+ that are submitted OR not submitted but are under current invoice
+ '''
+
+ from frappe.query_builder import Criterion
+ from frappe.query_builder.functions import Sum
+
+ item_doctype = frappe.qb.DocType(item.doctype)
+ based_on_field = frappe.qb.Field(based_on)
+ join_field = frappe.qb.Field(item_ref_dn)
+
+ result = (
+ frappe.qb.from_(item_doctype)
+ .select(Sum(based_on_field))
+ .where(
+ join_field == item.get(item_ref_dn)
+ ).where(
+ Criterion.any([ # select all items from other invoices OR current invoices
+ Criterion.all([ # for selecting items from other invoices
+ item_doctype.docstatus == 1,
+ item_doctype.parent != self.name
+ ]),
+ Criterion.all([ # for selecting items from current invoice, that are linked to same reference
+ item_doctype.docstatus == 0,
+ item_doctype.parent == self.name,
+ item_doctype.name != item.name
+ ])
+ ])
+ )
+ ).run()
+
+ return result[0][0] if result else 0
+
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
@@ -1403,8 +1369,8 @@
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
- if flt(total, self.precision("grand_total")) != flt(grand_total, self.precision("grand_total")) or \
- flt(base_total, self.precision("base_grand_total")) != flt(base_grand_total, self.precision("base_grand_total")):
+ if flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total")) > 0.1 or \
+ flt(base_total, self.precision("base_grand_total")) - flt(base_grand_total, self.precision("base_grand_total")) > 0.1:
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 9965c87..a3d2502 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, msgprint
+from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, getdate
@@ -17,6 +17,9 @@
from erpnext.stock.utils import get_incoming_rate
+class QtyMismatchError(ValidationError):
+ pass
+
class BuyingController(StockController, Subcontracting):
def get_feed(self):
@@ -360,19 +363,15 @@
def validate_accepted_rejected_qty(self):
for d in self.get("items"):
self.validate_negative_quantity(d, ["received_qty","qty", "rejected_qty"])
- if not flt(d.received_qty) and flt(d.qty):
- d.received_qty = flt(d.qty) - flt(d.rejected_qty)
- elif not flt(d.qty) and flt(d.rejected_qty):
- d.qty = flt(d.received_qty) - flt(d.rejected_qty)
+ if not flt(d.received_qty) and (flt(d.qty) or flt(d.rejected_qty)):
+ d.received_qty = flt(d.qty) + flt(d.rejected_qty)
- elif not flt(d.rejected_qty):
- d.rejected_qty = flt(d.received_qty) - flt(d.qty)
-
- val = flt(d.qty) + flt(d.rejected_qty)
# Check Received Qty = Accepted Qty + Rejected Qty
+ val = flt(d.qty) + flt(d.rejected_qty)
if (flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty"))):
- frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code))
+ message = _("Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}").format(d.idx, d.item_code)
+ frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError)
def validate_negative_quantity(self, item_row, field_list):
if self.is_return:
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 76a7d1a..dc04dab 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -539,6 +539,10 @@
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
+ or_filters = []
+ fields = ['name']
+
+ searchfields = frappe.get_meta(doctype).get_search_fields()
meta = frappe.get_meta(doctype)
if meta.is_tree:
@@ -550,8 +554,9 @@
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
- if txt:
- query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
+ for field in searchfields:
+ or_filters.append([field, 'LIKE', "%%%s%%" % txt])
+ fields.append(field)
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
@@ -566,10 +571,9 @@
query_filters.append(['name', query_selector, dimensions])
- output = frappe.get_list(doctype, filters=query_filters)
- result = [d.name for d in output]
+ output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1)
- return [(d,) for d in set(result)]
+ return [tuple(d) for d in set(output)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index dad3ed7..cc773b7 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -120,13 +120,27 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if self.meta.get_field("commission_rate"):
- self.round_floats_in(self, ["base_net_total", "commission_rate"])
- if self.commission_rate > 100.0:
- throw(_("Commission rate cannot be greater than 100"))
+ if not self.meta.get_field("commission_rate"):
+ return
- self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0,
- self.precision("total_commission"))
+ self.round_floats_in(
+ self, ("amount_eligible_for_commission", "commission_rate")
+ )
+
+ if not (0 <= self.commission_rate <= 100.0):
+ throw("{} {}".format(
+ _(self.meta.get_label("commission_rate")),
+ _("must be between 0 and 100"),
+ ))
+
+ self.amount_eligible_for_commission = sum(
+ item.base_net_amount for item in self.items if item.grant_commission
+ )
+
+ self.total_commission = flt(
+ self.amount_eligible_for_commission * self.commission_rate / 100.0,
+ self.precision("total_commission")
+ )
def calculate_contribution(self):
if not self.meta.get_field("sales_team"):
@@ -138,7 +152,7 @@
self.round_floats_in(sales_person)
sales_person.allocated_amount = flt(
- self.base_net_total * sales_person.allocated_percentage / 100.0,
+ self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
if sales_person.commission_rate:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 08d422d..7073e32 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -17,7 +17,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
-from erpnext.stock.stock_ledger import get_valuation_rate
+from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -134,7 +134,7 @@
"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",
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(sle.stock_value_difference, precision),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
@@ -143,7 +143,7 @@
"account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
- "remarks": self.get("remarks") or "Accounting Entry for Stock",
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"),
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No"
@@ -544,7 +544,12 @@
"company": self.company
})
if future_sle_exists(args):
- create_repost_item_valuation_entry(args)
+ item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
+ if item_based_reposting:
+ create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
+ else:
+ create_repost_item_valuation_entry(args)
+
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
@@ -676,5 +681,38 @@
repost_entry.company = args.company
repost_entry.allow_zero_rate = args.allow_zero_rate
repost_entry.flags.ignore_links = True
+ repost_entry.flags.ignore_permissions = True
repost_entry.save()
repost_entry.submit()
+
+
+def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
+ """Using a voucher create repost item valuation records for all item-warehouse pairs."""
+
+ stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
+
+ distinct_item_warehouses = set()
+ repost_entries = []
+
+ for sle in stock_ledger_entries:
+ item_wh = (sle.item_code, sle.warehouse)
+ if item_wh in distinct_item_warehouses:
+ continue
+ distinct_item_warehouses.add(item_wh)
+
+ repost_entry = frappe.new_doc("Repost Item Valuation")
+ repost_entry.based_on = "Item and Warehouse"
+ repost_entry.voucher_type = voucher_type
+ repost_entry.voucher_no = voucher_no
+
+ repost_entry.item_code = sle.item_code
+ repost_entry.warehouse = sle.warehouse
+ repost_entry.posting_date = sle.posting_date
+ repost_entry.posting_time = sle.posting_time
+ repost_entry.allow_zero_rate = allow_zero_rate
+ repost_entry.flags.ignore_links = True
+ repost_entry.flags.ignore_permissions = True
+ repost_entry.submit()
+ repost_entries.append(repost_entry)
+
+ return repost_entries
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 667edab..746c6fd 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -50,6 +50,7 @@
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
+ self.calculate_shipping_charges()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
@@ -258,6 +259,11 @@
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
+ def calculate_shipping_charges(self):
+ if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule:
+ shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
+ shipping_rule.apply(self.doc)
+
def calculate_taxes(self):
if not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = 0
diff --git a/erpnext/crm/doctype/competitor/__init__.py b/erpnext/crm/doctype/competitor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/competitor/__init__.py
diff --git a/erpnext/crm/doctype/competitor/competitor.js b/erpnext/crm/doctype/competitor/competitor.js
new file mode 100644
index 0000000..a5b617d
--- /dev/null
+++ b/erpnext/crm/doctype/competitor/competitor.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Competitor', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/competitor/competitor.json b/erpnext/crm/doctype/competitor/competitor.json
new file mode 100644
index 0000000..280441f
--- /dev/null
+++ b/erpnext/crm/doctype/competitor/competitor.json
@@ -0,0 +1,68 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:competitor_name",
+ "creation": "2021-10-21 10:28:52.071316",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "competitor_name",
+ "website"
+ ],
+ "fields": [
+ {
+ "fieldname": "competitor_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Competitor Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "website",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Website",
+ "options": "URL"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-10-21 12:43:59.106807",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Competitor",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/competitor/competitor.py b/erpnext/crm/doctype/competitor/competitor.py
new file mode 100644
index 0000000..a292e461
--- /dev/null
+++ b/erpnext/crm/doctype/competitor/competitor.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class Competitor(Document):
+ pass
diff --git a/erpnext/crm/doctype/competitor/test_competitor.py b/erpnext/crm/doctype/competitor/test_competitor.py
new file mode 100644
index 0000000..f77d7e6
--- /dev/null
+++ b/erpnext/crm/doctype/competitor/test_competitor.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestCompetitor(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/competitor_detail/__init__.py b/erpnext/crm/doctype/competitor_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/competitor_detail/__init__.py
diff --git a/erpnext/crm/doctype/competitor_detail/competitor_detail.json b/erpnext/crm/doctype/competitor_detail/competitor_detail.json
new file mode 100644
index 0000000..9512b22
--- /dev/null
+++ b/erpnext/crm/doctype/competitor_detail/competitor_detail.json
@@ -0,0 +1,33 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-10-21 10:34:58.841689",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "competitor"
+ ],
+ "fields": [
+ {
+ "fieldname": "competitor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Competitor",
+ "options": "Competitor",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-10-21 10:34:58.841689",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Competitor Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/competitor_detail/competitor_detail.py b/erpnext/crm/doctype/competitor_detail/competitor_detail.py
new file mode 100644
index 0000000..0ef7560
--- /dev/null
+++ b/erpnext/crm/doctype/competitor_detail/competitor_detail.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CompetitorDetail(Document):
+ pass
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index dc886b5..feb6044 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -23,7 +23,6 @@
"status",
"converted_by",
"sales_stage",
- "order_lost_reason",
"first_response_time",
"expected_closing",
"next_contact",
@@ -64,7 +63,11 @@
"transaction_date",
"language",
"amended_from",
- "lost_reasons"
+ "lost_detail_section",
+ "lost_reasons",
+ "order_lost_reason",
+ "column_break_56",
+ "competitors"
],
"fields": [
{
@@ -154,10 +157,9 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.status===\"Lost\"",
"fieldname": "order_lost_reason",
"fieldtype": "Small Text",
- "label": "Lost Reason",
+ "label": "Detailed Reason",
"no_copy": 1,
"read_only": 1
},
@@ -409,6 +411,7 @@
"width": "150px"
},
{
+ "depends_on": "eval:doc.status===\"Lost\"",
"fieldname": "lost_reasons",
"fieldtype": "Table MultiSelect",
"label": "Lost Reasons",
@@ -486,15 +489,33 @@
"label": "Grand Total",
"options": "currency",
"read_only": 1
+ },
+ {
+ "fieldname": "lost_detail_section",
+ "fieldtype": "Section Break",
+ "label": "Lost Reasons"
+ },
+ {
+ "fieldname": "column_break_56",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "competitors",
+ "fieldtype": "Table MultiSelect",
+ "label": "Competitors",
+ "options": "Competitor Detail",
+ "read_only": 1
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2021-09-06 10:02:18.609136",
+ "migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
+ "modified": "2021-10-21 11:04:30.151379",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 90eae8d..0bef80a 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -116,16 +116,20 @@
self.party_name = lead_name
@frappe.whitelist()
- def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
+ def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
if not self.has_active_quotation():
- frappe.db.set(self, 'status', 'Lost')
+ self.status = 'Lost'
+ self.lost_reasons = self.competitors = []
if detailed_reason:
- frappe.db.set(self, 'order_lost_reason', detailed_reason)
+ self.order_lost_reason = detailed_reason
for reason in lost_reasons_list:
self.append('lost_reasons', reason)
+ for competitor in competitors:
+ self.append('competitors', competitor)
+
self.save()
else:
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 53d1482..d9013b0 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -125,7 +125,7 @@
:param student: Student.
"""
- guardians = frappe.get_list("Student Guardian", fields=["guardian"] ,
+ guardians = frappe.get_all("Student Guardian", fields=["guardian"] ,
filters={"parent": student})
return guardians
@@ -137,10 +137,10 @@
:param student_group: Student Group.
"""
if include_inactive:
- students = frappe.get_list("Student Group Student", fields=["student", "student_name"] ,
+ students = frappe.get_all("Student Group Student", fields=["student", "student_name"] ,
filters={"parent": student_group}, order_by= "group_roll_number")
else:
- students = frappe.get_list("Student Group Student", fields=["student", "student_name"] ,
+ students = frappe.get_all("Student Group Student", fields=["student", "student_name"] ,
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
return students
@@ -164,7 +164,7 @@
:param fee_structure: Fee Structure.
"""
if fee_structure:
- fs = frappe.get_list("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
+ fs = frappe.get_all("Fee Component", fields=["fees_category", "description", "amount"] , filters={"parent": fee_structure}, order_by= "idx")
return fs
@@ -175,7 +175,7 @@
:param program: Program.
:param student_category: Student Category
"""
- fs = frappe.get_list("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] ,
+ fs = frappe.get_all("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"] ,
filters={"parent": program, "student_category": student_category }, order_by= "idx")
return fs
@@ -220,7 +220,7 @@
:param Course: Course
"""
- return frappe.get_list("Course Assessment Criteria", \
+ return frappe.get_all("Course Assessment Criteria",
fields=["assessment_criteria", "weightage"], filters={"parent": course}, order_by= "idx")
@@ -253,7 +253,7 @@
:param Assessment Plan: Assessment Plan
"""
- return frappe.get_list("Assessment Plan Criteria", \
+ return frappe.get_all("Assessment Plan Criteria",
fields=["assessment_criteria", "maximum_score", "docstatus"], filters={"parent": assessment_plan}, order_by= "idx")
diff --git a/erpnext/education/doctype/course_schedule/course_schedule.json b/erpnext/education/doctype/course_schedule/course_schedule.json
index 8c6746b..38d9b50 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule.json
+++ b/erpnext/education/doctype/course_schedule/course_schedule.json
@@ -1,520 +1,143 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2015-09-09 16:34:04.960369",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "instructor",
+ "instructor_name",
+ "column_break_2",
+ "naming_series",
+ "course",
+ "color",
+ "section_break_6",
+ "schedule_date",
+ "room",
+ "column_break_9",
+ "from_time",
+ "to_time",
+ "title"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "student_group",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Student Group",
- "length": 0,
- "no_copy": 0,
"options": "Student Group",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "instructor",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Instructor",
- "length": 0,
- "no_copy": 0,
"options": "Instructor",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "instructor.Instructor_name",
+ "fetch_from": "instructor.instructor_name",
"fieldname": "instructor_name",
"fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Instructor Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Naming Series",
- "length": 0,
- "no_copy": 0,
"options": "EDU-CSH-.YYYY.-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "course",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Course",
- "length": 0,
- "no_copy": 0,
"options": "Course",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "color",
"fieldtype": "Color",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Color",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "print_hide": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Today",
"fieldname": "schedule_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Schedule Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Schedule Date"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "room",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Room",
- "length": 0,
- "no_copy": 0,
"options": "Room",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "from_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "to_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "To Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Title"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2018-08-21 14:44:51.827225",
+ "links": [],
+ "modified": "2021-11-24 11:57:08.164449",
"modified_by": "Administrator",
"module": "Education",
"name": "Course Schedule",
- "name_case": "",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
"sort_field": "schedule_date",
"sort_order": "DESC",
- "title_field": "title",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "title_field": "title"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
index 2926fe8..13dfe38 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
@@ -1,661 +1,168 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2015-09-23 15:37:38.108475",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2015-09-23 15:37:38.108475",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "course",
+ "program",
+ "column_break_3",
+ "academic_year",
+ "academic_term",
+ "section_break_6",
+ "instructor",
+ "instructor_name",
+ "column_break_9",
+ "room",
+ "section_break_7",
+ "course_start_date",
+ "course_end_date",
+ "day",
+ "reschedule",
+ "column_break_15",
+ "from_time",
+ "to_time"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Group",
- "length": 0,
- "no_copy": 0,
- "options": "Student Group",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "student_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Group",
+ "options": "Student Group",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "course",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course",
- "length": 0,
- "no_copy": 0,
- "options": "Course",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "course",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Course",
+ "options": "Course",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "program",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Program",
- "length": 0,
- "no_copy": 0,
- "options": "Program",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "program",
+ "fieldtype": "Link",
+ "label": "Program",
+ "options": "Program",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_year",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Academic Year",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Year",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_year",
+ "fieldtype": "Link",
+ "label": "Academic Year",
+ "options": "Academic Year",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_term",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Academic Term",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "academic_term",
+ "fieldtype": "Link",
+ "label": "Academic Term",
+ "options": "Academic Term",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "instructor",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Instructor",
- "length": 0,
- "no_copy": 0,
- "options": "Instructor",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "instructor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Instructor",
+ "options": "Instructor",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "instructor.instructor_name",
- "fieldname": "instructor_name",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Instructor Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "instructor_name",
+ "fieldtype": "Read Only",
+ "label": "Instructor Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "room",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Room",
- "length": 0,
- "no_copy": 0,
- "options": "Room",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "room",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Room",
+ "options": "Room",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "from_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "label": "From Time",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course Start Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "course_start_date",
+ "fieldtype": "Date",
+ "label": "Course Start Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "day",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Day",
- "length": 0,
- "no_copy": 0,
- "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "day",
+ "fieldtype": "Select",
+ "label": "Day",
+ "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reschedule",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reschedule",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "reschedule",
+ "fieldtype": "Check",
+ "label": "Reschedule"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_15",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To TIme",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "label": "To TIme",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "course_end_date",
+ "fieldtype": "Date",
+ "label": "Course End Date",
+ "reqd": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 1,
- "hide_toolbar": 1,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2018-05-16 22:43:29.363798",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Course Scheduling Tool",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "hide_toolbar": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-11-11 09:33:18.874445",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Course Scheduling Tool",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Academics User",
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
index 4e3f98d..7deb6b1 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
@@ -14,24 +14,36 @@
student_list = []
student_attendance_list = []
- if based_on=="Course Schedule":
+ if based_on == "Course Schedule":
student_group = frappe.db.get_value("Course Schedule", course_schedule, "student_group")
if student_group:
- student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \
+ student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"],
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if not student_list:
- student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
+ student_list = frappe.get_all("Student Group Student", fields=["student", "student_name", "group_roll_number"],
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
+ table = frappe.qb.DocType("Student Attendance")
+
if course_schedule:
- student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
- course_schedule= %s''', (course_schedule), as_dict=1)
+ student_attendance_list = (
+ frappe.qb.from_(table)
+ .select(table.student, table.status)
+ .where(
+ (table.course_schedule == course_schedule)
+ )
+ ).run(as_dict=True)
else:
- student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
- student_group= %s and date= %s and \
- (course_schedule is Null or course_schedule='')''',
- (student_group, date), as_dict=1)
+ student_attendance_list = (
+ frappe.qb.from_(table)
+ .select(table.student, table.status)
+ .where(
+ (table.student_group == student_group)
+ & (table.date == date)
+ & (table.course_schedule == "") | (table.course_schedule.isnull())
+ )
+ ).run(as_dict=True)
for attendance in student_attendance_list:
for student in student_list:
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
index f5f9ce3..9409485 100644
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
@@ -19,8 +19,7 @@
)
if frappe.request.data and \
- frappe.get_request_header("X-Wc-Webhook-Signature") and \
- not sig == bytes(frappe.get_request_header("X-Wc-Webhook-Signature").encode()):
+ not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode():
frappe.throw(_("Unverified Webhook Data"))
frappe.set_user(woocommerce_settings.creation_user)
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index d2b6190..e7b4a30 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -141,6 +141,9 @@
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
+ if not isinstance(checkout_id, str):
+ frappe.throw(_("Invalid Checkout Request ID"))
+
integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0 # for multiple integration request made against a pos invoice
@@ -231,6 +234,9 @@
account_balance_response = frappe._dict(kwargs["Result"])
conversation_id = getattr(account_balance_response, "ConversationID", "")
+ if not isinstance(conversation_id, str):
+ frappe.throw(_("Invalid Conversation ID"))
+
request = frappe.get_doc("Integration Request", conversation_id)
if request.status == "Completed":
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index b52c3fc..d922d87 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -23,7 +23,6 @@
)
if frappe.request.data and \
- frappe.get_request_header(hmac_key) and \
not sig == bytes(frappe.get_request_header(hmac_key).encode()):
frappe.throw(_("Unverified Webhook Data"))
frappe.set_user(settings.modified_by)
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
index 5fe5afa..5efafd6 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
@@ -33,17 +33,6 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Shopify Settings",
- "link_count": 0,
- "link_to": "Shopify Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
"label": "Amazon MWS Settings",
"link_count": 0,
"link_to": "Amazon MWS Settings",
@@ -74,7 +63,7 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:58.951705",
+ "modified": "2021-11-23 04:30:33.106991",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations Settings",
@@ -86,4 +75,4 @@
"sequence_id": 11,
"shortcuts": [],
"title": "ERPNext Integrations Settings"
-}
+}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 2a277ee..05c46c5 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -248,20 +248,18 @@
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
- "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
+ "erpnext.regional.saudi_arabia.utils.create_qr_code",
"erpnext.erpnext_integrations.taxjar_integration.create_transaction"
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
- "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
- ],
- "on_trash": [
- "erpnext.regional.check_deletion_permission",
+ "erpnext.erpnext_integrations.taxjar_integration.delete_transaction",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
+ "on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
@@ -306,7 +304,8 @@
'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
},
"Company": {
- "on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
+ "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company",
+ "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]
},
"Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 79e8f61..88e5ca9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -96,15 +96,8 @@
'user': self.user_id
})
- if employee_user_permission_exists: return
-
- employee_user_permission_exists = frappe.db.exists('User Permission', {
- 'allow': 'Employee',
- 'for_value': self.name,
- 'user': self.user_id
- })
-
- if employee_user_permission_exists: return
+ if employee_user_permission_exists:
+ return
add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 8a8e8db..7aac2b6 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
import erpnext
@@ -41,24 +42,34 @@
self.status = "Cancelled"
def set_total_advance_paid(self):
- paid_amount = frappe.db.sql("""
- select ifnull(sum(debit), 0) as paid_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].paid_amount
+ gle = frappe.qb.DocType("GL Entry")
- return_amount = frappe.db.sql("""
- select ifnull(sum(credit), 0) as return_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and voucher_type != 'Expense Claim'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].return_amount
+ paid_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.debit).as_("paid_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].paid_amount or 0
+
+ return_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.credit).as_("return_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.voucher_type != 'Expense Claim')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].return_amount or 0
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 4ecfa60..5f2e720 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -34,6 +34,24 @@
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
+ def test_paid_amount_on_pe_cancellation(self):
+ employee_name = make_employee("_T@employe.advance")
+ advance = make_employee_advance(employee_name)
+
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 1000)
+ self.assertEqual(advance.status, "Paid")
+
+ pe.cancel()
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 0)
+ self.assertEqual(advance.status, "Unpaid")
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index 287dfba..64eee40 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -2,7 +2,6 @@
# See license.txt
import unittest
-from datetime import date
import frappe
from frappe.utils import add_days, getdate
@@ -12,16 +11,14 @@
class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
- make_employee("employee2@transfers.com")
- make_employee("employee3@transfers.com")
create_company()
- create_employee()
- create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self):
+ make_employee("employee2@transfers.com")
+
transfer_obj = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"),
@@ -43,6 +40,8 @@
self.assertEqual(transfer.docstatus, 1)
def test_new_employee_creation(self):
+ make_employee("employee3@transfers.com")
+
transfer = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"),
@@ -63,60 +62,51 @@
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
- name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
- doc = frappe.get_doc("Employee",name)
+ employee = make_employee("employee4@transfers.com",
+ company="Test Company",
+ date_of_birth=getdate("30-09-1980"),
+ date_of_joining=getdate("01-10-2021"),
+ department="Accounts - TC",
+ designation="Accountant"
+ )
+ transfer = create_employee_transfer(employee)
+
count = 0
department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"]
- dt = [getdate("01-10-2021"), date.today()]
+ dt = [getdate("01-10-2021"), getdate()]
- for data in doc.internal_work_history:
+ employee = frappe.get_doc("Employee", employee)
+ for data in employee.internal_work_history:
self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count])
count = count + 1
- data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
- doc = frappe.get_doc("Employee Transfer", data[0]["name"])
- doc.cancel()
- employee_doc = frappe.get_doc("Employee",name)
+ transfer.cancel()
+ employee.reload()
- for data in employee_doc.internal_work_history:
+ for data in employee.internal_work_history:
self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0])
-def create_employee():
- doc = frappe.get_doc({
- "doctype": "Employee",
- "first_name": "John",
- "company": "Test Company",
- "gender": "Male",
- "date_of_birth": getdate("30-09-1980"),
- "date_of_joining": getdate("01-10-2021"),
- "department": "Accounts - TC",
- "designation": "Accountant"
- })
-
- doc.save()
def create_company():
- exists = frappe.db.exists("Company", "Test Company")
- if not exists:
- doc = frappe.get_doc({
- "doctype": "Company",
- "company_name": "Test Company",
- "default_currency": "INR",
- "country": "India"
- })
+ if not frappe.db.exists("Company", "Test Company"):
+ frappe.get_doc({
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India"
+ }).insert()
- doc.save()
-def create_employee_transfer():
+def create_employee_transfer(employee):
doc = frappe.get_doc({
"doctype": "Employee Transfer",
- "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
- "transfer_date": date.today(),
+ "employee": employee,
+ "transfer_date": getdate(),
"transfer_details": [
{
"property": "Designation",
@@ -134,4 +124,6 @@
})
doc.save()
- doc.submit()
\ No newline at end of file
+ doc.submit()
+
+ return doc
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 218e97d..6655563 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -389,7 +389,9 @@
sanctioned_amount: function(frm, cdt, cdn) {
cur_frm.cscript.calculate_total(frm.doc, cdt, cdn);
frm.trigger("get_taxes");
+ frm.trigger("calculate_grand_total");
},
+
cost_center: function(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center");
}
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json
index a268c15..45b78bf 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.json
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.json
@@ -379,11 +379,12 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-04 05:35:12.040199",
+ "modified": "2021-11-22 16:26:57.787838",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
index 4550925..aa479c8 100644
--- a/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
+++ b/erpnext/hr/doctype/expense_claim_advance/expense_claim_advance.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2017-10-09 16:53:26.410762",
"doctype": "DocType",
"document_type": "Document",
@@ -50,7 +51,7 @@
"fieldname": "unclaimed_amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Unclaimed amount",
+ "label": "Unclaimed Amount",
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
@@ -65,7 +66,7 @@
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Allocated amount",
+ "label": "Allocated Amount",
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
@@ -87,7 +88,7 @@
],
"istable": 1,
"links": [],
- "modified": "2019-12-17 13:53:22.111766",
+ "modified": "2021-11-22 16:33:58.515819",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Advance",
diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
index 70a48f9..6edbcb5c 100644
--- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
+++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
@@ -94,7 +94,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Sanctioned Amount",
- "no_copy": 1,
"oldfieldname": "sanctioned_amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -120,7 +119,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-11-26 14:23:45.539922",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Detail",
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 4e829a3..5177302 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -19,8 +19,8 @@
validate_active_employee(self.employee)
self.validate_overlapping_dates()
- if self.end_date and self.end_date <= self.start_date:
- frappe.throw(_("End Date must not be lesser than Start Date"))
+ if self.end_date:
+ self.validate_from_to_dates('start_date', 'end_date')
def validate_overlapping_dates(self):
if not self.name:
diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
index a0c1a33..6a9e70a 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
@@ -3,7 +3,39 @@
import unittest
+import frappe
+from frappe.utils.data import today
+
# test_records = frappe.get_test_records('Maintenance Visit')
class TestMaintenanceVisit(unittest.TestCase):
pass
+
+def make_maintenance_visit():
+ mv = frappe.new_doc("Maintenance Visit")
+ mv.company = "_Test Company"
+ mv.customer = "_Test Customer"
+ mv.mntc_date = today()
+ mv.completion_status = "Partially Completed"
+
+ sales_person = make_sales_person("Dwight Schrute")
+
+ mv.append("purposes", {
+ "item_code": "_Test Item",
+ "sales_person": "Sales Team",
+ "description": "Test Item",
+ "work_done": "Test Work Done",
+ "service_person": sales_person.name
+ })
+ mv.insert(ignore_permissions=True)
+
+ return mv
+
+def make_sales_person(name):
+ sales_person = frappe.get_doc({
+ 'doctype': "Sales Person",
+ 'sales_person_name': name
+ })
+ sales_person.insert(ignore_if_duplicate = True)
+
+ return sales_person
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 5f5c20a..6d35d65 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -680,12 +680,6 @@
erpnext.bom.calculate_total(frm.doc);
});
-frappe.ui.form.on("BOM", "with_operations", function(frm) {
- if(!cint(frm.doc.with_operations)) {
- frm.set_value("operations", []);
- }
-});
-
frappe.tour['BOM'] = [
{
fieldname: "item",
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 6218707..218ac64 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -237,6 +237,7 @@
"options": "Price List"
},
{
+ "depends_on": "with_operations",
"fieldname": "operations_section",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -539,7 +540,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-10-27 14:52:04.500251",
+ "modified": "2021-11-18 13:04:16.271975",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
index e614a7e..eb4135e 100644
--- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html
+++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
@@ -16,26 +16,15 @@
</div>
<hr style="margin: 15px -15px;">
<p>
- {% if data.value %}
- <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
+ {% if data.value && data.value != "BOM" %}
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/bom/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
- <a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
</div>
</div>
- <hr style="margin: 15px -15px;">
- <p>
- {% if data.value %}
- <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/Form/BOM/{{ data.value }}">
- {{ __("Open BOM {0}", [data.value.bold()]) }}</a>
- {% endif %}
- {% if data.item_code %}
- <a class="btn btn-default btn-xs" href="/app/Form/Item/{{ data.item_code }}">
- {{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
- {% endif %}
- </p>
</div>
diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js
index 6e2599e..fb99add 100644
--- a/erpnext/manufacturing/doctype/bom/bom_tree.js
+++ b/erpnext/manufacturing/doctype/bom/bom_tree.js
@@ -66,6 +66,7 @@
var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.image = escape(bom.image) || "";
node.data.description = bom.description || "";
+ node.data.item_code = bom.item || "";
});
}
},
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 35be388..453ad50 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -28,6 +28,11 @@
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
+ if (frm.doc.__onload.work_order_closed) {
+ frm.disable_save();
+ return;
+ }
+
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 7dd38f4..6528199 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -396,6 +396,7 @@
"options": "Batch"
},
{
+ "collapsible": 1,
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
@@ -411,7 +412,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-09-14 00:38:46.873105",
+ "modified": "2021-11-12 10:15:03.572401",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index ff4feaa..8d00019 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -36,6 +36,7 @@
def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
self.set_onload("job_card_excess_transfer", excess_transfer)
+ self.set_onload("work_order_closed", self.is_work_order_closed())
def validate(self):
self.validate_time_logs()
@@ -44,6 +45,7 @@
self.validate_sequence_id()
self.set_sub_operations()
self.update_sub_operation_status()
+ self.validate_work_order()
def set_sub_operations(self):
if self.operation:
@@ -502,13 +504,11 @@
self.status = 'Work In Progress'
if (self.docstatus == 1 and
- (self.for_quantity <= self.transferred_qty or not self.items)):
- # consider excess transfer
- # completed qty is checked via separate validation
+ (self.for_quantity <= self.total_completed_qty or not self.items)):
self.status = 'Completed'
if self.status != 'Completed':
- if self.for_quantity == self.transferred_qty:
+ if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if update_status:
@@ -548,6 +548,18 @@
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
+ def validate_work_order(self):
+ if self.is_work_order_closed():
+ frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))
+
+ def is_work_order_closed(self):
+ if self.work_order:
+ status = frappe.get_value('Work Order', self.work_order)
+
+ if status == "Closed":
+ return True
+
+ return False
@frappe.whitelist()
def make_time_log(args):
@@ -615,17 +627,22 @@
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
- def update_item(obj, target, source_parent):
+ def update_item(source, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse
+
if not target.conversion_factor:
target.conversion_factor = 1
+ pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty)
+ if pending_rm_qty > 0:
+ target.qty = pending_rm_qty
+
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
# avoid negative 'For Quantity'
- pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+ pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index d799283..9b4fc8b 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -15,11 +15,22 @@
class TestJobCard(unittest.TestCase):
-
def setUp(self):
+ make_bom_for_jc_tests()
+
transfer_material_against, source_warehouse = None, None
- tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
- "test_job_card_excess_material_transfer")
+
+ tests_that_skip_setup = (
+ "test_job_card_material_transfer_correctness",
+ )
+ tests_that_transfer_against_jc = (
+ "test_job_card_multiple_materials_transfer",
+ "test_job_card_excess_material_transfer",
+ "test_job_card_partial_material_transfer"
+ )
+
+ if self._testMethodName in tests_that_skip_setup:
+ return
if self._testMethodName in tests_that_transfer_against_jc:
transfer_material_against = "Job Card"
@@ -190,4 +201,132 @@
job_card.submit()
# JC is Completed with excess transfer
- self.assertEqual(job_card.status, "Completed")
\ No newline at end of file
+ self.assertEqual(job_card.status, "Completed")
+
+ def test_job_card_partial_material_transfer(self):
+ "Test partial material transfer against Job Card"
+
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC",
+ qty=25, basic_rate=100)
+ make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
+ target="Stores - _TC", qty=15, basic_rate=100)
+
+ job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+ job_card = frappe.get_doc("Job Card", job_card_name)
+
+ # partially transfer
+ transfer_entry = make_stock_entry_from_jc(job_card_name)
+ transfer_entry.fg_completed_qty = 1
+ transfer_entry.get_items()
+ transfer_entry.insert()
+ transfer_entry.submit()
+
+ job_card.reload()
+ self.assertEqual(job_card.transferred_qty, 1)
+ self.assertEqual(transfer_entry.items[0].qty, 5)
+ self.assertEqual(transfer_entry.items[1].qty, 3)
+
+ # transfer remaining
+ transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
+
+ self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
+ self.assertEqual(transfer_entry_2.items[0].qty, 5)
+ self.assertEqual(transfer_entry_2.items[1].qty, 3)
+
+ transfer_entry_2.insert()
+ transfer_entry_2.submit()
+
+ job_card.reload()
+ self.assertEqual(job_card.transferred_qty, 2)
+
+ def test_job_card_material_transfer_correctness(self):
+ """
+ 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
+ 2. Test impact of changing 'For Qty' in such a Stock Entry
+ """
+ create_bom_with_multiple_operations()
+ work_order = make_wo_with_transfer_against_jc()
+
+ job_card_name = frappe.db.get_value(
+ "Job Card",
+ {"work_order": work_order.name,"operation": "Test Operation A"}
+ )
+ job_card = frappe.get_doc("Job Card", job_card_name)
+
+ self.assertEqual(len(job_card.items), 1)
+ self.assertEqual(job_card.items[0].item_code, "_Test Item")
+
+ # check if right items are mapped in transfer entry
+ transfer_entry = make_stock_entry_from_jc(job_card_name)
+ transfer_entry.insert()
+
+ self.assertEqual(len(transfer_entry.items), 1)
+ self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
+ self.assertEqual(transfer_entry.items[0].qty, 4)
+
+ # change 'For Qty' and check impact on items table
+ # no.of items should be the same with qty change
+ transfer_entry.fg_completed_qty = 2
+ transfer_entry.get_items()
+
+ self.assertEqual(len(transfer_entry.items), 1)
+ self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
+ self.assertEqual(transfer_entry.items[0].qty, 2)
+
+ # rollback via tearDown method
+
+def create_bom_with_multiple_operations():
+ "Create a BOM with multiple operations and Material Transfer against Job Card"
+ from erpnext.manufacturing.doctype.operation.test_operation import make_operation
+
+ test_record = frappe.get_test_records("BOM")[2]
+ bom_doc = frappe.get_doc(test_record)
+
+ row = {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "time_in_mins": 60
+ }
+ make_workstation(row)
+ make_operation(row)
+
+ bom_doc.append("operations", {
+ "operation": "Test Operation A",
+ "description": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate": 300,
+ "time_in_mins": 60,
+ "operating_cost": 100
+ })
+
+ bom_doc.transfer_material_against = "Job Card"
+ bom_doc.save()
+ bom_doc.submit()
+
+ return bom_doc
+
+def make_wo_with_transfer_against_jc():
+ "Create a WO with multiple operations and Material Transfer against Job Card"
+
+ work_order = make_wo_order_test_record(
+ item="_Test FG Item 2",
+ qty=4,
+ transfer_material_against="Job Card",
+ source_warehouse="Stores - _TC",
+ do_not_submit=True
+ )
+ work_order.required_items[0].operation = "Test Operation A"
+ work_order.required_items[1].operation = "_Test Operation 1"
+ work_order.submit()
+
+ return work_order
+
+def make_bom_for_jc_tests():
+ test_records = frappe.get_test_records('BOM')
+ bom = frappe.copy_doc(test_records[2])
+ bom.set_rate_of_sub_assembly_item_based_on_bom = 0
+ bom.rm_cost_as_per = "Valuation Rate"
+ bom.items[0].uom = "_Test UOM 1"
+ bom.items[0].conversion_factor = 5
+ bom.insert()
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py
index 804cc3f..e511084 100644
--- a/erpnext/manufacturing/doctype/operation/test_operation.py
+++ b/erpnext/manufacturing/doctype/operation/test_operation.py
@@ -17,15 +17,13 @@
args = frappe._dict(args)
- try:
+ if not frappe.db.exists("Operation", args.operation):
doc = frappe.get_doc({
"doctype": "Operation",
"name": args.operation,
"workstation": args.workstation
})
-
doc.insert()
-
return doc
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Operation", args.operation)
+
+ return frappe.get_doc("Operation", args.operation)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 2bd02da..0babf87 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -105,7 +105,7 @@
}
frm.trigger("material_requirement");
- const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
+ const projected_qty_formula = ` <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td style="padding-left:25px">
<div>
<h3 style="text-decoration: underline;">
@@ -238,6 +238,12 @@
method: "get_items",
freeze: true,
doc: frm.doc,
+ callback: function() {
+ frm.refresh_field("po_items");
+ if (frm.doc.sub_assembly_items.length > 0) {
+ frm.trigger("get_sub_assembly_items");
+ }
+ }
});
},
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 85b5bfb..f4a88dc 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -12,6 +12,7 @@
ItemHasVariantError,
OverProductionError,
StockOverProductionError,
+ close_work_order,
make_stock_entry,
stop_unstop,
)
@@ -800,6 +801,46 @@
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
+ def test_close_work_order(self):
+ items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
+ 'Test RM Item 2 for Closed WO']
+
+ company = '_Test Company with perpetual inventory'
+ for item_code in items:
+ create_item(item_code = item_code, is_stock_item = 1,
+ is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
+
+ item = 'Test FG Item for Closed WO'
+ raw_materials = ['Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO']
+ if not frappe.db.get_value('BOM', {'item': item}):
+ bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
+ bom.with_operations = 1
+ bom.append('operations', {
+ 'operation': '_Test Operation 1',
+ 'workstation': '_Test Workstation 1',
+ 'hour_rate': 20,
+ 'time_in_mins': 60
+ })
+
+ bom.submit()
+
+ wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
+ job_cards = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
+
+ if len(job_cards) == len(bom.operations):
+ for jc in job_cards:
+ job_card_doc = frappe.get_doc('Job Card', jc)
+ job_card_doc.append('time_logs', {
+ 'from_time': now(),
+ 'time_in_mins': 60,
+ 'completed_qty': job_card_doc.for_quantity
+ })
+
+ job_card_doc.submit()
+
+ close_work_order(wo_order, "Closed")
+ self.assertEqual(wo_order.get('status'), "Closed")
+
def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 51c46f6..5ffbb03 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -135,24 +135,26 @@
frm.set_intro(__("Submit this Work Order for further processing."));
}
- if (frm.doc.docstatus===1) {
- frm.trigger('show_progress_for_items');
- frm.trigger('show_progress_for_operations');
- }
+ if (frm.doc.status != "Closed") {
+ if (frm.doc.docstatus===1) {
+ frm.trigger('show_progress_for_items');
+ frm.trigger('show_progress_for_operations');
+ }
- if (frm.doc.docstatus === 1
- && frm.doc.operations && frm.doc.operations.length) {
+ if (frm.doc.docstatus === 1
+ && frm.doc.operations && frm.doc.operations.length) {
- const not_completed = frm.doc.operations.filter(d => {
- if(d.status != 'Completed') {
- return true;
+ const not_completed = frm.doc.operations.filter(d => {
+ if (d.status != 'Completed') {
+ return true;
+ }
+ });
+
+ if (not_completed && not_completed.length) {
+ frm.add_custom_button(__('Create Job Card'), () => {
+ frm.trigger("make_job_card");
+ }).addClass('btn-primary');
}
- });
-
- if(not_completed && not_completed.length) {
- frm.add_custom_button(__('Create Job Card'), () => {
- frm.trigger("make_job_card");
- }).addClass('btn-primary');
}
}
@@ -440,7 +442,7 @@
additional_operating_cost: function(frm) {
erpnext.work_order.calculate_cost(frm.doc);
erpnext.work_order.calculate_total_cost(frm);
- }
+ },
});
frappe.ui.form.on("Work Order Item", {
@@ -517,14 +519,22 @@
erpnext.work_order = {
set_custom_buttons: function(frm) {
var doc = frm.doc;
- if (doc.docstatus === 1) {
+ if (doc.docstatus === 1 && doc.status != "Closed") {
+ frm.add_custom_button(__('Close'), function() {
+ frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."),
+ () => {
+ erpnext.work_order.change_work_order_status(frm, "Closed");
+ }
+ );
+ }, __("Status"));
+
if (doc.status != 'Stopped' && doc.status != 'Completed') {
frm.add_custom_button(__('Stop'), function() {
- erpnext.work_order.stop_work_order(frm, "Stopped");
+ erpnext.work_order.change_work_order_status(frm, "Stopped");
}, __("Status"));
} else if (doc.status == 'Stopped') {
frm.add_custom_button(__('Re-open'), function() {
- erpnext.work_order.stop_work_order(frm, "Resumed");
+ erpnext.work_order.change_work_order_status(frm, "Resumed");
}, __("Status"));
}
@@ -713,9 +723,10 @@
});
},
- stop_work_order: function(frm, status) {
+ change_work_order_status: function(frm, status) {
+ let method_name = status=="Closed" ? "close_work_order" : "stop_unstop";
frappe.call({
- method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
+ method: `erpnext.manufacturing.doctype.work_order.work_order.${method_name}`,
freeze: true,
freeze_message: __("Updating Work Order status"),
args: {
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 7f8e816..12cd58f 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -99,7 +99,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled",
+ "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nClosed\nCancelled",
"read_only": 1,
"reqd": 1,
"search_index": 1
@@ -326,6 +326,7 @@
"label": "Expected Delivery Date"
},
{
+ "collapsible": 1,
"fieldname": "operations_section",
"fieldtype": "Section Break",
"label": "Operations",
@@ -337,7 +338,7 @@
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
- "options": "\nWork Order\nJob Card"
+ "options": "Work Order\nJob Card"
},
{
"fieldname": "operations",
@@ -573,7 +574,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-10-27 19:21:35.139888",
+ "modified": "2021-11-08 17:36:07.016300",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index f881e1b..0090f4d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -175,7 +175,7 @@
def update_status(self, status=None):
'''Update status of work order if unknown'''
- if status != "Stopped":
+ if status != "Stopped" and status != "Closed":
status = self.get_status(status)
if status != self.status:
@@ -624,7 +624,6 @@
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
- print(self.bom_no, self.production_item)
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):
@@ -967,6 +966,10 @@
frappe.throw(_("Not permitted"), frappe.PermissionError)
pro_order = frappe.get_doc("Work Order", work_order)
+
+ if pro_order.status == "Closed":
+ frappe.throw(_("Closed Work Order can not be stopped or Re-opened"))
+
pro_order.update_status(status)
pro_order.update_planned_qty()
frappe.msgprint(_("Work Order has been {0}").format(status))
@@ -1001,6 +1004,29 @@
if row.job_card_qty > 0:
create_job_card(work_order, row, auto_create=True)
+@frappe.whitelist()
+def close_work_order(work_order, status):
+ if not frappe.has_permission("Work Order", "write"):
+ frappe.throw(_("Not permitted"), frappe.PermissionError)
+
+ work_order = frappe.get_doc("Work Order", work_order)
+ if work_order.get("operations"):
+ job_cards = frappe.get_list("Job Card",
+ filters={
+ "work_order": work_order.name,
+ "status": "Work In Progress"
+ }, pluck='name')
+
+ if job_cards:
+ job_cards = ", ".join(job_cards)
+ frappe.throw(_("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format(job_cards))
+
+ work_order.update_status(status)
+ work_order.update_planned_qty()
+ frappe.msgprint(_("Work Order has been {0}").format(status))
+ work_order.notify_update()
+ return work_order.status
+
def split_qty_based_on_batch_size(wo_doc, row, qty):
if not cint(frappe.db.get_value("Operation",
row.operation, "create_job_card_based_on_batch_size")):
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index f7b8787..4e1a464 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -6,27 +6,27 @@
"field_order": [
"details",
"operation",
- "bom",
- "column_break_4",
- "description",
- "sequence_id",
- "col_break1",
- "completed_qty",
"status",
+ "completed_qty",
+ "column_break_4",
+ "bom",
"workstation",
+ "sequence_id",
+ "section_break_10",
+ "description",
"estimated_time_and_cost",
"planned_start_time",
- "planned_end_time",
- "column_break_10",
- "time_in_mins",
"hour_rate",
+ "time_in_mins",
+ "column_break_10",
+ "planned_end_time",
"batch_size",
"planned_operating_cost",
"section_break_9",
"actual_start_time",
- "actual_end_time",
- "column_break_11",
"actual_operation_time",
+ "column_break_11",
+ "actual_end_time",
"actual_operating_cost"
],
"fields": [
@@ -42,7 +42,6 @@
"oldfieldname": "operation_no",
"oldfieldtype": "Data",
"options": "Operation",
- "read_only": 1,
"reqd": 1
},
{
@@ -52,20 +51,14 @@
"label": "BOM",
"no_copy": 1,
"options": "BOM",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Operation Description",
"oldfieldname": "opn_description",
- "oldfieldtype": "Text",
- "read_only": 1
- },
- {
- "fieldname": "col_break1",
- "fieldtype": "Column Break"
+ "oldfieldtype": "Text"
},
{
"columns": 1,
@@ -74,19 +67,16 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Completed Qty",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"columns": 1,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
- "in_list_view": 1,
"label": "Status",
"no_copy": 1,
- "options": "Pending\nWork in Progress\nCompleted",
- "read_only": 1
+ "options": "Pending\nWork in Progress\nCompleted"
},
{
"fieldname": "workstation",
@@ -106,15 +96,13 @@
"fieldname": "planned_start_time",
"fieldtype": "Datetime",
"label": "Planned Start Time",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "planned_end_time",
"fieldtype": "Datetime",
"label": "Planned End Time",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "column_break_10",
@@ -122,7 +110,7 @@
},
{
"columns": 1,
- "description": "in Minutes",
+ "description": "In Minutes",
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
@@ -152,6 +140,7 @@
"label": "Actual Time and Cost"
},
{
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_start_time",
"fieldtype": "Datetime",
"label": "Actual Start Time",
@@ -159,7 +148,7 @@
"read_only": 1
},
{
- "description": "Updated via 'Time Log'",
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_end_time",
"fieldtype": "Datetime",
"label": "Actual End Time",
@@ -171,7 +160,7 @@
"fieldtype": "Column Break"
},
{
- "description": "in Minutes\nUpdated via 'Time Log'",
+ "description": "Updated via 'Time Log' (In Minutes)",
"fieldname": "actual_operation_time",
"fieldtype": "Float",
"label": "Actual Operation Time",
@@ -189,26 +178,30 @@
},
{
"fieldname": "batch_size",
- "fieldtype": "Int",
+ "fieldtype": "Float",
"label": "Batch Size",
"read_only": 1
},
{
"fieldname": "sequence_id",
"fieldtype": "Int",
+ "hidden": 1,
"label": "Sequence ID",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-24 14:36:12.835543",
+ "modified": "2021-11-29 16:37:18.824489",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
@@ -217,4 +210,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c77cef2..5ed5153 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -89,7 +89,7 @@
args = frappe._dict(args)
workstation_name = args.workstation_name or args.workstation
- try:
+ if not frappe.db.exists("Workstation", workstation_name):
doc = frappe.get_doc({
"doctype": "Workstation",
"workstation_name": workstation_name
@@ -99,5 +99,5 @@
doc.insert()
return doc
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Workstation", workstation_name)
+
+ return frappe.get_doc("Workstation", workstation_name)
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index cf19cbf..090a3e7 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -89,7 +89,7 @@
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
+ details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
index b9ddc9f..7741823 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -1,7 +1,6 @@
-# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.utils import flt
@@ -30,7 +29,6 @@
for row in job_cards:
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
- update_raw_material_cost(row, report_filters)
data.append(row)
return data
@@ -46,12 +44,6 @@
return filters
-def update_raw_material_cost(row, filters):
- row.rm_cost = 0.0
- for data in frappe.get_all("Job Card Item", fields = ["amount"],
- filters={"parent": row.name, "docstatus": 1}):
- row.rm_cost += data.amount
-
def get_columns(filters):
return [
{
@@ -59,7 +51,7 @@
"fieldtype": "Link",
"fieldname": "name",
"options": "Job Card",
- "width": "100"
+ "width": "120"
},
{
"label": _("Work Order"),
@@ -111,18 +103,12 @@
"label": _("Operating Cost"),
"fieldtype": "Currency",
"fieldname": "operating_cost",
- "width": "100"
- },
- {
- "label": _("Raw Material Cost"),
- "fieldtype": "Currency",
- "fieldname": "rm_cost",
- "width": "100"
+ "width": "150"
},
{
"label": _("Total Time (in Mins)"),
"fieldtype": "Float",
"fieldname": "total_time_in_mins",
- "width": "100"
+ "width": "150"
}
]
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
index 43ef12e..55b1a3f 100644
--- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -28,8 +28,15 @@
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
for row in production_plan_doc.po_items:
- work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
- "bom_no": row.bom_no, "production_item": row.item_code}, "name")
+ work_order = frappe.get_value(
+ "Work Order",
+ {
+ "production_plan_item": row.name,
+ "bom_no": row.bom_no,
+ "production_item": row.item_code
+ },
+ "name"
+ )
if row.item_code not in itemwise_indent:
itemwise_indent.setdefault(row.item_code, {})
@@ -40,10 +47,10 @@
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": row.planned_qty,
"document_type": "Work Order",
- "document_name": work_order,
+ "document_name": work_order or "",
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
- "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
- "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
+ "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
+ "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
})
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
@@ -54,11 +61,23 @@
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
if subcontracted_item:
- docname = frappe.get_cached_value("Purchase Order Item",
- {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
+ docname = frappe.get_value(
+ "Purchase Order Item",
+ {
+ "production_plan_sub_assembly_item": item.name,
+ "docstatus": ("<", 2)
+ },
+ "parent"
+ )
else:
- docname = frappe.get_cached_value("Work Order",
- {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
+ docname = frappe.get_value(
+ "Work Order",
+ {
+ "production_plan_sub_assembly_item": item.name,
+ "docstatus": ("<", 2)
+ },
+ "name"
+ )
data.append({
"indent": 1,
@@ -66,10 +85,10 @@
"item_name": item.item_name,
"qty": item.qty,
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
- "document_name": docname,
+ "document_name": docname or "",
"bom_level": item.bom_level,
- "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
- "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
+ "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0),
+ "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0))
})
def get_work_order_details(filters, order_details):
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
new file mode 100644
index 0000000..b2428e8
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
@@ -0,0 +1,70 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Work Order Consumed Materials"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Work Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Work Order",
+ get_query: function() {
+ return {
+ filters: {
+ status: ["in", ["In Process", "Completed", "Stopped"]]
+ }
+ }
+ }
+ },
+ {
+ label: __("Production Item"),
+ fieldname: "production_item",
+ fieldtype: "Link",
+ depends_on: "eval: !doc.name",
+ options: "Item"
+ },
+ {
+ label: __("Status"),
+ fieldname: "status",
+ fieldtype: "Select",
+ options: ["In Process", "Completed", "Stopped"]
+ },
+ {
+ label: __("Excess Materials Consumed"),
+ fieldname: "show_extra_consumed_materials",
+ fieldtype: "Check"
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) {
+ value = `<div style="color:red">${value}</div>`;
+ }
+
+ return value;
+ },
+};
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
new file mode 100644
index 0000000..2fc986a
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
@@ -0,0 +1,30 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-11-22 17:36:11.886939",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Gadgets International",
+ "modified": "2021-11-22 17:36:14.999091",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Work Order Consumed Materials",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Work Order",
+ "report_name": "Work Order Consumed Materials",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Stock User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
new file mode 100644
index 0000000..0528348
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ fields = get_fields()
+ filters = get_filter_condition(report_filters)
+
+ wo_items = {}
+ for d in frappe.get_all("Work Order", filters = filters, fields=fields):
+ d.extra_consumed_qty = 0.0
+ if d.consumed_qty and d.consumed_qty > d.required_qty:
+ d.extra_consumed_qty = d.consumed_qty - d.required_qty
+
+ if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials:
+ wo_items.setdefault((d.name, d.production_item), []).append(d)
+
+ data = []
+ for key, wo_data in wo_items.items():
+ for index, row in enumerate(wo_data):
+ if index != 0:
+ #If one work order has multiple raw materials then show parent data in the first row only
+ for field in ["name", "status", "production_item", "qty", "produced_qty"]:
+ row[field] = ""
+
+ data.append(row)
+
+ return data
+
+def get_fields():
+ return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code",
+ "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`",
+ "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`",
+ "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`",
+ "`tabWork Order`.`produced_qty`"]
+
+def get_filter_condition(report_filters):
+ filters = {
+ "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]),
+ "creation": ("between", [report_filters.from_date, report_filters.to_date])
+ }
+
+ for field in ["name", "production_item", "company", "status"]:
+ value = report_filters.get(field)
+ if value:
+ key = f"`{field}`"
+ filters.update({key: value})
+
+ return filters
+
+def get_columns():
+ return [
+ {
+ "label": _("Id"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Work Order",
+ "width": 80
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Production Item"),
+ "fieldname": "production_item",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 130
+ },
+ {
+ "label": _("Qty to Produce"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Produced Qty"),
+ "fieldname": "produced_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Raw Material Item"),
+ "fieldname": "raw_material_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 150
+ },
+ {
+ "label": _("Item Name"),
+ "fieldname": "raw_material_name",
+ "width": 130
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Transferred Qty"),
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Extra Consumed Qty"),
+ "fieldname": "extra_consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ }
+ ]
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
index eb23f17..832be23 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js
@@ -51,7 +51,7 @@
label: __("Status"),
fieldname: "status",
fieldtype: "Select",
- options: ["", "Not Started", "In Process", "Completed", "Stopped"]
+ options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"]
},
{
label: __("Sales Orders"),
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
index 6207904..d7469dd 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -1,6 +1,7 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+from collections import defaultdict
import frappe
from frappe import _
@@ -58,21 +59,16 @@
return get_chart_based_on_qty(data, filters)
def get_chart_based_on_status(data):
- labels = ["Completed", "In Process", "Stopped", "Not Started"]
+ labels = frappe.get_meta("Work Order").get_options("status").split("\n")
+ if "" in labels:
+ labels.remove("")
- status_wise_data = {
- "Not Started": 0,
- "In Process": 0,
- "Stopped": 0,
- "Completed": 0,
- "Draft": 0
- }
+ status_wise_data = defaultdict(int)
for d in data:
status_wise_data[d.status] += 1
- values = [status_wise_data["Completed"], status_wise_data["In Process"],
- status_wise_data["Stopped"], status_wise_data["Not Started"]]
+ values = [status_wise_data[label] for label in labels]
chart = {
"data": {
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index cfa80f8..65b4d02 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,10 +1,6 @@
{
- "charts": [
- {
- "chart_name": "Produced Quantity"
- }
- ],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
+ "charts": [],
+ "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"docstatus": 0,
"doctype": "Workspace",
@@ -141,14 +137,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Reports",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
@@ -295,9 +283,126 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "link_count": 10,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Planning Report",
+ "link_count": 0,
+ "link_to": "Production Planning Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Work Order Summary",
+ "link_count": 0,
+ "link_to": "Work Order Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Quality Inspection",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Quality Inspection Summary",
+ "link_count": 0,
+ "link_to": "Quality Inspection Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Downtime Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Downtime Analysis",
+ "link_count": 0,
+ "link_to": "Downtime Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Job Card",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Job Card Summary",
+ "link_count": 0,
+ "link_to": "Job Card Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Search",
+ "link_count": 0,
+ "link_to": "BOM Search",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Stock Report",
+ "link_count": 0,
+ "link_to": "BOM Stock Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Analytics",
+ "link_count": 0,
+ "link_to": "Production Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Operations Time",
+ "link_count": 0,
+ "link_to": "BOM Operations Time",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Work Order Consumed Materials",
+ "link_count": 0,
+ "link_to": "Work Order Consumed Materials",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-08-05 12:16:00.825742",
+ "modified": "2021-11-22 17:55:03.524496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 1006e21..897e70c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -287,7 +287,7 @@
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
-erpnext.patches.v13_0.create_gst_payment_entry_fields
+erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.fix_invoice_statuses
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
@@ -303,8 +303,10 @@
erpnext.patches.v13_0.add_default_interview_notification_templates
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
+erpnext.patches.v13_0.update_job_card_status
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.healthcare_deprecation_warning
+erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py
index 1e230a7..36fe18d 100644
--- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py
+++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py
@@ -7,6 +7,7 @@
def execute():
frappe.reload_doc("setup", "doctype", "target_detail")
+ frappe.reload_doc("core", "doctype", "prepared_report")
for d in ['Sales Person', 'Sales Partner', 'Territory']:
frappe.db.sql("""
diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
index 7e6d67c..4166945 100644
--- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
+++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
@@ -9,24 +9,29 @@
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
frappe.reload_doc('accounts', 'doctype', 'payment_entry')
- custom_fields = {
- 'Payment Entry': [
- dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
- print_hide=1, collapsible=1),
- dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
- print_hide=1, options='Address'),
- dict(fieldname='company_gstin', label='Company GSTIN',
- fieldtype='Data', insert_after='company_address',
- fetch_from='company_address.gstin', print_hide=1, read_only=1),
- dict(fieldname='place_of_supply', label='Place of Supply',
- fieldtype='Data', insert_after='company_gstin',
- print_hide=1, read_only=1),
- dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
- print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
- dict(fieldname='customer_gstin', label='Customer GSTIN',
- fieldtype='Data', insert_after='customer_address',
- fetch_from='customer_address.gstin', print_hide=1, read_only=1)
- ]
- }
+ if frappe.db.exists('Company', {'country': 'India'}):
+ custom_fields = {
+ 'Payment Entry': [
+ dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
+ print_hide=1, collapsible=1),
+ dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
+ print_hide=1, options='Address'),
+ dict(fieldname='company_gstin', label='Company GSTIN',
+ fieldtype='Data', insert_after='company_address',
+ fetch_from='company_address.gstin', print_hide=1, read_only=1),
+ dict(fieldname='place_of_supply', label='Place of Supply',
+ fieldtype='Data', insert_after='company_gstin',
+ print_hide=1, read_only=1),
+ dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
+ print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
+ dict(fieldname='customer_gstin', label='Customer GSTIN',
+ fieldtype='Data', insert_after='customer_address',
+ fetch_from='customer_address.gstin', print_hide=1, read_only=1)
+ ]
+ }
- create_custom_fields(custom_fields, update=True)
\ No newline at end of file
+ create_custom_fields(custom_fields, update=True)
+ else:
+ fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin']
+ for field in fields:
+ frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/create_pan_field_for_india.py b/erpnext/patches/v13_0/create_pan_field_for_india.py
index c37651a..6df6e1e 100644
--- a/erpnext/patches/v13_0/create_pan_field_for_india.py
+++ b/erpnext/patches/v13_0/create_pan_field_for_india.py
@@ -5,6 +5,7 @@
def execute():
frappe.reload_doc('buying', 'doctype', 'supplier', force=True)
frappe.reload_doc('selling', 'doctype', 'customer', force=True)
+ frappe.reload_doc('core', 'doctype', 'doctype', force=True)
custom_fields = {
'Supplier': [
diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py
new file mode 100644
index 0000000..5fe85a4
--- /dev/null
+++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py
@@ -0,0 +1,11 @@
+import frappe
+
+from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+
+
+def execute():
+
+ stock_settings = frappe.get_doc("Stock Settings")
+
+ set_by_naming_series("Item", "item_code",
+ stock_settings.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0)
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
index e4cb9ae..0f2ac4b 100644
--- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -6,10 +6,19 @@
def execute():
- for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item',
- 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'):
- frappe.reload_doc('stock', 'doctype', doctype)
- frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied')
+ doctypes_to_reload = [
+ ("stock", "repost_item_valuation"),
+ ("stock", "stock_entry_detail"),
+ ("stock", "purchase_receipt_item"),
+ ("stock", "delivery_note_item"),
+ ("stock", "packed_item"),
+ ("accounts", "sales_invoice_item"),
+ ("accounts", "purchase_invoice_item"),
+ ("buying", "purchase_receipt_item_supplied")
+ ]
+
+ for module, doctype in doctypes_to_reload:
+ frappe.reload_doc(module, 'doctype', doctype)
reposting_project_deployed_on = get_creation_time()
posting_date = getdate(reposting_project_deployed_on)
diff --git a/erpnext/patches/v13_0/update_job_card_status.py b/erpnext/patches/v13_0/update_job_card_status.py
new file mode 100644
index 0000000..797a3e2
--- /dev/null
+++ b/erpnext/patches/v13_0/update_job_card_status.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2021, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+
+def execute():
+
+ job_card = frappe.qb.DocType("Job Card")
+ (frappe.qb
+ .update(job_card)
+ .set(job_card.status, "Completed")
+ .where(
+ (job_card.docstatus == 1)
+ & (job_card.for_quantity <= job_card.total_completed_qty)
+ & (job_card.status.isin(["Work In Progress", "Material Transferred"]))
+ )
+ ).run()
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
index f563c08..476990a 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -193,7 +193,7 @@
sal_slip = get_last_salary_slip(employee)
if not sal_slip:
frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee)))
- component_and_amounts = frappe.get_list("Salary Detail",
+ component_and_amounts = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 78355ca..93cba06 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -48,7 +48,7 @@
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
- component_amount = frappe.get_list("Salary Detail",
+ component_amount = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
@@ -84,7 +84,7 @@
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
- component_amount = frappe.get_list("Salary Detail",
+ component_amount = frappe.get_all("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py
index 13ee691..78deb22 100644
--- a/erpnext/payroll/report/salary_register/salary_register.py
+++ b/erpnext/payroll/report/salary_register/salary_register.py
@@ -134,11 +134,11 @@
ss_earning_map = {}
for d in ss_earnings:
- ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, [])
+ ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0)
if currency == company_currency:
- ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1)
+ ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1)
else:
- ss_earning_map[d.parent][d.salary_component] = flt(d.amount)
+ ss_earning_map[d.parent][d.salary_component] += flt(d.amount)
return ss_earning_map
@@ -149,10 +149,10 @@
ss_ded_map = {}
for d in ss_deductions:
- ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, [])
+ ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0)
if currency == company_currency:
- ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1)
+ ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1)
else:
- ss_ded_map[d.parent][d.salary_component] = flt(d.amount)
+ ss_ded_map[d.parent][d.salary_component] += flt(d.amount)
return ss_ded_map
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 2cf9d38..0415690 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -1,7 +1,7 @@
import unittest
import frappe
-from frappe.utils import add_days, getdate, nowdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -13,21 +13,26 @@
class TestProjectProfitability(unittest.TestCase):
-
def setUp(self):
+ frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
+
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
+
make_salary_structure_for_timesheet(emp, company='_Test Company')
- self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
+ date = getdate()
+
+ self.timesheet = make_timesheet(emp, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
- holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
+
+ holidays = self.salary_slip.get_holidays_for_employee(date, date)
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
- self.sales_invoice.due_date = nowdate()
+ self.sales_invoice.due_date = date
self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
@@ -63,6 +68,4 @@
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
- frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
- frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
- frappe.get_doc("Timesheet", self.timesheet.name).cancel()
+ frappe.db.rollback()
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 86dadd3..d696ef5 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -165,45 +165,33 @@
}
qty(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
- frappe.model.round_floats_in(item, ["qty", "received_qty"]);
-
- if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "received_qty"])){ return }
-
- if(!item.rejected_qty && item.qty) {
- item.received_qty = item.qty;
- }
-
- frappe.model.round_floats_in(item, ["qty", "received_qty"]);
- item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
- item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
+ this.calculate_received_qty(doc, cdt, cdn)
}
super.qty(doc, cdt, cdn);
}
+ rejected_qty(doc, cdt, cdn) {
+ this.calculate_received_qty(doc, cdt, cdn)
+ }
+
+ calculate_received_qty(doc, cdt, cdn){
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["qty", "rejected_qty"]);
+
+ if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["qty", "rejected_qty"])){ return }
+
+ let received_qty = flt(item.qty + item.rejected_qty, precision("received_qty", item));
+ let received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(received_qty);
+
+ frappe.model.set_value(cdt, cdn, "received_qty", received_qty);
+ frappe.model.set_value(cdt, cdn, "received_stock_qty", received_stock_qty);
+ }
+
batch_no(doc, cdt, cdn) {
super.batch_no(doc, cdt, cdn);
}
- received_qty(doc, cdt, cdn) {
- this.calculate_accepted_qty(doc, cdt, cdn)
- }
-
- rejected_qty(doc, cdt, cdn) {
- this.calculate_accepted_qty(doc, cdt, cdn)
- }
-
- calculate_accepted_qty(doc, cdt, cdn){
- var item = frappe.get_doc(cdt, cdn);
- frappe.model.round_floats_in(item, ["received_qty", "rejected_qty"]);
-
- if(!doc.is_return && this.validate_negative_quantity(cdt, cdn, item, ["received_qty", "rejected_qty"])){ return }
-
- item.qty = flt(item.received_qty - item.rejected_qty, precision("qty", item));
- this.qty(doc, cdt, cdn);
- }
-
validate_negative_quantity(cdt, cdn, item, fieldnames){
if(!item || !fieldnames) { return }
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index b5a6d8f..7c1c8c7 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -81,6 +81,7 @@
this.initialize_taxes();
this.determine_exclusive_rate();
this.calculate_net_total();
+ this.calculate_shipping_charges();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
@@ -266,8 +267,13 @@
me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount;
});
+ }
+ calculate_shipping_charges() {
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
+ if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
+ this.shipping_rule();
+ }
}
add_taxes_from_item_tax_template(item_tax_map) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 63fd8a1..0cfc008 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1085,16 +1085,8 @@
return this.frm.call({
doc: this.frm.doc,
method: "apply_shipping_rule",
- callback: function(r) {
- if(!r.exc) {
- me.calculate_taxes_and_totals();
- }
- }
}).fail(() => this.frm.set_value('shipping_rule', ''));
}
- else {
- me.calculate_taxes_and_totals();
- }
}
set_margin_amount_based_on_currency(exchange_rate) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 0323a42..f0facdd 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -751,9 +751,13 @@
}
frappe.form.link_formatters['Employee'] = function(value, doc) {
- if(doc && doc.employee_name && doc.employee_name !== value) {
- return value? value + ': ' + doc.employee_name: doc.employee_name;
+ if (doc && value && doc.employee_name && doc.employee_name !== value && doc.employee === value) {
+ return value + ': ' + doc.employee_name;
+ } else if (!value && doc.doctype && doc.employee_name) {
+ // format blank value in child table
+ return doc.employee;
} else {
+ // if value is blank in report view or project name and name are the same, return as is
return value;
}
}
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
index 1677e9b..7a3854c 100644
--- a/erpnext/public/scss/point-of-sale.scss
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -495,6 +495,11 @@
font-size: var(--text-md);
}
+ > .item-qty-total-container {
+ @extend .net-total-container;
+ padding: 5px 0px 0px 0px;
+ }
+
> .taxes-container {
display: none;
flex-direction: column;
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 0f0c0b9..e12e3d7 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -103,6 +103,45 @@
gst_settings.round_off_gst_values = 1
gst_settings.save()
+ def test_gst_category_auto_update(self):
+ if not frappe.db.exists("Customer", "_Test GST Customer With GSTIN"):
+ customer = frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": "_Test GST Customer With GSTIN",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory"
+ }).insert()
+
+ self.assertEqual(customer.gst_category, 'Unregistered')
+
+ if not frappe.db.exists('Address', '_Test GST Category-1-Billing'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test GST Category-1",
+ "address_type": "Billing",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+91 0000000000",
+ "gstin": "29AZWPS7135H1ZG",
+ "gst_state": "Karnataka",
+ "gst_state_number": "29"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Customer",
+ "link_name": "_Test GST Customer With GSTIN"
+ })
+
+ address.save()
+
+ customer.load_from_db()
+ self.assertEqual(customer.gst_category, 'Registered Regular')
+
+
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 42ee271..5865424 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -12,7 +12,7 @@
def setup(company=None, patch=True):
# Company independent fixtures should be called only once at the first company setup
- if frappe.db.count('Company', {'country': 'India'}) <=1:
+ if patch or frappe.db.count('Company', {'country': 'India'}) <=1:
setup_company_independent_fixtures(patch=patch)
if not patch:
@@ -791,7 +791,7 @@
accounts = [dict(company=company, account=tds_account)]
try:
- fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company)
+ fiscal_year_details = get_fiscal_year(today(), verbose=0)
except FiscalYearError:
pass
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 54d592a..9746fde 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -74,11 +74,11 @@
frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
def update_gst_category(doc, method):
- if hasattr(doc, 'gst_category'):
- for link in doc.links:
- if link.link_doctype in ['Customer', 'Supplier']:
- if doc.get('gstin'):
- frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
+ for link in doc.links:
+ if link.link_doctype in ['Customer', 'Supplier']:
+ meta = frappe.get_meta(link.link_doctype)
+ if doc.get('gstin') and meta.has_field('gst_category'):
+ frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
def set_gst_state_and_state_number(doc):
if not doc.gst_state:
@@ -569,17 +569,17 @@
}
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
- for hsn_code, taxable_amount in hsn_taxable_amount.items():
+ for item_or_hsn, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict()
- if not hsn_code:
+ if not item_or_hsn:
frappe.throw(_('GST HSN Code does not exist for one or more items'))
- item_data.hsnCode = int(hsn_code)
+ item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn
item_data.taxableAmount = taxable_amount
item_data.qtyUnit = ""
for attr in item_data_attrs:
item_data[attr] = 0
- for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items():
+ for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items():
account_type = gst_accounts.get(account, '')
for tax_acc, attrs in tax_map.items():
if account_type == tax_acc:
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
index 36d6536..8e9a728 100644
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
@@ -2,7 +2,7 @@
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-10-29 22:46:26.039023",
- "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n color: #888a8e;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n background-color: #265e4a !important;\n color: #fff;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
+ "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
"custom_format": 1,
"default_print_language": "en",
"disabled": 0,
@@ -10,14 +10,14 @@
"docstatus": 0,
"doctype": "Print Format",
"font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[item.item_code][0])-%}\n <span>{{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format(data_object[item.item_code][1], {'fieldtype': 'Currency'}) }}</span>\n {% set total.amount = total.amount + data_object[item.item_code][1] %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format(total.amount, {'fieldtype': 'Currency'}) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
+ "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2021-11-08 09:19:18.660806",
+ "modified": "2021-11-29 13:47:37.870818",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Invoice",
diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py
index 91a4767..f3fe5e8 100644
--- a/erpnext/regional/report/eway_bill/eway_bill.py
+++ b/erpnext/regional/report/eway_bill/eway_bill.py
@@ -106,14 +106,14 @@
row.update({'ship_to_state': row.to_state})
def set_taxes(row, filters):
- taxes = frappe.get_list("Sales Taxes and Charges",
+ taxes = frappe.get_all("Sales Taxes and Charges",
filters={
'parent': row.dn_id
},
fields=('item_wise_tax_detail', 'account_head'))
account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"]
- taxes_list = frappe.get_list("GST Account",
+ taxes_list = frappe.get_all("GST Account",
filters={
"parent": "GST Settings",
"company": filters.company
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 27cfaf7..11b684d 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -248,18 +248,17 @@
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
for d in items:
- if d.item_code not in self.invoice_items.get(d.parent, {}):
- self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
- self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+ self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
- item_tax_rate = {}
+ item_tax_rate = {}
- if d.item_tax_rate:
- item_tax_rate = json.loads(d.item_tax_rate)
+ if d.item_tax_rate:
+ item_tax_rate = json.loads(d.item_tax_rate)
- for account, rate in item_tax_rate.items():
- tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
- tax_rate_dict.append(rate)
+ for account, rate in item_tax_rate.items():
+ tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
+ tax_rate_dict.append(rate)
def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql("""
diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
index 5a281a4..17e5064 100644
--- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py
+++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
@@ -41,7 +41,7 @@
return self.columns, self.data
def get_sa_vat_accounts(self):
- self.sa_vat_accounts = frappe.get_list("South Africa VAT Account",
+ self.sa_vat_accounts = frappe.get_all("South Africa VAT Account",
filters = {"parent": self.filters.company}, pluck="account")
if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings")
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index cc6c0af..1051315 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -1,7 +1,10 @@
import io
import os
+from base64 import b64encode
import frappe
+from frappe import _
+from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
@@ -28,16 +31,74 @@
for field in meta.get_image_fields():
if field.fieldname == 'qr_code':
- # Creating public url to print format
- default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
+ ''' TLV conversion for
+ 1. Seller's Name
+ 2. VAT Number
+ 3. Time Stamp
+ 4. Invoice Amount
+ 5. VAT Amount
+ '''
+ tlv_array = []
+ # Sellers Name
- # System Language
- language = frappe.get_system_settings('language')
+ seller_name = frappe.db.get_value(
+ 'Company',
+ doc.company,
+ 'company_name_in_arabic')
- # creating qr code for the url
- url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
+ if not seller_name:
+ frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
+
+ tag = bytes([1]).hex()
+ length = bytes([len(seller_name.encode('utf-8'))]).hex()
+ value = seller_name.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # VAT Number
+ tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
+ if not tax_id:
+ frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
+
+ tag = bytes([2]).hex()
+ length = bytes([len(tax_id)]).hex()
+ value = tax_id.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Time Stamp
+ posting_date = getdate(doc.posting_date)
+ time = get_time(doc.posting_time)
+ seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
+ time_stamp = add_to_date(posting_date, seconds=seconds)
+ time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ tag = bytes([3]).hex()
+ length = bytes([len(time_stamp)]).hex()
+ value = time_stamp.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Invoice Amount
+ invoice_amount = str(doc.total)
+ tag = bytes([4]).hex()
+ length = bytes([len(invoice_amount)]).hex()
+ value = invoice_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # VAT Amount
+ vat_amount = str(doc.total_taxes_and_charges)
+
+ tag = bytes([5]).hex()
+ length = bytes([len(vat_amount)]).hex()
+ value = vat_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Joining bytes into one
+ tlv_buff = ''.join(tlv_array)
+
+ # base64 conversion for QR Code
+ base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
+
qr_image = io.BytesIO()
- url = qr_create(url, error='L')
+ url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1)
# making file
@@ -74,4 +135,11 @@
'file_url': doc.get('qr_code')
})
if len(file_doc):
- frappe.delete_doc('File', file_doc[0].name)
\ No newline at end of file
+ frappe.delete_doc('File', file_doc[0].name)
+
+def delete_vat_settings_for_company(doc, method):
+ if doc.country != 'Saudi Arabia':
+ return
+
+ settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
+ settings_doc.delete()
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index 4b0bbd5..107e4a4 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -134,6 +134,12 @@
frm.trigger("get_customer_group_details");
}, __('Actions'));
+ if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ frm.add_custom_button(__('Link with Supplier'), function () {
+ frm.trigger('show_party_link_dialog');
+ }, __('Actions'));
+ }
+
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@@ -158,5 +164,42 @@
}
});
+ },
+ show_party_link_dialog: function(frm) {
+ const dialog = new frappe.ui.Dialog({
+ title: __('Select a Supplier'),
+ fields: [{
+ fieldtype: 'Link', label: __('Supplier'),
+ options: 'Supplier', fieldname: 'supplier', reqd: 1
+ }],
+ primary_action: function({ supplier }) {
+ frappe.call({
+ method: 'erpnext.accounts.doctype.party_link.party_link.create_party_link',
+ args: {
+ primary_role: 'Customer',
+ primary_party: frm.doc.name,
+ secondary_party: supplier
+ },
+ freeze: true,
+ callback: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Successfully linked to Supplier'),
+ alert: true
+ });
+ },
+ error: function() {
+ dialog.hide();
+ frappe.msgprint({
+ message: __('Linking to Supplier Failed. Please try again.'),
+ title: __('Linking Failed'),
+ indicator: 'red'
+ });
+ }
+ });
+ },
+ primary_action_label: __('Create Link')
+ });
+ dialog.show();
}
});
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index d4ad719..0c8c53a 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -18,7 +18,11 @@
from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role
-from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
+from erpnext.accounts.party import ( # noqa
+ get_dashboard_info,
+ get_timeline_data,
+ validate_party_accounts,
+)
from erpnext.utilities.transaction_base import TransactionBase
@@ -463,11 +467,14 @@
def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0):
+ credit_limit = get_credit_limit(customer, company)
+ if not credit_limit:
+ return
+
customer_outstanding = get_customer_outstanding(customer, company, ignore_outstanding_sales_order)
if extra_amount > 0:
customer_outstanding += flt(extra_amount)
- credit_limit = get_credit_limit(customer, company)
if credit_limit > 0 and flt(customer_outstanding) > credit_limit:
msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})")
.format(customer, customer_outstanding, credit_limit))
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 43a4490..ee5b0ea 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -110,7 +110,8 @@
"enq_det",
"supplier_quotation",
"opportunity",
- "lost_reasons"
+ "lost_reasons",
+ "competitors"
],
"fields": [
{
@@ -946,17 +947,25 @@
"label": "Bundle Items",
"options": "fa fa-suitcase",
"print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "competitors",
+ "fieldtype": "Table MultiSelect",
+ "label": "Competitors",
+ "options": "Competitor Detail",
+ "read_only": 1
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
- "max_attachments": 1,
- "modified": "2021-08-27 20:10:07.864951",
+ "modified": "2021-11-30 01:33:21.106073",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 31b62d6..c4752ae 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -68,7 +68,7 @@
opp.set_status(status=status, update=True)
@frappe.whitelist()
- def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
+ def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
if not self.has_sales_order():
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
fields = ["name"])
@@ -84,6 +84,9 @@
else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
+ for competitor in competitors:
+ self.append('competitors', competitor)
+
self.update_opportunity('Lost')
self.update_lead()
self.save()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index d46c46f..79e9e17 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -319,7 +319,7 @@
title: __('Select Items to Manufacture'),
fields: fields,
primary_action: function() {
- var data = d.get_values();
+ var data = {items: d.fields_dict.items.grid.get_selected_children()};
me.frm.call({
method: 'make_work_orders',
args: {
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 7c7ed9a..7e99a06 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -134,6 +134,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1507,16 +1508,23 @@
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:09:51.515542",
+ "modified": "2021-10-05 12:16:40.775704",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 8cb0f29..47b8ebd 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -223,60 +223,15 @@
check_credit_limit(self.customer, self.company)
def check_nextdoc_docstatus(self):
- # Checks Delivery Note
- submit_dn = frappe.db.sql_list("""
- select t1.name
- from `tabDelivery Note` t1,`tabDelivery Note Item` t2
- where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", self.name)
-
- if submit_dn:
- submit_dn = [get_link_to_form("Delivery Note", dn) for dn in submit_dn]
- frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order")
- .format(", ".join(submit_dn)))
-
- # Checks Sales Invoice
- submit_rv = frappe.db.sql_list("""select t1.name
+ linked_invoices = frappe.db.sql_list("""select distinct t1.name
from `tabSales Invoice` t1,`tabSales Invoice Item` t2
- where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""",
+ where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 0""",
self.name)
- if submit_rv:
- submit_rv = [get_link_to_form("Sales Invoice", si) for si in submit_rv]
- frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order")
- .format(", ".join(submit_rv)))
-
- #check maintenance schedule
- submit_ms = frappe.db.sql_list("""
- select t1.name
- from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2
- where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", self.name)
-
- if submit_ms:
- submit_ms = [get_link_to_form("Maintenance Schedule", ms) for ms in submit_ms]
- frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order")
- .format(", ".join(submit_ms)))
-
- # check maintenance visit
- submit_mv = frappe.db.sql_list("""
- select t1.name
- from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2
- where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""",self.name)
-
- if submit_mv:
- submit_mv = [get_link_to_form("Maintenance Visit", mv) for mv in submit_mv]
- frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order")
- .format(", ".join(submit_mv)))
-
- # check work order
- pro_order = frappe.db.sql_list("""
- select name
- from `tabWork Order`
- where sales_order = %s and docstatus = 1""", self.name)
-
- if pro_order:
- pro_order = [get_link_to_form("Work Order", po) for po in pro_order]
- frappe.throw(_("Work Order {0} must be cancelled before cancelling this Sales Order")
- .format(", ".join(pro_order)))
+ if linked_invoices:
+ linked_invoices = [get_link_to_form("Sales Invoice", si) for si in linked_invoices]
+ frappe.throw(_("Sales Invoice {0} must be deleted before cancelling this Sales Order")
+ .format(", ".join(linked_invoices)))
def check_modified_date(self):
mod_db = frappe.db.get_value("Sales Order", self.name, "modified")
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 644c0ac..2a0752e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -10,6 +10,12 @@
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.controllers.accounts_controller import update_child_qty_rate
+from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
+ make_maintenance_schedule,
+)
+from erpnext.maintenance.doctype.maintenance_visit.test_maintenance_visit import (
+ make_maintenance_visit,
+)
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import (
@@ -1278,6 +1284,72 @@
self.assertRaises(frappe.ValidationError, so.cancel)
+ def test_so_cancellation_after_si_submission(self):
+ """
+ Test to check if Sales Order gets cancelled when linked Sales Invoice has been Submitted
+ Expected result: Sales Order should not get cancelled
+ """
+ so = make_sales_order()
+ so.submit()
+ si = make_sales_invoice(so.name)
+ si.submit()
+
+ so.load_from_db()
+ self.assertRaises(frappe.LinkExistsError, so.cancel)
+
+ def test_so_cancellation_after_dn_submission(self):
+ """
+ Test to check if Sales Order gets cancelled when linked Delivery Note has been Submitted
+ Expected result: Sales Order should not get cancelled
+ """
+ so = make_sales_order()
+ so.submit()
+ dn = make_delivery_note(so.name)
+ dn.submit()
+
+ so.load_from_db()
+ self.assertRaises(frappe.LinkExistsError, so.cancel)
+
+ def test_so_cancellation_after_maintenance_schedule_submission(self):
+ """
+ Expected result: Sales Order should not get cancelled
+ """
+ so = make_sales_order()
+ so.submit()
+ ms = make_maintenance_schedule()
+ ms.items[0].sales_order = so.name
+ ms.submit()
+
+ so.load_from_db()
+ self.assertRaises(frappe.LinkExistsError, so.cancel)
+
+ def test_so_cancellation_after_maintenance_visit_submission(self):
+ """
+ Expected result: Sales Order should not get cancelled
+ """
+ so = make_sales_order()
+ so.submit()
+ mv = make_maintenance_visit()
+ mv.purposes[0].prevdoc_doctype = "Sales Order"
+ mv.purposes[0].prevdoc_docname = so.name
+ mv.submit()
+
+ so.load_from_db()
+ self.assertRaises(frappe.LinkExistsError, so.cancel)
+
+ def test_so_cancellation_after_work_order_submission(self):
+ """
+ Expected result: Sales Order should not get cancelled
+ """
+ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+
+ so = make_sales_order(item_code="_Test FG Item", qty=10)
+ so.submit()
+ make_wo_order_test_record(sales_order=so.name)
+
+ so.load_from_db()
+ self.assertRaises(frappe.LinkExistsError, so.cancel)
+
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
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 1e5590e..95f6c4e 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -48,6 +48,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_24",
"net_rate",
"net_amount",
@@ -789,15 +790,23 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:15:05.803091",
+ "modified": "2021-10-05 12:27:25.014789",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/selling/doctype/sales_team/sales_team.json b/erpnext/selling/doctype/sales_team/sales_team.json
index 8767891..cac5b76 100644
--- a/erpnext/selling/doctype/sales_team/sales_team.json
+++ b/erpnext/selling/doctype/sales_team/sales_team.json
@@ -1,247 +1,100 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-04-19 13:30:51",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2013-04-19 13:30:51",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "sales_person",
+ "contact_no",
+ "allocated_percentage",
+ "allocated_amount",
+ "commission_rate",
+ "incentives"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "sales_person",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Sales Person",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "sales_person",
- "oldfieldtype": "Link",
- "options": "Sales Person",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "200px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
+ "allow_on_submit": 1,
+ "fieldname": "sales_person",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Sales Person",
+ "oldfieldname": "sales_person",
+ "oldfieldtype": "Link",
+ "options": "Sales Person",
+ "print_width": "200px",
+ "reqd": 1,
+ "search_index": 1,
"width": "200px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_no",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Contact No.",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "contact_no",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
+ "allow_on_submit": 1,
+ "fieldname": "contact_no",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Contact No.",
+ "oldfieldname": "contact_no",
+ "oldfieldtype": "Data",
+ "print_width": "100px",
"width": "100px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "allocated_percentage",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Contribution (%)",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "allocated_percentage",
- "oldfieldtype": "Currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
+ "allow_on_submit": 1,
+ "fieldname": "allocated_percentage",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Contribution (%)",
+ "oldfieldname": "allocated_percentage",
+ "oldfieldtype": "Currency",
+ "print_width": "100px",
"width": "100px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "allocated_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Contribution to Net Total",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "allocated_amount",
- "oldfieldtype": "Currency",
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
+ "allow_on_submit": 1,
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Contribution to Net Total",
+ "oldfieldname": "allocated_amount",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "print_width": "120px",
+ "read_only": 1,
"width": "120px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "commission_rate",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Commission Rate",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fetch_from": "sales_person.commission_rate",
+ "fetch_if_empty": 1,
+ "fieldname": "commission_rate",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Commission Rate",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "incentives",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Incentives",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "incentives",
- "oldfieldtype": "Currency",
- "options": "Company:company:default_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "allow_on_submit": 1,
+ "fieldname": "incentives",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Incentives",
+ "oldfieldname": "incentives",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-09-17 13:03:14.755974",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Sales Team",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-09 23:55:20.670475",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Sales Team",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 9d8338e..4920584 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -49,11 +49,11 @@
this.$component.append(
`<div class="cart-container">
<div class="abs-cart-container">
- <div class="cart-label">Item Cart</div>
+ <div class="cart-label">${__('Item Cart')}</div>
<div class="cart-header">
- <div class="name-header">Item</div>
- <div class="qty-header">Qty</div>
- <div class="rate-amount-header">Amount</div>
+ <div class="name-header">${__('Item')}</div>
+ <div class="qty-header">${__('Quantity')}</div>
+ <div class="rate-amount-header">${__('Amount')}</div>
</div>
<div class="cart-items-section"></div>
<div class="cart-totals-section"></div>
@@ -78,7 +78,7 @@
make_no_items_placeholder() {
this.$cart_header.css('display', 'none');
this.$cart_items_wrapper.html(
- `<div class="no-item-wrapper">No items in cart</div>`
+ `<div class="no-item-wrapper">${__('No items in cart')}</div>`
);
}
@@ -98,19 +98,23 @@
this.$totals_section.append(
`<div class="add-discount-wrapper">
- ${this.get_discount_icon()} Add Discount
+ ${this.get_discount_icon()} ${__('Add Discount')}
+ </div>
+ <div class="item-qty-total-container">
+ <div class="item-qty-total-label">${__('Total Items')}</div>
+ <div class="item-qty-total-value">0.00</div>
</div>
<div class="net-total-container">
- <div class="net-total-label">Net Total</div>
+ <div class="net-total-label">${__("Net Total")}</div>
<div class="net-total-value">0.00</div>
</div>
<div class="taxes-container"></div>
<div class="grand-total-container">
- <div>Grand Total</div>
+ <div>${__('Grand Total')}</div>
<div>0.00</div>
</div>
- <div class="checkout-btn">Checkout</div>
- <div class="edit-cart-btn">Edit Cart</div>`
+ <div class="checkout-btn">${__('Checkout')}</div>
+ <div class="edit-cart-btn">${__('Edit Cart')}</div>`
)
this.$add_discount_elem = this.$component.find(".add-discount-wrapper");
@@ -126,10 +130,10 @@
},
cols: 5,
keys: [
- [ 1, 2, 3, 'Quantity' ],
- [ 4, 5, 6, 'Discount' ],
- [ 7, 8, 9, 'Rate' ],
- [ '.', 0, 'Delete', 'Remove' ]
+ [ 1, 2, 3, __('Quantity') ],
+ [ 4, 5, 6, __('Discount') ],
+ [ 7, 8, 9, __('Rate') ],
+ [ '.', 0, __('Delete'), __('Remove') ]
],
css_classes: [
[ '', '', '', 'col-span-2' ],
@@ -142,13 +146,14 @@
this.$numpad_section.prepend(
`<div class="numpad-totals">
+ <span class="numpad-item-qty-total"></span>
<span class="numpad-net-total"></span>
<span class="numpad-grand-total"></span>
</div>`
)
this.$numpad_section.append(
- `<div class="numpad-btn checkout-btn" data-button-value="checkout">Checkout</div>`
+ `<div class="numpad-btn checkout-btn" data-button-value="checkout">${__('Checkout')}</div>`
)
}
@@ -386,7 +391,7 @@
'border': '1px dashed var(--gray-500)',
'padding': 'var(--padding-sm) var(--padding-md)'
});
- me.$add_discount_elem.html(`${me.get_discount_icon()} Add Discount`);
+ me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`);
me.discount_field = undefined;
}
},
@@ -411,7 +416,7 @@
});
this.$add_discount_elem.html(
`<div class="edit-discount-btn">
- ${this.get_discount_icon()} Additional ${String(discount).bold()}% discount applied
+ ${this.get_discount_icon()} ${__("Additional")} ${String(discount).bold()}% ${__("discount applied")}
</div>`
);
}
@@ -445,7 +450,7 @@
function get_customer_description() {
if (!email_id && !mobile_no) {
- return `<div class="customer-desc">Click to add email / phone</div>`;
+ return `<div class="customer-desc">${__('Click to add email / phone')}</div>`;
} else if (email_id && !mobile_no) {
return `<div class="customer-desc">${email_id}</div>`;
} else if (mobile_no && !email_id) {
@@ -470,6 +475,7 @@
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
+ this.render_total_item_qty(frm.doc.items);
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
@@ -479,22 +485,37 @@
render_net_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.net-total-container').html(
- `<div>Net Total</div><div>${format_currency(value, currency)}</div>`
+ `<div>${__('Net Total')}</div><div>${format_currency(value, currency)}</div>`
)
this.$numpad_section.find('.numpad-net-total').html(
- `<div>Net Total: <span>${format_currency(value, currency)}</span></div>`
+ `<div>${__('Net Total')}: <span>${format_currency(value, currency)}</span></div>`
+ );
+ }
+
+ render_total_item_qty(items) {
+ var total_item_qty = 0;
+ items.map((item) => {
+ total_item_qty = total_item_qty + item.qty;
+ });
+
+ this.$totals_section.find('.item-qty-total-container').html(
+ `<div>${__('Total Quantity')}</div><div>${total_item_qty}</div>`
+ );
+
+ this.$numpad_section.find('.numpad-item-qty-total').html(
+ `<div>${__('Total Quantity')}: <span>${total_item_qty}</span></div>`
);
}
render_grand_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total-container').html(
- `<div>Grand Total</div><div>${format_currency(value, currency)}</div>`
+ `<div>${__('Grand Total')}</div><div>${format_currency(value, currency)}</div>`
)
this.$numpad_section.find('.numpad-grand-total').html(
- `<div>Grand Total: <span>${format_currency(value, currency)}</span></div>`
+ `<div>${__('Grand Total')}: <span>${format_currency(value, currency)}</span></div>`
);
}
@@ -502,6 +523,7 @@
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
const taxes_html = taxes.map(t => {
+ if (t.tax_amount_after_discount_amount == 0.0) return;
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
return `<div class="tax-row">
<div class="tax-label">${description}</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index ec861d7..fb69b63 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -28,7 +28,7 @@
init_child_components() {
this.$component.html(
`<div class="item-details-header">
- <div class="label">Item Details</div>
+ <div class="label">${__('Item Details')}</div>
<div class="close-btn">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
@@ -201,8 +201,9 @@
`<div class="grid-filler no-select"></div>`
);
}
+ const label = __('Auto Fetch Serial Numbers');
this.$form_container.append(
- `<div class="btn btn-sm btn-secondary auto-fetch-btn">Auto Fetch Serial Numbers</div>`
+ `<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
);
this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem');
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 8352b14..4963852 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -24,7 +24,7 @@
this.wrapper.append(
`<section class="items-selector">
<div class="filter-section">
- <div class="label">All Items</div>
+ <div class="label">${__('All Items')}</div>
<div class="search-field"></div>
<div class="item-group-field"></div>
</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
index e0993e2..a0475c7 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
@@ -16,7 +16,7 @@
this.wrapper.append(
`<section class="past-order-list">
<div class="filter-section">
- <div class="label">Recent Orders</div>
+ <div class="label">${__('Recent Orders')}</div>
<div class="search-field"></div>
<div class="status-field"></div>
</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index dd9e05a..eeb8523 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -17,16 +17,16 @@
this.wrapper.append(
`<section class="past-order-summary">
<div class="no-summary-placeholder">
- Select an invoice to load summary data
+ ${__('Select an invoice to load summary data')}
</div>
<div class="invoice-summary-wrapper">
<div class="abs-container">
<div class="upper-section"></div>
- <div class="label">Items</div>
+ <div class="label">${__('Items')}</div>
<div class="items-container summary-container"></div>
- <div class="label">Totals</div>
+ <div class="label">${__('Totals')}</div>
<div class="totals-container summary-container"></div>
- <div class="label">Payments</div>
+ <div class="label">${__('Payments')}</div>
<div class="payments-container summary-container"></div>
<div class="summary-btns"></div>
</div>
@@ -82,7 +82,7 @@
return `<div class="left-section">
<div class="customer-name">${doc.customer}</div>
<div class="customer-email">${this.customer_email}</div>
- <div class="cashier">Sold by: ${doc.owner}</div>
+ <div class="cashier">${__('Sold by')}: ${doc.owner}</div>
</div>
<div class="right-section">
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
@@ -121,7 +121,7 @@
get_net_total_html(doc) {
return `<div class="summary-row-wrapper">
- <div>Net Total</div>
+ <div>${__('Net Total')}</div>
<div>${format_currency(doc.net_total, doc.currency)}</div>
</div>`;
}
@@ -144,14 +144,14 @@
get_grand_total_html(doc) {
return `<div class="summary-row-wrapper grand-total">
- <div>Grand Total</div>
+ <div>${__('Grand Total')}</div>
<div>${format_currency(doc.grand_total, doc.currency)}</div>
</div>`;
}
get_payment_html(doc, payment) {
return `<div class="summary-row-wrapper payments">
- <div>${payment.mode_of_payment}</div>
+ <div>${__(payment.mode_of_payment)}</div>
<div>${format_currency(payment.amount, doc.currency)}</div>
</div>`;
}
@@ -285,8 +285,9 @@
if (m.condition) {
m.visible_btns.forEach(b => {
const class_name = b.split(' ')[0].toLowerCase();
+ const btn = __(b);
this.$summary_btns.append(
- `<div class="summary-btn btn btn-default ${class_name}-btn">${b}</div>`
+ `<div class="summary-btn btn btn-default ${class_name}-btn">${btn}</div>`
);
});
}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 7ddbf45..b9b6559 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -18,11 +18,11 @@
prepare_dom() {
this.wrapper.append(
`<section class="payment-container">
- <div class="section-label payment-section">Payment Method</div>
+ <div class="section-label payment-section">${__('Payment Method')}</div>
<div class="payment-modes"></div>
<div class="fields-numpad-container">
<div class="fields-section">
- <div class="section-label">Additional Information</div>
+ <div class="section-label">${__('Additional Information')}</div>
<div class="invoice-fields"></div>
</div>
<div class="number-pad"></div>
@@ -30,7 +30,7 @@
<div class="totals-section">
<div class="totals"></div>
</div>
- <div class="submit-order-btn">Complete Order</div>
+ <div class="submit-order-btn">${__("Complete Order")}</div>
</section>`
);
this.$component = this.wrapper.find('.payment-container');
@@ -518,12 +518,12 @@
this.$totals.html(
`<div class="col">
- <div class="total-label">Grand Total</div>
+ <div class="total-label">${__('Grand Total')}</div>
<div class="value">${format_currency(grand_total, currency)}</div>
</div>
<div class="seperator-y"></div>
<div class="col">
- <div class="total-label">Paid Amount</div>
+ <div class="total-label">${__('Paid Amount')}</div>
<div class="value">${format_currency(paid_amount, currency)}</div>
</div>
<div class="seperator-y"></div>
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index a86e604..e2e0db4 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -157,25 +157,19 @@
commission_rate() {
this.calculate_commission();
- refresh_field("total_commission");
}
total_commission() {
- if(this.frm.doc.base_net_total) {
- frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]);
+ frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
- if(this.frm.doc.base_net_total < this.frm.doc.total_commission) {
- var msg = (__("[Error]") + " " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission",
- this.frm.doc.name)) + " > " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name)));
- frappe.msgprint(msg);
- throw msg;
- }
+ const { amount_eligible_for_commission } = this.frm.doc;
+ if(!amount_eligible_for_commission) return;
- this.frm.set_value("commission_rate",
- flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total));
- }
+ this.frm.set_value(
+ "commission_rate", flt(
+ this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
+ )
+ );
}
allocated_percentage(doc, cdt, cdn) {
@@ -185,7 +179,7 @@
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person));
- sales_person.allocated_amount = flt(this.frm.doc.base_net_total *
+ sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person);
@@ -259,28 +253,39 @@
}
calculate_commission() {
- if(this.frm.fields_dict.commission_rate) {
- if(this.frm.doc.commission_rate > 100) {
- var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) +
- " " + __("cannot be greater than 100");
- frappe.msgprint(msg);
- throw msg;
- }
+ if(!this.frm.fields_dict.commission_rate) return;
- this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0,
- precision("total_commission"));
+ if(this.frm.doc.commission_rate > 100) {
+ this.frm.set_value("commission_rate", 100);
+ frappe.throw(`${__(frappe.meta.get_label(
+ this.frm.doc.doctype, "commission_rate", this.frm.doc.name
+ ))} ${__("cannot be greater than 100")}`);
}
+
+ this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
+ (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
+ )
+
+ this.frm.doc.total_commission = flt(
+ this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
+ precision("total_commission")
+ );
+
+ refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_contribution() {
var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person);
- if(sales_person.allocated_percentage) {
- sales_person.allocated_amount = flt(
- me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0,
- precision("allocated_amount", sales_person));
- }
+ if (!sales_person.allocated_percentage) return;
+
+ sales_person.allocated_amount = flt(
+ me.frm.doc.amount_eligible_for_commission
+ * sales_person.allocated_percentage
+ / 100.0,
+ precision("allocated_amount", sales_person)
+ );
});
}
@@ -474,33 +479,37 @@
"reqd": 1
},
{
+ "fieldtype": "Table MultiSelect",
+ "label": __("Competitors"),
+ "fieldname": "competitors",
+ "options": "Competitor Detail"
+ },
+ {
"fieldtype": "Text",
"label": __("Detailed Reason"),
"fieldname": "detailed_reason"
},
],
primary_action: function() {
- var values = dialog.get_values();
- var reasons = values["lost_reason"];
- var detailed_reason = values["detailed_reason"];
+ let values = dialog.get_values();
frm.call({
doc: frm.doc,
method: 'declare_enquiry_lost',
args: {
- 'lost_reasons_list': reasons,
- 'detailed_reason': detailed_reason
+ 'lost_reasons_list': values.lost_reason,
+ 'competitors': values.competitors,
+ 'detailed_reason': values.detailed_reason
},
callback: function(r) {
dialog.hide();
frm.reload_doc();
},
});
- refresh_field("lost_reason");
},
primary_action_label: __('Declare Lost')
});
dialog.show();
}
-})
+})
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 5ebfa04..dedd2d3 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -413,7 +413,7 @@
frappe.get_attr(module_name)(company, False)
except Exception as e:
frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country)))
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 4c42b61..c94b346 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -43,7 +43,6 @@
def on_update(self):
NestedSet.on_update(self)
invalidate_cache_for(self)
- self.validate_name_with_item()
self.validate_one_root()
self.delete_child_item_groups_key()
@@ -67,10 +66,6 @@
WebsiteGenerator.on_trash(self)
self.delete_child_item_groups_key()
- def validate_name_with_item(self):
- if frappe.db.exists("Item", self.name):
- frappe.throw(frappe._("An item exists with same name ({0}), please change the item group name or rename the item").format(self.name), frappe.NameError)
-
def get_context(self, context):
context.show_search=True
context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6
diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
index 4def6eb..986b4e8 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ b/erpnext/setup/doctype/naming_series/naming_series.py
@@ -180,11 +180,11 @@
prefix = parse_naming_series(parts)
return prefix
-def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True):
+def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1):
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
if naming_series:
make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False)
- make_property_setter(doctype, "naming_series", "reqd", 1, "Check", validate_fields_for_doctype=False)
+ make_property_setter(doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False)
# set values for mandatory
try:
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index a7d90ea..ebbe233 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -194,7 +194,9 @@
def create_lead_for_item_inquiry(lead, subject, message):
lead = frappe.parse_json(lead)
lead_doc = frappe.new_doc('Lead')
- lead_doc.update(lead)
+ for fieldname in ("lead_name", "company_name", "email_id", "phone"):
+ lead_doc.set(fieldname, lead.get(fieldname))
+
lead_doc.set('lead_owner', '')
if not frappe.db.exists('Lead Source', 'Product Inquiry'):
@@ -202,6 +204,7 @@
'doctype': 'Lead Source',
'source_name' : 'Product Inquiry'
}).insert(ignore_permissions=True)
+
lead_doc.set('source', 'Product Inquiry')
try:
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 48b1cc5..da9c66d 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -4,6 +4,8 @@
import frappe
from frappe.model.document import Document
+from frappe.query_builder import Case
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, nowdate
@@ -19,34 +21,42 @@
- flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract))
def get_first_sle(self):
- sle = frappe.db.sql("""
- select * from `tabStock Ledger Entry`
- where item_code = %s
- and warehouse = %s
- order by timestamp(posting_date, posting_time) asc, creation asc
- limit 1
- """, (self.item_code, self.warehouse), as_dict=1)
- return sle and sle[0] or None
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ first_sle = (
+ frappe.qb.from_(sle)
+ .select("*")
+ .where((sle.item_code == self.item_code) & (sle.warehouse == self.warehouse))
+ .orderby(sle.posting_date, sle.posting_time, sle.creation)
+ .limit(1)
+ ).run(as_dict=True)
+
+ return first_sle and first_sle[0] or None
def update_reserved_qty_for_production(self):
'''Update qty reserved for production from Production Item tables
in open work orders'''
- self.reserved_qty_for_production = frappe.db.sql('''
- SELECT
- CASE WHEN ifnull(skip_transfer, 0) = 0 THEN
- SUM(item.required_qty - item.transferred_qty)
- ELSE
- SUM(item.required_qty - item.consumed_qty)
- END
- FROM `tabWork Order` pro, `tabWork Order Item` item
- WHERE
- item.item_code = %s
- and item.parent = pro.name
- and pro.docstatus = 1
- and item.source_warehouse = %s
- and pro.status not in ("Stopped", "Completed")
- and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty)
- ''', (self.item_code, self.warehouse))[0][0]
+
+ wo = frappe.qb.DocType("Work Order")
+ wo_item = frappe.qb.DocType("Work Order Item")
+
+ self.reserved_qty_for_production = (
+ frappe.qb
+ .from_(wo)
+ .from_(wo_item)
+ .select(Case()
+ .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty))
+ .else_(Sum(wo_item.required_qty - wo_item.consumed_qty))
+ )
+ .where(
+ (wo_item.item_code == self.item_code)
+ & (wo_item.parent == wo.name)
+ & (wo.docstatus == 1)
+ & (wo_item.source_warehouse == self.warehouse)
+ & (wo.status.notin(["Stopped", "Completed"]))
+ & ((wo_item.required_qty > wo_item.transferred_qty)
+ | (wo_item.required_qty > wo_item.consumed_qty))
+ )
+ ).run()[0][0] or 0.0
self.set_projected_qty()
@@ -55,36 +65,53 @@
def update_reserved_qty_for_sub_contracting(self):
#reserved qty
- reserved_qty_for_sub_contract = frappe.db.sql('''
- select ifnull(sum(itemsup.required_qty),0)
- from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup
- where
- itemsup.rm_item_code = %s
- and itemsup.parent = po.name
- and po.docstatus = 1
- and po.is_subcontracted = 'Yes'
- and po.status != 'Closed'
- and po.per_received < 100
- and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0]
- #Get Transferred Entries
- materials_transferred = frappe.db.sql("""
- select
- ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
- from
- `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
- where
- se.docstatus=1
- and se.purpose='Send to Subcontractor'
- and ifnull(se.purchase_order, '') !=''
- and (sed.item_code = %(item)s or sed.original_item = %(item)s)
- and se.name = sed.parent
- and se.purchase_order = po.name
- and po.docstatus = 1
- and po.is_subcontracted = 'Yes'
- and po.status != 'Closed'
- and po.per_received < 100
- """, {'item': self.item_code})[0][0]
+ po = frappe.qb.DocType("Purchase Order")
+ supplied_item = frappe.qb.DocType("Purchase Order Item Supplied")
+
+ reserved_qty_for_sub_contract = (
+ frappe.qb
+ .from_(po)
+ .from_(supplied_item)
+ .select(Sum(Coalesce(supplied_item.required_qty, 0)))
+ .where(
+ (supplied_item.rm_item_code == self.item_code)
+ & (po.name == supplied_item.parent)
+ & (po.docstatus == 1)
+ & (po.is_subcontracted == "Yes")
+ & (po.status != "Closed")
+ & (po.per_received < 100)
+ & (supplied_item.reserve_warehouse == self.warehouse)
+ )
+ ).run()[0][0] or 0.0
+
+ se = frappe.qb.DocType("Stock Entry")
+ se_item = frappe.qb.DocType("Stock Entry Detail")
+
+ materials_transferred = (
+ frappe.qb
+ .from_(se)
+ .from_(se_item)
+ .from_(po)
+ .select(Sum(
+ Case()
+ .when(se.is_return == 1, se_item.transfer_qty * -1)
+ .else_(se_item.transfer_qty)
+ ))
+ .where(
+ (se.docstatus == 1)
+ & (se.purpose == "Send to Subcontractor")
+ & (Coalesce(se.purchase_order, "") != "")
+ & ((se_item.item_code == self.item_code)
+ | (se_item.original_item == self.item_code))
+ & (se.name == se_item.parent)
+ & (po.name == se.purchase_order)
+ & (po.docstatus == 1)
+ & (po.is_subcontracted == "Yes")
+ & (po.status != "Closed")
+ & (po.per_received < 100)
+ )
+ ).run()[0][0] or 0.0
if reserved_qty_for_sub_contract > materials_transferred:
reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred
@@ -160,4 +187,4 @@
'indented_qty': indented_qty,
'planned_qty': planned_qty,
'projected_qty': projected_qty
- })
\ No newline at end of file
+ })
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index ad1b3b4..e78d501 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -145,6 +145,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1302,6 +1303,12 @@
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
@@ -1312,6 +1319,7 @@
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
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 a96c299..51c88be 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -49,6 +49,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_25",
"net_rate",
"net_amount",
@@ -753,13 +754,20 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:12:44.018872",
+ "modified": "2021-10-06 12:12:44.018872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 4b314a0..5469a9f 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -88,6 +88,7 @@
"sales_details",
"sales_uom",
"is_sales_item",
+ "grant_commission",
"column_break3",
"max_discount",
"deferred_revenue",
@@ -1020,6 +1021,12 @@
"fieldname": "website_image_alt",
"fieldtype": "Data",
"label": "Image Description"
+ },
+ {
+ "default": "1",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission"
}
],
"has_web_view": 1,
@@ -1028,8 +1035,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "max_attachments": 1,
- "modified": "2021-10-27 21:04:00.324786",
+ "modified": "2021-11-30 02:33:06.572442",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index fa42c7d..c9b8a37 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -152,7 +152,6 @@
def on_update(self):
invalidate_cache_for_item(self)
- self.validate_name_with_item_group()
self.update_variants()
self.update_item_price()
self.update_template_item()
@@ -223,10 +222,11 @@
'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
def validate_website_image(self):
+ """Validate if the website image is a public file"""
+
if frappe.flags.in_import:
return
- """Validate if the website image is a public file"""
auto_set_website_image = False
if not self.website_image and self.image:
auto_set_website_image = True
@@ -256,10 +256,11 @@
self.website_image = None
def make_thumbnail(self):
+ """Make a thumbnail of `website_image`"""
+
if frappe.flags.in_import:
return
- """Make a thumbnail of `website_image`"""
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
@@ -628,12 +629,6 @@
where item_code = %s and is_cancelled = 0 limit 1""", self.name))
return self._stock_ledger_created
- def validate_name_with_item_group(self):
- # causes problem with tree build
- if frappe.db.exists("Item Group", self.name):
- frappe.throw(
- _("An Item Group exists with same name, please change the item name or rename the item group"))
-
def update_item_price(self):
frappe.db.sql("""
UPDATE `tabItem Price`
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 7237178..8b1224b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -488,7 +488,7 @@
item_doc.save()
# Check values saved correctly
- barcodes = frappe.get_list(
+ barcodes = frappe.get_all(
'Item Barcode',
fields=['barcode', 'barcode_type'],
filters={'parent': item_code})
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
index 433f78a..9c1a809 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
@@ -35,7 +35,7 @@
refresh() {
var help_content =
`<br><br>
- <table class="table table-bordered" style="background-color: #f9f9f9;">
+ <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 102730b..2909a2d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -10,6 +10,7 @@
import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account
+from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
@@ -22,20 +23,54 @@
def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
+ def test_purchase_receipt_received_qty(self):
+ """
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
+ """
+ pr = make_purchase_receipt(
+ qty=1,
+ rejected_qty=1,
+ received_qty=3,
+ item_code="_Test Item Home Desktop 200",
+ do_not_save=True
+ )
+ self.assertRaises(QtyMismatchError, pr.save)
+
+ pr.items[0].received_qty = 0
+ pr.save()
+ self.assertEqual(pr.items[0].received_qty, 2)
+
+ # teardown
+ pr.delete()
+
def test_reverse_purchase_receipt_sle(self):
pr = make_purchase_receipt(qty=0.5, item_code="_Test Item Home Desktop 200")
- sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": pr.name}, ['actual_qty'])
+ sl_entry = frappe.db.get_all(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name
+ },
+ ['actual_qty']
+ )
self.assertEqual(len(sl_entry), 1)
self.assertEqual(sl_entry[0].actual_qty, 0.5)
pr.cancel()
- sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": pr.name}, ['actual_qty'], order_by='creation')
+ sl_entry_cancelled = frappe.db.get_all(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name
+ },
+ ['actual_qty'],
+ order_by='creation'
+ )
self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
@@ -61,8 +96,15 @@
}]
}).insert()
- template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice')
- old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms")
+ template = frappe.db.get_value(
+ "Payment Terms Template",
+ "_Test Payment Terms Template For Purchase Invoice"
+ )
+ old_template_in_supplier = frappe.db.get_value(
+ "Supplier",
+ "_Test Supplier",
+ "payment_terms"
+ )
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template)
pr = make_purchase_receipt(do_not_save=True)
@@ -88,30 +130,59 @@
# teardown
pi.delete() # draft PI
pr.cancel()
- frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
- frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
+ frappe.db.set_value(
+ "Supplier",
+ "_Test Supplier",
+ "payment_terms",
+ old_template_in_supplier
+ )
+ frappe.get_doc(
+ "Payment Terms Template",
+ "_Test Payment Terms Template For Purchase Invoice"
+ ).delete()
def test_purchase_receipt_no_gl_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
-
- existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ ["actual_qty", "stock_value"]
+ )
if existing_bin_qty < 0:
- make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
+ make_stock_entry(
+ item_code="_Test Item",
+ target="_Test Warehouse - _TC",
+ qty=abs(existing_bin_qty)
+ )
pr = make_purchase_receipt()
- stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
- {"voucher_type": "Purchase Receipt", "voucher_no": pr.name,
- "item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value_difference")
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ "stock_value_difference"
+ )
self.assertEqual(stock_value_difference, 250)
- current_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ current_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ "stock_value"
+ )
self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250)
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
@@ -133,13 +204,17 @@
pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500)
- self.assertTrue(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}))
+ self.assertTrue(
+ frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
+ )
pr.load_from_db()
batch_no = pr.items[0].batch_no
pr.cancel()
- self.assertFalse(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}))
+ self.assertFalse(
+ frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
+ )
self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no}))
def test_duplicate_serial_nos(self):
@@ -158,42 +233,78 @@
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
pr.load_from_db()
- serial_nos = frappe.db.get_value('Stock Ledger Entry',
- {'voucher_type': 'Purchase Receipt', 'voucher_no': pr.name, 'item_code': item.name}, 'serial_no')
+ serial_nos = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "item_code": item.name
+ },
+ "serial_no"
+ )
serial_nos = get_serial_nos(serial_nos)
self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos)
# Then tried to receive same serial nos in difference company
- pr_different_company = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True,
- warehouse = 'Stores - _TC1')
+ pr_different_company = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ serial_no='\n'.join(serial_nos),
+ company='_Test Company 1',
+ do_not_submit=True,
+ warehouse = 'Stores - _TC1'
+ )
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
# Then made delivery note to remove the serial nos from stock
- dn = create_delivery_note(item_code=item.name, qty=2, rate = 1500, serial_no='\n'.join(serial_nos))
+ dn = create_delivery_note(
+ item_code=item.name,
+ qty=2,
+ rate=1500,
+ serial_no='\n'.join(serial_nos)
+ )
dn.load_from_db()
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
posting_date = add_days(today(), -3)
# Try to receive same serial nos again in the same company with backdated.
- pr1 = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- posting_date=posting_date, serial_no='\n'.join(serial_nos), do_not_submit=True)
+ pr1 = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ posting_date=posting_date,
+ serial_no='\n'.join(serial_nos),
+ do_not_submit=True
+ )
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
# Try to receive same serial nos with different company with backdated.
- pr2 = make_purchase_receipt(item_code=item.name, qty=2, rate=500,
- posting_date=posting_date, serial_no='\n'.join(serial_nos), company='_Test Company 1', do_not_submit=True,
- warehouse = 'Stores - _TC1')
+ pr2 = make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ posting_date=posting_date,
+ serial_no='\n'.join(serial_nos),
+ company="_Test Company 1",
+ do_not_submit=True,
+ warehouse="Stores - _TC1"
+ )
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
# Receive the same serial nos after the delivery note posting date and time
- make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no='\n'.join(serial_nos))
+ make_purchase_receipt(
+ item_code=item.name,
+ qty=2,
+ rate=500,
+ serial_no='\n'.join(serial_nos)
+ )
# Raise the error for backdated deliver note entry cancel
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
@@ -236,11 +347,23 @@
def test_subcontracting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
- qty=100, basic_rate=100)
- pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes")
+ frappe.db.set_value(
+ "Buying Settings", None,
+ "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
+
+ make_stock_entry(
+ item_code="_Test Item", qty=100,
+ target="_Test Warehouse 1 - _TC", basic_rate=100
+ )
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100", qty=100,
+ target="_Test Warehouse 1 - _TC", basic_rate=100
+ )
+ pr = make_purchase_receipt(
+ item_code="_Test FG Item", qty=10,
+ rate=500, is_subcontracted="Yes"
+ )
self.assertEqual(len(pr.get("supplied_items")), 2)
rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
@@ -250,17 +373,33 @@
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
+ frappe.db.set_value(
+ "Buying Settings", None,
+ "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
- se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
- qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+ 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")
+ 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')
+ 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"
+ )
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -294,13 +433,23 @@
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
- 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)
- se3 = make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 2", qty=1, basic_rate=100)
+ # stock raw materials in a warehouse before transfer
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 1",
+ qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "_Test FG Item",
+ qty=1, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 2",
+ qty=1, basic_rate=100
+ )
+
rm_items = [
{
"item_code": item_code,
@@ -334,11 +483,17 @@
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"),
- pr.supplier)
+ pr_row_1_serial_no = pr.get("items")[0].serial_no
+
+ self.assertEqual(
+ frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"),
+ pr.supplier
+ )
pr.cancel()
- self.assertFalse(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "warehouse"))
+ self.assertFalse(
+ frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")
+ )
def test_rejected_serial_no(self):
pr = frappe.copy_doc(test_records[0])
@@ -365,18 +520,33 @@
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")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
- is_return=1, return_against=pr.name, qty=-2, do_not_submit=1)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-2,
+ do_not_submit=1
+ )
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
# check sle
- outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name}, "outgoing_rate")
+ outgoing_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name
+ },
+ "outgoing_rate"
+ )
self.assertEqual(outgoing_rate, 50)
@@ -440,11 +610,21 @@
pr.cancel()
def test_purchase_return_full(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1"
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-5,
+ do_not_submit=1
+ )
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
@@ -466,15 +646,41 @@
rejected_warehouse="_Test Rejected Warehouse - TCP1"
if not frappe.db.exists("Warehouse", rejected_warehouse):
- get_warehouse(company = "_Test Company with perpetual inventory",
- abbr = " - TCP1", warehouse_name = "_Test Rejected Warehouse").name
+ get_warehouse(
+ company = "_Test Company with perpetual inventory",
+ abbr = " - TCP1",
+ warehouse_name = "_Test Rejected Warehouse"
+ ).name
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", received_qty=4, qty=2, rejected_warehouse=rejected_warehouse)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ qty=2,
+ rejected_qty=2,
+ rejected_warehouse=rejected_warehouse
+ )
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, received_qty = -4, qty=-2, rejected_warehouse=rejected_warehouse)
+ return_pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1,
+ return_against=pr.name,
+ qty=-2,
+ rejected_qty = -2,
+ rejected_warehouse=rejected_warehouse
+ )
- actual_qty = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name, 'warehouse': return_pr.items[0].rejected_warehouse}, "actual_qty")
+ actual_qty = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name,
+ "warehouse": return_pr.items[0].rejected_warehouse
+ },
+ "actual_qty"
+ )
self.assertEqual(actual_qty, -2)
@@ -499,8 +705,13 @@
"purchase_document_no": pr.name
})
- return_pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=-1,
- is_return=1, return_against=pr.name, serial_no=serial_no)
+ return_pr = make_purchase_receipt(
+ item_code="_Test Serialized Item With Series",
+ qty=-1,
+ is_return=1,
+ return_against=pr.name,
+ serial_no=serial_no
+ )
_check_serial_no_values(serial_no, {
"warehouse": "",
@@ -522,9 +733,21 @@
})
row.db_update()
- pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0)
- return_pr = make_purchase_receipt(item_code=item_code, qty=-10, uom="Unit",
- stock_uom="Box", conversion_factor=0.1, is_return=1, return_against=pr.name)
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ uom="Box",
+ conversion_factor=1.0
+ )
+ return_pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=-10,
+ uom="Unit",
+ stock_uom="Box",
+ conversion_factor=0.1,
+ is_return=1,
+ return_against=pr.name
+ )
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
@@ -540,13 +763,19 @@
pr.submit()
update_purchase_receipt_status(pr.name, "Closed")
- self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "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
+ """Flow:
+ 1. PO -> PR1 -> PI
+ 2. PO -> PI
+ 3. PO -> PR2.
+ """
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_purchase_invoice_from_po,
)
@@ -610,21 +839,39 @@
pr_doc = make_purchase_receipt(item_code=item_code,
qty=1, serial_no = serial_no)
- self.assertEqual(serial_no, frappe.db.get_value("Serial No",
- {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, "name"))
+ self.assertEqual(
+ serial_no,
+ frappe.db.get_value(
+ "Serial No",
+ {
+ "purchase_document_type": "Purchase Receipt",
+ "purchase_document_no": pr_doc.name
+ },
+ "name"
+ )
+ )
pr_doc.cancel()
- #check for the auto created serial nos
+ # check for the auto created serial nos
item_code = "Test Auto Created Serial No"
if not frappe.db.exists("Item", item_code):
- item = make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###"))
+ make_item(item_code, dict(has_serial_no=1, serial_no_series="KLJL.###"))
new_pr_doc = make_purchase_receipt(item_code=item_code, qty=1)
serial_no = get_serial_nos(new_pr_doc.items[0].serial_no)[0]
- self.assertEqual(serial_no, frappe.db.get_value("Serial No",
- {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
+ 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()
@@ -700,8 +947,12 @@
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"
- create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory")
+ create_cost_center(
+ cost_center_name="_Test Cost Center for BS Account",
+ company="_Test Company with perpetual inventory"
+ )
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@@ -709,10 +960,16 @@
'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)
+ stock_in_hand_account = get_inventory_account(
+ pr.company, pr.get("items")[0].warehouse
+ )
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -736,9 +993,16 @@
'doctype': 'Location',
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(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)
+ pr = make_purchase_receipt(
+ 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)
self.assertTrue(gl_entries)
@@ -766,7 +1030,11 @@
po = create_purchase_order()
pr = create_pr_against_po(po.name)
- pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True)
+ pr1 = make_purchase_receipt(
+ qty=-1,
+ is_return=1, return_against=pr.name,
+ do_not_submit=True
+ )
pr1.items[0].purchase_order = po.name
pr1.items[0].purchase_order_item = po.items[0].name
pr1.items[0].purchase_receipt_item = pr.items[0].name
@@ -799,7 +1067,11 @@
pi1.save()
pi1.submit()
- pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True)
+ pr2 = make_purchase_receipt(
+ qty=-2,
+ is_return=1, return_against=pr1.name,
+ do_not_submit=True
+ )
pr2.items[0].purchase_receipt_item = pr1.items[0].name
pr2.submit()
@@ -841,14 +1113,22 @@
pr1.cancel()
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'})
+ 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")
+ 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 = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1",
+ do_not_save=1
+ )
pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = ''
@@ -930,10 +1210,24 @@
}
rm_items = [
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name},
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
- "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}
+ {
+ "item_code":item_code,
+ "rm_item_code":"Sub Contracted Raw Material 3",
+ "item_name":"_Test Item",
+ "qty":300,
+ "warehouse":"_Test Warehouse - _TC",
+ "stock_uom":"Nos",
+ "name": po.supplied_items[0].name
+ },
+ {
+ "item_code":item_code,
+ "rm_item_code":"Sub Contracted Raw Material 3",
+ "item_name":"_Test Item",
+ "qty":200,
+ "warehouse":"_Test Warehouse - _TC",
+ "stock_uom":"Nos",
+ "name": po.supplied_items[0].name
+ }
]
rm_item_string = json.dumps(rm_items)
@@ -943,8 +1237,14 @@
se.items[1].batch_no = ste2.items[0].batch_no
se.submit()
- supplied_qty = frappe.db.get_value("Purchase Order Item Supplied",
- {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty")
+ supplied_qty = frappe.db.get_value(
+ "Purchase Order Item Supplied",
+ {
+ "parent": po.name,
+ "rm_item_code": "Sub Contracted Raw Material 3"
+ },
+ "supplied_qty"
+ )
self.assertEqual(supplied_qty, 500.00)
@@ -1016,10 +1316,18 @@
company = '_Test Company with perpetual inventory'
service_item = '_Test Non Stock Item'
- before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items')
- frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1)
+ before_test_value = frappe.db.get_value(
+ 'Company', company, 'enable_perpetual_inventory_for_non_stock_items'
+ )
+ frappe.db.set_value(
+ 'Company', company,
+ 'enable_perpetual_inventory_for_non_stock_items', 1
+ )
srbnb_account = 'Stock Received But Not Billed - TCP1'
- frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account)
+ frappe.db.set_value(
+ 'Company', company,
+ 'service_received_but_not_billed', srbnb_account
+ )
pr = make_purchase_receipt(
company=company, item=service_item,
@@ -1051,7 +1359,10 @@
self.assertEqual(len(item_one_gl_entry), 1)
self.assertEqual(len(item_two_gl_entry), 1)
- frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
+ frappe.db.set_value(
+ 'Company', company,
+ 'enable_perpetual_inventory_for_non_stock_items', before_test_value
+ )
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
@@ -1076,10 +1387,19 @@
pr.submit()
# Get exchnage gain and loss account
- exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
+ exchange_gain_loss_account = frappe.db.get_value(
+ 'Company', pr.company, 'exchange_gain_loss_account'
+ )
# fetching the latest GL Entry with exchange gain and loss account account
- amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
+ amount = frappe.db.get_value(
+ 'GL Entry',
+ {
+ 'account': exchange_gain_loss_account,
+ 'voucher_no': pr.name
+ },
+ 'credit'
+ )
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
@@ -1225,8 +1545,8 @@
pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule
qty = args.qty or 5
- received_qty = args.received_qty or qty
- rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty)
+ rejected_qty = args.rejected_qty or 0
+ received_qty = args.received_qty or flt(rejected_qty) + flt(qty)
item_code = args.item or args.item_code or "_Test Item"
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
@@ -1249,9 +1569,12 @@
if args.get_multiple_items:
pr.items = []
- for item in get_items(warehouse= args.warehouse, cost_center = args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center')):
- pr.append("items", item)
+ company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center')
+ cost_center = args.cost_center or company_cost_center
+
+ for item in get_items(warehouse=args.warehouse, cost_center=cost_center):
+ pr.append("items", item)
if args.get_taxes_and_charges:
for tax in get_taxes():
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 3efa66e..30ea1c3 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -197,6 +197,7 @@
},
{
"bold": 1,
+ "default": "0",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Quantity",
@@ -204,6 +205,7 @@
"oldfieldtype": "Currency",
"print_hide": 1,
"print_width": "100px",
+ "read_only": 1,
"reqd": 1,
"width": "100px"
},
@@ -219,8 +221,10 @@
"width": "100px"
},
{
+ "columns": 1,
"fieldname": "rejected_qty",
"fieldtype": "Float",
+ "in_list_view": 1,
"label": "Rejected Quantity",
"oldfieldname": "rejected_qty",
"oldfieldtype": "Currency",
@@ -327,7 +331,7 @@
},
{
"bold": 1,
- "columns": 3,
+ "columns": 2,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -543,6 +547,7 @@
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Accepted Qty in Stock UOM",
+ "no_copy": 1,
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -882,7 +887,9 @@
"fieldname": "received_stock_qty",
"fieldtype": "Float",
"label": "Received Qty in Stock UOM",
- "print_hide": 1
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
@@ -969,10 +976,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 16:02:40.338597",
+ "modified": "2021-11-15 15:46:10.591600",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
index a800bf8..cd7e63b 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -64,7 +64,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "Queued\nIn Progress\nCompleted\nFailed",
+ "options": "Queued\nIn Progress\nCompleted\nSkipped\nFailed",
"read_only": 1
},
{
@@ -177,10 +177,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-07-22 18:59:43.057878",
+ "modified": "2021-11-24 02:18:10.524560",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -206,27 +207,12 @@
"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,
@@ -234,7 +220,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "Accounts User",
+ "role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
@@ -242,4 +228,4 @@
],
"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
index 86b4b86..965a32d 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -1,7 +1,6 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.model.document import Document
@@ -19,7 +18,7 @@
class RepostItemValuation(Document):
def validate(self):
- self.set_status()
+ self.set_status(write=False)
self.reset_field_values()
self.set_company()
@@ -27,23 +26,27 @@
if self.based_on == 'Transaction':
self.item_code = None
self.warehouse = None
- else:
- self.voucher_type = None
- self.voucher_no = None
+
+ self.allow_negative_stock = self.allow_negative_stock or \
+ cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
def set_company(self):
- if self.voucher_type and self.voucher_no:
+ if self.based_on == "Transaction":
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):
+ def set_status(self, status=None, write=True):
+ status = status or self.status
if not status:
- status = 'Queued'
- self.db_set('status', status)
+ self.status = 'Queued'
+ else:
+ self.status = status
+ if write:
+ self.db_set('status', self.status)
def on_submit(self):
- if not frappe.flags.in_test:
+ if not frappe.flags.in_test or self.flags.dont_run_in_test:
return
frappe.enqueue(repost, timeout=1800, queue='long',
@@ -55,6 +58,37 @@
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=True, doc=self)
+ def deduplicate_similar_repost(self):
+ """ Deduplicate similar reposts based on item-warehouse-posting combination."""
+ if self.based_on != "Item and Warehouse":
+ return
+
+ filters = {
+ "item_code": self.item_code,
+ "warehouse": self.warehouse,
+ "name": self.name,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ }
+
+ frappe.db.sql("""
+ update `tabRepost Item Valuation`
+ set status = 'Skipped'
+ WHERE item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and name != %(name)s
+ and TIMESTAMP(posting_date, posting_time) > TIMESTAMP(%(posting_date)s, %(posting_time)s)
+ and docstatus = 1
+ and status = 'Queued'
+ and based_on = 'Item and Warehouse'
+ """,
+ filters
+ )
+
+def on_doctype_update():
+ frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse")
+
+
def repost(doc):
try:
if not frappe.db.exists("Repost Item Valuation", doc.name):
@@ -130,8 +164,10 @@
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
- doc = frappe.get_cached_doc('Repost Item Valuation', row.name)
- repost(doc)
+ doc = frappe.get_doc('Repost Item Valuation', row.name)
+ if doc.status in ('Queued', 'In Progress'):
+ doc.deduplicate_similar_repost()
+ repost(doc)
riv_entries = get_repost_item_valuation_entries()
if riv_entries:
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
index c086f93..de79316 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -5,6 +5,8 @@
import frappe
+from erpnext.controllers.stock_controller import create_item_wise_repost_entries
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot,
)
@@ -70,3 +72,69 @@
in_configured_timeslot(repost_settings, case.get("current_time")),
msg=f"Exepcted false from : {case}",
)
+
+ def test_create_item_wise_repost_item_valuation_entries(self):
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ get_multiple_items=True,
+ )
+
+ rivs = create_item_wise_repost_entries(pr.doctype, pr.name)
+ self.assertGreaterEqual(len(rivs), 2)
+ self.assertIn("_Test Item", [d.item_code for d in rivs])
+
+ for riv in rivs:
+ self.assertEqual(riv.company, "_Test Company with perpetual inventory")
+ self.assertEqual(riv.warehouse, "Stores - TCP1")
+
+ def test_deduplication(self):
+ def _assert_status(doc, status):
+ doc.load_from_db()
+ self.assertEqual(doc.status, status)
+
+ riv_args = frappe._dict(
+ doctype="Repost Item Valuation",
+ item_code="_Test Item",
+ warehouse="_Test Warehouse - _TC",
+ based_on="Item and Warehouse",
+ voucher_type="Sales Invoice",
+ voucher_no="SI-1",
+ posting_date="2021-01-02",
+ posting_time="00:01:00",
+ )
+
+ # new repost without any duplicates
+ riv1 = frappe.get_doc(riv_args)
+ riv1.flags.dont_run_in_test = True
+ riv1.submit()
+ _assert_status(riv1, "Queued")
+ self.assertEqual(riv1.voucher_type, "Sales Invoice") # traceability
+ self.assertEqual(riv1.voucher_no, "SI-1")
+
+ # newer than existing duplicate - riv1
+ riv2 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-03"}))
+ riv2.flags.dont_run_in_test = True
+ riv2.submit()
+ riv1.deduplicate_similar_repost()
+ _assert_status(riv2, "Skipped")
+
+ # older than exisitng duplicate - riv1
+ riv3 = frappe.get_doc(riv_args.update({"posting_date": "2021-01-01"}))
+ riv3.flags.dont_run_in_test = True
+ riv3.submit()
+ riv3.deduplicate_similar_repost()
+ _assert_status(riv3, "Queued")
+ _assert_status(riv1, "Skipped")
+
+ # unrelated reposts, shouldn't do anything to others.
+ riv4 = frappe.get_doc(riv_args.update({"warehouse": "Stores - _TC"}))
+ riv4.flags.dont_run_in_test = True
+ riv4.submit()
+ riv4.deduplicate_similar_repost()
+ _assert_status(riv4, "Queued")
+ _assert_status(riv3, "Queued")
+
+ # to avoid breaking other tests accidentaly
+ riv4.set_status("Skipped")
+ riv3.set_status("Skipped")
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index d9d1957..38291d1 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -342,7 +342,7 @@
is_stock_reco = sle.voucher_type == "Stock Reconciliation"
msg = None
- if sr and (actual_qty < 0 or is_stock_reco) and sr.warehouse != sle.warehouse:
+ if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse):
# receipt(inward) is being cancelled
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format(
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse))
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index fa2436b..d31e65a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1450,7 +1450,7 @@
item_dict[item]["qty"] = 0
# delete items with 0 qty
- list_of_items = item_dict.keys()
+ list_of_items = list(item_dict.keys())
for item in list_of_items:
if not item_dict[item]["qty"]:
del item_dict[item]
@@ -1462,52 +1462,94 @@
return item_dict
def get_pro_order_required_items(self, backflush_based_on=None):
- item_dict = frappe._dict()
- pro_order = frappe.get_doc("Work Order", self.work_order)
- if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"):
- wip_warehouse = pro_order.wip_warehouse
+ """
+ Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**.
+ """
+ item_dict, job_card_items = frappe._dict(), []
+ work_order = frappe.get_doc("Work Order", self.work_order)
+
+ consider_job_card = work_order.transfer_material_against == "Job Card" and self.get("job_card")
+ if consider_job_card:
+ job_card_items = self.get_job_card_item_codes(self.get("job_card"))
+
+ if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
+ wip_warehouse = work_order.wip_warehouse
else:
wip_warehouse = None
- for d in pro_order.get("required_items"):
- if ( ((flt(d.required_qty) > flt(d.transferred_qty)) or
- (backflush_based_on == "Material Transferred for Manufacture")) and
- (d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")):
+ for d in work_order.get("required_items"):
+ if consider_job_card and (d.item_code not in job_card_items):
+ continue
+
+ transfer_pending = flt(d.required_qty) > flt(d.transferred_qty)
+ can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture")
+
+ if not can_transfer:
+ continue
+
+ if d.include_item_in_manufacturing:
item_row = d.as_dict()
+ item_row["idx"] = len(item_dict) + 1
+
+ if consider_job_card:
+ job_card_item = frappe.db.get_value(
+ "Job Card Item",
+ {
+ "item_code": d.item_code,
+ "parent": self.get("job_card")
+ }
+ )
+ item_row["job_card_item"] = job_card_item or None
+
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
item_row["from_warehouse"] = d.source_warehouse
item_row["to_warehouse"] = wip_warehouse
if item_row["allow_alternative_item"]:
- item_row["allow_alternative_item"] = pro_order.allow_alternative_item
+ item_row["allow_alternative_item"] = work_order.allow_alternative_item
item_dict.setdefault(d.item_code, item_row)
return item_dict
+ def get_job_card_item_codes(self, job_card=None):
+ if not job_card:
+ return []
+
+ job_card_items = frappe.get_all(
+ "Job Card Item",
+ filters={
+ "parent": job_card
+ },
+ fields=["item_code"],
+ distinct=True
+ )
+ return [d.item_code for d in job_card_items]
+
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
for d in item_dict:
- stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
+ item_row = item_dict[d]
+ stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
se_child = self.append('items')
- se_child.s_warehouse = item_dict[d].get("from_warehouse")
- se_child.t_warehouse = item_dict[d].get("to_warehouse")
- se_child.item_code = item_dict[d].get('item_code') or cstr(d)
- se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom
+ se_child.s_warehouse = item_row.get("from_warehouse")
+ se_child.t_warehouse = item_row.get("to_warehouse")
+ se_child.item_code = item_row.get('item_code') or cstr(d)
+ se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom
se_child.stock_uom = stock_uom
- se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty"))
- se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0)
- 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)
- se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
+ se_child.qty = flt(item_row["qty"], se_child.precision("qty"))
+ se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0)
+ se_child.subcontracted_item = item_row.get("main_item_code")
+ se_child.cost_center = (item_row.get("cost_center") or
+ get_default_cost_center(item_row, company = self.company))
+ se_child.is_finished_item = item_row.get("is_finished_item", 0)
+ se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
+ se_child.is_process_loss = item_row.get("is_process_loss", 0)
for field in ["idx", "po_detail", "original_item", "expense_account",
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
- if item_dict[d].get(field):
- se_child.set(field, item_dict[d].get(field))
+ if item_row.get(field):
+ se_child.set(field, item_row.get(field))
if se_child.s_warehouse==None:
se_child.s_warehouse = self.from_warehouse
@@ -1515,12 +1557,11 @@
se_child.t_warehouse = self.to_warehouse
# in stock uom
- se_child.conversion_factor = flt(item_dict[d].get("conversion_factor")) or 1
- se_child.transfer_qty = flt(item_dict[d]["qty"]*se_child.conversion_factor, se_child.precision("qty"))
+ se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1
+ se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty"))
-
- # to be assigned for finished item
- se_child.bom_no = bom_no
+ se_child.bom_no = bom_no # to be assigned for finished item
+ se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None
def validate_with_material_request(self):
for item in self.get("items"):
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index b7d1497..3402972 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@@ -153,11 +154,12 @@
"icon": "fa fa-upload-alt",
"idx": 1,
"is_submittable": 1,
- "max_attachments": 1,
- "modified": "2020-04-08 17:02:47.196206",
+ "links": [],
+ "modified": "2021-11-30 01:33:51.437194",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index de89b2b..48e339a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -399,6 +399,34 @@
, do_not_submit=True)
self.assertRaises(frappe.ValidationError, sr.submit)
+ def test_serial_no_cancellation(self):
+
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
+ if not item.has_serial_no:
+ item.has_serial_no = 1
+ item.serial_no_series = "SRS9.####"
+ item.save()
+
+ item_code = item.name
+ warehouse = "_Test Warehouse - _TC"
+
+ se1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=700)
+
+ serial_nos = get_serial_nos(se1.items[0].serial_no)
+ # reduce 1 item
+ serial_nos.pop()
+ new_serial_nos = "\n".join(serial_nos)
+
+ sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9)
+ sr.cancel()
+
+ active_sr_no = frappe.get_all("Serial No",
+ filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+
+ self.assertEqual(len(active_sr_no), 10)
+
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no:
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
index 2474059..0facae8 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_rename": 1,
+ "beta": 1,
"creation": "2021-10-01 10:56:30.814787",
"doctype": "DocType",
"editable_grid": 1,
@@ -10,7 +11,8 @@
"limit_reposting_timeslot",
"start_time",
"end_time",
- "limits_dont_apply_on"
+ "limits_dont_apply_on",
+ "item_based_reposting"
],
"fields": [
{
@@ -44,12 +46,18 @@
"fieldname": "limit_reposting_timeslot",
"fieldtype": "Check",
"label": "Limit timeslot for Stock Reposting"
+ },
+ {
+ "default": "0",
+ "fieldname": "item_based_reposting",
+ "fieldtype": "Check",
+ "label": "Use Item based reposting"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-10-01 11:27:28.981594",
+ "modified": "2021-11-02 01:22:45.155841",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 2dd103d..1de48b6 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -20,7 +20,7 @@
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
set_by_naming_series("Item", "item_code",
- self.get("item_naming_by")=="Naming Series", hide_name_field=True)
+ self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0)
stock_frozen_limit = 356
submitted_stock_frozen = self.stock_frozen_upto_days or 0
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index e00382b..9889a22 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -299,7 +299,7 @@
"warehouse": warehouse,
"income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults),
"expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) ,
- "discount_account": None or get_default_discount_account(args, item_defaults),
+ "discount_account": get_default_discount_account(args, item_defaults),
"cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults),
'has_serial_no': item.has_serial_no,
'has_batch_no': item.has_batch_no,
@@ -317,6 +317,7 @@
"net_rate": 0.0,
"net_amount": 0.0,
"discount_percentage": 0.0,
+ "discount_amount": 0.0,
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
"update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0,
"delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0,
@@ -326,7 +327,8 @@
"against_blanket_order": args.get("against_blanket_order"),
"bom_no": item.get("default_bom"),
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
- "weight_uom": args.get("weight_uom") or item.get("weight_uom")
+ "weight_uom": args.get("weight_uom") or item.get("weight_uom"),
+ "grant_commission": item.get("grant_commission")
})
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
index de7e38e..adab478 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
@@ -1,19 +1,19 @@
{% for d in data %}
<div class="dashboard-list-item" style="padding: 7px 15px;">
<div class="row">
- <div class="col-sm-2 small" style="margin-top: 8px;">
+ <div class="col-sm-2" style="margin-top: 8px;">
<a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
</div>
- <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <div class="col-sm-2" style="margin-top: 8px; ">
<a data-type="item" data-name="{{ d.item_code }}">{{ d.item_code }}</a>
</div>
- <div class="col-sm-1 small" style="margin-top: 8px; ">
+ <div class="col-sm-1" style="margin-top: 8px; ">
{{ d.stock_capacity }}
</div>
- <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <div class="col-sm-2" style="margin-top: 8px; ">
{{ d.actual_qty }}
</div>
- <div class="col-sm-2 small">
+ <div class="col-sm-2">
<div class="progress" title="Occupied Qty: {{ d.actual_qty }}" style="margin-bottom: 4px; height: 7px; margin-top: 14px;">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ d.percent_occupied }}"
@@ -23,16 +23,19 @@
</div>
</div>
</div>
- <div class="col-sm-1 small" style="margin-top: 8px;">
+ <div class="col-sm-1" style="margin-top: 8px;">
{{ d.percent_occupied }}%
</div>
{% if can_write %}
- <div class="col-sm-1 text-right" style="margin-top: 2px;">
- <button class="btn btn-default btn-xs btn-edit"
- style="margin-top: 4px;margin-bottom: 4px;"
- data-warehouse="{{ d.warehouse }}"
- data-item="{{ escape(d.item_code) }}"
- data-company="{{ escape(d.company) }}">{{ __("Edit Capacity") }}</a>
+ <div class="col-sm-2 text-right" style="margin-top: 2px;">
+ <button
+ class="btn btn-default btn-xs btn-edit"
+ style="margin: 4px 0; float: left;"
+ data-warehouse="{{ d.warehouse }}"
+ data-item="{{ escape(d.item_code) }}"
+ data-company="{{ escape(d.company) }}">
+ {{ __("Edit Capacity") }}
+ </button>
</div>
{% endif %}
</div>
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
index c0ffdc9..ea27dd2 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
@@ -4,7 +4,7 @@
title: 'Warehouse Capacity Summary',
single_column: true
});
- page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync');
+ page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'refresh');
page.start = 0;
page.company_field = page.add_field({
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
index 7ac5e64..1183ad4 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
@@ -1,18 +1,18 @@
<div class="dashboard-list-item" style="padding: 12px 15px;">
<div class="row">
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Warehouse
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Item
</div>
- <div class="col-sm-1 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-1 text-muted" style="margin-top: 8px;">
Stock Capacity
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
Balance Stock Qty
</div>
- <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ <div class="col-sm-2 text-muted" style="margin-top: 8px;">
% Occupied
</div>
</div>
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
index a381820..6aa12ac 100644
--- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
@@ -46,7 +46,7 @@
return row
def get_stock_ledger_entries(report_filters):
- filters = {}
+ filters = {"is_cancelled": 0}
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.js b/erpnext/stock/report/total_stock_summary/total_stock_summary.js
index 90648f1..88054aa 100644
--- a/erpnext/stock/report/total_stock_summary/total_stock_summary.js
+++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.js
@@ -10,23 +10,8 @@
"fieldtype": "Select",
"width": "80",
"reqd": 1,
- "options": ["", "Warehouse", "Company"],
- "change": function() {
- let group_by = frappe.query_report.get_filter_value("group_by")
- let company_filter = frappe.query_report.get_filter("company")
- if (group_by == "Company") {
- company_filter.df.reqd = 0;
- company_filter.df.hidden = 1;
- frappe.query_report.set_filter_value("company", "");
- company_filter.refresh();
- }
- else {
- company_filter.df.reqd = 1;
- company_filter.df.hidden = 0;
- company_filter.refresh();
- frappe.query_report.refresh();
- }
- }
+ "options": ["Warehouse", "Company"],
+ "default": "Warehouse",
},
{
"fieldname": "company",
@@ -34,8 +19,9 @@
"fieldtype": "Link",
"width": "80",
"options": "Company",
+ "reqd": 1,
"default": frappe.defaults.get_user_default("Company"),
- "reqd": 1
+ "depends_on": "eval: doc.group_by != 'Company'",
},
]
}
diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py
index 7e47b50..6f27558 100644
--- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py
+++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py
@@ -7,8 +7,9 @@
def execute(filters=None):
- if not filters: filters = {}
- validate_filters(filters)
+
+ if not filters:
+ filters = {}
columns = get_columns()
stock = get_total_stock(filters)
@@ -53,9 +54,3 @@
ON warehouse.name = ledger.warehouse
WHERE
ledger.actual_qty != 0 %s""" % (columns, conditions))
-
-def validate_filters(filters):
- if filters.get("group_by") == 'Company' and \
- filters.get("company"):
-
- frappe.throw(_("Please set Company filter blank if Group By is 'Company'"))
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 d3af5f6..4d1491b 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
@@ -46,8 +46,8 @@
item_balance.setdefault((item, item_map[item]["item_group"]), [])
total_stock_value = 0.00
for wh in warehouse_list:
- row += [qty_dict.bal_qty] if wh.name in warehouse else [0.00]
- total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00
+ row += [qty_dict.bal_qty] if wh.name == warehouse else [0.00]
+ total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00
item_balance[(item, item_map[item]["item_group"])].append(row)
item_value.setdefault((item, item_map[item]["item_group"]),[])
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 6ac8da1..6663458 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -160,7 +160,7 @@
def get_planned_qty(item_code, warehouse):
planned_qty = frappe.db.sql("""
select sum(qty - produced_qty) from `tabWork Order`
- where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed")
+ where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed", "Closed")
and docstatus=1 and qty > produced_qty""", (item_code, warehouse))
return flt(planned_qty[0][0]) if planned_qty else 0
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9c4c676..9d40982 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -111,6 +111,7 @@
frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
if repost_entry.status == 'Queued':
doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.flags.ignore_permissions = True
doc.cancel()
doc.delete()
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 9c80515..4df27f5 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -704,59 +704,9 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
- },
- {
- "dependencies": "Stock Ledger Entry",
- "hidden": 0,
- "is_query_report": 1,
- "label": "Stock and Account Value Comparison",
- "link_count": 0,
- "link_to": "Stock and Account Value Comparison",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Data Report",
- "link_count": 0,
- "link_type": "DocType",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Serial No Qty and Valuation",
- "link_count": 0,
- "link_to": "Incorrect Serial No Valuation",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Incorrect Balance Qty After Transaction",
- "link_count": 0,
- "link_to": "Incorrect Balance Qty After Transaction",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Stock and Account Value Comparison",
- "link_count": 0,
- "link_to": "Stock and Account Value Comparison",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.361519",
+ "modified": "2021-11-23 04:34:00.420870",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json
index 88ee4a3..45485ca 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.json
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json
@@ -256,6 +256,7 @@
"fieldname": "contact_email",
"fieldtype": "Data",
"label": "Contact Email",
+ "options": "Email",
"read_only": 1
},
{
@@ -361,7 +362,7 @@
],
"icon": "fa fa-bug",
"idx": 1,
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-11-09 17:26:09.703215",
"modified_by": "Administrator",
"module": "Support",
"name": "Warranty Claim",
@@ -385,4 +386,4 @@
"sort_order": "DESC",
"timeline_field": "customer",
"title_field": "customer_name"
-}
\ No newline at end of file
+}