diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 3afa0ce7dfe..7e270601fb5 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -375,12 +375,13 @@ def make_gl_entries(doc, credit_account, debit_account, against, frappe.db.commit() except Exception as e: if frappe.flags.in_test: + traceback = frappe.get_traceback() + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) raise e else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) - + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True def send_mail(deferred_process): @@ -447,10 +448,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, if submit: journal_entry.submit() + + frappe.db.commit() except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 745191712b2..48392074102 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from frappe.model.document import Document from frappe.utils import cint +from erpnext.stock.utils import check_pending_reposting + class AccountsSettings(Document): def on_update(self): @@ -25,6 +27,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() self.toggle_discount_accounting_fields() + self.validate_pending_reposts() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -56,3 +59,8 @@ class AccountsSettings(Document): make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + + + def validate_pending_reposts(self): + if self.acc_frozen_upto: + check_pending_reposting(self.acc_frozen_upto) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f5c08a7b1fa..f184b95a92d 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -57,7 +57,8 @@ class GLEntry(Document): # Update outstanding amt on against voucher if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] - and self.against_voucher and self.flags.update_outstanding == 'Yes'): + and self.against_voucher and self.flags.update_outstanding == 'Yes' + and not frappe.flags.is_reverse_depr_entry): update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index b6c4425fd6d..3aed3c89d53 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -57,7 +57,10 @@ class JournalEntry(AccountsController): if not frappe.flags.in_import: self.validate_total_debit_and_credit() - self.validate_against_jv() + if not frappe.flags.is_reverse_depr_entry: + self.validate_against_jv() + self.validate_stock_accounts() + self.validate_reference_doc() if self.docstatus == 0: self.set_against_account() @@ -68,7 +71,6 @@ class JournalEntry(AccountsController): self.validate_empty_accounts_table() self.set_account_and_party_balance() self.validate_inter_company_accounts() - self.validate_stock_accounts() if self.docstatus == 0: self.apply_tax_withholding() diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index bff85872781..0c6e7edeb02 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -171,6 +171,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -1561,16 +1562,23 @@ "label": "Coupon Code", "options": "Coupon Code", "print_hide": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-27 20:12:57.306772", + "modified": "2021-10-05 12:11:53.871828", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 8b71eb02fd7..3f85668eded 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -46,6 +46,7 @@ "base_amount", "pricing_rules", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -800,14 +801,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-04 17:34:49.924531", + "modified": "2021-10-05 12:23:47.506290", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index efdeb1a5e81..813d20dbf93 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -3,22 +3,20 @@ {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.ui.form.on("POS Profile", "onload", function(frm) { - frm.set_query("selling_price_list", function() { - return { filters: { selling: 1 } }; - }); - - frm.set_query("tc_name", function() { - return { filters: { selling: 1 } }; - }); - - erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); - }); -}); - frappe.ui.form.on('POS Profile', { setup: function(frm) { + frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + + frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + + erpnext.queries.setup_queries(frm, "Warehouse", function() { + return erpnext.queries.warehouse(frm.doc); + }); + frm.set_query("print_format", function() { return { filters: [ @@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("account_for_change_amount", function() { + frm.set_query("account_for_change_amount", function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + is_group: 0, + company: doc.company } }; }); @@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', { }); frm.set_query('company_address', function(doc) { - if(!doc.company) { + if (!doc.company) { frappe.throw(__('Please set Company')); } @@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', { }; }); + frm.set_query('income_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'is_group': 0, + 'company': doc.company, + 'account_type': "Income Account" + } + }; + }); + + frm.set_query('cost_center', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + 'company': doc.company, + 'is_group': 0 + } + }; + }); + + frm.set_query('expense_account', function(doc) { + if (!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + filters: { + "report_type": "Profit and Loss", + "company": doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("select_print_heading", function() { + return { + filters: [ + ['Print Heading', 'docstatus', '!=', 2] + ] + }; + }); + + frm.set_query("write_off_account", function(doc) { + return { + filters: { + 'report_type': 'Profit and Loss', + 'is_group': 0, + 'company': doc.company + } + }; + }); + + frm.set_query("write_off_cost_center", function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { - if(frm.doc.company) { + if (frm.doc.company) { frm.trigger("toggle_display_account_head"); } }, @@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', { frm.toggle_display('expense_account', erpnext.is_perpetual_inventory_enabled(frm.doc.company)); } -}) - -// Income Account -// -------------------------------- -cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company, - 'account_type': "Income Account" - } - }; -}; - - -// Cost Center -// ----------------------------- -cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) { - return{ - filters:{ - 'company': doc.company, - 'is_group': 0 - } - }; -}; - - -// Expense Account -// ----------------------------- -cur_frm.fields_dict["expense_account"].get_query = function(doc) { - return { - filters: { - "report_type": "Profit and Loss", - "company": doc.company, - "is_group": 0 - } - }; -}; - -// ------------------ Get Print Heading ------------------------------------ -cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) { - return{ - filters:[ - ['Print Heading', 'docstatus', '!=', 2] - ] - }; -}; - -cur_frm.fields_dict.write_off_account.get_query = function(doc) { - return{ - filters:{ - 'report_type': 'Profit and Loss', - 'is_group': 0, - 'company': doc.company - } - }; -}; - -// Write off cost center -// ----------------------- -cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) { - return{ - filters:{ - 'is_group': 0, - 'company': doc.company - } - }; -}; +}); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0d9ccb71ddd..33fbd748c7b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -112,6 +112,9 @@ class PurchaseInvoice(BuyingController): self.set_status() self.validate_purchase_receipt_if_update_stock() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): @@ -292,8 +295,15 @@ class PurchaseInvoice(BuyingController): item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): - item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, + asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) + if not asset_category_account: + form_link = get_link_to_form('Asset Category', asset_category) + throw( + _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), + title=_("Missing Account") + ) + item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 93e32f1a18c..545abf77e6b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -182,6 +182,7 @@ "sales_team_section_break", "sales_partner", "column_break10", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break2", @@ -2019,6 +2020,12 @@ "label": "Total Billing Hours", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2031,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-11 20:19:38.667508", + "modified": "2021-10-21 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2086,4 +2093,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5b369b9895b..fea213f4fd3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -36,7 +36,7 @@ from erpnext.assets.doctype.asset.depreciation import ( get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - post_depreciation_entries, + make_depreciation_entry, ) from erpnext.controllers.selling_controller import SellingController from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -157,6 +157,8 @@ class SalesInvoice(SellingController): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: validate_loyalty_points(self, self.loyalty_points) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: @@ -943,6 +945,7 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", None) if asset.calculate_depreciation: + self.reverse_depreciation_entry_made_after_sale(asset) self.reset_depreciation_schedule(asset) else: @@ -1006,22 +1009,20 @@ class SalesInvoice(SellingController): def depreciate_asset(self, asset): asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(self.posting_date) + asset.prepare_depreciation_data(date_of_sale=self.posting_date) asset.save() - post_depreciation_entries(self.posting_date) + make_depreciation_entry(asset.name, self.posting_date) def reset_depreciation_schedule(self, asset): asset.flags.ignore_validate_update_after_submit = True # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data() + asset.prepare_depreciation_data(date_of_return=self.posting_date) self.modify_depreciation_schedule_for_asset_repairs(asset) asset.save() - self.delete_depreciation_entry_made_after_sale(asset) - def modify_depreciation_schedule_for_asset_repairs(self, asset): asset_repairs = frappe.get_all( 'Asset Repair', @@ -1035,7 +1036,7 @@ class SalesInvoice(SellingController): asset_repair.modify_depreciation_schedule() asset.prepare_depreciation_data() - def delete_depreciation_entry_made_after_sale(self, asset): + def reverse_depreciation_entry_made_after_sale(self, asset): from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() @@ -1050,11 +1051,19 @@ class SalesInvoice(SellingController): row += 1 if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): + if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \ + or self.sale_happens_in_the_future(posting_date_of_original_invoice): + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True reverse_journal_entry.submit() + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + asset.save() + def get_posting_date_of_sales_invoice(self): return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') @@ -1069,6 +1078,12 @@ class SalesInvoice(SellingController): return True return False + def sale_happens_in_the_future(self, posting_date_of_original_invoice): + if posting_date_of_original_invoice > getdate(): + return True + + return False + @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7c6c507b033..ebe2a969b46 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2204,9 +2204,9 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) enable_discount_accounting(enable=0) - def test_asset_depreciation_on_sale(self): + def test_asset_depreciation_on_sale_with_pro_rata(self): """ - Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30. + Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. """ create_asset_data() @@ -2219,7 +2219,7 @@ class TestSalesInvoice(unittest.TestCase): expected_values = [ ["2020-06-30", 1311.48, 1311.48], ["2021-06-30", 20000.0, 21311.48], - ["2021-09-30", 3966.76, 25278.24] + ["2021-09-30", 5041.1, 26352.58] ] for i, schedule in enumerate(asset.schedules): @@ -2228,6 +2228,59 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertTrue(schedule.journal_entry) + def test_asset_depreciation_on_sale_without_pro_rata(self): + """ + Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. + """ + + create_asset_data() + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, + available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3, + expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1) + + post_depreciation_entries(getdate("2021-09-30")) + + create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31")) + asset.load_from_db() + + expected_values = [ + ["2020-12-31", 30000, 30000], + ["2021-12-31", 30000, 60000] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertTrue(schedule.journal_entry) + + def test_depreciation_on_return_of_sold_asset(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + create_asset_data() + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) + post_depreciation_entries(getdate("2021-09-30")) + + si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + return_si = make_return_doc("Sales Invoice", si.name) + return_si.submit() + asset.load_from_db() + + expected_values = [ + ["2020-06-30", 1311.48, 1311.48, True], + ["2021-06-30", 20000.0, 21311.48, True], + ["2022-06-30", 20000.0, 41311.48, False], + ["2023-06-30", 20000.0, 61311.48, False], + ["2024-06-30", 20000.0, 81311.48, False], + ["2025-06-06", 18688.52, 100000.0, False] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertEqual(schedule.journal_entry, schedule.journal_entry) + def test_sales_invoice_against_supplier(self): from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( make_customer, @@ -2317,6 +2370,29 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_sales_commission(self): + si = frappe.copy_doc(test_records[0]) + item = copy.deepcopy(si.get('items')[0]) + item.update({ + "qty": 1, + "rate": 500, + "grant_commission": 1 + }) + si.append("items", item) + + # Test valid values + for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): + si.commission_rate = commission_rate + si.save() + self.assertEqual(si.amount_eligible_for_commission, 500) + self.assertEqual(si.total_commission, total_commission) + + # Test invalid values + for commission_rate in (101, -1): + si.reload() + si.commission_rate = commission_rate + self.assertRaises(frappe.ValidationError, si.save) + def test_sales_invoice_submission_post_account_freezing_date(self): frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index d27a3a779ed..a9412d86396 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -47,6 +47,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_21", "net_rate", "net_amount", @@ -745,7 +746,6 @@ "fieldname": "asset", "fieldtype": "Link", "label": "Asset", - "no_copy": 1, "options": "Asset" }, { @@ -829,15 +829,23 @@ "fieldtype": "Link", "label": "Discount Account", "options": "Account" + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-19 13:41:53.435827", + "modified": "2021-10-05 12:24:54.968907", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88bcdad7102..a990f23cd6b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -109,7 +109,11 @@ class ReceivablePayableReport(object): invoiced = 0.0, paid = 0.0, credit_note = 0.0, - outstanding = 0.0 + outstanding = 0.0, + invoiced_in_account_currency = 0.0, + paid_in_account_currency = 0.0, + credit_note_in_account_currency = 0.0, + outstanding_in_account_currency = 0.0 ) self.get_invoices(gle) @@ -150,21 +154,28 @@ class ReceivablePayableReport(object): # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) + gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) + if gle_balance > 0: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency else: # invoice row.invoiced += gle_balance + row.invoiced_in_account_currency += gle_balance_in_account_currency else: # payment or credit note for receivables if self.is_invoice(gle): # stand alone debit / credit note row.credit_note -= gle_balance + row.credit_note_in_account_currency -= gle_balance_in_account_currency else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency + if gle.cost_center: row.cost_center = str(gle.cost_center) @@ -216,8 +227,13 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ + row.credit_note_in_account_currency, self.currency_precision) + row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + + if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ + (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -529,7 +545,9 @@ class ReceivablePayableReport(object): def set_ageing(self, row): if self.filters.ageing_based_on == "Due Date": - entry_date = row.due_date + # use posting date as a fallback for advances posted via journal and payment entry + # when ageing viewed by due date + entry_date = row.due_date or row.posting_date elif self.filters.ageing_based_on == "Supplier Invoice Date": entry_date = row.bill_date else: @@ -583,12 +601,14 @@ class ReceivablePayableReport(object): else: select_fields = "debit, credit" + doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" + remarks = ", remarks" if self.filters.get("show_remarks") else "" self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0} {remarks} + against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} from `tabGL Entry` where @@ -596,8 +616,8 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""" + .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -718,6 +738,13 @@ class ReceivablePayableReport(object): # get the balance of the GL (debit - credit) or reverse balance based on report type return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) + def get_gle_balance_in_account_currency(self, gle): + # get the balance of the GL (debit - credit) or reverse balance based on report type + return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + + def get_reverse_balance_in_account_currency(self, gle): + return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index e24a5f99184..d3e836afd10 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Include Default Book Entries"), "fieldtype": "Check", "default": 1 + }, + { + "fieldname": "show_zero_values", + "label": __("Show zero values"), + "fieldtype": "Check" } ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index c71bc17ca79..01799d58041 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import ( get_cash_flow_accounts, ) from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary -from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts +from erpnext.accounts.report.financial_statements import ( + filter_out_zero_value_rows, + get_fiscal_year_data, + sort_accounts, +) from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( get_chart_data as get_pl_chart_data, ) @@ -265,7 +269,7 @@ def get_columns(companies, filters): return columns def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name = get_account_heads(root_type, + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) if not accounts: return [] @@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) @@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - return accounts, accounts_by_name + return accounts, accounts_by_name, parent_children_map def update_parent_account_names(accounts): """Update parent_account_name in accounts list. diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json index a740de35729..9dd4e437f7f 100644 --- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json +++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json @@ -1,27 +1,30 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-06 12:28:23", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-03-06 05:52:57.645281", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Partners Commission", - "owner": "Administrator", - "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", - "ref_doctype": "Sales Invoice", - "report_name": "Sales Partners Commission", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-05-06 12:28:23", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "modified": "2021-10-06 06:26:07.881340", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Partners Commission", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"", + "ref_doctype": "Sales Invoice", + "report_name": "Sales Partners Commission", + "report_type": "Query Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } ] -} +} \ No newline at end of file diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index a3a45d1e79c..caee1a10bbb 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): posting_date = entry.posting_date voucher_type = entry.voucher_type + if not tax_withholding_category: + tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + rate = tax_rate_map.get(tax_withholding_category) + if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit) - if rate and tds_deducted: + if tds_deducted: row = { 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'supplier': supplier_map.get(supplier, {}).get('name') @@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) + suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) for d in suppliers: supplier_map[d.name] = d diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d5..153f5c537a2 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button("Transfer Asset", function() { + frm.add_custom_button(__("Transfer Asset"), function() { erpnext.asset.transfer_asset(frm); }, __("Manage")); - frm.add_custom_button("Scrap Asset", function() { + frm.add_custom_button(__("Scrap Asset"), function() { erpnext.asset.scrap_asset(frm); }, __("Manage")); - frm.add_custom_button("Sell Asset", function() { + 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() { + frm.add_custom_button(__("Restore Asset"), function() { erpnext.asset.restore_asset(frm); }, __("Manage")); } @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', { } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("View 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, @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 03824f7b64f..17a554bbbfd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -73,12 +73,12 @@ class Asset(AccountsController): if self.is_existing_asset and self.purchase_invoice: frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) - def prepare_depreciation_data(self, date_of_sale=None): + def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() self.make_depreciation_schedule(date_of_sale) - self.set_accumulated_depreciation(date_of_sale) + self.set_accumulated_depreciation(date_of_sale, date_of_return) else: self.finance_books = [] self.value_after_depreciation = (flt(self.gross_purchase_amount) - @@ -180,7 +180,7 @@ class Asset(AccountsController): d.precision("rate_of_depreciation")) def make_depreciation_schedule(self, date_of_sale): - if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: + if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'): self.schedules = [] if not self.available_for_use_date: @@ -193,8 +193,7 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(d.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -230,17 +229,19 @@ class Asset(AccountsController): depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, from_date, date_of_sale) - self.append("schedules", { - "schedule_date": date_of_sale, - "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx - }) + if depreciation_amount > 0: + self.append("schedules", { + "schedule_date": date_of_sale, + "depreciation_amount": depreciation_amount, + "depreciation_method": d.depreciation_method, + "finance_book": d.finance_book, + "finance_book_id": d.idx + }) + break # For first row - if has_pro_rata and n==0: + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) @@ -253,13 +254,17 @@ class Asset(AccountsController): 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)) + (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) + + depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, schedule_date, self.to_date) - monthly_schedule_date = add_months(schedule_date, 1) + depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, + depreciation_amount, d.finance_book) + monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) last_schedule_date = schedule_date @@ -349,7 +354,12 @@ class Asset(AccountsController): # 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 not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = self.get_modified_available_for_use_date(row) + days = date_diff(row.depreciation_start_date, from_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) @@ -359,6 +369,9 @@ class Asset(AccountsController): return has_pro_rata + def get_modified_available_for_use_date(self, row): + return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") @@ -395,7 +408,29 @@ class Asset(AccountsController): frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date") .format(row.idx)) - def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False): + # to ensure that final accumulated depreciation amount is accurate + def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) + + if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: + depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + + return depreciation_amount_for_last_row + + def get_depreciation_amount_for_first_row(self, finance_book): + if self.has_only_one_finance_book(): + return self.schedules[0].depreciation_amount + else: + for schedule in self.schedules: + if schedule.finance_book == finance_book: + return schedule.depreciation_amount + + def has_only_one_finance_book(self): + if len(self.finance_books) == 1: + return True + + def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False): straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] finance_books = [] @@ -412,7 +447,7 @@ class Asset(AccountsController): 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 and not date_of_sale: + if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return: book = self.get('finance_books')[cint(d.finance_book_id) - 1] depreciation_amount += flt(value_after_depreciation - flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) @@ -697,14 +732,14 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") - asset_maintenance.update({ +def create_asset_value_adjustment(asset, asset_category, company): + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): @@ -826,13 +861,11 @@ def get_total_days(date, frequency): @erpnext.allow_regional 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"): # 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 + depreciation_amount = (flt(asset.gross_purchase_amount) - + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ca10b1db19a..874fb630f87 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_entry = { - "account": accumulated_depreciation_account, + "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None): } debit_entry = { - "account": depreciation_expense_account, + "account": debit_account, "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -132,6 +134,20 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account +def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): + root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") + + if root_type == "Expense": + credit_account = accumulated_depreciation_account + debit_account = depreciation_expense_account + elif root_type == "Income": + credit_account = depreciation_expense_account + debit_account = accumulated_depreciation_account + else: + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) + + return credit_account, debit_account + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 0b5e41ee7ef..ba4dbee72da 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -19,12 +19,72 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt -class TestAsset(unittest.TestCase): - def setUp(self): +class AssetSetup(unittest.TestCase): + @classmethod + def setUpClass(cls): set_depreciation_settings_in_company() create_asset_data() + enable_cwip_accounting("Computers") + make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") frappe.db.sql("delete from `tabTax Rule`") + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + +class TestAsset(AssetSetup): + def test_asset_category_is_fetched(self): + """Tests if the Item's Asset Category value is assigned to the Asset, if the field is empty.""" + + asset = create_asset(item_code="Macbook Pro", do_not_save=1) + asset.asset_category = None + asset.save() + + self.assertEqual(asset.asset_category, "Computers") + + def test_gross_purchase_amount_is_mandatory(self): + asset = create_asset(item_code="Macbook Pro", do_not_save=1) + asset.gross_purchase_amount = 0 + + self.assertRaises(frappe.MandatoryError, asset.save) + + def test_pr_or_pi_mandatory_if_not_existing_asset(self): + """Tests if either PI or PR is present if CWIP is enabled and is_existing_asset=0.""" + + asset = create_asset(item_code="Macbook Pro", do_not_save=1) + asset.is_existing_asset=0 + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_available_for_use_date_is_after_purchase_date(self): + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1) + asset.is_existing_asset = 0 + asset.purchase_date = getdate("2021-10-10") + asset.available_for_use_date = getdate("2021-10-1") + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_item_exists(self): + asset = create_asset(item_code="MacBook", do_not_save=1) + + self.assertRaises(frappe.DoesNotExistError, asset.save) + + def test_validate_item(self): + asset = create_asset(item_code="MacBook Pro", do_not_save=1) + item = frappe.get_doc("Item", "MacBook Pro") + + item.disabled = 1 + item.save() + self.assertRaises(frappe.ValidationError, asset.save) + item.disabled = 0 + + item.is_fixed_asset = 0 + self.assertRaises(frappe.ValidationError, asset.save) + item.is_fixed_asset = 1 + + item.is_stock_item = 1 + self.assertRaises(frappe.ValidationError, asset.save) + def test_purchase_asset(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -87,302 +147,16 @@ class TestAsset(unittest.TestCase): doc.set_missing_values() self.assertEqual(doc.items[0].is_fixed_asset, 1) - def test_schedule_for_straight_line_method(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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-01-01' - asset.purchase_date = '2030-01-01' - - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 12, - "depreciation_start_date": "2030-12-31" - }) - asset.save() - - self.assertEqual(asset.status, "Draft") - expected_schedules = [ - ["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] - for d in asset.get("schedules")] - - self.assertEqual(schedules, expected_schedules) - - def test_schedule_for_straight_line_method_for_existing_asset(self): - create_asset(is_existing_asset=1) - asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - 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, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 12, - "depreciation_start_date": "2030-12-31" - }) - self.assertEqual(asset.status, "Draft") - asset.save() - expected_schedules = [ - ["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")] - - self.assertEqual(schedules, expected_schedules) - - def test_schedule_for_double_declining_method(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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-01-01' - asset.purchase_date = '2030-01-01' - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "depreciation_method": "Double Declining Balance", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 12, - "depreciation_start_date": '2030-12-31' - }) - asset.save() - self.assertEqual(asset.status, "Draft") - - expected_schedules = [ - ['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] - for d in asset.get("schedules")] - - self.assertEqual(schedules, expected_schedules) - - def test_schedule_for_double_declining_method_for_existing_asset(self): - create_asset(is_existing_asset = 1) - asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - asset.calculate_depreciation = 1 - 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, - "depreciation_method": "Double Declining Balance", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 12, - "depreciation_start_date": "2030-12-31" - }) - asset.save() - self.assertEqual(asset.status, "Draft") - - expected_schedules = [ - ["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] - for d in asset.get("schedules")] - - self.assertEqual(schedules, expected_schedules) - - def test_schedule_for_prorated_straight_line_method(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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.purchase_date = '2030-01-30' - asset.is_existing_asset = 0 - 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": 12, - "depreciation_start_date": "2030-12-31" - }) - - asset.save() - - expected_schedules = [ - ["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)] - for d in asset.get("schedules")] - - self.assertEqual(schedules, expected_schedules) - - def test_depreciation(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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.purchase_date = '2020-01-30' - asset.available_for_use_date = "2020-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" - }) - asset.submit() - asset.load_from_db() - self.assertEqual(asset.status, "Submitted") - - frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") - post_depreciation_entries(date="2021-01-01") - asset.load_from_db() - - # check depreciation entry series - self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") - - expected_gle = ( - ("_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` - where against_voucher_type='Asset' and against_voucher = %s - order by account""", asset.name) - - self.assertEqual(gle, expected_gle) - self.assertEqual(asset.get("value_after_depreciation"), 0) - - 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-01-01' - 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", 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)] - for d in asset.get("schedules")] - - self.assertEqual(schedules, expected_schedules) - - def test_depreciation_entry_cancellation(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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 = '2020-06-06' - asset.purchase_date = '2020-06-06' - 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" - }) - asset.submit() - post_depreciation_entries(date="2021-01-01") - - asset.load_from_db() - - # cancel depreciation entry - depr_entry = asset.get("schedules")[0].journal_entry - self.assertTrue(depr_entry) - frappe.get_doc("Journal Entry", depr_entry).cancel() - - asset.load_from_db() - depr_entry = asset.get("schedules")[0].journal_entry - self.assertFalse(depr_entry) - def test_scrap_asset(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.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 = '2020-01-01' - asset.purchase_date = '2020-01-01' - asset.append("finance_books", { - "expected_value_after_useful_life": 10000, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 10, - "frequency_of_depreciation": 1 - }) - asset.submit() + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = '2020-01-01', + purchase_date = '2020-01-01', + expected_value_after_useful_life = 10000, + total_number_of_depreciations = 10, + frequency_of_depreciation = 1, + submit = 1 + ) post_depreciation_entries(date=add_months('2020-01-01', 4)) @@ -409,23 +183,18 @@ class TestAsset(unittest.TestCase): self.assertFalse(asset.journal_entry_for_scrap) self.assertEqual(asset.status, "Partially Depreciated") - def test_asset_sale(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + def test_gle_made_by_asset_sale(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = '2020-06-06', + purchase_date = '2020-01-01', + expected_value_after_useful_life = 10000, + total_number_of_depreciations = 3, + frequency_of_depreciation = 10, + depreciation_start_date = '2020-12-31', + submit = 1 + ) - 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.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" - }) - asset.submit() post_depreciation_entries(date="2021-01-01") si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") @@ -453,30 +222,14 @@ class TestAsset(unittest.TestCase): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") - def test_asset_expected_value_after_useful_life(self): + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=100000.0, location="Test Location") + qty=2, rate=200000.0, location="Test Location") + doc = make_invoice(pr.name) - 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.append("finance_books", { - "expected_value_after_useful_life": 10000, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10 - }) - asset.save() - accumulated_depreciation_after_full_schedule = \ - max(d.accumulated_depreciation_amount for d in asset.get("schedules")) - - asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - - flt(accumulated_depreciation_after_full_schedule)) - - self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) + self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + # CWIP: Capital Work In Progress def test_cwip_accounting(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location") @@ -559,14 +312,6 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) - def test_expense_head(self): - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=2, rate=200000.0, location="Test Location") - - doc = make_invoice(pr.name) - - self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account) - def test_asset_cwip_toggling_cases(self): cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting") name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"]) @@ -635,41 +380,210 @@ class TestAsset(unittest.TestCase): frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) +class TestDepreciationMethods(AssetSetup): + def test_schedule_for_straight_line_method(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-01-01", + purchase_date = "2030-01-01", + expected_value_after_useful_life = 10000, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.status, "Draft") + expected_schedules = [ + ["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] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_straight_line_method_for_existing_asset(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-06-06", + is_existing_asset = 1, + number_of_depreciations_booked = 2, + opening_accumulated_depreciation = 47095.89, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2032-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.status, "Draft") + expected_schedules = [ + ["2032-12-31", 30000.0, 77095.89], + ["2033-06-06", 12904.11, 90000.0] + ] + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-01-01", + purchase_date = "2030-01-01", + depreciation_method = "Double Declining Balance", + expected_value_after_useful_life = 10000, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.status, "Draft") + + expected_schedules = [ + ['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] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method_for_existing_asset(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-01-01", + is_existing_asset = 1, + depreciation_method = "Double Declining Balance", + number_of_depreciations_booked = 1, + opening_accumulated_depreciation = 50000, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.status, "Draft") + + expected_schedules = [ + ["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] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_prorated_straight_line_method(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-01-30", + purchase_date = "2030-01-30", + depreciation_method = "Straight Line", + expected_value_after_useful_life = 10000, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + expected_schedules = [ + ["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)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + # WDV: Written Down Value method + def test_depreciation_entry_for_wdv_without_pro_rata(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-01-01", + purchase_date = "2030-01-01", + depreciation_method = "Written Down Value", + expected_value_after_useful_life = 12500, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 50000.0, 50000.0], + ["2031-12-31", 25000.0, 75000.0], + ["2032-12-31", 12500.0, 87500.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) + + # WDV: Written Down Value method + def test_pro_rata_depreciation_entry_for_wdv(self): + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-06-06", + purchase_date = "2030-01-01", + depreciation_method = "Written Down Value", + expected_value_after_useful_life = 12500, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 28493.15, 28493.15], + ["2031-12-31", 35753.43, 64246.58], + ["2032-12-31", 17876.71, 82123.29], + ["2033-06-06", 5376.71, 87500.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_discounted_wdv_depreciation_rate_for_indian_region(self): # set indian company company_flag = frappe.flags.company frappe.flags.company = "_Test Company" - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=8000.0, location="Test Location") - - finance_book = frappe.new_doc('Finance Book') - finance_book.finance_book_name = 'Income Tax' + finance_book = frappe.new_doc("Finance Book") + finance_book.finance_book_name = "Income Tax" finance_book.for_income_tax = 1 - finance_book.insert(ignore_if_duplicate=1) + finance_book.insert(ignore_if_duplicate = True) - 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-07-12' - asset.purchase_date = '2030-01-01' - asset.append("finance_books", { - "finance_book": finance_book.name, - "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) + asset = create_asset( + calculate_depreciation = 1, + available_for_use_date = "2030-07-12", + purchase_date = "2030-01-01", + finance_book = finance_book.name, + depreciation_method = "Written Down Value", + expected_value_after_useful_life = 12500, + depreciation_start_date = "2030-12-31", + total_number_of_depreciations = 3, + frequency_of_depreciation = 12 + ) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) expected_schedules = [ - ["2030-12-31", 942.47, 942.47], - ["2031-12-31", 3528.77, 4471.24], - ["2032-12-31", 1764.38, 6235.62], - ["2033-07-12", 764.38, 7000.00] + ["2030-12-31", 11780.82, 11780.82], + ["2031-12-31", 44109.59, 55890.41], + ["2032-12-31", 22054.8, 77945.21], + ["2033-07-12", 9554.79, 87500.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] @@ -701,6 +615,446 @@ class TestAsset(unittest.TestCase): asset.reload() self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) +class TestDepreciationBasics(AssetSetup): + def test_depreciation_without_pro_rata(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = getdate("2019-12-31"), + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = getdate("2020-12-31"), + submit = 1 + ) + + expected_values = [ + ["2020-12-31", 30000, 30000], + ["2021-12-31", 30000, 60000], + ["2022-12-31", 30000, 90000] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + + def test_depreciation_with_pro_rata(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = getdate("2019-12-31"), + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = getdate("2020-07-01"), + submit = 1 + ) + + expected_values = [ + ["2020-07-01", 15000, 15000], + ["2021-07-01", 30000, 45000], + ["2022-07-01", 30000, 75000], + ["2022-12-31", 15000, 90000] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + + def test_get_depreciation_amount(self): + """Tests if get_depreciation_amount() returns the right value.""" + + from erpnext.assets.doctype.asset.asset import get_depreciation_amount + + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31" + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + + depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + self.assertEqual(depreciation_amount, 30000) + + def test_make_depreciation_schedule(self): + """Tests if make_depreciation_schedule() returns the right values.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_method = "Straight Line", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-12-31" + ) + + expected_values = [ + ['2020-12-31', 30000.0], + ['2021-12-31', 30000.0], + ['2022-12-31', 30000.0] + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(expected_values[i][0], schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + + def test_set_accumulated_depreciation(self): + """Tests if set_accumulated_depreciation() returns the right values.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_method = "Straight Line", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-12-31" + ) + + expected_values = [30000.0, 60000.0, 90000.0] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount) + + def test_check_is_pro_rata(self): + """Tests if check_is_pro_rata() returns the right value(i.e. checks if has_pro_rata is accurate).""" + + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + + has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) + self.assertFalse(has_pro_rata) + + asset.finance_books = [] + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-07-01" + }) + + has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) + self.assertTrue(has_pro_rata) + + def test_expected_value_after_useful_life_greater_than_purchase_amount(self): + """Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000).""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 110000, + depreciation_start_date = "2020-07-01", + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_depreciation_start_date(self): + """Tests if an error is raised when neither depreciation_start_date nor available_for_use_date are specified.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 110000, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_opening_accumulated_depreciation(self): + """Tests if an error is raised when opening_accumulated_depreciation > (gross_purchase_amount - expected_value_after_useful_life).""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-07-01", + opening_accumulated_depreciation = 100000, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_number_of_depreciations_booked(self): + """Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-07-01", + opening_accumulated_depreciation = 10000, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_number_of_depreciations(self): + """Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-07-01", + opening_accumulated_depreciation = 10000, + number_of_depreciations_booked = 5, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_depreciation_start_date_is_before_purchase_date(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2014-07-01", + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_depreciation_start_date_is_before_available_for_use_date(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2018-07-01", + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_finance_books_are_present_if_calculate_depreciation_is_enabled(self): + asset = create_asset(item_code="Macbook Pro", do_not_save=1) + asset.calculate_depreciation = 1 + + self.assertRaises(frappe.ValidationError, asset.save) + + def test_post_depreciation_entries(self): + """Tests if post_depreciation_entries() works as expected.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + self.assertTrue(asset.schedules[0].journal_entry) + self.assertFalse(asset.schedules[1].journal_entry) + self.assertFalse(asset.schedules[2].journal_entry) + + def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): + """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + else: + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + + def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): + """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account.""" + + depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") + depr_expense_account.root_type = "Income" + depr_expense_account.parent_account = "Income - _TC" + depr_expense_account.save() + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + else: + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + + # resetting + depr_expense_account.root_type = "Expense" + depr_expense_account.parent_account = "Expenses - _TC" + depr_expense_account.save() + + def test_clear_depreciation_schedule(self): + """Tests if clear_depreciation_schedule() works as expected.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 1) + + def test_depreciation_entry_cancellation(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + purchase_date = "2020-06-06", + available_for_use_date = "2020-06-06", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 10, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-01-01") + + asset.load_from_db() + + # cancel depreciation entry + depr_entry = asset.get("schedules")[0].journal_entry + self.assertTrue(depr_entry) + frappe.get_doc("Journal Entry", depr_entry).cancel() + + asset.load_from_db() + depr_entry = asset.get("schedules")[0].journal_entry + self.assertFalse(depr_entry) + + def test_asset_expected_value_after_useful_life(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2020-06-06", + purchase_date = "2020-06-06", + frequency_of_depreciation = 10, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000 + ) + + accumulated_depreciation_after_full_schedule = \ + max(d.accumulated_depreciation_amount for d in asset.get("schedules")) + + asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - + flt(accumulated_depreciation_after_full_schedule)) + + self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) + + def test_gle_made_by_depreciation_entries(self): + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + purchase_date = "2020-01-30", + available_for_use_date = "2020-01-30", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 10, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + self.assertEqual(asset.status, "Submitted") + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date="2021-01-01") + asset.load_from_db() + + # check depreciation entry series + self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") + + expected_gle = ( + ("_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` + where against_voucher_type='Asset' and against_voucher = %s + order by account""", asset.name) + + self.assertEqual(gle, expected_gle) + self.assertEqual(asset.get("value_after_depreciation"), 0) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -722,32 +1076,37 @@ def create_asset(**args): asset = frappe.get_doc({ "doctype": "Asset", "asset_name": args.asset_name or "Macbook Pro 1", - "asset_category": "Computers", + "asset_category": args.asset_category or "Computers", "item_code": args.item_code or "Macbook Pro", - "company": args.company or"_Test Company", - "purchase_date": "2015-01-01", + "company": args.company or "_Test Company", + "purchase_date": args.purchase_date or "2015-01-01", "calculate_depreciation": args.calculate_depreciation or 0, - "gross_purchase_amount": 100000, - "purchase_receipt_amount": 100000, - "expected_value_after_useful_life": 10000, + "opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0, + "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, + "gross_purchase_amount": args.gross_purchase_amount or 100000, + "purchase_receipt_amount": args.purchase_receipt_amount or 100000, "warehouse": args.warehouse or "_Test Warehouse - _TC", - "available_for_use_date": "2020-06-06", - "location": "Test Location", - "asset_owner": "Company", - "is_existing_asset": 1 + "available_for_use_date": args.available_for_use_date or "2020-06-06", + "location": args.location or "Test Location", + "asset_owner": args.asset_owner or "Company", + "is_existing_asset": args.is_existing_asset or 1 }) if asset.calculate_depreciation: asset.append("finance_books", { - "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, - "total_number_of_depreciations": 5 + "finance_book": args.finance_book, + "depreciation_method": args.depreciation_method or "Straight Line", + "frequency_of_depreciation": args.frequency_of_depreciation or 12, + "total_number_of_depreciations": args.total_number_of_depreciations or 5, + "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, + "depreciation_start_date": args.depreciation_start_date }) - try: - asset.save() - except frappe.DuplicateEntryError: - pass + if not args.do_not_save: + try: + asset.save() + except frappe.DuplicateEntryError: + pass if args.submit: asset.submit() @@ -798,3 +1157,6 @@ def set_depreciation_settings_in_company(): # Enable booking asset depreciation entry automatically frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) + +def enable_cwip_accounting(asset_category, enable=1): + frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable) diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 51ce157a81c..c702687072d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { - "root_type": "Expense", + "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name } diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e2f3ca318f6..bd573bf479d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -42,10 +42,10 @@ class AssetCategory(Document): def validate_account_types(self): account_type_map = { - 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, - 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'root_type': 'Expense' }, - 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + 'fixed_asset_account': {'account_type': ['Fixed Asset']}, + 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, + 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, + 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} } for d in self.accounts: for fieldname in account_type_map.keys(): @@ -53,11 +53,11 @@ class AssetCategory(Document): selected_account = d.get(fieldname) key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) - expected_key_type = account_type_map[fieldname][key_to_match] + expected_key_types = account_type_map[fieldname][key_to_match] - if selected_key_type != expected_key_type: + if selected_key_type not in expected_key_types: frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), title=_("Invalid Account")) def valide_cwip_account(self): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 18a56d33e6d..d554d52a718 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -60,6 +60,10 @@ frappe.ui.form.on('Asset Repair', { if (frm.doc.repair_status == "Completed") { frm.set_value('completion_date', frappe.datetime.now_datetime()); } + }, + + stock_items_on_form_rendered() { + erpnext.setup_serial_or_batch_no(); } }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d780c18ad02..36848e9f15c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -118,9 +118,10 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, - "item_code": stock_item.item, + "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, - "basic_rate": stock_item.valuation_rate + "basic_rate": stock_item.valuation_rate, + "serial_no": stock_item.serial_no }) stock_entry.insert() diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 81b4f6c4499..7c0d05748e1 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -11,12 +11,15 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.stock.doctype.item.test_item import create_item class TestAssetRepair(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): set_depreciation_settings_in_company() create_asset_data() + create_item("_Test Stock Item") frappe.db.sql("delete from `tabTax Rule`") def test_update_status(self): @@ -70,9 +73,28 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.stock_entry_type, "Material Issue") self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) - self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + def test_serialized_item_consumption(self): + from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + stock_entry = make_serialized_item() + serial_nos = stock_entry.get("items")[0].serial_no + serial_no = serial_nos.split("\n")[0] + + # should not raise any error + create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code, + warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1) + + # should raise error + asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC", + item_code = stock_entry.get("items")[0].item_code) + + asset_repair.repair_status = "Completed" + self.assertRaises(SerialNoRequiredError, asset_repair.submit) + def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation = 1, submit=1) initial_asset_value = get_asset_value(asset) @@ -137,11 +159,12 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company) asset_repair.append("stock_items", { - "item": args.item or args.item_code or "_Test Item", + "item_code": args.item_code or "_Test Stock Item", "valuation_rate": args.rate if args.get("rate") is not None else 100, - "consumed_quantity": args.qty or 1 + "consumed_quantity": args.qty or 1, + "serial_no": args.serial_no }) asset_repair.insert(ignore_if_duplicate=True) @@ -158,7 +181,7 @@ def create_asset_repair(**args): }) stock_entry.append('items', { "t_warehouse": asset_repair.warehouse, - "item_code": asset_repair.stock_items[0].item, + "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity }) stock_entry.submit() diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 528f0ec986a..f63add12356 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -5,19 +5,13 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "item", + "item_code", "valuation_rate", "consumed_quantity", - "total_value" + "total_value", + "serial_no" ], "fields": [ - { - "fieldname": "item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item", - "options": "Item" - }, { "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", @@ -38,12 +32,24 @@ "in_list_view": 1, "label": "Total Value", "read_only": 1 + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-12 03:19:55.006300", + "modified": "2021-11-11 18:23:00.492483", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5eab21bd9d3..1b5f35efbb4 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController): self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference) + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(PurchaseOrder, self).validate_with_previous_doc({ diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 0a51a8e9a1c..023c95d697d 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -17,6 +17,7 @@ "company", "transaction_date", "valid_till", + "quotation_number", "amended_from", "address_section", "supplier_address", @@ -797,6 +798,11 @@ "fieldtype": "Date", "in_list_view": 1, "label": "Valid Till" + }, + { + "fieldname": "quotation_number", + "fieldtype": "Data", + "label": "Quotation Number" } ], "icon": "fa fa-shopping-cart", @@ -804,10 +810,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 00:58:20.995491", + "modified": "2021-12-11 06:43:20.924080", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f5cc2e4a42e..6ac720fec92 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -256,7 +256,12 @@ class AccountsController(TransactionBase): from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals calculate_taxes_and_totals(self) - if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: + if self.doctype in ( + 'Sales Order', + 'Delivery Note', + 'Sales Invoice', + 'POS Invoice', + ): self.calculate_commission() self.calculate_contribution() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dad3ed70933..cc773b75963 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -120,13 +120,27 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if self.meta.get_field("commission_rate"): - self.round_floats_in(self, ["base_net_total", "commission_rate"]) - if self.commission_rate > 100.0: - throw(_("Commission rate cannot be greater than 100")) + if not self.meta.get_field("commission_rate"): + return - self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0, - self.precision("total_commission")) + self.round_floats_in( + self, ("amount_eligible_for_commission", "commission_rate") + ) + + if not (0 <= self.commission_rate <= 100.0): + throw("{} {}".format( + _(self.meta.get_label("commission_rate")), + _("must be between 0 and 100"), + )) + + self.amount_eligible_for_commission = sum( + item.base_net_amount for item in self.items if item.grant_commission + ) + + self.total_commission = flt( + self.amount_eligible_for_commission * self.commission_rate / 100.0, + self.precision("total_commission") + ) def calculate_contribution(self): if not self.meta.get_field("sales_team"): @@ -138,7 +152,7 @@ class SellingController(StockController): self.round_floats_in(sales_person) sales_person.allocated_amount = flt( - self.base_net_total * sales_person.allocated_percentage / 100.0, + self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, self.precision("allocated_amount", sales_person)) if sales_person.commission_rate: diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py new file mode 100644 index 00000000000..13aa697610e --- /dev/null +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -0,0 +1,22 @@ +import unittest + +import frappe + + +class TestUtils(unittest.TestCase): + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) + + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") + + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 1b0480bbf0d..1110eb1accd 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -6,7 +6,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import comma_and, flt, unique -from erpnext.e_commerce.redisearch import ( +from erpnext.e_commerce.redisearch_utils import ( create_website_items_index, get_indexable_web_fields, is_search_module_loaded, diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index b4d06b3542f..2e60dfd9459 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -11,7 +11,7 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide from frappe.website.website_generator import WebsiteGenerator from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews -from erpnext.e_commerce.redisearch import ( +from erpnext.e_commerce.redisearch_utils import ( delete_item_from_index, insert_item_to_index, update_index_for_item, diff --git a/erpnext/e_commerce/redisearch.py b/erpnext/e_commerce/redisearch_utils.py similarity index 100% rename from erpnext/e_commerce/redisearch.py rename to erpnext/e_commerce/redisearch_utils.py diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json index 23ccb7e4dac..ae1f36e73fa 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json @@ -20,7 +20,6 @@ "configuration_cb", "shipping_account_head", "section_break_12", - "nexus_address", "nexus" ], "fields": [ @@ -87,15 +86,11 @@ "fieldtype": "Column Break" }, { + "depends_on": "nexus", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Nexus List" }, - { - "fieldname": "nexus_address", - "fieldtype": "HTML", - "label": "Nexus Address" - }, { "fieldname": "nexus", "fieldtype": "Table", @@ -107,20 +102,21 @@ "fieldname": "configuration_cb", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } ], "issingle": 1, "links": [], - "modified": "2021-11-08 18:02:29.232090", + "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f", + "modified": "2021-11-30 11:17:24.647979", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "TaxJar Settings", diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py index b9f24b65b38..d4bbe881d0c 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client class TaxJarSettings(Document): def on_update(self): - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") - TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") - TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") + TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions + TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax + TAXJAR_SANDBOX_MODE = self.is_sandbox fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9309929e99e..6d57c83e6a5 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -273,6 +273,9 @@ doc_events = { "erpnext.regional.india.utils.update_taxable_values" ] }, + "POS Invoice": { + "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"] + }, "Purchase Invoice": { "validate": [ "erpnext.regional.india.utils.validate_reverse_charge_transaction", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 79e8f6140aa..88e5ca9d4c5 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -96,15 +96,8 @@ class Employee(NestedSet): 'user': self.user_id }) - if employee_user_permission_exists: return - - employee_user_permission_exists = frappe.db.exists('User Permission', { - 'allow': 'Employee', - 'for_value': self.name, - 'user': self.user_id - }) - - if employee_user_permission_exists: return + if employee_user_permission_exists: + return add_user_permission("Employee", self.name, self.user_id) set_user_permission_if_allowed("Company", self.company, self.user_id) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index db126cde8e1..2ffae1a4f2a 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -323,10 +323,14 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - def update_sales(source, target, parent): + def update_sales_and_serial(source, target, parent): sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') target.service_person = sales_person - target.serial_no = '' + serial_nos = get_serial_nos(target.serial_no) + if len(serial_nos) == 1: + target.serial_no = serial_nos[0] + else: + target.serial_no = '' doclist = get_mapped_doc("Maintenance Schedule", source_name, { "Maintenance Schedule": { @@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", "condition": lambda doc: doc.item_name == item_name, - "postprocess": update_sales + "postprocess": update_sales_and_serial } }, target_doc) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 443fb6d2dc7..b920b6339ac 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', { } }); } - else { - frm.clear_table("purposes"); - } - if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { + frm.clear_table("purposes"); frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py index f8f6af34efc..24362f8246d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -5,10 +5,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'job_card', + 'non_standard_fieldnames': { + 'Quality Inspection': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Material Request', 'Stock Entry'] + }, + { + 'label': _('Reference'), + 'items': ['Quality Inspection'] } ] } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e6c6c80d16f..09ca3df92fd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -292,7 +292,7 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef -erpnext.patches.v13_0.create_gst_payment_entry_fields +erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items @@ -304,6 +304,7 @@ erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record +erpnext.patches.v13_0.remove_bad_selling_defaults erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") @@ -334,3 +335,5 @@ erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v13_0.rename_ksa_qr_field +erpnext.patches.v13_0.disable_ksa_print_format_for_others diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py index 7e6d67ce931..416694559cb 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -9,24 +9,29 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') frappe.reload_doc('accounts', 'doctype', 'payment_entry') - custom_fields = { - 'Payment Entry': [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', - print_hide=1, collapsible=1), - dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', - print_hide=1, options='Address'), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='company_gstin', - print_hide=1, read_only=1), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', - print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='customer_address', - fetch_from='customer_address.gstin', print_hide=1, read_only=1) - ] - } + if frappe.db.exists('Company', {'country': 'India'}): + custom_fields = { + 'Payment Entry': [ + dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions', + print_hide=1, collapsible=1), + dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section', + print_hide=1, options='Address'), + dict(fieldname='company_gstin', label='Company GSTIN', + fieldtype='Data', insert_after='company_address', + fetch_from='company_address.gstin', print_hide=1, read_only=1), + dict(fieldname='place_of_supply', label='Place of Supply', + fieldtype='Data', insert_after='company_gstin', + print_hide=1, read_only=1), + dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply', + print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'), + dict(fieldname='customer_gstin', label='Customer GSTIN', + fieldtype='Data', insert_after='customer_address', + fetch_from='customer_address.gstin', print_hide=1, read_only=1) + ] + } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) + else: + fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + for field in fields: + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") \ No newline at end of file diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py new file mode 100644 index 00000000000..c815b3bb3c9 --- /dev/null +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if company: + return + + if frappe.db.exists('DocType', 'Print Format'): + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + for d in ('KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 1) diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py new file mode 100644 index 00000000000..381c3902da0 --- /dev/null +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -0,0 +1,16 @@ +import frappe +from frappe import _ + + +def execute(): + frappe.reload_doctype("Selling Settings") + selling_settings = frappe.get_single("Selling Settings") + + if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"): + selling_settings.customer_group = None + + if selling_settings.territory in (_("All Territories"), "All Territories"): + selling_settings.territory = None + + selling_settings.flags.ignore_mandatory=True + selling_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py new file mode 100644 index 00000000000..0bb86e04509 --- /dev/null +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020, Wahni Green Technologies and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + if not company: + return + + if frappe.db.exists('DocType', 'Sales Invoice'): + frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + if frappe.db.has_column('Sales Invoice', 'qr_code'): + rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 577662bd58c..42e7ed68e30 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1086,7 +1086,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ $.each(this.frm.doc.taxes || [], function(i, d) { if(d.charge_type == "Actual") { frappe.model.set_value(d.doctype, d.name, "tax_amount", - flt(d.tax_amount) / flt(exchange_rate)); + flt(d.base_tax_amount) / flt(exchange_rate)); } }); }, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9482f190ede..7840c58d891 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -84,6 +84,10 @@ $.extend(erpnext, { }); }, + route_to_pending_reposts: (args) => { + frappe.set_route('List', 'Repost Item Valuation', args); + }, + proceed_save_with_reminders_frequency_change: () => { frappe.ui.hide_open_dialog(); diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 1677e9b3de4..7a3854cc611 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -495,6 +495,11 @@ font-size: var(--text-md); } + > .item-qty-total-container { + @extend .net-total-container; + padding: 5px 0px 0px 0px; + } + > .taxes-container { display: none; flex-direction: column; diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 9a72410f67b..0f0897841b4 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document): memberships = frappe.db.get_all('Membership', { 'member': self.member, 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], - 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'membership_status': ('!=', 'Cancelled') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e5196069bbf..9512a0d4a83 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -561,17 +561,17 @@ def get_item_list(data, doc, hsn_wise=False): } item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol'] hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise) - for hsn_code, taxable_amount in hsn_taxable_amount.items(): + for item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() - if not hsn_code: + if not item_or_hsn: frappe.throw(_('GST HSN Code does not exist for one or more items')) - item_data.hsnCode = int(hsn_code) + item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" for attr in item_data_attrs: item_data[attr] = 0 - for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items(): + for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): account_type = gst_accounts.get(account, '') for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: @@ -839,13 +839,11 @@ def update_taxable_values(doc, method): doc.get('items')[item_count - 1].taxable_value += diff 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"): # 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 + depreciation_amount = (flt(asset.gross_purchase_amount) - + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py @@ -0,0 +1 @@ + diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json new file mode 100644 index 00000000000..61c058734aa --- /dev/null +++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json @@ -0,0 +1,32 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-12-07 13:25:05.424827", + "css": "", + "custom_format": 1, + "default_print_language": "en", + "disabled": 1, + "doc_type": "POS Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 0, + "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}{{ item.get_formatted(\"net_amount\") }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t
\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "modified": "2021-12-08 10:25:01.930885", + "modified_by": "Administrator", + "module": "Regional", + "name": "KSA POS Invoice", + "owner": "Administrator", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json index 8e9a72897df..a4cb922ddbd 100644 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json @@ -5,19 +5,19 @@ "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", "custom_format": 1, "default_print_language": "en", - "disabled": 0, + "disabled": 1, "doc_type": "Sales Invoice", "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", + "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, "margin_left": 15.0, "margin_right": 15.0, "margin_top": 15.0, - "modified": "2021-11-29 13:47:37.870818", + "modified": "2021-12-07 13:43:38.018593", "modified_by": "Administrator", "module": "Regional", "name": "KSA VAT Invoice", @@ -29,4 +29,4 @@ "raw_printing": 0, "show_section_headings": 0, "standard": "Yes" -} \ No newline at end of file +} diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 38a089c6326..2e31c03d5c6 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -3,7 +3,7 @@ import frappe from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats +from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -13,6 +13,16 @@ def setup(company=None, patch=True): add_permissions() make_custom_fields() +def add_print_formats(): + frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "tax_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) + frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) + + for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'): + frappe.db.set_value("Print Format", d, "disabled", 0) + def add_permissions(): """Add Permissions for KSA VAT Setting.""" add_permission('KSA VAT Setting', 'All', 0) @@ -33,8 +43,16 @@ def make_custom_fields(): custom_fields = { 'Sales Invoice': [ dict( - fieldname='qr_code', - label='QR Code', + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ], + 'POS Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1 ) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 7d00d8b3928..674ea83cc65 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -1,147 +1,152 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + import io import os from base64 import b64encode import frappe from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.utils.data import add_to_date, get_time, getdate from pyqrcode import create as qr_create from erpnext import get_region -def create_qr_code(doc, method): - """Create QR Code after inserting Sales Inv - """ - +def create_qr_code(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return - # if QR Code field not present, do nothing - if not hasattr(doc, 'qr_code'): - return + # if QR Code field not present, create it. Invoices without QR are invalid as per law. + if not hasattr(doc, 'ksa_einv_qr'): + create_custom_fields({ + doc.doctype: [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ] + }) # Don't create QR Code if it already exists - qr_code = doc.get("qr_code") + qr_code = doc.get("ksa_einv_qr") if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): return - meta = frappe.get_meta('Sales Invoice') + meta = frappe.get_meta(doc.doctype) - for field in meta.get_image_fields(): - if field.fieldname == 'qr_code': - ''' TLV conversion for - 1. Seller's Name - 2. VAT Number - 3. Time Stamp - 4. Invoice Amount - 5. VAT Amount - ''' - tlv_array = [] - # Sellers Name + if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: + ''' TLV conversion for + 1. Seller's Name + 2. VAT Number + 3. Time Stamp + 4. Invoice Amount + 5. VAT Amount + ''' + tlv_array = [] + # Sellers Name - seller_name = frappe.db.get_value( - 'Company', - doc.company, - 'company_name_in_arabic') + seller_name = frappe.db.get_value( + 'Company', + doc.company, + 'company_name_in_arabic') - if not seller_name: - frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) + if not seller_name: + frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) - tag = bytes([1]).hex() - length = bytes([len(seller_name.encode('utf-8'))]).hex() - value = seller_name.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([1]).hex() + length = bytes([len(seller_name.encode('utf-8'))]).hex() + value = seller_name.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Number - tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') - if not tax_id: - frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) + # VAT Number + tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') + if not tax_id: + frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) - tag = bytes([2]).hex() - length = bytes([len(tax_id)]).hex() - value = tax_id.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([2]).hex() + length = bytes([len(tax_id)]).hex() + value = tax_id.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Time Stamp - posting_date = getdate(doc.posting_date) - time = get_time(doc.posting_time) - seconds = time.hour * 60 * 60 + time.minute * 60 + time.second - time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') + # Time Stamp + posting_date = getdate(doc.posting_date) + time = get_time(doc.posting_time) + seconds = time.hour * 60 * 60 + time.minute * 60 + time.second + time_stamp = add_to_date(posting_date, seconds=seconds) + time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') - tag = bytes([3]).hex() - length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([3]).hex() + length = bytes([len(time_stamp)]).hex() + value = time_stamp.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Invoice Amount - invoice_amount = str(doc.grand_total) - tag = bytes([4]).hex() - length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + # Invoice Amount + invoice_amount = str(doc.grand_total) + tag = bytes([4]).hex() + length = bytes([len(invoice_amount)]).hex() + value = invoice_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # VAT Amount - vat_amount = str(doc.total_taxes_and_charges) + # VAT Amount + vat_amount = str(doc.total_taxes_and_charges) - tag = bytes([5]).hex() - length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + tag = bytes([5]).hex() + length = bytes([len(vat_amount)]).hex() + value = vat_amount.encode('utf-8').hex() + tlv_array.append(''.join([tag, length, value])) - # Joining bytes into one - tlv_buff = ''.join(tlv_array) + # Joining bytes into one + tlv_buff = ''.join(tlv_array) - # base64 conversion for QR Code - base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() + # base64 conversion for QR Code + base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - qr_image = io.BytesIO() - url = qr_create(base64_string, error='L') - url.png(qr_image, scale=2, quiet_zone=1) + qr_image = io.BytesIO() + url = qr_create(base64_string, error='L') + url.png(qr_image, scale=2, quiet_zone=1) - name = frappe.generate_hash(doc.name, 5) + name = frappe.generate_hash(doc.name, 5) - # making file - filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "qr_code" - }) + # making file + filename = f"QRCode-{name}.png".replace(os.path.sep, "__") + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "is_private": 0, + "content": qr_image.getvalue(), + "attached_to_doctype": doc.get("doctype"), + "attached_to_name": doc.get("name"), + "attached_to_field": "ksa_einv_qr" + }) - _file.save() + _file.save() - # assigning to document - doc.db_set('qr_code', _file.file_url) - doc.notify_update() - - break + # assigning to document + doc.db_set('ksa_einv_qr', _file.file_url) + doc.notify_update() -def delete_qr_code_file(doc, method): - """Delete QR Code on deleted sales invoice""" - +def delete_qr_code_file(doc, method=None): region = get_region(doc.company) if region not in ['Saudi Arabia']: return - if hasattr(doc, 'qr_code'): - if doc.get('qr_code'): + if hasattr(doc, 'ksa_einv_qr'): + if doc.get('ksa_einv_qr'): file_doc = frappe.get_list('File', { - 'file_url': doc.get('qr_code') + 'file_url': doc.get('ksa_einv_qr') }) if len(file_doc): frappe.delete_doc('File', file_doc[0].name) -def delete_vat_settings_for_company(doc, method): +def delete_vat_settings_for_company(doc, method=None): if doc.country != 'Saudi Arabia': return - settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) - settings_doc.delete() \ No newline at end of file + if frappe.db.exists('KSA VAT Setting', doc.name): + frappe.delete_doc('KSA VAT Setting', doc.name) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 43a44900fcb..16a5e0b756d 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -952,8 +952,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "max_attachments": 1, - "modified": "2021-08-27 20:10:07.864951", + "modified": "2021-11-30 01:33:21.106073", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7c7ed9a9604..7e99a062439 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -134,6 +134,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1507,16 +1508,23 @@ "fieldtype": "Small Text", "label": "Dispatch Address", "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:09:51.515542", + "modified": "2021-10-05 12:16:40.775704", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 0a83c488bbd..658691548f1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -64,6 +64,8 @@ class SalesOrder(SellingController): if not self.billing_status: self.billing_status = 'Not Billed' if not self.delivery_status: self.delivery_status = 'Not Delivered' + self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_po(self): # validate p.o date v/s delivery date if self.po_date and not self.skip_delivery_note: @@ -1024,6 +1026,7 @@ def make_work_orders(items, sales_order, company, project=None): description=i['description'] )).insert() work_order.set_work_order_operations() + work_order.flags.ignore_mandatory = True work_order.save() out.append(work_order) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 1e5590e7489..95f6c4e96df 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -48,6 +48,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_24", "net_rate", "net_amount", @@ -789,15 +790,23 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:15:05.803091", + "modified": "2021-10-05 12:27:25.014789", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index e7c5e769965..fb86e614b6c 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -8,7 +8,6 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document from frappe.utils import cint -from frappe.utils.nestedset import get_root_of class SellingSettings(Document): @@ -37,9 +36,3 @@ class SellingSettings(Document): editable_bundle_item_rates = cint(self.editable_bundle_item_rates) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) - - def set_default_customer_group_and_territory(self): - if not self.customer_group: - self.customer_group = get_root_of('Customer Group') - if not self.territory: - self.territory = get_root_of('Territory') diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index a5b2d500414..4920584d95e 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -100,6 +100,10 @@ erpnext.PointOfSale.ItemCart = class { `
${this.get_discount_icon()} ${__('Add Discount')}
+
+
${__('Total Items')}
+
0.00
+
${__("Net Total")}
0.00
@@ -142,6 +146,7 @@ erpnext.PointOfSale.ItemCart = class { this.$numpad_section.prepend( `
+
` @@ -470,6 +475,7 @@ erpnext.PointOfSale.ItemCart = class { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.net_total); + this.render_total_item_qty(frm.doc.items); const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); @@ -487,6 +493,21 @@ erpnext.PointOfSale.ItemCart = class { ); } + render_total_item_qty(items) { + var total_item_qty = 0; + items.map((item) => { + total_item_qty = total_item_qty + item.qty; + }); + + this.$totals_section.find('.item-qty-total-container').html( + `
${__('Total Quantity')}
${total_item_qty}
` + ); + + this.$numpad_section.find('.numpad-item-qty-total').html( + `
${__('Total Quantity')}: ${total_item_qty}
` + ); + } + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total-container').html( diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index aebc273e4cc..47c4e4301a4 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -157,25 +157,19 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ commission_rate: function() { this.calculate_commission(); - refresh_field("total_commission"); }, total_commission: function() { - if(this.frm.doc.base_net_total) { - frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]); + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); - if(this.frm.doc.base_net_total < this.frm.doc.total_commission) { - var msg = (__("[Error]") + " " + - __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission", - this.frm.doc.name)) + " > " + - __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name))); - frappe.msgprint(msg); - throw msg; - } + const { amount_eligible_for_commission } = this.frm.doc; + if (!amount_eligible_for_commission) return; - this.frm.set_value("commission_rate", - flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total)); - } + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); }, allocated_percentage: function(doc, cdt, cdn) { @@ -185,7 +179,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ sales_person.allocated_percentage = flt(sales_person.allocated_percentage, precision("allocated_percentage", sales_person)); - sales_person.allocated_amount = flt(this.frm.doc.base_net_total * + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, precision("allocated_amount", sales_person)); refresh_field(["allocated_amount"], sales_person); @@ -259,28 +253,39 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ }, calculate_commission: function() { - if(this.frm.fields_dict.commission_rate) { - if(this.frm.doc.commission_rate > 100) { - var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) + - " " + __("cannot be greater than 100"); - frappe.msgprint(msg); - throw msg; - } + if (!this.frm.fields_dict.commission_rate) return; - this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0, - precision("total_commission")); + if (this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); }, calculate_contribution: function() { var me = this; $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { frappe.model.round_floats_in(sales_person); - if(sales_person.allocated_percentage) { - sales_person.allocated_amount = flt( - me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0, - precision("allocated_amount", sales_person)); - } + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); }); }, diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 95ca3867ee7..91f60fbd4e2 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -12,6 +12,10 @@ frappe.ui.form.on("Company", { } }); } + + frm.call('check_if_transactions_exist').then(r => { + frm.toggle_enable("default_currency", (!r.message)); + }); }, setup: function(frm) { erpnext.company.setup_queries(frm); @@ -87,9 +91,6 @@ frappe.ui.form.on("Company", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} - frm.toggle_enable("default_currency", (frm.doc.__onload && - !frm.doc.__onload.transactions_exist)); - if (frappe.perm.has_perm("Cost Center", 0, 'read')) { frm.add_custom_button(__('Cost Centers'), function() { frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index d40ed912f6e..955bfb41392 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -24,8 +24,8 @@ class Company(NestedSet): def onload(self): load_address_and_contact(self, "company") - self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist() + @frappe.whitelist() def check_if_transactions_exist(self): exists = False for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 9b9b925c805..def652aff97 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -304,7 +304,6 @@ def set_more_defaults(): def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") - selling_settings.set_default_customer_group_and_territory() selling_settings.cust_master_name = "Customer Name" selling_settings.so_required = "No" selling_settings.dn_required = "No" diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 8e9f6db0e8e..93b6e8d3af3 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -53,6 +53,7 @@ def before_tests(): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) enable_all_roles_and_domains() + set_defaults_for_tests() frappe.db.commit() @@ -127,6 +128,14 @@ def enable_all_roles_and_domains(): [d.name for d in domains]) add_all_roles_to('Administrator') +def set_defaults_for_tests(): + from frappe.utils.nestedset import get_root_of + + selling_settings = frappe.get_single("Selling Settings") + selling_settings.customer_group = get_root_of("Customer Group") + selling_settings.territory = get_root_of("Territory") + selling_settings.save() + def insert_record(records): for r in records: diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 48b1cc53967..17c8367a638 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,7 +4,7 @@ import frappe from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.utils import flt class Bin(Document): @@ -100,33 +100,11 @@ def on_doctype_update(): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): - '''Called from erpnext.stock.utils.update_bin''' + """WARNING: This function is deprecated. Inline this function instead of using it.""" + from erpnext.stock.stock_ledger import repost_current_voucher + update_qty(bin_name, args) - - if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle - - if not args.get("posting_date"): - args["posting_date"] = nowdate() - - if args.get("is_cancelled") and via_landed_cost_voucher: - return - - # Reposts only current voucher SL Entries - # Updates valuation rate, stock value, stock queue for current transaction - update_entries_after({ - "item_code": args.get('item_code'), - "warehouse": args.get('warehouse'), - "posting_date": args.get("posting_date"), - "posting_time": args.get("posting_time"), - "voucher_type": args.get("voucher_type"), - "voucher_no": args.get("voucher_no"), - "sle_id": args.get('name'), - "creation": args.get('creation') - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - - # update qty in future sle and Validate negative qty - update_qty_in_future_sle(args, allow_negative_stock) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) def get_bin_details(bin_name): return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', @@ -160,4 +138,4 @@ def update_qty(bin_name, args): 'indented_qty': indented_qty, 'planned_qty': planned_qty, 'projected_qty': projected_qty - }) \ No newline at end of file + }) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ad1b3b43aee..55a4c956a67 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -145,6 +145,7 @@ "sales_team_section_break", "sales_partner", "column_break7", + "amount_eligible_for_commission", "commission_rate", "total_commission", "section_break1", @@ -1302,16 +1303,23 @@ "label": "Dispatch Address", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "amount_eligible_for_commission", + "fieldtype": "Currency", + "label": "Amount Eligible for Commission", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-10-08 14:29:13.428984", + "modified": "2021-10-09 14:29:13.428984", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 52684607b4b..70d48a42d72 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ class DeliveryNote(SellingController): self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' + self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): super(DeliveryNote, self).validate_with_previous_doc({ diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index a96c29925e5..51c88bed61d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "grant_commission", "section_break_25", "net_rate", "net_amount", @@ -753,13 +754,20 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:12:44.018872", + "modified": "2021-10-06 12:12:44.018872", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index e360cca124e..39a94a0868b 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -89,6 +89,7 @@ "sales_details", "sales_uom", "is_sales_item", + "grant_commission", "column_break3", "max_discount", "deferred_revenue", @@ -942,6 +943,12 @@ "fieldtype": "Check", "label": "Published in Website", "read_only": 1 + }, + { + "default": "1", + "fieldname": "grant_commission", + "fieldtype": "Check", + "label": "Grant Commission" } ], "icon": "fa fa-tag", @@ -949,8 +956,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "max_attachments": 1, - "modified": "2021-09-10 12:23:07.277077", + "modified": "2021-12-03 08:32:03.869294", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1017,7 +1023,7 @@ "search_fields": "item_name,description,item_group,customer_code", "show_name_in_global_search": 1, "show_preview_popup": 1, - "sort_field": "idx desc,modified desc", + "sort_field": "modified", "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8f2985e2c21..9f3d9569f9f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -496,7 +496,6 @@ class Item(Document): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -510,7 +509,6 @@ class Item(Document): repost_stock(new_name, warehouse) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 def update_bom_item_desc(self): if self.is_new(): diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index a9ee4b07426..d85970665e1 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -81,6 +81,9 @@ class MaterialRequest(BuyingController): # NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated # Though the creation of Material Request from a Production Plan can be rethought to fix this + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def set_title(self): '''Set title as comma separated list of items''' if not self.title: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 05324cf713e..e7e9e9c1d7f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -119,6 +119,10 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_cwip_accounts(self): for item in self.get('items'): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index b3fc1258590..a61501a9cd5 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -168,8 +168,8 @@ def repost_entries(): for row in riv_entries: doc = frappe.get_doc('Repost Item Valuation', row.name) if doc.status in ('Queued', 'In Progress'): - doc.deduplicate_similar_repost() repost(doc) + doc.deduplicate_similar_repost() riv_entries = get_repost_item_valuation_entries() if riv_entries: diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index de793163fdb..78b432d564c 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -4,12 +4,14 @@ import unittest import frappe +from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) +from erpnext.stock.utils import PendingRepostingError class TestRepostItemValuation(unittest.TestCase): @@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase): # to avoid breaking other tests accidentaly riv4.set_status("Skipped") riv3.set_status("Skipped") + + def test_stock_freeze_validation(self): + + today = nowdate() + + riv = frappe.get_doc( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + posting_date=today, + posting_time="00:01:00", + ) + riv.flags.dont_run_in_test = True # keep it queued + riv.submit() + + stock_settings = frappe.get_doc("Stock Settings") + stock_settings.stock_frozen_upto = today + + self.assertRaises(PendingRepostingError, stock_settings.save) + + riv.set_status("Skipped") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e34b4ef267b..5b27106b825 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -104,6 +104,8 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() + self.reset_default_field_value("from_warehouse", "items", "s_warehouse") + self.reset_default_field_value("to_warehouse", "items", "t_warehouse") def on_submit(self): self.update_stock_ledger() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 002c446a52a..642c2636130 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -25,7 +25,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle +from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -39,7 +40,7 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(unittest.TestCase): +class TestStockEntry(ERPNextTestCase): def tearDown(self): frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -929,6 +930,83 @@ class TestStockEntry(unittest.TestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle(self): + # Initialize item, batch, warehouse, opening qty + item_code = '_Test Future Neg Item' + batch_no = '_Test Future Neg Batch' + warehouses = [ + '_Test Future Neg Warehouse Source', + '_Test Future Neg Warehouse Destination' + ] + warehouse_names = initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, + opening_qty=2, posting_date='2021-07-01' + ) + + # Executing an illegal sequence should raise an error + sequence_of_entries = [ + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-03', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[1], + to_warehouse=warehouse_names[0], + batch_no=batch_no, + posting_date='2021-07-04', + purpose='Material Transfer'), + dict(item_code=item_code, + qty=2, + from_warehouse=warehouse_names[0], + to_warehouse=warehouse_names[1], + batch_no=batch_no, + posting_date='2021-07-02', # Illegal SE + purpose='Material Transfer') + ] + + self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) + + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_future_negative_sle_batch(self): + from erpnext.stock.doctype.batch.test_batch import TestBatch + + # Initialize item, batch, warehouse, opening qty + item_code = '_Test MultiBatch Item' + TestBatch.make_batch_item(item_code) + + batch_nos = [] # store generate batches + warehouse = '_Test Warehouse - _TC' + + se1 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-01', + purpose='Material Receipt' + ) + batch_nos.append(se1.items[0].batch_no) + se2 = make_stock_entry( + item_code=item_code, + qty=2, + to_warehouse=warehouse, + posting_date='2021-09-03', + purpose='Material Receipt' + ) + batch_nos.append(se2.items[0].batch_no) + + with self.assertRaises(NegativeStockError) as nse: + make_stock_entry(item_code=item_code, + qty=1, + from_warehouse=warehouse, + batch_no=batch_nos[1], + posting_date='2021-09-02', # backdated consumption of 2nd batch + purpose='Material Issue') + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -999,3 +1077,31 @@ def get_multiple_items(): ] test_records = frappe.get_test_records('Stock Entry') + +def initialize_records_for_future_negative_sle_test( + item_code, batch_no, warehouses, opening_qty, posting_date): + from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + TestBatch.make_batch_item(item_code) + make_new_batch(item_code=item_code, batch_id=batch_no) + warehouse_names = [create_warehouse(w) for w in warehouses] + create_stock_reconciliation( + purpose='Opening Stock', + posting_date=posting_date, + posting_time='20:00:20', + item_code=item_code, + warehouse=warehouse_names[0], + valuation_rate=100, + qty=opening_qty, + batch_no=batch_no, + ) + return warehouse_names + + +def create_stock_entries(sequence_of_entries): + for entry_detail in sequence_of_entries: + make_stock_entry(**entry_detail) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 9f0af495949..39ca97a47bc 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -9,7 +9,7 @@ import frappe from frappe import _ from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -44,7 +44,6 @@ class StockLedgerEntry(Document): def on_submit(self): self.check_stock_frozen_date() - self.actual_amt_check() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): @@ -58,18 +57,6 @@ class StockLedgerEntry(Document): "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) - def actual_amt_check(self): - """Validate that qty at warehouse for selected batch is >=0""" - if self.batch_no and not self.get("allow_negative_stock"): - batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", - (self.warehouse, self.item_code, self.batch_no))[0][0]) - - if batch_bal_after_transaction < 0: - frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}") - .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse)) - def validate_mandatory(self): mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] for k in mandatory: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index b7d1497319f..3402972bb89 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-03-28 10:35:31", "description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.", @@ -153,11 +154,12 @@ "icon": "fa fa-upload-alt", "idx": 1, "is_submittable": 1, - "max_attachments": 1, - "modified": "2020-04-08 17:02:47.196206", + "links": [], + "modified": "2021-11-30 01:33:51.437194", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1de48b6f1f1..c1293cbf0fa 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -11,6 +11,8 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.utils.html_utils import clean_html +from erpnext.stock.utils import check_pending_reposting + class StockSettings(Document): def validate(self): @@ -36,6 +38,7 @@ class StockSettings(Document): self.validate_warehouses() self.cant_change_valuation_method() self.validate_clean_description_html() + self.validate_pending_reposts() def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] @@ -64,6 +67,11 @@ class StockSettings(Document): # changed to text frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + def validate_pending_reposts(self): + if self.stock_frozen_upto: + check_pending_reposting(self.stock_frozen_upto) + + def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index ca92936a1dc..26db2642e4b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) - def test_warehouse_renaming(self): - create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") - account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") - self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) - - # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") - - def test_warehouse_merging(self): - company = "_Test Company with perpetual inventory" - create_warehouse("Test Warehouse for Merging 1", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 2", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", - qty=1, rate=100, company=company) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", - qty=1, rate=100, company=company) - - existing_bin_qty = ( - cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) - + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) - ) - - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", - "Test Warehouse for Merging 2 - TCP1", merge=True) - - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) - - bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") - - self.assertEqual(bin_qty, existing_bin_qty) - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - TCP1"})) - def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 9b9093261c2..05076b51a3e 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "creation": "2013-03-07 18:50:32", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", @@ -245,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-04-09 19:54:56.263965", + "modified": "2021-12-03 04:40:06.414630", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index b9dbc388805..9cfad86f142 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet -import erpnext from erpnext.stock import get_warehouse_account @@ -68,57 +67,6 @@ class Warehouse(NestedSet): return frappe.db.sql("""select name from `tabWarehouse` where parent_warehouse = %s limit 1""", self.name) - def before_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).before_rename(old_name, new_name, merge) - - # Add company abbr if not provided - new_warehouse = erpnext.encode_company_abbr(new_name, self.company) - - if merge: - if not frappe.db.exists("Warehouse", new_warehouse): - frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse)) - - if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"): - frappe.throw(_("Both Warehouse must belong to same Company")) - - return new_warehouse - - def after_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).after_rename(old_name, new_name, merge) - - new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name) - self.db_set("warehouse_name", new_warehouse_name) - - if merge: - self.recalculate_bin_qty(new_name) - - def get_new_warehouse_name_without_abbr(self, name): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") - parts = name.rsplit(" - ", 1) - - if parts[-1].lower() == company_abbr.lower(): - name = parts[0] - - return name - - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - repost_stock_for_items = frappe.db.sql_list("""select distinct item_code - from tabBin where warehouse=%s""", new_name) - - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name) - - for item_code in repost_stock_for_items: - repost_stock(item_code, new_name) - - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 - def convert_to_group_or_ledger(self): if self.is_group: self.convert_to_ledger() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5dfabfc6d6d..6c119e6c2bd 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -328,7 +328,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_uom": args.get("weight_uom") or item.get("weight_uom"), + "grant_commission": item.get("grant_commission") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 440ce0549a2..d2e840faf02 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,10 +7,11 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate from six import iteritems import erpnext +from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -18,19 +19,15 @@ from erpnext.stock.utils import ( ) -# future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') -# _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: - from erpnext.stock.utils import update_bin - cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) @@ -65,7 +62,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # 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) + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + if is_stock_item: + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_bin_qty(bin_name, args) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) + else: + frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + +def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": + if not args.get("posting_date"): + args["posting_date"] = nowdate() + + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction + update_entries_after({ + "item_code": args.get('item_code'), + "warehouse": args.get('warehouse'), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get('name'), + "creation": args.get('creation') + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + # update qty in future sle and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def get_args_for_future_sle(row): return frappe._dict({ @@ -795,10 +823,10 @@ class update_entries_after(object): def update_bin(self): # update bin for each warehouse - for warehouse, data in iteritems(self.data): - bin_record = get_or_make_bin(self.item_code, warehouse) + for warehouse, data in self.data.items(): + bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_record, { + frappe.db.set_value('Bin', bin_name, { "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value @@ -1054,17 +1082,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "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( - abs(sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - sle[0]["posting_date"], sle[0]["posting_time"], - frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) + if allow_negative_stock: + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + return + + neg_sle = get_future_sle_with_negative_qty(args) + if neg_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) + + frappe.throw(message, NegativeStockError, title='Insufficient Stock') + + + if not args.batch_no: + return + + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if neg_batch_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link('Batch', args.batch_no), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1083,6 +1130,29 @@ def get_future_sle_with_negative_qty(args): limit 1 """, args, as_dict=1) + +def get_future_sle_with_negative_batch_qty(args): + return frappe.db.sql(""" + with batch_ledger as ( + select + posting_date, posting_time, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by posting_date, posting_time, creation + ) + select * from batch_ledger + where + cumulative_total < 0.0 + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + limit 1 + """, args, as_dict=1) + + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 6. diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 5aafecf018a..dd87e4ff499 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -13,6 +13,7 @@ import erpnext class InvalidWarehouseCompany(frappe.ValidationError): pass +class PendingRepostingError(frappe.ValidationError): pass def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -188,7 +189,7 @@ def get_bin(item_code, warehouse): bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code, warehouse) -> str: +def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: @@ -204,11 +205,12 @@ def get_or_make_bin(item_code, warehouse) -> str: return bin_record def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): + """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: - bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) - update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) @@ -417,3 +419,28 @@ def is_reposting_item_valuation_in_progress(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) + +def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: + """Check if there are pending reposting job till the specified posting date.""" + + filters = { + "docstatus": 1, + "status": ["in", ["Queued","In Progress", "Failed"]], + "posting_date": ["<=", posting_date], + } + + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + if reposting_pending and throw_error: + msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") + frappe.msgprint(msg, + raise_exception=PendingRepostingError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + } + ) + + return bool(reposting_pending) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 99ad648e530..a2351a71804 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -5,7 +5,7 @@ import frappe from frappe.utils import cint, cstr from redisearch import AutoCompleter, Client, Query -from erpnext.e_commerce.redisearch import ( +from erpnext.e_commerce.redisearch_utils import ( WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ca03a787cd1..d46ffb56096 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1847,7 +1847,7 @@ Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, Owner,Besitzer, -PAN,PFANNE, +PAN,PAN, POS,Verkaufsstelle, POS Profile,Verkaufsstellen-Profil, POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden", diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index a1c954b0604..76c183447ae 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -163,6 +163,28 @@ class TransactionBase(StatusUpdater): return ret + def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str): + """ Reset "Set default X" fields on forms to avoid confusion. + + example: + doc = { + "set_from_warehouse": "Warehouse A", + "items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}], + } + Since this has dissimilar values in child table, the default field will be erased. + + doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + """ + child_table_values = set() + + for row in self.get(child_table): + child_table_values.add(row.get(child_table_field)) + + if len(child_table_values) > 1: + self.set(default_field, None) + else: + self.set(default_field, list(child_table_values)[0]) + def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT distinct `tabEvent`.name diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000