Merge branch 'develop' into enable-discount-accounting
This commit is contained in:
commit
9910968398
@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
|
||||
start_date = add_months(today(), -1)
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
companies = frappe.get_all('Company')
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
for company in companies:
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
|
||||
|
@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
|
||||
if 'Bank Account' not in json.dumps(preview):
|
||||
if 'Bank Account' not in json.dumps(preview['columns']):
|
||||
frappe.throw(_("Please add the Bank Account column"))
|
||||
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
|
@ -231,25 +231,25 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference(self):
|
||||
pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
|
||||
pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
|
||||
|
||||
pi.items[0].purchase_receipt = pr.name
|
||||
pi.items[0].pr_detail = pr.items[0].name
|
||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
|
||||
currency = "USD", conversion_rate = 70)
|
||||
|
||||
pi = create_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 80
|
||||
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
|
||||
gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
|
||||
voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
|
||||
|
||||
self.assertEqual(pi.name, voucher_no)
|
||||
|
||||
exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit')
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
|
||||
|
||||
self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(test_records[1])
|
||||
@ -1031,21 +1031,21 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
# Check GLE for Purchase Invoice
|
||||
# Zero net effect on final TDS Payable on invoice
|
||||
expected_gle = [
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
|
||||
['_Test Account Excise Duty - _TC', 0, 3000],
|
||||
['Creditors - _TC', 0, 27000],
|
||||
['TDS Payable - _TC', 3000, 3000]
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000],
|
||||
['_Test Account Excise Duty - _TC', -3000],
|
||||
['Creditors - _TC', -27000],
|
||||
['TDS Payable - _TC', 0]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""", (purchase_invoice.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
||||
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||
|
||||
def update_tax_witholding_category(company, account, date):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry`
|
||||
where party_type = %s and party=%s
|
||||
and is_cancelled = 0
|
||||
group by company""", (party_type, party)))
|
||||
|
||||
for d in companies:
|
||||
|
@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data):
|
||||
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
|
||||
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
|
||||
]
|
||||
}
|
||||
},
|
||||
'type' : 'bar'
|
||||
}
|
||||
|
||||
|
@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
|
||||
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||
acc.account_name, acc.account_number
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
|
||||
{
|
||||
|
@ -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: {
|
||||
|
@ -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",
|
||||
|
@ -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,8 +789,15 @@ 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))
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
@ -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,139 @@
|
||||
"options": "Asset Repair",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.asset_name",
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Asset Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "capitalize_repair_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "Capitalize Repair Cost"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Stock Items",
|
||||
"mandatory_depends_on": "stock_consumption",
|
||||
"options": "Asset Repair Consumed Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_23",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "stock_consumption",
|
||||
"fieldtype": "Check",
|
||||
"label": "Stock Consumed During Repair"
|
||||
},
|
||||
{
|
||||
"depends_on": "stock_consumption",
|
||||
"fieldname": "stock_consumption_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stock Consumption Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
|
||||
"description": "Sum of Repair Cost and Value of Consumed Stock Items.",
|
||||
"fieldname": "total_repair_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Repair Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "stock_consumption",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "capitalize_repair_cost",
|
||||
"fieldname": "asset_depreciation_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Asset Depreciation Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "increase_in_asset_life",
|
||||
"fieldtype": "Int",
|
||||
"label": "Increase In Asset Life(Months)",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "purchase_invoice",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Invoice",
|
||||
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Invoice"
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_entry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Entry",
|
||||
"options": "Stock Entry",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-22 15:08:12.495850",
|
||||
"modified": "2021-06-25 13:14:38.307723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@ -203,6 +303,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
@ -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)
|
||||
return round(downtime, 2)
|
||||
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
|
||||
frappe.set_route("List", "Training Feedback");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
frm.events.set_employee_query(frm);
|
||||
},
|
||||
|
||||
frappe.ui.form.on("Training Event Employee", {
|
||||
employee: function (frm) {
|
||||
set_employee_query: function(frm) {
|
||||
let emp = [];
|
||||
for (let d in frm.doc.employees) {
|
||||
if (frm.doc.employees[d].employee) {
|
||||
@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Training Event Employee", {
|
||||
employee: function(frm) {
|
||||
frm.events.set_employee_query(frm);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee",
|
||||
"no_copy": 1,
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
@ -68,7 +69,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-21 12:41:59.336237",
|
||||
"modified": "2021-07-02 17:20:27.630176",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Event Employee",
|
||||
|
@ -1100,6 +1100,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
|
||||
},
|
||||
'BOM Item': {
|
||||
'doctype': 'BOM Item',
|
||||
# stop get_mapped_doc copying parent bom_no to children
|
||||
'field_no_map': ['bom_no'],
|
||||
'condition': lambda doc: doc.has_variants == 0
|
||||
},
|
||||
}, target_doc, postprocess)
|
||||
|
@ -8,6 +8,7 @@ import frappe
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.test_runner import make_test_records
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
||||
from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
|
||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@ -248,6 +249,37 @@ class TestBOM(unittest.TestCase):
|
||||
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||
self.assertEqual(reqd_item, created_item.item_code)
|
||||
|
||||
def test_generated_variant_bom(self):
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
|
||||
template_item = make_item(
|
||||
"_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
|
||||
)
|
||||
variant = create_variant(template_item.item_code, {"Test Size": "Large"})
|
||||
variant.insert(ignore_if_duplicate=True)
|
||||
|
||||
bom_tree = {
|
||||
template_item.item_code: {
|
||||
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
|
||||
"ChildPart5": {},
|
||||
}
|
||||
}
|
||||
template_bom = create_nested_bom(bom_tree, prefix="")
|
||||
variant_bom = make_variant_bom(
|
||||
template_bom.name, template_bom.name, variant.item_code, variant_items=[]
|
||||
)
|
||||
variant_bom.save()
|
||||
|
||||
reqd_order = template_bom.get_tree_representation().level_order_traversal()
|
||||
created_order = variant_bom.get_tree_representation().level_order_traversal()
|
||||
|
||||
self.assertEqual(len(reqd_order), len(created_order))
|
||||
|
||||
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||
self.assertEqual(reqd_item.item_code, created_item.item_code)
|
||||
self.assertEqual(reqd_item.qty, created_item.qty)
|
||||
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', {
|
||||
return {
|
||||
filters: {
|
||||
company: row.company,
|
||||
account_type: "Tax",
|
||||
is_group: 0
|
||||
}
|
||||
};
|
||||
|
@ -834,8 +834,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
|
||||
|
@ -74,7 +74,7 @@
|
||||
"stock_received_but_not_billed",
|
||||
"service_received_but_not_billed",
|
||||
"expenses_included_in_valuation",
|
||||
"fixed_asset_depreciation_settings",
|
||||
"fixed_asset_defaults",
|
||||
"accumulated_depreciation_account",
|
||||
"depreciation_expense_account",
|
||||
"series_for_depreciation_entry",
|
||||
@ -83,6 +83,7 @@
|
||||
"disposal_account",
|
||||
"depreciation_cost_center",
|
||||
"capital_work_in_progress_account",
|
||||
"repair_and_maintenance_account",
|
||||
"asset_received_but_not_billed",
|
||||
"budget_detail",
|
||||
"exception_budget_approver_role",
|
||||
@ -519,12 +520,6 @@
|
||||
"no_copy": 1,
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "fixed_asset_depreciation_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fixed Asset Depreciation Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "accumulated_depreciation_account",
|
||||
"fieldtype": "Link",
|
||||
@ -734,6 +729,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Payment Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "fixed_asset_defaults",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fixed Asset Defaults"
|
||||
},
|
||||
{
|
||||
"fieldname": "repair_and_maintenance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Repair and Maintenance Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
@ -741,7 +748,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-07 03:11:28.189740",
|
||||
"modified": "2021-05-12 16:51:08.187233",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
@ -11,10 +11,11 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "settings",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "ERPNext Settings",
|
||||
"links": [],
|
||||
"modified": "2020-12-01 13:38:37.759596",
|
||||
"modified": "2021-06-12 01:58:11.399566",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "ERPNext Settings",
|
||||
@ -109,6 +110,13 @@
|
||||
"label": "Domain Settings",
|
||||
"link_to": "Domain Settings",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"doc_view": "",
|
||||
"icon": "retail",
|
||||
"label": "Products Settings",
|
||||
"link_to": "Products Settings",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -291,7 +291,7 @@ class PurchaseReceipt(BuyingController):
|
||||
continue
|
||||
|
||||
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
|
||||
stock_rbnb, account_currency=warehouse_account_currency, item=d)
|
||||
stock_rbnb, account_currency=warehouse_account_currency, item=d)
|
||||
|
||||
# GL Entry for from warehouse or Stock Received but not billed
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
@ -318,11 +318,11 @@ class PurchaseReceipt(BuyingController):
|
||||
(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
|
||||
|
||||
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
|
||||
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
|
||||
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
|
||||
account_currency=credit_currency, item=d)
|
||||
|
||||
self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
|
||||
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
|
||||
self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
|
||||
remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
|
||||
account_currency=credit_currency, item=d)
|
||||
|
||||
# Amount added through landed-cos-voucher
|
||||
@ -407,6 +407,7 @@ class PurchaseReceipt(BuyingController):
|
||||
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
|
||||
total_valuation_amount = sum(valuation_tax.values())
|
||||
amount_including_divisional_loss = negative_expense_to_be_booked
|
||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||
i = 1
|
||||
for tax in self.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
|
@ -1054,30 +1054,30 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
|
||||
def test_purchase_receipt_with_exchange_rate_difference(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
|
||||
|
||||
pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
|
||||
|
||||
create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
|
||||
properties={"account": '_Test Account Stock In Hand - TCP1'})
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
|
||||
|
||||
pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
|
||||
company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
|
||||
do_not_save = "True")
|
||||
|
||||
pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
|
||||
cost_center = "Main - TCP1",
|
||||
warehouse = "Stores - TCP1",
|
||||
expense_account ="_Test Account Cost for Goods Sold - TCP1",
|
||||
currency = "USD", conversion_rate = 70)
|
||||
|
||||
pr = create_purchase_receipt(pi.name)
|
||||
pr.conversion_rate = 80
|
||||
pr.items[0].purchase_invoice = pi.name
|
||||
pr.items[0].purchase_invoice_item = pi.items[0].name
|
||||
|
||||
pr.insert()
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
# fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
|
||||
gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
|
||||
voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
|
||||
self.assertEqual(pr.name, voucher_no)
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
|
||||
|
||||
exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
|
||||
self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def get_sl_entries(voucher_type, voucher_no):
|
||||
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
|
||||
|
@ -54,7 +54,7 @@ class TestStockLedgerEntry(unittest.TestCase):
|
||||
)
|
||||
|
||||
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
|
||||
make_stock_entry(
|
||||
se = make_stock_entry(
|
||||
item_code="_Test Item for Reposting",
|
||||
source="Stores - _TC",
|
||||
target="Finished Goods - _TC",
|
||||
@ -64,29 +64,29 @@ class TestStockLedgerEntry(unittest.TestCase):
|
||||
posting_date='2020-04-30',
|
||||
posting_time='14:00'
|
||||
)
|
||||
target_wh_sle = get_previous_sle({
|
||||
target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
|
||||
"item_code": "_Test Item for Reposting",
|
||||
"warehouse": "Finished Goods - _TC",
|
||||
"posting_date": '2020-04-30',
|
||||
"posting_time": '14:00'
|
||||
})
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se.name
|
||||
}, ["valuation_rate"], as_dict=1)
|
||||
|
||||
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
|
||||
|
||||
# Repack entry on 5-5-2020
|
||||
repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
|
||||
|
||||
finished_item_sle = get_previous_sle({
|
||||
finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
|
||||
"item_code": "_Test Finished Item for Reposting",
|
||||
"warehouse": "Finished Goods - _TC",
|
||||
"posting_date": '2020-05-05',
|
||||
"posting_time": '14:00'
|
||||
})
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": repack.name
|
||||
}, ["incoming_rate", "valuation_rate"], as_dict=1)
|
||||
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
|
||||
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
|
||||
|
||||
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
|
||||
create_stock_reconciliation(
|
||||
sr = create_stock_reconciliation(
|
||||
item_code="_Test Item for Reposting",
|
||||
warehouse="Stores - _TC",
|
||||
qty=50,
|
||||
@ -109,12 +109,12 @@ class TestStockLedgerEntry(unittest.TestCase):
|
||||
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
|
||||
|
||||
# Check valuation rate of repacked item after back-dated entry at Stores
|
||||
finished_item_sle = get_previous_sle({
|
||||
finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
|
||||
"item_code": "_Test Finished Item for Reposting",
|
||||
"warehouse": "Finished Goods - _TC",
|
||||
"posting_date": '2020-05-05',
|
||||
"posting_time": '14:00'
|
||||
})
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": repack.name
|
||||
}, ["incoming_rate", "valuation_rate"], as_dict=1)
|
||||
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
|
||||
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
|
||||
|
||||
|
@ -357,6 +357,7 @@ class StockReconciliation(StockController):
|
||||
if row.current_qty:
|
||||
data.actual_qty = -1 * row.current_qty
|
||||
data.qty_after_transaction = flt(row.current_qty)
|
||||
data.previous_qty_after_transaction = flt(row.qty)
|
||||
data.valuation_rate = flt(row.current_valuation_rate)
|
||||
data.stock_value = data.qty_after_transaction * data.valuation_rate
|
||||
data.stock_value_difference = -1 * flt(row.amount_difference)
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, unittest
|
||||
from frappe.utils import flt, nowdate, nowtime
|
||||
from frappe.utils import flt, nowdate, nowtime, add_days
|
||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
|
||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
|
||||
@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
class TestStockReconciliation(unittest.TestCase):
|
||||
@classmethod
|
||||
@ -204,6 +205,117 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
|
||||
self.assertEqual(sr.get("items")[0].amount, 0)
|
||||
|
||||
def test_backdated_stock_reco_qty_reposting(self):
|
||||
"""
|
||||
Test if a backdated stock reco recalculates future qty until next reco.
|
||||
-------------------------------------------
|
||||
Var | Doc | Qty | Balance
|
||||
-------------------------------------------
|
||||
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
|
||||
PR1 | PR | 10 | 18 (posting date: today-3)
|
||||
PR2 | PR | 1 | 19 (posting date: today-2)
|
||||
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
|
||||
PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
|
||||
"""
|
||||
item_code = "Backdated-Reco-Item"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
create_item(item_code)
|
||||
|
||||
pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
|
||||
posting_date=add_days(nowdate(), -3))
|
||||
pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
|
||||
posting_date=add_days(nowdate(), -2))
|
||||
pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
|
||||
posting_date=nowdate())
|
||||
|
||||
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
self.assertEqual(pr1_balance, 10)
|
||||
self.assertEqual(pr3_balance, 12)
|
||||
|
||||
# post backdated stock reco in between
|
||||
sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100,
|
||||
posting_date=add_days(nowdate(), -1))
|
||||
pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
self.assertEqual(pr3_balance, 7)
|
||||
|
||||
# post backdated stock reco at the start
|
||||
sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100,
|
||||
posting_date=add_days(nowdate(), -4))
|
||||
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
self.assertEqual(pr1_balance, 18)
|
||||
self.assertEqual(pr2_balance, 19)
|
||||
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
|
||||
|
||||
# cancel backdated stock reco and check future impact
|
||||
sr5.cancel()
|
||||
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
self.assertEqual(pr1_balance, 10)
|
||||
self.assertEqual(pr2_balance, 11)
|
||||
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
|
||||
|
||||
# teardown
|
||||
sr4.cancel()
|
||||
pr3.cancel()
|
||||
pr2.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def test_backdated_stock_reco_future_negative_stock(self):
|
||||
"""
|
||||
Test if a backdated stock reco causes future negative stock and is blocked.
|
||||
-------------------------------------------
|
||||
Var | Doc | Qty | Balance
|
||||
-------------------------------------------
|
||||
PR1 | PR | 10 | 10 (posting date: today-2)
|
||||
SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked]
|
||||
DN2 | DN | -2 | 8(-1) (posting date: today)
|
||||
"""
|
||||
from erpnext.stock.stock_ledger import NegativeStockError
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
item_code = "Backdated-Reco-Item"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
create_item(item_code)
|
||||
|
||||
negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
|
||||
|
||||
pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
|
||||
posting_date=add_days(nowdate(), -2))
|
||||
dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120,
|
||||
posting_date=nowdate())
|
||||
|
||||
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0},
|
||||
"qty_after_transaction")
|
||||
self.assertEqual(pr1_balance, 10)
|
||||
self.assertEqual(dn2_balance, 8)
|
||||
|
||||
# check if stock reco is blocked
|
||||
sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
|
||||
posting_date=add_days(nowdate(), -1), do_not_submit=True)
|
||||
self.assertRaises(NegativeStockError, sr3.submit)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting)
|
||||
sr3.cancel()
|
||||
dn2.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def insert_existing_sle(warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
args = sle_doc.as_dict()
|
||||
|
||||
if sle.get("voucher_type") == "Stock Reconciliation":
|
||||
# preserve previous_qty_after_transaction for qty reposting
|
||||
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
|
||||
|
||||
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
def get_args_for_future_sle(row):
|
||||
@ -215,7 +220,7 @@ class update_entries_after(object):
|
||||
"""
|
||||
self.data.setdefault(args.warehouse, frappe._dict())
|
||||
warehouse_dict = self.data[args.warehouse]
|
||||
previous_sle = self.get_previous_sle_of_current_voucher(args)
|
||||
previous_sle = get_previous_sle_of_current_voucher(args)
|
||||
warehouse_dict.previous_sle = previous_sle
|
||||
|
||||
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
|
||||
@ -227,29 +232,6 @@ class update_entries_after(object):
|
||||
"stock_value_difference": 0.0
|
||||
})
|
||||
|
||||
def get_previous_sle_of_current_voucher(self, args):
|
||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||
|
||||
args['time_format'] = '%H:%i:%s'
|
||||
if not args.get("posting_date"):
|
||||
args["posting_date"] = "1900-01-01"
|
||||
if not args.get("posting_time"):
|
||||
args["posting_time"] = "00:00"
|
||||
|
||||
sle = frappe.db.sql("""
|
||||
select *, timestamp(posting_date, posting_time) as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||
order by timestamp(posting_date, posting_time) desc, creation desc
|
||||
limit 1
|
||||
for update""", args, as_dict=1)
|
||||
|
||||
return sle[0] if sle else frappe._dict()
|
||||
|
||||
|
||||
def build(self):
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
|
||||
@ -734,6 +716,35 @@ class update_entries_after(object):
|
||||
bin_doc.flags.via_stock_ledger_entry = True
|
||||
bin_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
|
||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||
|
||||
args['time_format'] = '%H:%i:%s'
|
||||
if not args.get("posting_date"):
|
||||
args["posting_date"] = "1900-01-01"
|
||||
if not args.get("posting_time"):
|
||||
args["posting_time"] = "00:00"
|
||||
|
||||
voucher_condition = ""
|
||||
if exclude_current_voucher:
|
||||
voucher_no = args.get("voucher_no")
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
sle = frappe.db.sql("""
|
||||
select *, timestamp(posting_date, posting_time) as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
{voucher_condition}
|
||||
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
|
||||
order by timestamp(posting_date, posting_time) desc, creation desc
|
||||
limit 1
|
||||
for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
|
||||
|
||||
return sle[0] if sle else frappe._dict()
|
||||
|
||||
def get_previous_sle(args, for_update=False):
|
||||
"""
|
||||
get the last sle on or before the current time-bucket,
|
||||
@ -862,9 +873,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
|
||||
return valuation_rate
|
||||
|
||||
def update_qty_in_future_sle(args, allow_negative_stock=None):
|
||||
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
|
||||
datetime_limit_condition = ""
|
||||
qty_shift = args.actual_qty
|
||||
|
||||
# find difference/shift in qty caused by stock reconciliation
|
||||
if args.voucher_type == "Stock Reconciliation":
|
||||
qty_shift = get_stock_reco_qty_shift(args)
|
||||
|
||||
# find the next nearest stock reco so that we only recalculate SLEs till that point
|
||||
next_stock_reco_detail = get_next_stock_reco(args)
|
||||
if next_stock_reco_detail:
|
||||
detail = next_stock_reco_detail[0]
|
||||
# add condition to update SLEs before this date & time
|
||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||
|
||||
frappe.db.sql("""
|
||||
update `tabStock Ledger Entry`
|
||||
set qty_after_transaction = qty_after_transaction + {qty}
|
||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
@ -876,15 +902,70 @@ def update_qty_in_future_sle(args, allow_negative_stock=None):
|
||||
and creation > %(creation)s
|
||||
)
|
||||
)
|
||||
""".format(qty=args.actual_qty), args)
|
||||
{datetime_limit_condition}
|
||||
""".format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
|
||||
|
||||
validate_negative_qty_in_future_sle(args, allow_negative_stock)
|
||||
|
||||
def get_stock_reco_qty_shift(args):
|
||||
stock_reco_qty_shift = 0
|
||||
if args.get("is_cancelled"):
|
||||
if args.get("previous_qty_after_transaction"):
|
||||
# get qty (balance) that was set at submission
|
||||
last_balance = args.get("previous_qty_after_transaction")
|
||||
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
||||
else:
|
||||
stock_reco_qty_shift = flt(args.actual_qty)
|
||||
else:
|
||||
# reco is being submitted
|
||||
last_balance = get_previous_sle_of_current_voucher(args,
|
||||
exclude_current_voucher=True).get("qty_after_transaction")
|
||||
|
||||
if last_balance is not None:
|
||||
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
||||
else:
|
||||
stock_reco_qty_shift = args.qty_after_transaction
|
||||
|
||||
return stock_reco_qty_shift
|
||||
|
||||
def get_next_stock_reco(args):
|
||||
"""Returns next nearest stock reconciliaton's details."""
|
||||
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
name, posting_date, posting_time, creation, voucher_no
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and voucher_type = 'Stock Reconciliation'
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
|
||||
or (
|
||||
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and creation > %(creation)s
|
||||
)
|
||||
)
|
||||
limit 1
|
||||
""", args, as_dict=1)
|
||||
|
||||
def get_datetime_limit_condition(detail):
|
||||
return f"""
|
||||
and
|
||||
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
|
||||
or (
|
||||
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
|
||||
and creation < '{detail.creation}'
|
||||
)
|
||||
)"""
|
||||
|
||||
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
|
||||
allow_negative_stock = allow_negative_stock \
|
||||
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||
|
||||
if args.actual_qty < 0 and not allow_negative_stock:
|
||||
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
|
||||
sle = get_future_sle_with_negative_qty(args)
|
||||
if sle:
|
||||
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
|
||||
|
@ -81,10 +81,9 @@ class TestServiceLevelAgreement(unittest.TestCase):
|
||||
|
||||
# check SLA custom fields created for leads
|
||||
sla_fields = get_service_level_agreement_fields()
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
|
||||
for field in sla_fields:
|
||||
self.assertTrue(meta.has_field(field.get("fieldname")))
|
||||
self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}))
|
||||
|
||||
def test_docfield_creation_for_sla_on_custom_dt(self):
|
||||
doctype = create_custom_doctype()
|
||||
@ -102,10 +101,9 @@ class TestServiceLevelAgreement(unittest.TestCase):
|
||||
|
||||
# check SLA docfields created
|
||||
sla_fields = get_service_level_agreement_fields()
|
||||
meta = frappe.get_meta(doctype.name, cached=False)
|
||||
|
||||
for field in sla_fields:
|
||||
self.assertTrue(meta.has_field(field.get("fieldname")))
|
||||
self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}))
|
||||
|
||||
def test_sla_application(self):
|
||||
# Default Service Level Agreement
|
||||
|
Loading…
Reference in New Issue
Block a user