diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 7c6b8432b8..1d180f251e 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -1,16 +1,25 @@
name: Backport
on:
- pull_request:
+ pull_request_target:
types:
- closed
- labeled
jobs:
- backport:
- runs-on: ubuntu-18.04
- name: Backport
+ main:
+ runs-on: ubuntu-latest
steps:
- - name: Backport
- uses: tibdex/backport@v1
+ - name: Checkout Actions
+ uses: actions/checkout@v2
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ repository: "frappe/backport"
+ path: ./actions
+ ref: develop
+ - name: Install Actions
+ run: npm install --production --prefix ./actions
+ - name: Run backport
+ uses: ./actions/backport
+ with:
+ token: ${{secrets.BACKPORT_BOT_TOKEN}}
+ labelsToAdd: "backport"
+ title: "{{originalTitle}}"
diff --git a/CODEOWNERS b/CODEOWNERS
index 219b6bb782..a4a14de1b8 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -21,13 +21,13 @@ erpnext/quality_management/ @marination @rohitwaghchaure
erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush
-erpnext/crm/ @ruchamahabal
-erpnext/education/ @ruchamahabal
-erpnext/healthcare/ @ruchamahabal
-erpnext/hr/ @ruchamahabal
+erpnext/crm/ @ruchamahabal @pateljannat
+erpnext/education/ @ruchamahabal @pateljannat
+erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
+erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal
-erpnext/payroll @ruchamahabal
-erpnext/projects/ @ruchamahabal
+erpnext/payroll @ruchamahabal @pateljannat
+erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 1be2fbf5c8..f763df0852 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -230,7 +230,7 @@ class Account(NestedSet):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
elif self.account_type and not self.flags.exclude_account_type_check:
- throw(_("Cannot covert to Group because Account Type is selected."))
+ throw(_("Cannot convert to Group because Account Type is selected."))
else:
self.is_group = 1
self.save()
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index 603e21ea24..6c25f0024d 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project":
- budget_against = "_Test Project"
+ budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"})
else:
budget_against = budget_against_CC or "_Test Cost Center - _TC"
@@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate())
+ "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
def make_budget(**args):
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index d3ac3a6676..439b1edbce 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
+
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 7459c11d4d..33c3e0432b 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1545,6 +1545,7 @@
"fieldname": "consolidated_invoice",
"fieldtype": "Link",
"label": "Consolidated Sales Invoice",
+ "no_copy": 1,
"options": "Sales Invoice",
"read_only": 1
}
@@ -1552,7 +1553,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-02-01 15:03:33.800707",
+ "modified": "2021-07-29 13:37:20.636171",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index 428989aa96..0be41b4063 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -558,7 +558,8 @@
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
- "label": "Condition"
+ "label": "Condition",
+ "options": "PythonExpression"
},
{
"fieldname": "column_break_42",
@@ -575,7 +576,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2021-03-06 22:01:24.840422",
+ "modified": "2021-08-06 15:10:04.219321",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index dc9094c3e9..c588d45a9f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
},
get_query_filters: {
docstatus: 1,
- status: ["not in", ["Closed", "Completed"]],
+ status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company,
is_return: 0
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e7dd6b8a60..0a9a105b7c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -48,6 +48,8 @@
"shipping_address",
"company_address",
"company_address_display",
+ "dispatch_address_name",
+ "dispatch_address",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -1966,6 +1968,21 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address_name",
+ "fieldtype": "Link",
+ "label": "Dispatch Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address",
+ "fieldtype": "Small Text",
+ "label": "Dispatch Address",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -1978,7 +1995,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-05-20 22:48:33.988881",
+ "modified": "2021-07-08 14:03:55.502522",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index dbc7f8632f..70bccd7166 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
+ self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
+ self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self):
# init
@@ -2062,6 +2064,30 @@ def make_test_address_for_ewaybill():
address.save()
+ if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Dispatch Address Line 1",
+ "address_title": "_Test Dispatch-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 0,
+ "phone": "+910000000000",
+ "gstin": "07AAACC1206D1ZI",
+ "gst_state": "Delhi",
+ "gst_state_number": "07",
+ "pincode": "1100101"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Company",
+ "link_name": "_Test Company"
+ })
+
+ address.save()
+
def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({
@@ -2100,6 +2126,7 @@ def make_sales_invoice_for_ewaybill():
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
+ si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road'
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
index 46ce0939e4..771611a786 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
@@ -78,7 +78,7 @@
"label": "Cost"
},
{
- "depends_on": "eval:doc.price_determination==\"Based on price list\"",
+ "depends_on": "eval:doc.price_determination==\"Based On Price List\"",
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
@@ -147,7 +147,7 @@
}
],
"links": [],
- "modified": "2020-06-25 10:53:44.205774",
+ "modified": "2021-08-09 10:53:44.205774",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28f37..922cc4a7b2 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', {
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 @@ frappe.ui.form.on('Asset', {
"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 @@ frappe.ui.form.on('Asset', {
})
},
+ 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 421b9a6c37..de060757e2 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 8799275fc4..ecc35b05b3 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@ class Asset(AccountsController):
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 @@ class Asset(AccountsController):
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 @@ class Asset(AccountsController):
# 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 @@ class Asset(AccountsController):
"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 @@ class Asset(AccountsController):
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 -
@@ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
})
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,9 +789,16 @@ def get_depreciation_amount(asset, depreciable_value, row):
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))
- return depreciation_amount
\ No newline at end of file
+ return depreciation_amount
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 8845f24d10..59fbe3b030 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ def create_asset(**args):
"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 @@ def create_asset(**args):
"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 d9b7b695f7..e5a5f194c1 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 4ba2b4474a..1cebfff66e 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 @@ frappe.ui.form.on('Asset Repair', {
}
});
}
+
+ 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 d338fc0fb7..19528a26cc 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,38 +7,43 @@
"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",
@@ -50,18 +55,6 @@
"fieldname": "column_break_2",
"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",
@@ -74,33 +67,20 @@
"label": "Failure Date",
"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,138 @@
"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"
+ },
+ {
+ "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 +302,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 049b931b5e..d32fdf7054 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 3d325a9683..30bbb37851 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 0000000000..e69de29bb2
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 0000000000..528f0ec986
--- /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 0000000000..fa22a5712f
--- /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/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index a9b7efbe98..498f3cf0e9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1507,7 +1507,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
-def add_taxes_from_tax_template(child_item, parent_doc):
+def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
@@ -1530,7 +1530,8 @@ def add_taxes_from_tax_template(child_item, parent_doc):
"category" : "Total",
"add_deduct_tax" : "Add"
})
- tax_row.db_insert()
+ if db_insert:
+ tax_row.db_insert()
def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
"""
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index ac374a95f4..089a63fc1c 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
+ status:function(frm){
+ if (frm.doc.status == "Lost"){
+ frm.trigger('set_as_lost_dialog');
+ }
+
+ },
+
customer_address: function(frm, cdt, cdn) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false);
},
@@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", {
frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create'));
- if(doc.status!=="Quotation") {
- frm.add_custom_button(__('Lost'), () => {
- frm.trigger('set_as_lost_dialog');
- });
- }
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index afa0be9b9f..4493a3fef1 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -34,11 +34,14 @@ def enroll_student(source_name):
}
}}, ignore_permissions=True)
student.save()
+
+ student_applicant = frappe.db.get_value("Student Applicant", source_name,
+ ["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name
- program_enrollment.student_category = student.student_category
+ program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title
- program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program")
+ program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment
diff --git a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json
index 9be292b65e..1d7497387f 100644
--- a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json
+++ b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json
@@ -1,195 +1,68 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-06-10 03:29:02.539914",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-06-10 03:29:02.539914",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "student_applicant",
+ "student",
+ "student_name",
+ "column_break_3",
+ "student_batch_name",
+ "student_category"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "student_applicant",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Applicant",
- "length": 0,
- "no_copy": 0,
- "options": "Student Applicant",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "student_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Applicant",
+ "options": "Student Applicant"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "student",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student",
- "length": 0,
- "no_copy": 0,
- "options": "Student",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "student",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student",
+ "options": "Student"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "student_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Student Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_batch_name",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Batch Name",
- "length": 0,
- "no_copy": 0,
- "options": "Student Batch Name",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "student_batch_name",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Batch Name",
+ "options": "Student Batch Name"
+ },
+ {
+ "fieldname": "student_category",
+ "fieldtype": "Link",
+ "label": "Student Category",
+ "options": "Student Category",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-01-02 12:03:53.890741",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Program Enrollment Tool Student",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2021-07-29 18:19:54.471594",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Program Enrollment Tool Student",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js
index d3fe7d2b4d..12faeecc87 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js
+++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js
@@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', {
})
}).addClass('btn-primary');
}
+
+ let app_link = "Ecommerce Integrations"
+ frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
}
});
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js
index 1574795dfa..a926a7e52a 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js
@@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
}
+
+ let app_link = "Ecommerce Integrations"
+ frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
+
})
$.extend(erpnext_integrations.shopify_settings, {
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 1ba752a146..e6b6cc4a8a 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -430,7 +430,8 @@ regional_overrides = {
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
- 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
+ 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
+ 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
},
'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index ece627c50d..cbb3cc813b 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -24,7 +24,6 @@ class EmployeeAdvance(Document):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
- self.set_status()
def set_status(self):
if self.docstatus == 0:
@@ -231,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry"
- return voucher_type
\ No newline at end of file
+ return voucher_type
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 8f8dbb2224..95e2806aed 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -36,8 +36,8 @@ class ExpenseClaim(AccountsController):
if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project")
- def set_status(self):
- self.status = {
+ def set_status(self, update=False):
+ status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
@@ -45,14 +45,18 @@ class ExpenseClaim(AccountsController):
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total")
- if (self.is_paid or (flt(self.total_sanctioned_amount) > 0
- and flt(self.grand_total, precision) == flt(paid_amount, precision))) \
- and self.docstatus == 1 and self.approval_status == 'Approved':
- self.status = "Paid"
+ if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
+ and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
+ status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
- self.status = "Unpaid"
+ status = "Unpaid"
elif self.docstatus == 1 and self.approval_status == 'Rejected':
- self.status = 'Rejected'
+ status = 'Rejected'
+
+ if update:
+ self.db_set("status", status)
+ else:
+ self.status = status
def on_update(self):
share_doc_with_approver(self, self.expense_approver)
@@ -75,7 +79,7 @@ class ExpenseClaim(AccountsController):
if self.is_paid:
update_reimbursed_amount(self)
- self.set_status()
+ self.set_status(update=True)
self.update_claimed_amount_in_employee_advance()
def on_cancel(self):
@@ -87,7 +91,6 @@ class ExpenseClaim(AccountsController):
if self.is_paid:
update_reimbursed_amount(self)
- self.set_status()
self.update_claimed_amount_in_employee_advance()
def update_claimed_amount_in_employee_advance(self):
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index ebd9ae2dc5..0ba85078ea 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -717,9 +717,8 @@ def get_bom_item_rate(args, bom_doc):
"ignore_conversion_rate": True
})
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
- out = frappe._dict()
- get_price_list_rate(bom_args, item_doc, out)
- rate = out.price_list_rate
+ price_list_data = get_price_list_rate(bom_args, item_doc)
+ rate = price_list_data.price_list_rate
return rate
@@ -774,7 +773,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.image,
bom.project,
bom_item.rate,
- bom_item.amount,
+ sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
item.stock_uom,
item.item_group,
item.allow_alternative_item,
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 69c7f5c614..66e2394b84 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -608,6 +608,11 @@ def make_stock_entry(source_name, target_doc=None):
target.set_missing_values()
target.set_stock_entry_type()
+ wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
+ for item in target.items:
+ item.allow_alternative_item = int(wo_allows_alternate_item and
+ frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
+
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Stock Entry",
@@ -698,4 +703,4 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
}
}, target_doc, set_missing_values)
- return doclist
\ No newline at end of file
+ return doclist
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 6a024f275a..8c27d6ccc9 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -109,6 +109,15 @@ class ProductionPlan(Document):
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
return so_mr_list
+ def get_bom_item(self):
+ """Check if Item or if its Template has a BOM."""
+ bom_item = None
+ has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
+ if not has_bom:
+ template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
+ bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
+ return bom_item
+
def get_so_items(self):
# Check for empty table or empty rows
if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
@@ -117,16 +126,26 @@ class ProductionPlan(Document):
so_list = self.get_so_mr_list("sales_order", "sales_orders")
item_condition = ""
- if self.item_code:
+ bom_item = "bom.item = so_item.item_code"
+ if self.item_code and frappe.db.exists('Item', self.item_code):
+ bom_item = self.get_bom_item() or bom_item
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
- items = frappe.db.sql("""select distinct parent, item_code, warehouse,
- (qty - work_order_qty) * conversion_factor as pending_qty, description, name
- from `tabSales Order Item` so_item
- where parent in (%s) and docstatus = 1 and qty > work_order_qty
- and exists (select name from `tabBOM` bom where bom.item=so_item.item_code
- and bom.is_active = 1) %s""" % \
- (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
+ items = frappe.db.sql("""
+ select
+ distinct parent, item_code, warehouse,
+ (qty - work_order_qty) * conversion_factor as pending_qty,
+ description, name
+ from
+ `tabSales Order Item` so_item
+ where
+ parent in (%s) and docstatus = 1 and qty > work_order_qty
+ and exists (select name from `tabBOM` bom where %s
+ and bom.is_active = 1) %s""" %
+ (", ".join(["%s"] * len(so_list)),
+ bom_item,
+ item_condition),
+ tuple(so_list), as_dict=1)
if self.item_code:
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
@@ -683,6 +702,7 @@ def get_material_request_items(row, sales_order, company,
def get_sales_orders(self):
so_filter = item_filter = ""
+ bom_item = "bom.item = so_item.item_code"
if self.from_date:
so_filter += " and so.transaction_date >= %(from_date)s"
if self.to_date:
@@ -694,7 +714,8 @@ def get_sales_orders(self):
if self.sales_order_status:
so_filter += "and so.status = %(sales_order_status)s"
- if self.item_code:
+ if self.item_code and frappe.db.exists('Item', self.item_code):
+ bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item)s"
open_so = frappe.db.sql("""
@@ -704,13 +725,13 @@ def get_sales_orders(self):
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {0} {1}
- and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code
+ and (exists (select name from `tabBOM` bom where {2}
and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
- """.format(so_filter, item_filter), {
+ """.format(so_filter, item_filter, bom_item), {
"from_date": self.from_date,
"to_date": self.to_date,
"customer": self.customer,
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 93e6d7a97f..af8de8ee0e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import get_sa
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
+from erpnext.controllers.item_variant import create_variant
class TestProductionPlan(unittest.TestCase):
def setUp(self):
@@ -271,6 +272,60 @@ class TestProductionPlan(unittest.TestCase):
self.assertEqual(warehouses, expected_warehouses)
+ def test_get_sales_order_with_variant(self):
+ if not frappe.db.exists('Item', {"item_code": 'PIV'}):
+ item = create_item('PIV', valuation_rate = 100)
+ variant_settings = {
+ "attributes": [
+ {
+ "attribute": "Colour"
+ },
+ ],
+ "has_variants": 1
+ }
+ item.update(variant_settings)
+ item.save()
+ parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
+ if not frappe.db.exists('BOM', {"item": 'PIV'}):
+ parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
+ else:
+ parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
+
+ if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
+ variant = create_variant("PIV", {"Colour": "Red"})
+ variant.save()
+ variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
+ else:
+ variant = frappe.get_doc('Item', 'PIV-RED')
+ if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
+ variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
+
+ """Testing when item variant has a BOM"""
+ so = make_sales_order(item_code="PIV-RED", qty=5)
+ pln = frappe.new_doc('Production Plan')
+ pln.company = so.company
+ pln.get_items_from = 'Sales Order'
+ pln.item_code = 'PIV-RED'
+ pln.get_open_sales_orders()
+ self.assertEqual(pln.sales_orders[0].sales_order, so.name)
+ pln.get_so_items()
+ self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
+ self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
+ so.cancel()
+ frappe.delete_doc('Sales Order', so.name)
+ variant_bom.cancel()
+ frappe.delete_doc('BOM', variant_bom.name)
+
+ """Testing when item variant doesn't have a BOM"""
+ so = make_sales_order(item_code="PIV-RED", qty=5)
+ pln.get_open_sales_orders()
+ self.assertEqual(pln.sales_orders[0].sales_order, so.name)
+ pln.po_items = []
+ pln.get_so_items()
+ self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
+ self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
+
+ frappe.db.rollback()
def create_production_plan(**args):
args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 69812c7452..282b5d0afe 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -655,7 +655,7 @@ class WorkOrder(Document):
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', {
'rate': item.rate,
- 'amount': item.amount,
+ 'amount': item.rate * item.qty,
'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 32763754d2..9ca24988f2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -293,5 +293,9 @@ erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
+erpnext.patches.v13_0.update_amt_in_work_order_required_items
+erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
+erpnext.patches.v13_0.update_recipient_email_digest
+erpnext.patches.v13_0.shopify_deprecation_warning
diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
index d7ad1fc696..0d8109c41a 100644
--- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
+++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
@@ -30,19 +30,20 @@ def execute():
return
repost_stock_entries = []
+
stock_entries = frappe.db.sql_list('''
SELECT
se.name
FROM
`tabStock Entry` se
WHERE
- se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders}
+ se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s
and not exists(
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
)
- Order BY
+ ORDER BY
se.posting_date, se.posting_time
- '''.format(work_orders=tuple(work_orders)))
+ ''', (work_orders,))
if stock_entries:
print('Length of stock entries', len(stock_entries))
diff --git a/erpnext/patches/v13_0/delete_orphaned_tables.py b/erpnext/patches/v13_0/delete_orphaned_tables.py
new file mode 100644
index 0000000000..1d6eebe039
--- /dev/null
+++ b/erpnext/patches/v13_0/delete_orphaned_tables.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe.utils import getdate
+
+def execute():
+ frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record')
+
+ if has_deleted_company_transactions():
+ child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected()
+
+ for doctype in child_doctypes:
+ docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation'])
+
+ for doc in docs:
+ if not frappe.db.exists(doc['parenttype'], doc['parent']):
+ frappe.db.delete(doctype, {'name': doc['name']})
+
+ elif check_for_new_doc_with_same_name_as_deleted_parent(doc):
+ frappe.db.delete(doctype, {'name': doc['name']})
+
+def has_deleted_company_transactions():
+ return frappe.get_all('Transaction Deletion Record')
+
+def get_child_doctypes_whose_parent_doctypes_were_affected():
+ parent_doctypes = get_affected_doctypes()
+ child_doctypes = frappe.get_all(
+ 'DocField',
+ filters={
+ 'fieldtype': 'Table',
+ 'parent':['in', parent_doctypes]
+ }, pluck='options')
+
+ return child_doctypes
+
+def get_affected_doctypes():
+ affected_doctypes = []
+ tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name")
+
+ for tdr in tdr_docs:
+ tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr)
+
+ for doctype in tdr_doc.doctypes:
+ if is_not_child_table(doctype.doctype_name):
+ affected_doctypes.append(doctype.doctype_name)
+
+ affected_doctypes = remove_duplicate_items(affected_doctypes)
+ return affected_doctypes
+
+def is_not_child_table(doctype):
+ return not bool(frappe.get_value('DocType', doctype, 'istable'))
+
+def remove_duplicate_items(affected_doctypes):
+ return list(set(affected_doctypes))
+
+def check_for_new_doc_with_same_name_as_deleted_parent(doc):
+ """
+ Compares creation times of parent and child docs.
+ Since Transaction Deletion Record resets the naming series after deletion,
+ it allows the creation of new docs with the same names as the deleted ones.
+ """
+
+ parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation')
+ child_creation_time = doc['creation']
+
+ return getdate(parent_creation_time) > getdate(child_creation_time)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
index fa1dfed643..41c51c36dc 100644
--- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py
+++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
@@ -37,7 +37,7 @@ def execute():
if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
- frappe.reload_doc('crm', 'doctype', 'opportunity')
+ frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration
diff --git a/erpnext/patches/v13_0/shopify_deprecation_warning.py b/erpnext/patches/v13_0/shopify_deprecation_warning.py
new file mode 100644
index 0000000000..8b0f1935cf
--- /dev/null
+++ b/erpnext/patches/v13_0/shopify_deprecation_warning.py
@@ -0,0 +1,15 @@
+import click
+import frappe
+
+
+def execute():
+
+ frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings")
+ if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"):
+ return
+
+ click.secho(
+ "Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
+ "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
+ fg="yellow",
+ )
diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py
new file mode 100644
index 0000000000..eae5ff60b9
--- /dev/null
+++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py
@@ -0,0 +1,10 @@
+import frappe
+
+def execute():
+ """ Correct amount in child table of required items table."""
+
+ frappe.reload_doc("manufacturing", "doctype", "work_order")
+ frappe.reload_doc("manufacturing", "doctype", "work_order_item")
+
+ frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")
+
diff --git a/erpnext/patches/v13_0/update_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py
new file mode 100644
index 0000000000..d9aa03f0fd
--- /dev/null
+++ b/erpnext/patches/v13_0/update_recipient_email_digest.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("setup", "doctype", "Email Digest")
+ frappe.reload_doc("setup", "doctype", "Email Digest Recipient")
+ email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list'])
+ for email_digest in email_digests:
+ if email_digest.recipient_list:
+ for recipient in email_digest.recipient_list.split("\n"):
+ doc = frappe.get_doc({
+ 'doctype': 'Email Digest Recipient',
+ 'parenttype': 'Email Digest',
+ 'parentfield': 'recipients',
+ 'parent': email_digest.name,
+ 'recipient': recipient
+ })
+ doc.insert()
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index b978cbe2b5..381f399e9f 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -112,11 +112,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
-@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql("""
- select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
- deduct_full_tax_on_selected_payroll_date, is_recurring
+ select name, salary_component as component, type, amount,
+ overwrite_salary_structure_amount as overwrite,
+ deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1
diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js
index dbf75140ac..e9e6f81862 100644
--- a/erpnext/payroll/doctype/salary_component/salary_component.js
+++ b/erpnext/payroll/doctype/salary_component/salary_component.js
@@ -4,11 +4,18 @@
frappe.ui.form.on('Salary Component', {
setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) {
- var d = locals[cdt][cdn];
+ let d = frappe.get_doc(cdt, cdn);
+
+ let root_type = "Liability";
+ if (frm.doc.type == "Deduction") {
+ root_type = "Expense";
+ }
+
return {
filters: {
"is_group": 0,
- "company": d.company
+ "company": d.company,
+ "root_type": root_type
}
};
});
diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json
index 97608d72f3..393f647cc8 100644
--- a/erpnext/payroll/doctype/salary_detail/salary_detail.json
+++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json
@@ -12,7 +12,6 @@
"year_to_date",
"section_break_5",
"additional_salary",
- "is_recurring_additional_salary",
"statistical_component",
"depends_on_payment_days",
"exempted_from_income_tax",
@@ -236,19 +235,11 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
- },
- {
- "default": "0",
- "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
- "fieldname": "is_recurring_additional_salary",
- "fieldtype": "Check",
- "label": "Is Recurring Additional Salary",
- "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-03-14 13:39:15.847158",
+ "modified": "2021-01-14 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 3e82c0d428..7e1fb0616d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -7,12 +7,12 @@ import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname
-from frappe.utils.background_jobs import enqueue
from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase
+from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
@@ -618,8 +618,7 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component),
additional_salary.amount,
component_type,
- additional_salary,
- is_recurring = additional_salary.is_recurring
+ additional_salary
)
def add_tax_components(self, payroll_period):
@@ -640,7 +639,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
- def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
@@ -681,7 +680,6 @@ class SalarySlip(TransactionBase):
component_row.set('abbr', abbr)
if additional_salary:
- component_row.is_recurring_additional_salary = is_recurring
component_row.default_amount = 0
component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name
@@ -715,7 +713,6 @@ class SalarySlip(TransactionBase):
# get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
-
# get taxable_earnings, paid_taxes for previous period
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
self.start_date, tax_slab.allow_tax_exemption)
@@ -875,16 +872,8 @@ class SalarySlip(TransactionBase):
if earning.is_tax_applicable:
if additional_amount:
- if not earning.is_recurring_additional_salary:
- taxable_earnings += (amount - additional_amount)
- additional_income += additional_amount
- else:
- to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
- period = (getdate(to_date).month - getdate(self.start_date).month) + 1
- if period > 0:
- taxable_earnings += (amount - additional_amount) * period
- additional_income += additional_amount * period
-
+ taxable_earnings += (amount - additional_amount)
+ additional_income += additional_amount
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
continue
diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py
index 500543ceb0..05a5366a5c 100644
--- a/erpnext/payroll/report/bank_remittance/bank_remittance.py
+++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py
@@ -95,6 +95,7 @@ def execute(filters=None):
"amount": salary.net_pay,
}
data.append(row)
+
return columns, data
def get_bank_accounts():
@@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters):
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
payment_accounts = [d.payment_account for d in entries]
- set_company_account(payment_accounts, entries)
+ entries = set_company_account(payment_accounts, entries)
return entries
def get_salary_slips(payroll_entries):
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 7b651fc62f..53d5278bbf 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -67,8 +67,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
calculate_discount_amount: function() {
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
- this.calculate_item_values();
- this.calculate_net_total();
this.set_discount_amount();
this.apply_discount_amount();
}
diff --git a/erpnext/regional/address_template/templates/france.html b/erpnext/regional/address_template/templates/france.html
new file mode 100644
index 0000000000..752331eeec
--- /dev/null
+++ b/erpnext/regional/address_template/templates/france.html
@@ -0,0 +1,5 @@
+{% if address_line1 %}{{ address_line1 }}{% endif -%}
+{% if address_line2 %}
{{ address_line2 }}{% endif -%}
+{% if pincode %}
{{ pincode }}{% endif -%}
+{% if city %} {{ city }}{% endif -%}
+{% if country %}
{{ country }}{% endif -%}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index ea600d9097..e65442dbff 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -316,10 +316,6 @@ def get_payment_details(invoice):
))
def get_return_doc_reference(invoice):
- if not invoice.return_against:
- frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
- .format(frappe.bold('Return Against')), title=_('Missing Field'))
-
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
@@ -435,7 +431,7 @@ def make_einvoice(invoice):
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
- if invoice.is_return:
+ if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter and not invoice.is_return:
@@ -966,7 +962,7 @@ class GSPConnector():
"attached_to_doctype": doctype,
"attached_to_name": docname,
"attached_to_field": "qrcode_image",
- "is_private": 1,
+ "is_private": 0,
"content": qr_image.getvalue()})
_file.save()
frappe.db.commit()
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index a4466e78f2..88c350ac89 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -431,9 +431,11 @@ def get_ewb_data(dt, dn):
company_address = frappe.get_doc('Address', doc.company_address)
billing_address = frappe.get_doc('Address', doc.customer_address)
+ #added dispatch address
+ dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address
shipping_address = frappe.get_doc('Address', doc.shipping_address_name)
- data = get_address_details(data, doc, company_address, billing_address)
+ data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
data.itemList = []
data.totalValue = doc.total
@@ -519,10 +521,10 @@ def get_gstins_for_company(company):
`tabDynamic Link`.link_name = %(company)s""", {"company": company})
return company_gstins
-def get_address_details(data, doc, company_address, billing_address):
+def get_address_details(data, doc, company_address, billing_address, dispatch_address):
data.fromPincode = validate_pincode(company_address.pincode, 'Company Address')
- data.fromStateCode = data.actualFromStateCode = validate_state_code(
- company_address.gst_state_number, 'Company Address')
+ data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address')
+ data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address')
if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15:
data.toGstin = 'URP'
@@ -834,8 +836,16 @@ def get_depreciation_amount(asset, depreciable_value, row):
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
@@ -849,4 +859,15 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
- return depreciation_amount
\ No newline at end of file
+ return depreciation_amount
+
+def set_item_tax_from_hsn_code(item):
+ if not item.taxes and item.gst_hsn_code:
+ hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code)
+
+ for tax in hsn_doc.taxes:
+ item.append('taxes', {
+ 'item_tax_template': tax.item_tax_template,
+ 'tax_category': tax.tax_category,
+ 'valid_from': tax.valid_from
+ })
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 762b6f1d6c..d31db820ab 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -38,6 +38,8 @@
"col_break46",
"shipping_address_name",
"shipping_address",
+ "dispatch_address_name",
+ "dispatch_address",
"customer_group",
"territory",
"currency_and_price_list",
@@ -1486,13 +1488,29 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address_name",
+ "fieldtype": "Link",
+ "label": "Dispatch Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "dispatch_address_name",
+ "fieldname": "dispatch_address",
+ "fieldtype": "Small Text",
+ "label": "Dispatch Address",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-15 23:55:13.439068",
+ "modified": "2021-07-08 21:37:44.177493",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
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 6e36d2809a..a4a4b0e0ed 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -564,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class {
)
set_dynamic_rate_header_width();
- this.scroll_to_item($item_to_update);
function set_dynamic_rate_header_width() {
const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount"));
@@ -639,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class {
$($img).parent().replaceWith(`