Merge pull request #26351 from rohitwaghchaure/fixed-putaway-fixing-for-material-receipt-develop
fix: stock entry with putaway rule not working
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f7a7122..455ab86 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,6 +42,6 @@
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
-bench start &
+bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 4bc55da..412a05b 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -102,3 +102,7 @@
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+ - name: Show bench console if tests failed
+ if: ${{ failure() }}
+ run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.gitignore b/.gitignore
index 63c51c4..89f5626 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@
.idea/
.vscode/
node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
new file mode 100644
index 0000000..fcb7533
--- /dev/null
+++ b/cypress/integration/test_item.js
@@ -0,0 +1,44 @@
+describe("Test Item Dashboard", () => {
+ before(() => {
+ cy.login();
+ cy.visit("/app/item");
+ cy.insert_doc(
+ "Item",
+ {
+ item_code: "e2e_test_item",
+ item_group: "All Item Groups",
+ opening_stock: 42,
+ valuation_rate: 100,
+ },
+ true
+ );
+ cy.go_to_doc("item", "e2e_test_item");
+ });
+
+ it("should show dashboard with correct data on first load", () => {
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").contains("e2e_test_item").should("exist");
+
+ // reserved and available qty
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(0)
+ .contains("0")
+ .should("exist");
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(1)
+ .contains("42")
+ .should("exist");
+ });
+
+ it("should persist on field change", () => {
+ cy.get('input[data-fieldname="disabled"]').check();
+ cy.wait(500);
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").should("have.length", 1);
+ });
+
+ it("should persist on reload", () => {
+ cy.reload();
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ });
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 7929a2e..7ddc80a 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -23,3 +23,9 @@
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
+
+const slug = (name) => name.toLowerCase().replace(" ", "-");
+
+Cypress.Commands.add("go_to_doc", (doctype, name) => {
+ cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
+});
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 2f86c6c..335e8a1 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -301,17 +301,21 @@
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ companies = frappe.get_all('Company')
- doc.insert()
- doc.submit()
+ for company in companies:
+ for record_type in ('Income', 'Expense'):
+ doc = frappe.get_doc(dict(
+ doctype='Process Deferred Accounting',
+ company=company.name,
+ posting_date=posting_date,
+ start_date=start_date,
+ end_date=end_date,
+ type=record_type
+ ))
+
+ doc.insert()
+ doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 5f110e2..ffc9d1c 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -51,7 +51,7 @@
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview):
+ if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 3b764aa..cb1f2df 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -13,7 +13,9 @@
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
- pass
+ def validate(self):
+ validate_accounts(self.import_file)
+
@frappe.whitelist()
def validate_company(company):
@@ -301,28 +303,28 @@
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
- message = validate_root(accounts_dict)
- if message: return message
- message = validate_account_types(accounts_dict)
- if message: return message
+ validate_root(accounts_dict)
+
+ validate_account_types(accounts_dict)
+
return [True, len(accounts)]
def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
- return _("Number of root accounts cannot be less than 4")
+ frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = []
for account in roots:
if not account.get("root_type") and account.get("account_name"):
- error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
+ error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
- error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
+ error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
if error_messages:
- return "<br>".join(error_messages)
+ frappe.throw("<br>".join(error_messages))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -356,7 +358,7 @@
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
- return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
@@ -364,7 +366,7 @@
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
- return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index c6c6892..1ef512a 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -25,7 +25,7 @@
def validate_amount(self):
amounts = calculate_interest_and_amount(
- self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
+ self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
if self.interest_amount != amounts.get('interest_amount'):
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
if self.dunning_amount != amounts.get('dunning_amount'):
@@ -91,13 +91,13 @@
for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
-def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
+def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
- grand_total = 0
+ grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365
- grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
+ grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
'interest_amount': interest_amount,
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index e2d4d82..31cb078 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -16,6 +16,7 @@
@classmethod
def setUpClass(self):
create_dunning_type()
+ create_dunning_type_with_zero_interest_rate()
unlink_payment_on_cancel_of_invoice()
@classmethod
@@ -25,11 +26,19 @@
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
- dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
+ def test_dunning_with_zero_interest_rate(self):
+ dunning = create_dunning_with_zero_interest_rate()
+ amounts = calculate_interest_and_amount(
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
+ self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
+ self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
+ self.assertEqual(round(amounts.get('grand_total'), 2), 120)
+
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
@@ -83,6 +92,27 @@
dunning.save()
return dunning
+def create_dunning_with_zero_interest_rate():
+ posting_date = add_days(today(), -20)
+ due_date = add_days(today(), -15)
+ sales_invoice = create_sales_invoice_against_cost_center(
+ posting_date=posting_date, due_date=due_date, status='Overdue')
+ dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
+ dunning = frappe.new_doc("Dunning")
+ dunning.sales_invoice = sales_invoice.name
+ dunning.customer_name = sales_invoice.customer_name
+ dunning.outstanding_amount = sales_invoice.outstanding_amount
+ dunning.debit_to = sales_invoice.debit_to
+ dunning.currency = sales_invoice.currency
+ dunning.company = sales_invoice.company
+ dunning.posting_date = nowdate()
+ dunning.due_date = sales_invoice.due_date
+ dunning.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning.rate_of_interest = dunning_type.rate_of_interest
+ dunning.dunning_fee = dunning_type.dunning_fee
+ dunning.save()
+ return dunning
+
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice'
@@ -98,3 +128,19 @@
}
)
dunning_type.save()
+
+def create_dunning_type_with_zero_interest_rate():
+ dunning_type = frappe.new_doc("Dunning Type")
+ dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning_type.start_day = 10
+ dunning_type.end_day = 20
+ dunning_type.dunning_fee = 20
+ dunning_type.rate_of_interest = 0
+ dunning_type.append(
+ "dunning_letter_text", {
+ 'language': 'en',
+ 'body_text': 'We have still not received payment for our invoice ',
+ 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
+ }
+ )
+ dunning_type.save()
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index adaf99a..0c21aae 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1318,9 +1318,9 @@
return frappe._dict({
"due_date": ref_doc.get("due_date"),
- "total_amount": total_amount,
- "outstanding_amount": outstanding_amount,
- "exchange_rate": exchange_rate,
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
"bill_no": bill_no
})
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 4641d6b..d1302f5 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -589,9 +589,9 @@
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center)
- self.assertEqual(expected_account_balance, account_balance)
- self.assertEqual(expected_party_balance, party_balance)
- self.assertEqual(expected_party_account_balance, party_account_balance)
+ self.assertEqual(flt(expected_account_balance), account_balance)
+ self.assertEqual(flt(expected_party_balance), party_balance)
+ self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 0b0ee90..500952e 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -207,10 +207,9 @@
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
billing_email = frappe.db.sql("""
- SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
- WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
- c.is_billing_contact=1 \
- order by c.creation desc""")
+ SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
+ WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
+ order by c.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ec93314..189260a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -231,25 +231,25 @@
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_exchange_rate_difference(self):
- pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
- pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
- pi.items[0].purchase_receipt = pr.name
- pi.items[0].pr_detail = pr.items[0].name
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ currency = "USD", conversion_rate = 70)
+
+ pi = create_purchase_invoice(pr.name)
+ pi.conversion_rate = 80
pi.insert()
pi.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
- self.assertEqual(pi.name, voucher_no)
-
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # 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': pi.name}, 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
@@ -1031,21 +1031,21 @@
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
- ['_Test Account Cost for Goods Sold - _TC', 30000, 0],
- ['_Test Account Excise Duty - _TC', 0, 3000],
- ['Creditors - _TC', 0, 27000],
- ['TDS Payable - _TC', 3000, 3000]
+ ['_Test Account Cost for Goods Sold - _TC', 30000],
+ ['_Test Account Excise Duty - _TC', -3000],
+ ['Creditors - _TC', -27000],
+ ['TDS Payable - _TC', 0]
]
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
+ group by account
order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
- self.assertEqual(expected_gle[i][1], gle.debit)
- self.assertEqual(expected_gle[i][2], gle.credit)
+ self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc6..b97dc40 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
+ and is_cancelled = 0
group by company""", (party_type, party)))
for d in companies:
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 9c9ada8..f1b231b 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -397,6 +397,7 @@
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
]
- }
+ },
+ 'type' : 'bar'
}
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 7793af7..56a67bb 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -380,7 +380,7 @@
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
- from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
+ from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 744ada9..e724e9b 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -48,13 +48,12 @@
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
-
- for account in filters.account:
- if not account_details.get(account):
- frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account'))
+ for account in filters.account:
+ if not account_details.get(account):
+ frappe.throw(_("Account {0} does not exists").format(account))
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0):
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28..922cc4a 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
erpnext.asset.transfer_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
frm.trigger("make_sales_invoice");
- });
+ }, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm);
- });
+ }, __("Manage"));
+ }
+
+ if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
+ frm.add_custom_button(__("Maintain Asset"), function() {
+ frm.trigger("create_asset_maintenance");
+ }, __("Manage"));
+ }
+
+ frm.add_custom_button(__("Repair Asset"), function() {
+ frm.trigger("create_asset_repair");
+ }, __("Manage"));
+
+ if (frm.doc.status != 'Fully Depreciated') {
+ frm.add_custom_button(__("Adjust Asset Value"), function() {
+ frm.trigger("create_asset_adjustment");
+ }, __("Manage"));
+ }
+
+ if (!frm.doc.calculate_depreciation) {
+ frm.add_custom_button(__("Create Depreciation Entry"), function() {
+ frm.trigger("make_journal_entry");
+ }, __("Manage"));
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
- frm.add_custom_button("General Ledger", function() {
+ frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
@@ -107,27 +129,9 @@
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
- });
+ }, __("Manage"));
}
- if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
- frm.add_custom_button(__("Asset Maintenance"), function() {
- frm.trigger("create_asset_maintenance");
- }, __('Create'));
- }
- if (frm.doc.status != 'Fully Depreciated') {
- frm.add_custom_button(__("Asset Value Adjustment"), function() {
- frm.trigger("create_asset_adjustment");
- }, __('Create'));
- }
-
- if (!frm.doc.calculate_depreciation) {
- frm.add_custom_button(__("Depreciation Entry"), function() {
- frm.trigger("make_journal_entry");
- }, __('Create'));
- }
-
- frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("setup_chart");
}
@@ -304,6 +308,20 @@
})
},
+ create_asset_repair: function(frm) {
+ frappe.call({
+ args: {
+ "asset": frm.doc.name,
+ "asset_name": frm.doc.asset_name
+ },
+ method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
+ callback: function(r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ },
+
create_asset_adjustment: function(frm) {
frappe.call({
args: {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 421b9a6..de06075 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -502,7 +502,7 @@
"link_fieldname": "asset"
}
],
- "modified": "2021-01-22 12:38:59.091510",
+ "modified": "2021-06-24 14:58:51.097908",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 8799275..66f0bdc 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self):
- if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
+ if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = []
- if self.get("schedules") or not self.available_for_use_date:
+ if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
+
+ start = self.clear_depreciation_schedule()
- value_after_depreciation = (flt(self.gross_purchase_amount) -
- flt(self.opening_accumulated_depreciation))
+ # value_after_depreciation - current Asset value
+ if d.value_after_depreciation:
+ value_after_depreciation = (flt(d.value_after_depreciation) -
+ flt(self.opening_accumulated_depreciation))
+ else:
+ value_after_depreciation = (flt(self.gross_purchase_amount) -
+ flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
@@ -191,7 +198,7 @@
number_of_pending_depreciations += 1
skip_row = False
- for n in range(number_of_pending_depreciations):
+ for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
@@ -216,11 +223,13 @@
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
- to_date = add_months(self.available_for_use_date,
- n * cint(d.frequency_of_depreciation))
+ if not self.flags.increase_in_asset_life:
+ # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
+ self.to_date = add_months(self.available_for_use_date,
+ n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = self.get_pro_rata_amt(d,
- depreciation_amount, schedule_date, to_date)
+ depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1)
@@ -284,10 +293,23 @@
"finance_book_id": d.idx
})
+ # used when depreciation schedule needs to be modified due to increase in asset life
+ def clear_depreciation_schedule(self):
+ start = 0
+ for n in range(len(self.schedules)):
+ if not self.schedules[n].journal_entry:
+ del self.schedules[n:]
+ start = n
+ break
+ return start
+
+
+ # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
-
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+ # if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
@@ -346,11 +368,12 @@
if d.finance_book_id not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
- finance_books.append(d.finance_book_id)
+ finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
+ # for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1:
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation -
@@ -626,8 +649,17 @@
return asset_maintenance
@frappe.whitelist()
+def create_asset_repair(asset, asset_name):
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset,
+ "asset_name": asset_name
+ })
+ return asset_repair
+
+@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
- asset_maintenance = frappe.new_doc("Asset Value Adjustment")
+ asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
"asset": asset,
"company": company,
@@ -757,8 +789,15 @@
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
- depreciation_amount = (flt(row.value_after_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ # if the Depreciation Schedule is being prepared for the first time
+ if not asset.flags.increase_in_asset_life:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / depreciation_left
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 8845f24..59fbe3b 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
@@ -154,9 +153,8 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": '2030-12-31'
})
- asset.insert()
- self.assertEqual(asset.status, "Draft")
asset.save()
+ self.assertEqual(asset.status, "Draft")
expected_schedules = [
['2030-12-31', 66667.00, 66667.00],
@@ -185,7 +183,7 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
+ asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [
@@ -216,7 +214,6 @@
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
asset.load_from_db()
self.assertEqual(asset.status, "Submitted")
@@ -350,7 +346,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -380,7 +375,6 @@
"total_number_of_depreciations": 10,
"frequency_of_depreciation": 1
})
- asset.insert()
asset.submit()
post_depreciation_entries(date=add_months('2020-01-01', 4))
@@ -424,7 +418,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -468,7 +461,7 @@
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10
})
- asset.insert()
+ asset.save()
accumulated_depreciation_after_full_schedule = \
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
@@ -699,7 +692,7 @@
"item_code": args.item_code or "Macbook Pro",
"company": args.company or"_Test Company",
"purchase_date": "2015-01-01",
- "calculate_depreciation": 0,
+ "calculate_depreciation": args.calculate_depreciation or 0,
"gross_purchase_amount": 100000,
"purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000,
@@ -707,9 +700,16 @@
"available_for_use_date": "2020-06-06",
"location": "Test Location",
"asset_owner": "Company",
- "is_existing_asset": args.is_existing_asset or 0
+ "is_existing_asset": 1
})
+ if asset.calculate_depreciation:
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 5
+ })
+
try:
asset.save()
except frappe.DuplicateEntryError:
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index d9b7b69..e5a5f19 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -67,7 +67,6 @@
{
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
- "hidden": 1,
"label": "Value After Depreciation",
"no_copy": 1,
"options": "Company:company:default_currency",
@@ -85,7 +84,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 16:30:09.213479",
+ "modified": "2021-06-17 12:59:05.743683",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 4ba2b44..1cebfff 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -2,6 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Asset Repair', {
+ setup: function(frm) {
+ frm.fields_dict.cost_center.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.project.get_query = function(doc) {
+ return {
+ filters: {
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.warehouse.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+ },
+
+ refresh: function(frm) {
+ if (frm.doc.docstatus) {
+ frm.add_custom_button("View General Ledger", function() {
+ frappe.route_options = {
+ "voucher_no": frm.doc.name
+ };
+ frappe.set_route("query-report", "General Ledger");
+ });
+ }
+ },
+
repair_status: (frm) => {
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
frappe.call ({
@@ -17,5 +56,16 @@
}
});
}
+
+ if (frm.doc.repair_status == "Completed") {
+ frm.set_value('completion_date', frappe.datetime.now_datetime());
+ }
}
});
+
+frappe.ui.form.on('Asset Repair Consumed Item', {
+ consumed_quantity: function(frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
+ },
+});
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index d338fc0..ba31898 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,39 +7,44 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "naming_series",
- "asset_name",
+ "asset",
+ "company",
"column_break_2",
- "item_code",
- "item_name",
+ "asset_name",
+ "naming_series",
"section_break_5",
"failure_date",
- "assign_to",
- "assign_to_name",
+ "repair_status",
"column_break_6",
"completion_date",
- "repair_status",
+ "accounting_dimensions_section",
+ "cost_center",
+ "column_break_14",
+ "project",
+ "accounting_details",
"repair_cost",
+ "capitalize_repair_cost",
+ "stock_consumption",
+ "column_break_8",
+ "purchase_invoice",
+ "stock_consumption_details_section",
+ "warehouse",
+ "stock_items",
+ "total_repair_cost",
+ "stock_entry",
+ "asset_depreciation_details_section",
+ "increase_in_asset_life",
"section_break_9",
"description",
"column_break_9",
"actions_performed",
- "section_break_17",
+ "section_break_23",
"downtime",
"column_break_19",
"amended_from"
],
"fields": [
{
- "columns": 1,
- "fieldname": "asset_name",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Asset",
- "options": "Asset",
- "reqd": 1
- },
- {
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
@@ -51,18 +56,6 @@
"fieldtype": "Column Break"
},
{
- "fetch_from": "asset_name.item_code",
- "fieldname": "item_code",
- "fieldtype": "Read Only",
- "label": "Item Code"
- },
- {
- "fetch_from": "asset_name.item_name",
- "fieldname": "item_name",
- "fieldtype": "Read Only",
- "label": "Item Name"
- },
- {
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Repair Details"
@@ -75,32 +68,19 @@
"reqd": 1
},
{
- "allow_on_submit": 1,
- "fieldname": "assign_to",
- "fieldtype": "Link",
- "label": "Assign To",
- "options": "User"
- },
- {
- "allow_on_submit": 1,
- "fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_name",
- "fieldtype": "Read Only",
- "label": "Assign To Name"
- },
- {
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date",
"fieldtype": "Datetime",
- "label": "Completion Date"
+ "label": "Completion Date",
+ "no_copy": 1
},
{
- "allow_on_submit": 1,
"default": "Pending",
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status",
"fieldtype": "Select",
"label": "Repair Status",
@@ -116,25 +96,18 @@
{
"fieldname": "description",
"fieldtype": "Long Text",
- "label": "Error Description",
- "reqd": 1
+ "label": "Error Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
"fieldname": "actions_performed",
"fieldtype": "Long Text",
"label": "Actions performed"
},
{
- "fieldname": "section_break_17",
- "fieldtype": "Section Break"
- },
- {
- "allow_on_submit": 1,
"fieldname": "downtime",
"fieldtype": "Data",
"in_list_view": 1,
@@ -146,7 +119,7 @@
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "default": "0",
"fieldname": "repair_cost",
"fieldtype": "Currency",
"label": "Repair Cost"
@@ -159,12 +132,139 @@
"options": "Asset Repair",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "columns": 1,
+ "fieldname": "asset",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Asset",
+ "options": "Asset",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "asset.asset_name",
+ "fieldname": "asset_name",
+ "fieldtype": "Read Only",
+ "label": "Asset Name"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "capitalize_repair_cost",
+ "fieldtype": "Check",
+ "label": "Capitalize Repair Cost"
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "stock_items",
+ "fieldtype": "Table",
+ "label": "Stock Items",
+ "mandatory_depends_on": "stock_consumption",
+ "options": "Asset Repair Consumed Item"
+ },
+ {
+ "fieldname": "section_break_23",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "stock_consumption",
+ "fieldtype": "Check",
+ "label": "Stock Consumed During Repair"
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "stock_consumption_details_section",
+ "fieldtype": "Section Break",
+ "label": "Stock Consumption Details"
+ },
+ {
+ "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
+ "description": "Sum of Repair Cost and Value of Consumed Stock Items.",
+ "fieldname": "total_repair_cost",
+ "fieldtype": "Currency",
+ "label": "Total Repair Cost",
+ "read_only": 1
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "capitalize_repair_cost",
+ "fieldname": "asset_depreciation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Asset Depreciation Details"
+ },
+ {
+ "fieldname": "increase_in_asset_life",
+ "fieldtype": "Int",
+ "label": "Increase In Asset Life(Months)",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "purchase_invoice",
+ "fieldtype": "Link",
+ "label": "Purchase Invoice",
+ "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
+ "no_copy": 1,
+ "options": "Purchase Invoice"
+ },
+ {
+ "fetch_from": "asset.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "stock_entry",
+ "fieldtype": "Link",
+ "label": "Stock Entry",
+ "options": "Stock Entry",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-22 15:08:12.495850",
+ "modified": "2021-06-25 13:14:38.307723",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -203,6 +303,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 049b931..d32fdf7 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -5,16 +5,252 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import time_diff_in_hours
-from frappe.model.document import Document
+from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.controllers.accounts_controller import AccountsController
-class AssetRepair(Document):
+class AssetRepair(AccountsController):
def validate(self):
- if self.repair_status == "Completed" and not self.completion_date:
- frappe.throw(_("Please select Completion Date for Completed Repair"))
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+ self.update_status()
+ if self.get('stock_items'):
+ self.set_total_value()
+ self.calculate_total_repair_cost()
+
+ def update_status(self):
+ if self.repair_status == 'Pending':
+ frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
+ else:
+ self.asset_doc.set_status()
+
+ def set_total_value(self):
+ for item in self.get('stock_items'):
+ item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+
+ def calculate_total_repair_cost(self):
+ self.total_repair_cost = flt(self.repair_cost)
+
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+ self.total_repair_cost += total_value_of_stock_consumed
+
+ def before_submit(self):
+ self.check_repair_status()
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.increase_asset_value()
+ if self.get('stock_consumption'):
+ self.check_for_stock_items_and_warehouse()
+ self.decrease_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.make_gl_entries()
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.modify_depreciation_schedule()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def before_cancel(self):
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.decrease_asset_value()
+ if self.get('stock_consumption'):
+ self.increase_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.make_gl_entries(cancel=True)
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.revert_depreciation_schedule_on_cancellation()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def check_repair_status(self):
+ if self.repair_status == "Pending":
+ frappe.throw(_("Please update Repair Status."))
+
+ def check_for_stock_items_and_warehouse(self):
+ if not self.get('stock_items'):
+ frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
+ if not self.warehouse:
+ frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
+
+ def increase_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation += total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation += self.repair_cost
+
+ def decrease_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation -= total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation -= self.repair_cost
+
+ def get_total_value_of_stock_consumed(self):
+ total_value_of_stock_consumed = 0
+ if self.get('stock_consumption'):
+ for item in self.get('stock_items'):
+ total_value_of_stock_consumed += item.total_value
+
+ return total_value_of_stock_consumed
+
+ def decrease_stock_quantity(self):
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Issue",
+ "company": self.company
+ })
+
+ for stock_item in self.get('stock_items'):
+ stock_entry.append('items', {
+ "s_warehouse": self.warehouse,
+ "item_code": stock_item.item,
+ "qty": stock_item.consumed_quantity,
+ "basic_rate": stock_item.valuation_rate
+ })
+
+ stock_entry.insert()
+ stock_entry.submit()
+
+ self.db_set('stock_entry', stock_entry.name)
+
+ def increase_stock_quantity(self):
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ stock_entry.flags.ignore_links = True
+ stock_entry.cancel()
+
+ def make_gl_entries(self, cancel=False):
+ if flt(self.repair_cost) > 0:
+ gl_entries = self.get_gl_entries()
+ make_gl_entries(gl_entries, cancel)
+
+ def get_gl_entries(self):
+ gl_entries = []
+ repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
+ fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
+ expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": expense_account,
+ "credit": self.repair_cost,
+ "credit_in_account_currency": self.repair_cost,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ if self.get('stock_consumption'):
+ # creating GL Entries for each row in Stock Items based on the Stock Entry created for it
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ for item in stock_entry.items:
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "credit": item.amount,
+ "credit_in_account_currency": item.amount,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": fixed_asset_account,
+ "debit": self.total_repair_cost,
+ "debit_in_account_currency": self.total_repair_cost,
+ "against": expense_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "against_voucher_type": "Purchase Invoice",
+ "against_voucher": self.purchase_invoice,
+ "company": self.company
+ }, item=self)
+ )
+
+ return gl_entries
+
+ def modify_depreciation_schedule(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
+
+ # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
+ def calculate_last_schedule_date(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the old Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the new Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, extra_months)
+
+ # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ number_of_pending_depreciations * cint(row.frequency_of_depreciation))
+
+ if asset.to_date > schedule_date:
+ row.total_number_of_depreciations += 1
+
+ def revert_depreciation_schedule_on_cancellation(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
+
+ def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the modified Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the original Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, -extra_months)
+
+ # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
+
+ if asset.to_date < schedule_date:
+ row.total_number_of_depreciations -= 1
@frappe.whitelist()
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
- return round(downtime, 2)
\ No newline at end of file
+ return round(downtime, 2)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3d325a9..30bbb37 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -2,8 +2,167 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
-
+import frappe
+from frappe.utils import nowdate, flt
import unittest
+from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
class TestAssetRepair(unittest.TestCase):
- pass
+ def setUp(self):
+ set_depreciation_settings_in_company()
+ create_asset_data()
+ frappe.db.sql("delete from `tabTax Rule`")
+
+ def test_update_status(self):
+ asset = create_asset()
+ initial_status = asset.status
+ asset_repair = create_asset_repair(asset = asset)
+
+ if asset_repair.repair_status == "Pending":
+ asset.reload()
+ self.assertEqual(asset.status, "Out of Order")
+
+ asset_repair.repair_status = "Completed"
+ asset_repair.save()
+ asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
+ self.assertEqual(asset_status, initial_status)
+
+ def test_stock_item_total_value(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ for item in asset_repair.stock_items:
+ total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+ self.assertEqual(item.total_value, total_value)
+
+ def test_total_repair_cost(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ total_repair_cost = asset_repair.repair_cost
+ self.assertEqual(total_repair_cost, asset_repair.repair_cost)
+ for item in asset_repair.stock_items:
+ total_repair_cost += item.total_value
+
+ self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
+
+ def test_repair_status_after_submit(self):
+ asset_repair = create_asset_repair(submit = 1)
+ self.assertNotEqual(asset_repair.repair_status, "Pending")
+
+ def test_stock_items(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.stock_items)
+
+ def test_warehouse(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.warehouse)
+
+ def test_decrease_stock_quantity(self):
+ asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
+ stock_entry = frappe.get_last_doc('Stock Entry')
+
+ 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].qty, asset_repair.stock_items[0].consumed_quantity)
+
+ def test_increase_in_asset_value_due_to_stock_consumption(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
+
+ def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
+
+ def test_purchase_invoice(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ self.assertTrue(asset_repair.purchase_invoice)
+
+ def test_gl_entries(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ gl_entry = frappe.get_last_doc('GL Entry')
+ self.assertEqual(asset_repair.name, gl_entry.voucher_no)
+
+ def test_increase_in_asset_life(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_num_of_depreciations = num_of_depreciations(asset)
+ create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
+ self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
+
+def get_asset_value(asset):
+ return asset.finance_books[0].value_after_depreciation
+
+def num_of_depreciations(asset):
+ return asset.finance_books[0].total_number_of_depreciations
+
+def create_asset_repair(**args):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+
+ args = frappe._dict(args)
+
+ if args.asset:
+ asset = args.asset
+ else:
+ asset = create_asset(is_existing_asset = 1)
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset.name,
+ "asset_name": asset.asset_name,
+ "failure_date": nowdate(),
+ "description": "Test Description",
+ "repair_cost": 0,
+ "company": asset.company
+ })
+
+ if args.stock_consumption:
+ asset_repair.stock_consumption = 1
+ asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.append("stock_items", {
+ "item": args.item or args.item_code or "_Test Item",
+ "valuation_rate": args.rate if args.get("rate") is not None else 100,
+ "consumed_quantity": args.qty or 1
+ })
+
+ asset_repair.insert(ignore_if_duplicate=True)
+
+ if args.submit:
+ asset_repair.repair_status = "Completed"
+ asset_repair.cost_center = "_Test Cost Center - _TC"
+
+ if args.stock_consumption:
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Receipt",
+ "company": asset.company
+ })
+ stock_entry.append('items', {
+ "t_warehouse": asset_repair.warehouse,
+ "item_code": asset_repair.stock_items[0].item,
+ "qty": asset_repair.stock_items[0].consumed_quantity
+ })
+ stock_entry.submit()
+
+ if args.capitalize_repair_cost:
+ asset_repair.capitalize_repair_cost = 1
+ asset_repair.repair_cost = 1000
+ if asset.calculate_depreciation:
+ asset_repair.increase_in_asset_life = 12
+ asset_repair.purchase_invoice = make_purchase_invoice().name
+
+ asset_repair.submit()
+ return asset_repair
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
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
new file mode 100644
index 0000000..528f0ec
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2021-05-12 02:41:54.161024",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item",
+ "valuation_rate",
+ "consumed_quantity",
+ "total_value"
+ ],
+ "fields": [
+ {
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
+ },
+ {
+ "fetch_from": "item.valuation_rate",
+ "fieldname": "valuation_rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Valuation Rate",
+ "read_only": 1
+ },
+ {
+ "fieldname": "consumed_quantity",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Consumed Quantity"
+ },
+ {
+ "fieldname": "total_value",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Value",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-12 03:19:55.006300",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Repair Consumed Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
new file mode 100644
index 0000000..fa22a57
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
@@ -0,0 +1,8 @@
+# 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 AssetRepairConsumedItem(Document):
+ pass
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8196cff..2526e6d 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -356,42 +356,68 @@
}, update_modified)
def validate_inspection(self):
- '''Checks if quality inspection is set for Items that require inspection.
- On submit, throw an exception'''
- inspection_required_fieldname = None
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- inspection_required_fieldname = "inspection_required_before_purchase"
- elif self.doctype in ["Delivery Note", "Sales Invoice"]:
- inspection_required_fieldname = "inspection_required_before_delivery"
+ """Checks if quality inspection is set/ is valid for Items that require inspection."""
+ inspection_fieldname_map = {
+ "Purchase Receipt": "inspection_required_before_purchase",
+ "Purchase Invoice": "inspection_required_before_purchase",
+ "Sales Invoice": "inspection_required_before_delivery",
+ "Delivery Note": "inspection_required_before_delivery"
+ }
+ inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
+ # return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return
- for d in self.get('items'):
- qa_required = False
- if (inspection_required_fieldname and not d.quality_inspection and
- frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
- qa_required = True
- elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
- qa_required = True
- if self.docstatus == 1 and d.quality_inspection:
- qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
- if qa_doc.docstatus == 0:
- link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
- frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
+ for row in self.get('items'):
+ qi_required = False
+ if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
+ qi_required = True
+ elif self.doctype == "Stock Entry" and row.t_warehouse:
+ qi_required = True # inward stock needs inspection
- if qa_doc.status != 'Accepted':
- frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
- .format(d.idx, d.item_code), QualityInspectionRejectedError)
- elif qa_required :
- action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
- if self.docstatus==1 and action == 'Stop':
- frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
- exc=QualityInspectionRequiredError)
- else:
- frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
+ if qi_required: # validate row only if inspection is required on item level
+ self.validate_qi_presence(row)
+ if self.docstatus == 1:
+ self.validate_qi_submission(row)
+ self.validate_qi_rejection(row)
+
+ def validate_qi_presence(self, row):
+ """Check if QI is present on row level. Warn on save and stop on submit if missing."""
+ if not row.quality_inspection:
+ msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
+ if self.docstatus == 1:
+ frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
+ else:
+ frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
+
+ def validate_qi_submission(self, row):
+ """Check if QI is submitted on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
+ qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
+
+ if not qa_docstatus == 1:
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
+
+ def validate_qi_rejection(self, row):
+ """Check if QI is rejected on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
+ qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
+
+ if qa_status == "Rejected":
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index 064dfb2..d5f6e5f 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -33,7 +33,8 @@
frm.set_query("employee", "employees", function () {
return {
filters: {
- name: ["NOT IN", emp]
+ name: ["NOT IN", emp],
+ status: "Active"
}
};
});
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 36e728f..13cc423 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -117,7 +117,6 @@
Creates salary slip for selected employees if already not created
"""
self.check_permission('write')
- self.created = 1
employees = [emp.employee for emp in self.employees]
if employees:
args = frappe._dict({
@@ -686,7 +685,7 @@
if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters)
- emp = filters.get('employees')
+ emp = filters.get('employees') or []
include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
filters.pop('start_date')
filters.pop('end_date')
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index aa9bba1..5c9a453 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -991,7 +991,7 @@
label: "Nested BOM Structure",
url:
docsUrl +
- "user/manual/en/manufacturing/articles/nested-bom-structure",
+ "user/manual/en/manufacturing/articles/managing-multi-level-bom",
},
];
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js
index 808f9bc..cd682c5 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.js
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.js
@@ -35,6 +35,7 @@
return {
filters: {
company: row.company,
+ account_type: "Tax",
is_group: 0
}
};
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 23d4fe9..8ad30fa 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,6 +1,8 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
+ if (frm.doc.docstatus == 2) return;
+
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
@@ -111,7 +113,7 @@
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const action = () => {
- let message = __('Cancellation of e-way bill is currently not supported. ');
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 5d33c1b..81c7a6b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -42,7 +42,10 @@
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
- no_taxes_applied = not doc.get('taxes')
+
+ # if export invoice, then taxes can be empty
+ # invoice can only be ineligible if no taxes applied and is not an export invoice
+ no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
@@ -188,9 +191,10 @@
item.qty = abs(item.qty)
- item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
- item.gross_amount = abs(item.taxable_value) + item.discount_amount
+ item.unit_rate = abs(item.taxable_value / item.qty)
+ item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
+ item.discount_amount = 0
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index a4466e7..81c0918 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -834,8 +834,16 @@
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
- depreciation_amount = (flt(row.value_after_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ # if the Depreciation Schedule is being prepared for the first time
+ if not asset.flags.increase_in_asset_life:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / depreciation_left
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
+
else:
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
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 7cae0e4..38508c2 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -472,12 +472,7 @@
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
- const taxes = frm.doc.taxes.map(t => {
- return {
- description: t.description, rate: t.rate
- };
- });
- this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
+ this.render_taxes(frm.doc.taxes);
}
render_net_total(value) {
@@ -502,14 +497,14 @@
);
}
- render_taxes(value, taxes) {
+ render_taxes(taxes) {
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
const taxes_html = taxes.map(t => {
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
return `<div class="tax-row">
<div class="tax-label">${description}</div>
- <div class="tax-value">${format_currency(value, currency)}</div>
+ <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
</div>`;
}).join('');
this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html);
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index c484873..f1a166b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -56,7 +56,7 @@
);
let df_events = {
onchange: function() {
- frm.set_value(this.df.fieldname, this.value);
+ frm.set_value(this.df.fieldname, this.get_value());
}
};
if (df.fieldtype == "Button") {
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 061986d..e6ec496 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -74,7 +74,7 @@
"stock_received_but_not_billed",
"service_received_but_not_billed",
"expenses_included_in_valuation",
- "fixed_asset_depreciation_settings",
+ "fixed_asset_defaults",
"accumulated_depreciation_account",
"depreciation_expense_account",
"series_for_depreciation_entry",
@@ -83,6 +83,7 @@
"disposal_account",
"depreciation_cost_center",
"capital_work_in_progress_account",
+ "repair_and_maintenance_account",
"asset_received_but_not_billed",
"budget_detail",
"exception_budget_approver_role",
@@ -520,12 +521,6 @@
"options": "Account"
},
{
- "collapsible": 1,
- "fieldname": "fixed_asset_depreciation_settings",
- "fieldtype": "Section Break",
- "label": "Fixed Asset Depreciation Settings"
- },
- {
"fieldname": "accumulated_depreciation_account",
"fieldtype": "Link",
"label": "Accumulated Depreciation Account",
@@ -734,6 +729,18 @@
"fieldtype": "Link",
"label": "Default Payment Discount Account",
"options": "Account"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "fixed_asset_defaults",
+ "fieldtype": "Section Break",
+ "label": "Fixed Asset Defaults"
+ },
+ {
+ "fieldname": "repair_and_maintenance_account",
+ "fieldtype": "Link",
+ "label": "Repair and Maintenance Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -741,7 +748,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-05-07 03:11:28.189740",
+ "modified": "2021-05-12 16:51:08.187233",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 13269a8..d5dbd4c 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -28,21 +28,21 @@
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
if not frappe.get_list("Company"):
setup_complete({
- "currency" :"USD",
- "full_name" :"Test User",
- "company_name" :"Wind Power LLC",
- "timezone" :"America/New_York",
- "company_abbr" :"WP",
- "industry" :"Manufacturing",
- "country" :"United States",
- "fy_start_date" :"2011-01-01",
- "fy_end_date" :"2011-12-31",
- "language" :"english",
- "company_tagline" :"Testing",
- "email" :"test@erpnext.com",
- "password" :"test",
+ "currency" :"USD",
+ "full_name" :"Test User",
+ "company_name" :"Wind Power LLC",
+ "timezone" :"America/New_York",
+ "company_abbr" :"WP",
+ "industry" :"Manufacturing",
+ "country" :"United States",
+ "fy_start_date" :"2021-01-01",
+ "fy_end_date" :"2021-12-31",
+ "language" :"english",
+ "company_tagline" :"Testing",
+ "email" :"test@erpnext.com",
+ "password" :"test",
"chart_of_accounts" : "Standard",
- "domains" : ["Manufacturing"],
+ "domains" : ["Manufacturing"],
})
frappe.db.sql("delete from `tabLeave Allocation`")
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 014f409..6ca3d63 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -11,10 +11,11 @@
"hide_custom": 0,
"icon": "settings",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "ERPNext Settings",
"links": [],
- "modified": "2020-12-01 13:38:37.759596",
+ "modified": "2021-06-12 01:58:11.399566",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -109,6 +110,13 @@
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "retail",
+ "label": "Products Settings",
+ "link_to": "Products Settings",
+ "type": "DocType"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 3eab4ff..6c92d96 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -87,7 +87,8 @@
filters=self.filters,
or_filters=self.or_filters,
start=start,
- limit=self.page_length
+ limit=self.page_length,
+ order_by="weightage desc"
)
# Combine results having context of website item groups into item results
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 45e3c21..568f0ef 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -93,7 +93,7 @@
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
-
+
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
@@ -381,7 +381,8 @@
// Show Stock Levels only if is_stock_item
if (frm.doc.is_stock_item) {
frappe.require('item-dashboard.bundle.js', function() {
- const section = frm.dashboard.add_section('', __("Stock Levels"));
+ frm.dashboard.parent.find('.stock-levels').remove();
+ const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels');
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
parent: section,
item_code: frm.doc.name,
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 5df4d87..bf969f9 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -41,7 +41,7 @@
def validate(self):
self.check_mandatory()
- self.validate_purchase_receipts()
+ self.validate_receipt_documents()
init_landed_taxes_and_totals(self)
self.set_total_taxes_and_charges()
if not self.get("items"):
@@ -56,14 +56,23 @@
frappe.throw(_("Please enter Receipt Document"))
- def validate_purchase_receipts(self):
+ def validate_receipt_documents(self):
receipt_documents = []
for d in self.get("purchase_receipts"):
- if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1:
- frappe.throw(_("Receipt document must be submitted"))
- else:
- receipt_documents.append(d.receipt_document)
+ docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
+ if docstatus != 1:
+ msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
+ frappe.throw(_(msg), title=_("Invalid Document"))
+
+ if d.receipt_document_type == "Purchase Invoice":
+ update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock")
+ if not update_stock:
+ msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document))
+ msg += "<br>" + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.")
+ frappe.throw(msg, title=_("Incorrect Invoice"))
+
+ receipt_documents.append(d.receipt_document)
for item in self.get("items"):
if not item.receipt_document:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 5ba9c70..41800e3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -291,7 +291,7 @@
continue
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
- stock_rbnb, account_currency=warehouse_account_currency, item=d)
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -318,11 +318,11 @@
(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
- self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
# Amount added through landed-cos-voucher
@@ -407,6 +407,7 @@
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
+ stock_rbnb = self.get_company_default("stock_received_but_not_billed")
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index d56822a..dbba21f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1054,30 +1054,30 @@
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
-
- pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
-
- create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
- properties={"account": '_Test Account Stock In Hand - TCP1'})
+ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
- pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
- company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
- do_not_save = "True")
-
+ pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
+ cost_center = "Main - TCP1",
+ warehouse = "Stores - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ currency = "USD", conversion_rate = 70)
+
+ pr = create_purchase_receipt(pi.name)
+ pr.conversion_rate = 80
pr.items[0].purchase_invoice = pi.name
pr.items[0].purchase_invoice_item = pi.items[0].name
- pr.insert()
+ pr.save()
pr.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
- self.assertEqual(pr.name, voucher_no)
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # 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')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 7f3d701..f5d076a 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -14,7 +14,7 @@
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
# test_records = frappe.get_test_records('Quality Inspection')
@@ -159,6 +159,47 @@
frappe.delete_doc("Quality Inspection", qi)
dn.delete()
+ def test_rejected_qi_validation(self):
+ """Test if rejected QI blocks Stock Entry as per Stock Settings."""
+ se = make_stock_entry(
+ item_code="_Test Item with QA",
+ target="_Test Warehouse - _TC",
+ qty=1,
+ basic_rate=100,
+ inspection_required=True,
+ do_not_submit=True
+ )
+
+ readings = [
+ {
+ "specification": "Iron Content",
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ }
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Stock Entry",
+ reference_name=se.name,
+ readings=readings,
+ status="Rejected"
+ )
+
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
+ se.reload()
+ self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI
+
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn")
+ se.reload()
+ se.submit() # when allowed in Stock settings, allow rejected QI
+
+ # teardown
+ qa.reload()
+ qa.cancel()
+ se.reload()
+ se.cancel()
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
def create_quality_inspection(**args):
args = frappe._dict(args)
@@ -175,12 +216,11 @@
if not args.readings:
create_quality_inspection_parameter("Size")
readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
else:
readings = args.readings
- if args.status == "Rejected":
- readings["reading_1"] = "12" # status is auto set in child on save
-
if isinstance(readings, list):
for entry in readings:
create_quality_inspection_parameter(entry["specification"])
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index b12a854..563fcb0 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -45,6 +45,8 @@
s.posting_date = args.posting_date
if args.posting_time:
s.posting_time = args.posting_time
+ if args.inspection_required:
+ s.inspection_required = args.inspection_required
# map names
if args.from_warehouse:
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index a178283..22f412a 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -307,6 +307,7 @@
"fieldname": "quality_inspection",
"fieldtype": "Link",
"label": "Quality Inspection",
+ "no_copy": 1,
"options": "Quality Inspection"
},
{
@@ -548,7 +549,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-22 20:08:23.799715",
+ "modified": "2021-06-21 16:03:18.834880",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index ba31ad7..af2ada8 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -54,7 +54,7 @@
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
- make_stock_entry(
+ se = make_stock_entry(
item_code="_Test Item for Reposting",
source="Stores - _TC",
target="Finished Goods - _TC",
@@ -64,29 +64,29 @@
posting_date='2020-04-30',
posting_time='14:00'
)
- target_wh_sle = get_previous_sle({
+ target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-04-30',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": se.name
+ }, ["valuation_rate"], as_dict=1)
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
# Repack entry on 5-5-2020
repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name
+ }, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
- create_stock_reconciliation(
+ sr = create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
@@ -109,12 +109,12 @@
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name
+ }, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index cf5d98d..2a9dcfb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -23,7 +23,10 @@
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
+ "quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
+ "column_break_21",
+ "action_if_quality_inspection_is_rejected",
"section_break_7",
"automatically_set_serial_nos_based_on_fifo",
"set_qty_in_transactions_based_on_serial_no_input",
@@ -264,6 +267,22 @@
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "quality_inspection_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Quality Inspection Settings"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Stop",
+ "fieldname": "action_if_quality_inspection_is_rejected",
+ "fieldtype": "Select",
+ "label": "Action If Quality Inspection Is Rejected",
+ "options": "Stop\nWarn"
}
],
"icon": "icon-cog",
@@ -271,7 +290,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-30 17:27:42.709231",
+ "modified": "2021-07-10 16:17:42.159829",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 61ca3a3..de3389a 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -150,7 +150,8 @@
"fieldtype": "Link",
"label": "Document Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"default": "1",
@@ -178,7 +179,7 @@
}
],
"links": [],
- "modified": "2021-05-29 13:35:41.956849",
+ "modified": "2021-07-08 12:28:46.283334",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 2a8446d..7c18a65 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -81,10 +81,9 @@
# check SLA custom fields created for leads
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}))
def test_docfield_creation_for_sla_on_custom_dt(self):
doctype = create_custom_doctype()
@@ -102,10 +101,9 @@
# check SLA docfields created
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype.name, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}))
def test_sla_application(self):
# Default Service Level Agreement
@@ -330,16 +328,11 @@
"entity": entity
})
- service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters)
+ sla = frappe.db.exists("Service Level Agreement", filters)
+ if sla:
+ frappe.delete_doc("Service Level Agreement", sla, force=1)
- if not service_level_agreement_exists:
- doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
- else:
- doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
- doc.update(service_level_agreement)
- doc.save()
-
- return doc
+ return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
def create_customer():