diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 529723b390c..70ceeff4613 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '11.1.48' +__version__ = '11.1.49' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7edd1eb1bc6..41210f090ce 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -336,6 +336,7 @@ class PurchaseInvoice(BuyingController): if not self.is_return: self.update_against_document_in_jv() + self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt") self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_in_pr() @@ -776,6 +777,7 @@ class PurchaseInvoice(BuyingController): if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): unlink_ref_doc_from_payment_entries(self) + self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt") self.update_billing_status_for_zero_amount_refdoc("Purchase Order") self.update_billing_status_in_pr() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 99196b29847..ecb841ce9dd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -166,6 +166,7 @@ class SalesInvoice(SellingController): self.make_gl_entries() if not self.is_return: + self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.check_credit_limit() @@ -222,6 +223,7 @@ class SalesInvoice(SellingController): self.update_billing_status_in_dn() if not self.is_return: + self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_serial_no(in_cancel=True) @@ -407,7 +409,7 @@ class SalesInvoice(SellingController): for field in ['taxes_and_charges', 'company_address']: if pos.get(field): - self.set(field, pos.get(fieldname)) + self.set(field, pos.get(field)) if not customer_price_list: self.set('selling_price_list', pos.get('selling_price_list')) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 2e656c1bd0e..1923f78cf89 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -56,10 +56,17 @@ def get_columns(): "width": 90 }, { - "fieldname": "payment_entry", - "label": _("Payment Entry"), + "fieldname": "payment_document", + "label": _("Payment Document Type"), "fieldtype": "Link", - "options": "Payment Entry", + "options": "DocType", + "width": 220 + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", "width": 220 }, { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index aad9621e75f..b731f049653 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -121,7 +121,12 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company allow_cost_center_in_entry_of_bs_account = get_allow_cost_center_in_entry_of_bs_account() - if cost_center and (allow_cost_center_in_entry_of_bs_account or acc.report_type =='Profit and Loss'): + if account: + report_type = acc.report_type + else: + report_type = "" + + if cost_center and (allow_cost_center_in_entry_of_bs_account or report_type =='Profit and Loss'): cc = frappe.get_doc("Cost Center", cost_center) if cc.is_group: cond.append(""" exists ( @@ -138,7 +143,7 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company if not frappe.flags.ignore_account_permission: acc.check_permission("read") - if acc.report_type == 'Profit and Loss': + if report_type == 'Profit and Loss': # for pl accounts, get balance within a fiscal year cond.append("posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" \ % year_start_date) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 3b41006f2b3..2aa74829f62 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -303,14 +303,17 @@ frappe.ui.form.on('Asset', { }, set_depreciation_rate: function(frm, row) { - if (row.total_number_of_depreciations && row.frequency_of_depreciation) { + if (row.total_number_of_depreciations && row.frequency_of_depreciation + && row.expected_value_after_useful_life) { frappe.call({ method: "get_depreciation_rate", doc: frm.doc, args: row, callback: function(r) { if (r.message) { - frappe.model.set_value(row.doctype, row.name, "rate_of_depreciation", r.message); + frappe.flags.dont_change_rate = true; + frappe.model.set_value(row.doctype, row.name, + "rate_of_depreciation", flt(r.message, precision("rate_of_depreciation", row))); } } }); @@ -338,6 +341,14 @@ frappe.ui.form.on('Asset Finance Book', { total_number_of_depreciations: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); + }, + + rate_of_depreciation: function(frm, cdt, cdn) { + if(!frappe.flags.dont_change_rate) { + frappe.model.set_value(cdt, cdn, "expected_value_after_useful_life", 0); + } + + frappe.flags.dont_change_rate = false; } }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 45f7b30ae8a..45d2ec2c516 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, add_days from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -101,97 +101,88 @@ class Asset(AccountsController): def set_depreciation_rate(self): for d in self.get("finance_books"): - d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True) + d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), + d.precision("rate_of_depreciation")) def make_depreciation_schedule(self): - depreciation_method = [d.depreciation_method for d in self.finance_books] - - if 'Manual' not in depreciation_method: + if 'Manual' not in [d.depreciation_method for d in self.finance_books]: self.schedules = [] - if not self.get("schedules") and self.available_for_use_date: - total_depreciations = sum([d.total_number_of_depreciations for d in self.get('finance_books')]) + if self.get("schedules") or not self.available_for_use_date: + return - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + for d in self.get('finance_books'): + self.validate_asset_finance_books(d) - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + d.value_after_depreciation = value_after_depreciation - no_of_depreciations = cint(d.total_number_of_depreciations - 1) - cint(self.number_of_depreciations_booked) - end_date = add_months(d.depreciation_start_date, - no_of_depreciations * cint(d.frequency_of_depreciation)) + number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + cint(self.number_of_depreciations_booked) - total_days = date_diff(end_date, self.available_for_use_date) - rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days + has_pro_rata = self.check_is_pro_rata(d) - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ - cint(self.number_of_depreciations_booked) + if has_pro_rata: + number_of_pending_depreciations += 1 - from_date = self.available_for_use_date - if number_of_pending_depreciations: - next_depr_date = getdate(add_months(self.available_for_use_date, - number_of_pending_depreciations * 12)) - if (cint(frappe.db.get_value("Asset Settings", None, "schedule_based_on_fiscal_year")) == 1 - and getdate(d.depreciation_start_date) < next_depr_date): + skip_row = False + for n in range(number_of_pending_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: continue - number_of_pending_depreciations += 1 - for n in range(number_of_pending_depreciations): - if n == list(range(number_of_pending_depreciations))[-1]: - schedule_date = add_months(self.available_for_use_date, n * 12) - previous_scheduled_date = add_months(d.depreciation_start_date, (n-1) * 12) - depreciation_amount = \ - self.get_depreciation_amount_prorata_temporis(value_after_depreciation, - d, previous_scheduled_date, schedule_date) + depreciation_amount = self.get_depreciation_amount(value_after_depreciation, + d.total_number_of_depreciations, d) - elif n == list(range(number_of_pending_depreciations))[0]: - schedule_date = d.depreciation_start_date - depreciation_amount = \ - self.get_depreciation_amount_prorata_temporis(value_after_depreciation, - d, self.available_for_use_date, schedule_date) + if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + schedule_date = add_months(d.depreciation_start_date, + n * cint(d.frequency_of_depreciation)) - else: - schedule_date = add_months(d.depreciation_start_date, n * 12) - depreciation_amount = \ - self.get_depreciation_amount_prorata_temporis(value_after_depreciation, d) + # For first row + if has_pro_rata and n==0: + depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount, + self.available_for_use_date, d.depreciation_start_date) + # 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 value_after_depreciation != 0: - value_after_depreciation -= flt(depreciation_amount) + depreciation_amount, days = get_pro_rata_amt(d, + depreciation_amount, schedule_date, to_date) - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx - }) - else: - for n in range(number_of_pending_depreciations): - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_days(schedule_date, days) - if d.depreciation_method in ("Straight Line", "Manual"): - days = date_diff(schedule_date, from_date) - if n == 0: days += 1 + if not depreciation_amount: continue + value_after_depreciation -= flt(depreciation_amount, + self.precision("gross_purchase_amount")) - depreciation_amount = days * rate_per_day - from_date = schedule_date - else: - depreciation_amount = self.get_depreciation_amount(value_after_depreciation, - d.total_number_of_depreciations, d) + # Adjust depreciation amount in the last period based on the expected value after useful life + if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != d.expected_value_after_useful_life) + or value_after_depreciation < d.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + skip_row = True - if depreciation_amount: - value_after_depreciation -= flt(depreciation_amount) + if depreciation_amount > 0: + self.append("schedules", { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": d.depreciation_method, + "finance_book": d.finance_book, + "finance_book_id": d.idx + }) - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx - }) + def check_is_pro_rata(self, row): + has_pro_rata = False + + days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) + + if days < total_days: + has_pro_rata = True + + return has_pro_rata def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): @@ -261,31 +252,14 @@ class Asset(AccountsController): return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row): - if row.depreciation_method in ["Straight Line", "Manual"]: - amt = (flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - - flt(self.opening_accumulated_depreciation)) - - depreciation_amount = amt * row.rate_of_depreciation - else: - depreciation_amount = flt(depreciable_value) * (flt(row.rate_of_depreciation) / 100) - value_after_depreciation = flt(depreciable_value) - depreciation_amount - if value_after_depreciation < flt(row.expected_value_after_useful_life): - depreciation_amount = flt(depreciable_value) - flt(row.expected_value_after_useful_life) - - return depreciation_amount - - def get_depreciation_amount_prorata_temporis(self, depreciable_value, row, start_date=None, end_date=None): - if start_date and end_date: - prorata_temporis = min(abs(flt(date_diff(str(end_date), str(start_date)))) / flt(frappe.db.get_value("Asset Settings", None, "number_of_days_in_fiscal_year")), 1) - else: - prorata_temporis = 1 + precision = self.precision("gross_purchase_amount") if row.depreciation_method in ("Straight Line", "Manual"): depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (cint(row.total_number_of_depreciations) - - cint(self.number_of_depreciations_booked)) * prorata_temporis + cint(self.number_of_depreciations_booked)) else: - depreciation_amount = self.get_depreciation_amount(depreciable_value, row.total_number_of_depreciations, row) + depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision) return depreciation_amount @@ -301,9 +275,12 @@ class Asset(AccountsController): flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) - if row.expected_value_after_useful_life < asset_value_after_full_schedule: + if (row.expected_value_after_useful_life and + row.expected_value_after_useful_life < asset_value_after_full_schedule): frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") .format(row.idx, asset_value_after_full_schedule)) + elif not row.expected_value_after_useful_life: + row.expected_value_after_useful_life = asset_value_after_full_schedule def validate_cancellation(self): if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): @@ -388,7 +365,8 @@ class Asset(AccountsController): "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "posting_date": self.available_for_use_date, "credit": self.purchase_receipt_amount, - "credit_in_account_currency": self.purchase_receipt_amount + "credit_in_account_currency": self.purchase_receipt_amount, + "cost_center": self.cost_center })) gl_entries.append(self.get_gl_dict({ @@ -397,7 +375,8 @@ class Asset(AccountsController): "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "posting_date": self.available_for_use_date, "debit": self.purchase_receipt_amount, - "debit_in_account_currency": self.purchase_receipt_amount + "debit_in_account_currency": self.purchase_receipt_amount, + "cost_center": self.cost_center })) if gl_entries: @@ -410,15 +389,7 @@ class Asset(AccountsController): if isinstance(args, string_types): args = json.loads(args) - number_of_depreciations_booked = 0 - if self.is_existing_asset: - number_of_depreciations_booked = self.number_of_depreciations_booked - float_precision = cint(frappe.db.get_default("float_precision")) or 2 - tot_no_of_depreciation = flt(args.get("total_number_of_depreciations")) - flt(number_of_depreciations_booked) - - if args.get("depreciation_method") in ["Straight Line", "Manual"]: - return 1.0 / tot_no_of_depreciation if args.get("depreciation_method") == 'Double Declining Balance': return 200.0 / args.get("total_number_of_depreciations") @@ -598,3 +569,15 @@ def make_journal_entry(asset_name): def is_cwip_accounting_disabled(): return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")) + +def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): + days = date_diff(to_date, from_date) + total_days = get_total_days(to_date, row.frequency_of_depreciation) + + return (depreciation_amount * flt(days)) / flt(total_days), days + +def get_total_days(date, frequency): + period_start_date = add_months(date, + cint(frequency) * -1) + + return date_diff(date, period_start_date) \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fceccfbd1c9..481ee7d9f4e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -88,23 +88,23 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2020-06-06' - asset.purchase_date = '2020-06-06' + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" }) asset.save() + self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2020-06-06", 147.54, 147.54], - ["2021-04-06", 44852.46, 45000.0], - ["2022-02-06", 45000.0, 90000.00] + ["2030-12-31", 30000.00, 30000.00], + ["2031-12-31", 30000.00, 60000.00], + ["2032-12-31", 30000.00, 90000.00] ] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] @@ -118,20 +118,21 @@ class TestAsset(unittest.TestCase): asset.calculate_depreciation = 1 asset.number_of_depreciations_booked = 1 asset.opening_accumulated_depreciation = 40000 + asset.available_for_use_date = "2030-06-06" asset.append("finance_books", { "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" }) asset.insert() self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ - ["2020-06-06", 164.47, 40164.47], - ["2021-04-06", 49835.53, 90000.00] + ["2030-12-31", 14246.58, 54246.58], + ["2031-12-31", 25000.00, 79246.58], + ["2032-06-06", 10753.42, 90000.00] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] @@ -145,24 +146,23 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2020-06-06' - asset.purchase_date = '2020-06-06' + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", "depreciation_method": "Double Declining Balance", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 12, + "depreciation_start_date": '2030-12-31' }) asset.insert() self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ - ["2020-06-06", 66666.67, 66666.67], - ["2021-04-06", 22222.22, 88888.89], - ["2022-02-06", 1111.11, 90000.0] + ['2030-12-31', 66667.00, 66667.00], + ['2031-12-31', 22222.11, 88889.11], + ['2032-12-31', 1110.89, 90000.0] ] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] @@ -177,23 +177,21 @@ class TestAsset(unittest.TestCase): asset.is_existing_asset = 1 asset.number_of_depreciations_booked = 1 asset.opening_accumulated_depreciation = 50000 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2029-11-30' asset.append("finance_books", { "expected_value_after_useful_life": 10000, - "next_depreciation_date": "2020-12-31", "depreciation_method": "Double Declining Balance", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" }) asset.insert() self.assertEqual(asset.status, "Draft") - asset.save() - - asset.save() expected_schedules = [ - ["2020-06-06", 33333.33, 83333.33], - ["2021-04-06", 6666.67, 90000.0] + ["2030-12-31", 33333.50, 83333.50], + ["2031-12-31", 6666.50, 90000.0] ] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] @@ -209,25 +207,25 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.purchase_date = '2020-01-30' + asset.purchase_date = '2030-01-30' asset.is_existing_asset = 0 - asset.available_for_use_date = "2020-01-30" + asset.available_for_use_date = "2030-01-30" asset.append("finance_books", { "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-12-31" + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" }) asset.insert() asset.save() expected_schedules = [ - ["2020-12-31", 28000.0, 28000.0], - ["2021-12-31", 30000.0, 58000.0], - ["2022-12-31", 30000.0, 88000.0], - ["2023-01-30", 2000.0, 90000.0] + ["2030-12-31", 27534.25, 27534.25], + ["2031-12-31", 30000.0, 57534.25], + ["2032-12-31", 30000.0, 87534.25], + ["2033-01-30", 2465.75, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -266,8 +264,8 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 32129.24), - ("_Test Depreciations - _TC", 32129.24, 0.0) + ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), + ("_Test Depreciations - _TC", 30000.0, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` @@ -277,15 +275,15 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) self.assertEqual(asset.get("value_after_depreciation"), 0) - def test_depreciation_entry_for_wdv(self): + def test_depreciation_entry_for_wdv_without_pro_rata(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=8000.0, location="Test Location") asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2030-06-06' - asset.purchase_date = '2030-06-06' + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 1000, "depreciation_method": "Written Down Value", @@ -298,9 +296,41 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 4000.0, 4000.0], - ["2031-12-31", 2000.0, 6000.0], - ["2032-12-31", 1000.0, 7000.0], + ["2030-12-31", 4000.00, 4000.00], + ["2031-12-31", 2000.00, 6000.00], + ["2032-12-31", 1000.00, 7000.0], + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_pro_rata_depreciation_entry_for_wdv(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=8000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-06-06' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 1000, + "depreciation_method": "Written Down Value", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save(ignore_permissions=True) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 2279.45, 2279.45], + ["2031-12-31", 2860.28, 5139.73], + ["2032-12-31", 1430.14, 6569.87], + ["2033-06-06", 430.13, 7000.0], ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -346,18 +376,19 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = '2020-06-06' - asset.purchase_date = '2020-06-06' + asset.available_for_use_date = nowdate() + asset.purchase_date = nowdate() asset.append("finance_books", { "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "depreciation_start_date": nowdate() }) asset.insert() asset.submit() - post_depreciation_entries(date="2021-01-01") + + post_depreciation_entries(date=add_months(nowdate(), 10)) scrap_asset(asset.name) @@ -366,9 +397,9 @@ class TestAsset(unittest.TestCase): self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 147.54, 0.0), + ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 99852.46, 0.0) + ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` @@ -412,9 +443,9 @@ class TestAsset(unittest.TestCase): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 23051.47, 0.0), + ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 51948.53, 0.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), ("Debtors - _TC", 25000.0, 0.0) ) diff --git a/erpnext/assets/doctype/asset_settings/asset_settings.json b/erpnext/assets/doctype/asset_settings/asset_settings.json index a3fee96f4ee..edc5ce169ca 100644 --- a/erpnext/assets/doctype/asset_settings/asset_settings.json +++ b/erpnext/assets/doctype/asset_settings/asset_settings.json @@ -46,75 +46,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "schedule_based_on_fiscal_year", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Calculate Prorated Depreciation Schedule Based on Fiscal Year", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "360", - "depends_on": "eval:doc.schedule_based_on_fiscal_year", - "description": "This value is used for pro-rata temporis calculation", - "fetch_if_empty": 0, - "fieldname": "number_of_days_in_fiscal_year", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Number of Days in Fiscal Year", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -159,7 +90,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2019-03-08 10:44:41.924547", + "modified": "2019-05-26 18:31:19.930563", "modified_by": "Administrator", "module": "Assets", "name": "Asset Settings", diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a3018029bc0..08c40c63628 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -38,7 +38,6 @@ status_map = { ["To Bill", "eval:self.per_delivered == 100 and self.per_billed < 100 and self.docstatus == 1"], ["To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_delivered == 100 and self.per_billed == 100 and self.docstatus == 1"], - ["Completed", "eval:self.order_type == 'Maintenance' and self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], ["Closed", "eval:self.status=='Closed'"], ], @@ -94,7 +93,8 @@ status_map = { ["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"], ["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"], ["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"], - ["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"] + ["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"], + ["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"] ], "Bank Transaction": [ ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 97eb0e752af..3cc884135f9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -516,10 +516,14 @@ class BOM(WebsiteGenerator): return erpnext.get_company_currency(self.company) def add_to_cur_exploded_items(self, args): - if self.cur_exploded_items.get(args.item_code): - self.cur_exploded_items[args.item_code]["stock_qty"] += args.stock_qty + key = (args.item_code) + if args.operation: + key = (args.item_code, args.operation) + + if key in self.cur_exploded_items: + self.cur_exploded_items[key]["stock_qty"] += args.stock_qty else: - self.cur_exploded_items[args.item_code] = args + self.cur_exploded_items[key] = args def get_child_exploded_items(self, bom_no, stock_qty): """ Add all items from Flat BOM of child BOM""" @@ -609,7 +613,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite and bom.name = %(bom)s and item.is_stock_item in (1, {is_stock_item}) {where_conditions} - group by item_code, stock_uom + group by item_code, stock_uom {groupby_columns} order by idx""" is_stock_item = 0 if include_non_stock_items else 1 @@ -619,23 +623,29 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite is_stock_item=is_stock_item, qty_field="stock_qty", select_columns = """, bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, - (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""") + (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""", + groupby_columns = """, bom_item.operation""") items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) elif fetch_scrap_items: - query = query.format(table="BOM Scrap Item", where_conditions="", select_columns=", bom_item.idx", is_stock_item=is_stock_item, qty_field="stock_qty") + query = query.format(table="BOM Scrap Item", where_conditions="", select_columns=", bom_item.idx", is_stock_item=is_stock_item, qty_field="stock_qty", groupby_columns="") items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", - select_columns = ", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing") + select_columns = ", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing", + groupby_columns = """, bom_item.operation""") items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) for item in items: - if item.item_code in item_dict: - item_dict[item.item_code]["qty"] += flt(item.qty) + key = (item.item_code) + if item.operation: + key = (item.item_code, item.operation) + + if key in item_dict: + item_dict[key]["qty"] += flt(item.qty) else: - item_dict[item.item_code] = item + item_dict[key] = item for item, item_details in item_dict.items(): for d in [["Account", "expense_account", "default_expense_account"], diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fc2a806ae01..b009ca6eab0 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -605,3 +605,4 @@ erpnext.patches.v11_1.renamed_delayed_item_report erpnext.patches.v11_1.set_missing_opportunity_from erpnext.patches.v11_1.set_quotation_status erpnext.patches.v11_1.update_default_supplier_in_item_defaults +erpnext.patches.v11_1.set_status_for_material_request_type_manufacture diff --git a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py new file mode 100644 index 00000000000..d41cff523d5 --- /dev/null +++ b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.db.sql(""" + update `tabMaterial Request` + set status='Manufactured' + where docstatus=1 and material_request_type='Manufacture' and per_ordered=100 and status != 'Stopped' + """) \ No newline at end of file diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 670b4e98bf3..c9ab680eb8c 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -115,6 +115,11 @@ def get_data(): {"sales_order_item": ("!=",""), "docstatus": 1}, ["parent", "qty", "sales_order", "item_code"]) + packed_items = get_packed_items([row.name for row in sales_order_entry]) + + item_with_product_bundle = get_item_with_product_bundle([row.item_code for row in sales_order_entry]) + item_with_product_bundle = [row.new_item_code for row in item_with_product_bundle] + materials_request_dict = {} for record in mr_records: @@ -139,19 +144,57 @@ def get_data(): # check for pending sales order if cint(so.net_qty) > cint(materials_request.get('qty')): - so_record = { - "item_code": so.item_code, - "item_name": so.item_name, - "description": so.description, - "sales_order_no": so.name, - "date": so.transaction_date, - "material_request": ','.join(materials_request.get('material_requests', [])), - "customer": so.customer, - "territory": so.territory, - "so_qty": so.net_qty, - "requested_qty": cint(materials_request.get('qty')), - "pending_qty": so.net_qty - cint(materials_request.get('qty')), - "company": so.company - } - pending_so.append(so_record) - return pending_so \ No newline at end of file + + if so.item_code not in item_with_product_bundle: + so_record = { + "item_code": so.item_code, + "item_name": so.item_name, + "description": so.description, + "sales_order_no": so.name, + "date": so.transaction_date, + "material_request": ','.join(materials_request.get('material_requests', [])), + "customer": so.customer, + "territory": so.territory, + "so_qty": so.net_qty, + "requested_qty": cint(materials_request.get('qty')), + "pending_qty": so.net_qty - cint(materials_request.get('qty')), + "company": so.company + } + pending_so.append(so_record) + else: + for item in packed_items: + material_request_qty = materials_request.get('qty') if materials_request.get('qty') else 0 + so_record = { + "item_code": item.item_code, + "item_name": item.item_name, + "description": item.description, + "sales_order_no": so.name, + "date": so.transaction_date, + "material_request": ','.join(materials_request.get('material_requests', [])), + "customer": so.customer, + "territory": so.territory, + "so_qty": item.qty, + "requested_qty": cint(material_request_qty * item.qty), + "pending_qty": (so.net_qty - cint(material_request_qty)) * item.qty, + "company": so.company + } + pending_so.append(so_record) + + + return pending_so + +def get_item_with_product_bundle(item_list): + + bundled_item = frappe.get_all("Product Bundle", filters = [ + ("new_item_code", "IN", item_list) + ], fields = ["new_item_code"]) + + return bundled_item + +def get_packed_items(sales_order_list): + + packed_items = frappe.get_all("Packed Item", filters = [ + ("parent", "IN", sales_order_list) + ], fields = ["*"]) + + return packed_items diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c5697eda7a8..f33c525ce88 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -189,6 +189,9 @@ class Item(WebsiteGenerator): 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5)) def validate_website_image(self): + if frappe.flags.in_import: + return + """Validate if the website image is a public file""" auto_set_website_image = False if not self.website_image and self.image: @@ -208,8 +211,7 @@ class Item(WebsiteGenerator): if not file_doc: if not auto_set_website_image: - frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found") - .format(self.website_image, self.name)) + frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name)) self.website_image = None @@ -220,6 +222,9 @@ class Item(WebsiteGenerator): self.website_image = None def make_thumbnail(self): + if frappe.flags.in_import: + return + """Make a thumbnail of `website_image`""" import requests.exceptions diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 6611a2053dd..5332da0c271 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -14,6 +14,8 @@ frappe.listview_settings['Material Request'] = { return [__("Transfered"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Material Issue") { return [__("Issued"), "green", "per_ordered,=,100"]; + } else if (doc.material_request_type == "Manufacture") { + return [__("Manufactured"), "green", "per_ordered,=,100"]; } } }